How it works
This page walks through the complete execution flow of a Longshot build, from user request to delivered project.Execution overview
┌──────────────────────────────────────────────────────────────────┐
│ Phase 1: Initial Planning │
│ User request → Root planner analyzes repo → Creates tasks │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ Phase 2: Parallel Execution │
│ Tasks dispatch to workers → Sandboxes execute → Branches push │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ Phase 3: Merge Queue Processing │
│ Branches merge sequentially → Conflicts retry or fix │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ Phase 4: Continuous Reconciliation │
│ Build/test checks → Fix task generation → Re-dispatch │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ Phase 5: Finalization │
│ Drain queue → Retry unmerged → Final sweep → Report │
└──────────────────────────────────────────────────────────────────┘
Example scenario
Let’s trace a realistic build request through the entire system: User request:longshot "Build a REST API with user authentication and todo CRUD according to SPEC.md"
SPEC.mddefines API endpoints and data modelsFEATURES.jsonlists planned features- Empty
src/directory package.jsonwith Express + TypeScript
Phase 1: Initial planning
Step 1.1: Orchestrator startup
// main.ts
const orchestrator = await createOrchestrator({
projectRoot: process.cwd(),
maxIterations: 100,
callbacks: {
onTaskCreated: (task) => console.log(`Created ${task.id}`),
onTaskCompleted: (task, handoff) => console.log(`Completed ${task.id}`)
}
});
await orchestrator.run("Build a REST API with user authentication and todo CRUD");
- Load configuration from
.env - Read system prompts from
prompts/directory - Wire components: planner → worker pool → merge queue → reconciler
- Start background services (monitor, reconciler, merge queue)
Step 1.2: Root planner initialization
// planner.ts - runLoop()
const piSession = await createPlannerPiSession({
systemPrompt: rootPlannerPrompt,
targetRepoPath: config.targetRepoPath,
llmConfig: config.llm
});
- Creates temporary directory for planner workspace
- Initializes LLM session with root-planner system prompt
- Sets up tool access (bash, read, write, edit, glob, grep)
Step 1.3: Repository analysis
const repoState = await readRepoState(targetRepoPath);
// Returns:
{
fileTree: [
"package.json",
"tsconfig.json",
"SPEC.md",
"FEATURES.json",
"src/"
],
recentCommits: [
"abc123 Initial commit"
],
specMd: "# API Specification\n\n## Endpoints\n...",
featuresJson: "[{\"id\": \"auth\", \"status\": \"planned\"}, ...]",
agentsMd: "# Coding Conventions\n...",
decisionsMd: "# Architecture Decisions\n..."
}
Step 1.4: Initial planning prompt
The planner constructs a comprehensive initial prompt:const prompt = `
## Request
${request}
## SPEC.md (Product Specification)
${repoState.specMd}
## FEATURES.json
${repoState.featuresJson}
## AGENTS.md (Coding Conventions)
${repoState.agentsMd}
## Repository File Tree
${repoState.fileTree.join('\n')}
## Recent Commits
${repoState.recentCommits.join('\n')}
This is the initial planning call. SPEC.md and FEATURES.json are binding.
Produce your first batch of tasks and scratchpad.
`;
await piSession.session.prompt(prompt);
Step 1.5: LLM response and task parsing
LLM response (example):{
"scratchpad": "The request requires 5 main tasks: (1) Express server setup, (2) Auth middleware with JWT, (3) User registration endpoint, (4) Todo CRUD (complex, 5 files), (5) Integration tests. Task 4 should go to subplanner due to scope.",
"tasks": [
{
"id": "task-001",
"description": "Set up Express server with TypeScript, middleware, and error handling",
"scope": ["src/server.ts", "src/middleware/error-handler.ts"],
"acceptance": "Server starts on port 3000, handles 404s, returns JSON errors",
"priority": 5
},
{
"id": "task-002",
"description": "Implement JWT authentication middleware with token verification",
"scope": ["src/middleware/auth.ts", "src/utils/jwt.ts"],
"acceptance": "Middleware validates JWT tokens, returns 401 for invalid/missing tokens",
"priority": 5
},
{
"id": "task-003",
"description": "Create user registration endpoint with password hashing",
"scope": ["src/routes/auth.ts", "src/models/user.ts"],
"acceptance": "POST /auth/register creates user with bcrypt-hashed password, returns JWT",
"priority": 5
},
{
"id": "task-004",
"description": "Create todo CRUD endpoints (GET, POST, PUT, DELETE)",
"scope": [
"src/routes/todos.ts",
"src/models/todo.ts",
"src/controllers/todo.controller.ts",
"src/middleware/validate-todo.ts",
"src/types/todo.types.ts"
],
"acceptance": "All CRUD operations work, todos are user-scoped, validation errors return 400",
"priority": 5
},
{
"id": "task-005",
"description": "Add API integration tests with supertest",
"scope": ["src/__tests__/api.test.ts"],
"acceptance": "Tests cover auth + todo CRUD, all tests pass",
"priority": 5
}
]
}
const { scratchpad, tasks: rawTasks } = parsePlannerResponse(responseText);
const tasks: Task[] = rawTasks.map((raw) => ({
id: raw.id || `task-${++taskCounter}`,
description: raw.description,
scope: raw.scope || [],
acceptance: raw.acceptance || '',
branch: raw.branch || `worker/${raw.id}-${slugify(raw.description)}`,
status: 'pending',
createdAt: Date.now(),
priority: raw.priority || 5
}));
// Result: 5 tasks ready for dispatch
Phase 2: Parallel execution
Step 2.1: Task dispatch and routing
for (const task of tasks) {
taskQueue.enqueue(task);
dispatchedTaskIds.add(task.id);
activeTasks.add(task.id);
// Fire-and-forget dispatch
dispatchSingleTask(task);
}
async dispatchSingleTask(task: Task) {
await dispatchLimiter.acquire(); // Concurrency control
// Route based on scope size
if (shouldDecompose(task, config, 0)) {
// task-004 has 5 files → route to subplanner
handoff = await subplanner.decomposeAndExecute(task, 0);
} else {
// task-001, task-002, task-003, task-005 → direct to workers
handoff = await workerPool.assignTask(task);
}
pendingHandoffs.push({ task, handoff });
dispatchLimiter.release();
}
- Tasks 001, 002, 003, 005 → Worker pool (parallel)
- Task 004 → Subplanner (will decompose further)
Step 2.2: Subplanner decomposition (task-004)
// subplanner.ts - decomposeAndExecute()
const piSession = await createPlannerPiSession({
systemPrompt: subplannerPrompt,
targetRepoPath,
llmConfig
});
const prompt = `
## Parent Task
- **ID**: task-004
- **Description**: Create todo CRUD endpoints (GET, POST, PUT, DELETE)
- **Scope**: src/routes/todos.ts, src/models/todo.ts, ...
- **Decomposition Depth**: 0
## Repository File Tree
${fileTree.join('\n')}
Decompose this task into atomic subtasks. Return JSON.
`;
await piSession.session.prompt(prompt);
{
"scratchpad": "Breaking todo CRUD into 5 atomic tasks: model definition, GET endpoint, POST endpoint, PUT endpoint, DELETE endpoint.",
"tasks": [
{
"id": "task-004-sub-1",
"description": "Create todo model and TypeScript types",
"scope": ["src/models/todo.ts", "src/types/todo.types.ts"],
"acceptance": "Todo model exports interface and validation schema",
"priority": 5
},
{
"id": "task-004-sub-2",
"description": "Create GET /todos and GET /todos/:id endpoints",
"scope": ["src/routes/todos.ts", "src/controllers/todo.controller.ts"],
"acceptance": "GET endpoints return user's todos, 404 for missing ID",
"priority": 5
},
// ... task-004-sub-3, sub-4, sub-5
]
}
for (const subtask of subtasks) {
taskQueue.enqueue(subtask);
// All subtasks are atomic (scope < 4) → dispatch to workers
handoff = await workerPool.assignTask(subtask);
subtaskHandoffs.push(handoff);
}
// Aggregate all subtask results into parent handoff
return aggregateHandoffs(task004, subtasks, subtaskHandoffs);
Step 2.3: Worker sandbox execution
Each worker follows this lifecycle: Python sandbox spawner (infra/spawn_sandbox.py):
import modal
import json
import sys
payload = json.loads(sys.argv[1])
task = payload['task']
repo_url = payload['repoUrl']
llm_config = payload['llmConfig']
# Create Modal sandbox
sandbox = modal.Sandbox.create(
"ubuntu-22.04",
cpu=4,
memory=8192,
timeout=1800
)
print("[spawn] Sandbox created", flush=True)
# Clone repository
sandbox.exec(f"git clone {repo_url} /workspace")
print("[spawn] Repo cloned", flush=True)
# Checkout task branch
sandbox.exec(
f"cd /workspace && git checkout -b {task['branch']}"
)
print(f"[spawn] Checked out branch {task['branch']}", flush=True)
# Write task to sandbox
task_json = json.dumps(task)
sandbox.exec(f"echo '{task_json}' > /workspace/task.json")
# Start Pi agent (worker-runner.ts)
result = sandbox.exec(
"node",
args=["/app/worker-runner.js"],
env={
"TASK_FILE": "/workspace/task.json",
"LLM_ENDPOINT": llm_config['endpoint'],
"LLM_MODEL": llm_config['model'],
"LLM_API_KEY": llm_config['apiKey']
},
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
print(f"[spawn] Worker completed with exit code {result.returncode}", flush=True)
# Push branch to remote
sandbox.exec(
f"cd /workspace && git push origin {task['branch']}"
)
print(f"[spawn] Pushed branch {task['branch']}", flush=True)
# Read handoff result
handoff_json = sandbox.exec("cat /workspace/handoff.json").stdout
handoff = json.loads(handoff_json)
# Print handoff as final line (parsed by TypeScript)
print(json.dumps(handoff), flush=True)
packages/sandbox/src/worker-runner.ts):
import { Pi } from '@mariozechner/pi-coding-agent';
import fs from 'fs';
const task = JSON.parse(fs.readFileSync('/workspace/task.json', 'utf-8'));
const pi = new Pi({
systemPrompt: workerSystemPrompt,
workingDirectory: '/workspace',
llm: {
endpoint: process.env.LLM_ENDPOINT,
model: process.env.LLM_MODEL,
apiKey: process.env.LLM_API_KEY
}
});
// Construct user prompt
const prompt = `
## Task
${task.description}
## Scope
Focus on these files: ${task.scope.join(', ')}
## Acceptance Criteria
${task.acceptance}
## Instructions
- Use bash, read, write, edit tools to complete the task
- Commit changes with descriptive message
- Ensure code compiles (run tsc --noEmit)
- Write handoff.json with summary, concerns, suggestions
`;
const result = await pi.run(prompt);
// Agent executes tools, writes code, commits
// Example tool sequence:
// 1. read('src/routes/todos.ts') // Check existing code
// 2. write('src/routes/todos.ts', content) // Create file
// 3. bash('tsc --noEmit') // Verify compilation
// 4. bash('git add src/routes/todos.ts')
// 5. bash('git commit -m "Add todo routes"')
// Build handoff report
const diff = execSync('git diff HEAD~1').toString();
const stats = execSync('git diff --numstat HEAD~1').toString();
const handoff = {
taskId: task.id,
status: result.success ? 'complete' : 'failed',
summary: result.summary,
diff,
filesChanged: task.scope,
concerns: result.concerns,
suggestions: result.suggestions,
metrics: {
tokensUsed: result.tokens,
toolCallCount: result.toolCalls,
durationMs: result.duration,
linesAdded: parseStats(stats).added,
linesRemoved: parseStats(stats).removed,
filesCreated: task.scope.length,
filesModified: task.scope.length
}
};
fs.writeFileSync('/workspace/handoff.json', JSON.stringify(handoff));
t=0s: All 9 workers spawn sandboxes simultaneously
t=10s: Repos cloned, branches created
t=20s: Pi agents start executing
t=90s: task-001 completes (simple server setup)
t=120s: task-002 completes (auth middleware)
t=150s: task-004-sub-1 completes (todo model)
t=180s: task-003 completes (registration endpoint)
t=200s: task-004-sub-2 completes (GET endpoints)
t=220s: task-004-sub-3 completes (POST endpoint)
t=240s: task-004-sub-4 completes (PUT endpoint)
t=260s: task-004-sub-5 completes (DELETE endpoint)
t=300s: task-005 completes (integration tests)
Phase 3: Merge queue processing
Step 3.1: Handoff collection
As workers complete, the planner collects handoffs:while (pendingHandoffs.length > 0) {
const { task, handoff } = pendingHandoffs.shift();
allHandoffs.push(handoff);
handoffsSinceLastPlan.push(handoff);
if (handoff.status === 'complete') {
// Enqueue branch for merging
mergeQueue.enqueue(task.branch, task.priority);
} else if (handoff.status === 'failed' && task.retryCount < 1) {
// Auto-retry failed tasks once
taskQueue.retryTask(task.id);
dispatchSingleTask(task);
}
}
Step 3.2: Background merge processing
The merge queue processes branches in priority order:// Merge queue runs every 5 seconds
while (queue.length > 0) {
const branch = dequeue(); // e.g., "worker/task-001-server-setup"
const result = await mergeBranch(branch);
if (result.success) {
merged.add(branch);
stats.totalMerged++;
} else if (result.conflicted) {
handleConflict(branch, result.conflicts);
} else {
stats.totalFailed++;
}
}
t=90s: worker/task-001 → main (merged successfully)
t=120s: worker/task-002 → main (merged successfully)
t=150s: worker/task-004-sub-1 → main (merged successfully)
t=180s: worker/task-003 → main (CONFLICT: auth.ts overlaps with task-002)
→ Rebase onto latest main
→ Retry merge (attempt 1/2)
t=185s: worker/task-003 → main (merged after rebase)
t=200s: worker/task-004-sub-2 → main (merged successfully)
t=220s: worker/task-004-sub-3 → main (CONFLICT: todos.ts overlaps with sub-2)
→ Rebase onto latest main
→ Retry merge (attempt 1/2)
t=225s: worker/task-004-sub-3 → main (CONFLICT PERSISTS)
→ Retry exhausted (2/2)
→ Create conflict-fix-001 task
Step 3.3: Conflict resolution task
When retries are exhausted, the merge queue creates a conflict-fix task:mergeQueue.onConflict((info) => {
const fixTask = {
id: `conflict-fix-${++conflictCounter}`,
description: `
Resolve merge conflict from branch "${info.branch}".
Conflicting files: ${info.conflictingFiles.join(', ')}
The sandbox will check out the original branch and rebase it onto main.
Conflict markers will be present in the working tree.
Open each file, find <<<<<<< / ======= / >>>>>>> blocks, and resolve by
keeping the correct version based on surrounding code context.
Remove all conflict markers, run 'git add' on each file, then
'git rebase --continue'. Ensure the file compiles after resolution.
`,
scope: info.conflictingFiles.slice(0, 5),
acceptance: "No <<<<<<< markers remain. tsc --noEmit returns 0. Rebase completes.",
branch: info.branch, // Reuse original branch
status: 'pending',
priority: 1, // High priority
conflictSourceBranch: info.branch
};
planner.injectTask(fixTask);
});
Phase 4: Continuous reconciliation
Step 4.1: Background health checks
While tasks execute and merge, the reconciler runs periodic sweeps:// Reconciler sweep every 5 minutes (1 minute when errors detected)
setInterval(async () => {
const result = await reconciler.sweep();
if (!result.buildOk || !result.testsOk || result.hasConflictMarkers) {
// Inject fix tasks into planner
for (const fixTask of result.fixTasks) {
planner.injectTask(fixTask);
}
}
}, intervalMs);
async sweep(): Promise<SweepResult> {
// Check build
const tscResult = await exec('npx', ['tsc', '--noEmit'], targetRepo);
const buildOk = tscResult.code === 0;
// Check tests
const testResult = await exec('npm', ['test'], targetRepo);
const testsOk = testResult.code === 0;
// Check for conflict markers
const conflictResult = await exec('git', ['grep', '-rl', '<<<<<<<'], targetRepo);
const hasConflictMarkers = conflictResult.stdout.length > 0;
if (buildOk && testsOk && !hasConflictMarkers) {
return { buildOk: true, testsOk: true, fixTasks: [] };
}
// Build failed — analyze and create fix tasks
const prompt = `
## Build Output
${tscResult.stderr}
## Test Output
${testResult.stderr}
Analyze failures and generate fix tasks with specific scope.
`;
const response = await llm.complete([{ role: 'user', content: prompt }]);
const fixTasks = parseTasks(response.content);
return { buildOk, testsOk, hasConflictMarkers, fixTasks };
}
t=280s: Reconciler sweep detects test failure:
"TypeError: Cannot read property 'id' of undefined at todo.controller.ts:42"
LLM analyzes and generates:
fix-001: "Add null check in getTodoById controller before accessing todo.id"
scope: ["src/controllers/todo.controller.ts"]
priority: 1
Task dispatches to worker immediately (high priority).
Step 4.2: Follow-up planning
When 3 or more handoffs accumulate, the planner runs a follow-up planning iteration:if (handoffsSinceLastPlan.length >= 3 || activeTasks.size === 0) {
const repoState = await readRepoState(targetRepoPath);
const prompt = buildFollowUpPrompt(repoState, handoffsSinceLastPlan);
await piSession.session.prompt(prompt);
const tasks = parseTasks(response);
handoffsSinceLastPlan = [];
if (tasks.length > 0) {
dispatchTasks(tasks);
} else if (activeTasks.size === 0) {
planningDone = true;
}
}
const prompt = `
## Updated Repository State
File tree (${currentTree.size} files):
New: src/controllers/todo.controller.ts, src/types/todo.types.ts
Removed: (none)
Recent commits:
def456 Add todo model and types
ghi789 Add GET /todos endpoints
## New Worker Handoffs (3 since last plan)
### Task task-004-sub-1 — complete
Summary: Created todo model with Mongoose schema and TypeScript types.
Files changed: src/models/todo.ts, src/types/todo.types.ts
Concerns: None
Suggestions: Add validation for todo status enum
### Task task-004-sub-2 — complete
Summary: Implemented GET /todos and GET /todos/:id endpoints.
Files changed: src/routes/todos.ts, src/controllers/todo.controller.ts
Concerns: Missing error handling for invalid todo ID
Suggestions: Add 404 response for non-existent todos
### Task task-004-sub-3 — complete
Summary: Implemented POST /todos endpoint with validation.
Files changed: src/routes/todos.ts, src/controllers/todo.controller.ts
Concerns: None
Suggestions: None
## Currently Active Tasks (2)
- task-004-sub-4: Implement PUT /todos/:id endpoint
- task-004-sub-5: Implement DELETE /todos/:id endpoint
## Merge Queue Health
Merged: 7 | Conflicts: 1 | Failed: 0 | Queue depth: 2
Success rate: 88%
Continue planning. Review handoffs and emit next batch of tasks.
`;
- “All core features complete, no new tasks needed”
- OR: “Add error handling task based on worker concerns”
Phase 5: Finalization
Step 5.1: Wait for active tasks
while (activeTasks.size > 0) {
collectCompletedHandoffs();
await sleep(500);
}
logger.info('All tasks completed', {
totalTasks: dispatchedTaskIds.size,
completedTasks: completedCount,
failedTasks: failedCount
});
Step 5.2: Drain merge queue
if (finalizationEnabled) {
const mergeQueueDepth = mergeQueue.getQueueLength();
if (mergeQueueDepth > 0) {
logger.info('Draining merge queue', { depth: mergeQueueDepth });
const results = await mergeQueue.processQueue();
logger.info('Merge queue drained', {
merged: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length
});
}
}
Step 5.3: Re-attempt unmerged branches
const allBranches = planner.getAllDispatchedBranches();
const unmerged = allBranches.filter(b => !mergeQueue.isBranchMerged(b));
if (unmerged.length > 0) {
logger.info('Re-enqueuing unmerged branches', { count: unmerged.length });
for (const branch of unmerged) {
mergeQueue.resetRetryCount(branch);
mergeQueue.enqueue(branch, 1); // High priority
}
await mergeQueue.processQueue();
}
Step 5.4: Final reconciliation sweep
logger.info('Running final build + test sweep');
const sweepResult = await reconciler.sweep();
if (sweepResult.buildOk && sweepResult.testsOk && !sweepResult.hasConflictMarkers) {
logger.info('✅ Finalization PASSED — all green!');
return { success: true };
}
if (sweepResult.fixTasks.length > 0) {
logger.info('Issues detected, dispatching fix tasks', {
count: sweepResult.fixTasks.length
});
for (const task of sweepResult.fixTasks) {
planner.injectTask(task);
}
// Wait for fix tasks to complete
while (planner.getActiveTaskCount() > 0) {
await sleep(1000);
}
// Re-run sweep
const finalSweep = await reconciler.sweep();
return finalSweep;
}
Step 5.5: Final report
const snapshot = monitor.getSnapshot();
logger.info('🎉 Longshot run complete', {
totalTasks: snapshot.completedTasks + snapshot.failedTasks,
completedTasks: snapshot.completedTasks,
failedTasks: snapshot.failedTasks,
mergeSuccessRate: snapshot.mergeSuccessRate,
totalTokensUsed: snapshot.totalTokensUsed,
totalCostUsd: snapshot.totalCostUsd,
finalizationBuildPassed: snapshot.finalizationBuildPassed,
finalizationTestsPassed: snapshot.finalizationTestsPassed,
finalizationAllMerged: snapshot.finalizationAllMerged
});
if (snapshot.finalizationAllMerged) {
console.log('\n✅ All branches merged successfully');
} else {
console.log(`\n⚠️ ${snapshot.finalizationUnmergedCount} branches remain unmerged`);
console.log('Check remote branches for manual recovery');
}
Key execution patterns
Fire-and-forget dispatch
Workers dispatch asynchronously without blocking:for (const task of tasks) {
dispatchSingleTask(task); // Does not await
}
// All tasks start in parallel immediately
Concurrency limiting
class ConcurrencyLimiter {
private active = 0;
private queue: Array<() => void> = [];
async acquire() {
if (this.active < this.maxWorkers) {
this.active++;
return;
}
await new Promise(resolve => this.queue.push(resolve));
this.active++;
}
release() {
this.active--;
const next = this.queue.shift();
if (next) next();
}
}
Adaptive intervals
// Reconciler speeds up when errors detected
if (consecutiveGreenSweeps >= 3) {
adjustInterval(5 * 60 * 1000); // 5 minutes
} else {
adjustInterval(1 * 60 * 1000); // 1 minute
}
Scope-based routing
function shouldDecompose(task: Task): boolean {
return task.scope.length >= 4; // 4+ files → subplanner
}
Next steps
Architecture
Deep dive into component architecture and design
Configuration
Configure workers, timeouts, and sandbox resources