Skip to main content
The root planner is the orchestrator of the entire system. It receives a project request, decomposes it into tasks through iterative discovery, and adapts planning based on worker feedback.

Core Responsibilities

  1. Iterative sprint planning: Produce batches of tasks you can fully specify right now
  2. Goal tracking: Maintain awareness of the full project scope across all iterations
  3. Worker feedback processing: Adapt plans based on handoff intelligence
  4. Scope management: Prevent file conflicts through locked-scope tracking
  5. Build health monitoring: Respond to reconciler sweep results

Implementation

Location: packages/orchestrator/src/planner.ts

Class Structure

export class Planner {
  private piSession: PiSessionResult | null = null;
  private taskQueue: TaskQueue;
  private workerPool: WorkerPool;
  private mergeQueue: MergeQueue;
  private monitor: Monitor;
  private subplanner: Subplanner | null;
  
  // Conversation state
  private pendingHandoffs: { task: Task; handoff: Handoff }[];
  private allHandoffs: Handoff[];
  private handoffsSinceLastPlan: Handoff[];
  private activeTasks: Set<string>;
  private dispatchedTaskIds: Set<string>;
  private scopeTracker: ScopeTracker;
  
  // Delta tracking (context optimization)
  private previousFileTree: Set<string>;
  private previousFeaturesHash: number;
  private previousDecisionsHash: number;

  constructor(
    config: OrchestratorConfig,
    plannerConfig: PlannerConfig,
    taskQueue: TaskQueue,
    workerPool: WorkerPool,
    mergeQueue: MergeQueue,
    monitor: Monitor,
    systemPrompt: string,
    subplanner?: Subplanner,
    deps?: Partial<PlannerDeps>,
  ) { /* ... */ }

  async runLoop(request: string): Promise<void> { /* ... */ }
  async plan(request: string, repoState: RepoState, newHandoffs: Handoff[]): Promise<Task[]> { /* ... */ }
}

Planning Loop

The root planner operates as a continuous loop that triggers replanning when conditions are met:
async runLoop(request: string): Promise<void> {
  this.running = true;
  let iteration = 0;
  let planningDone = false;

  while (this.running && iteration < this.plannerConfig.maxIterations) {
    try {
      this.collectCompletedHandoffs();

      const hasCapacity = this.dispatchLimiter.getActive() < this.config.maxWorkers;
      const hasEnoughHandoffs = this.handoffsSinceLastPlan.length >= MIN_HANDOFFS_FOR_REPLAN;
      const noActiveWork = this.activeTasks.size === 0 && iteration > 0;
      const needsPlan = hasCapacity && (iteration === 0 || hasEnoughHandoffs || noActiveWork);

      if (needsPlan && !planningDone) {
        const repoState = await this.readRepoState();
        const newHandoffs = [...this.handoffsSinceLastPlan];
        const tasks = await this.plan(request, repoState, newHandoffs);

        iteration++;
        this.handoffsSinceLastPlan = [];

        if (tasks.length === 0 && this.activeTasks.size === 0 && this.taskQueue.getPendingCount() === 0) {
          planningDone = true;
        } else if (tasks.length > 0) {
          this.dispatchTasks(tasks);
          // Trigger iteration callbacks
        }
      }

      await this.deps.sleep(LOOP_SLEEP_MS);
    } catch (error) {
      // Backoff retry logic
    }
  }
}
Replanning triggers:
  • First iteration (iteration === 0): Initial planning
  • Handoff threshold (handoffsSinceLastPlan.length >= 3): Workers completed work
  • No active work (activeTasks.size === 0): All workers idle but project incomplete

Message Building

The planner constructs different messages for initial vs. follow-up planning:

Initial Message

private buildInitialMessage(request: string, repoState: RepoState): string {
  let msg = `## Request\n${request}\n\n`;

  if (repoState.specMd) {
    msg += `## SPEC.md (Product Specification)\n${repoState.specMd}\n\n`;
  }

  if (repoState.featuresJson) {
    msg += `## FEATURES.json\n${repoState.featuresJson}\n\n`;
  }

  if (repoState.agentsMd) {
    msg += `## AGENTS.md (Coding Conventions)\n${repoState.agentsMd}\n\n`;
  }

  if (repoState.decisionsMd) {
    msg += `## DECISIONS.md (Architecture Decisions)\n${repoState.decisionsMd}\n\n`;
  }

  msg += `## Repository File Tree\n${repoState.fileTree.join("\n")}\n\n`;
  msg += `## Recent Commits\n${repoState.recentCommits.join("\n")}\n\n`;

  msg += `This is the initial planning call. SPEC.md and FEATURES.json above are binding — your tasks must conform...`;

  return msg;
}

Follow-up Message (with Delta Optimization)

private buildFollowUpMessage(repoState: RepoState, newHandoffs: Handoff[]): string {
  let msg = `## Updated Repository State\n`;

  // Delta file tree (only changed files)
  const currentTree = new Set(repoState.fileTree);
  const newFiles = repoState.fileTree.filter((f) => !this.previousFileTree.has(f));
  const removedFiles = [...this.previousFileTree].filter((f) => !currentTree.has(f));

  if (newFiles.length === 0 && removedFiles.length === 0) {
    msg += `File tree: unchanged (${currentTree.size} files).\n\n`;
  } else {
    msg += `File tree (${currentTree.size} files):\n`;
    if (newFiles.length > 0) msg += `  New: ${newFiles.join(", ")}\n`;
    if (removedFiles.length > 0) msg += `  Removed: ${removedFiles.join(", ")}\n`;
  }

  // Worker handoffs
  if (newHandoffs.length > 0) {
    msg += `## New Worker Handoffs (${newHandoffs.length} since last plan)\n`;
    for (const h of newHandoffs) {
      msg += `### Task ${h.taskId} — ${h.status}\n`;
      msg += `Summary: ${h.summary.slice(0, MAX_HANDOFF_SUMMARY_CHARS)}\n`;
      msg += `Files changed: ${h.filesChanged.slice(0, MAX_FILES_PER_HANDOFF).join(", ")}\n`;
      if (h.concerns.length > 0) msg += `Concerns: ${h.concerns.join("; ")}\n`;
      if (h.suggestions.length > 0) msg += `Suggestions: ${h.suggestions.join("; ")}\n`;
    }
  }

  // Locked file scopes
  const lockedFiles = this.scopeTracker.getLockedFiles();
  if (lockedFiles.length > 0) {
    msg += `## Currently Locked File Scopes (${lockedFiles.length} files)\n`;
    msg += `These files are being modified by active tasks — avoid assigning new work to them:\n`;
    msg += `${lockedFiles.join(", ")}\n\n`;
  }

  // Build/test health from reconciler
  if (this.lastSweepResult) {
    msg += `## Build/Test Health\n`;
    msg += `Build: ${this.lastSweepResult.buildOk ? "PASS" : "FAIL"}\n`;
    msg += `Tests: ${this.lastSweepResult.testsOk ? "PASS" : "FAIL"}\n`;
  }

  return msg;
}
Delta optimization saves ~40K characters per iteration by only sending changed state.

Task Dispatch

Parsed tasks are dispatched with complexity-based routing:
private dispatchSingleTask(task: Task): void {
  const promise = (async () => {
    await this.dispatchLimiter.acquire();

    // Register scope locks to prevent conflicts
    if (task.scope.length > 0) {
      const overlaps = this.scopeTracker.getOverlaps(task.id, task.scope);
      if (overlaps.length > 0) {
        logger.warn("Scope overlap detected", { taskId: task.id, overlaps });
      }
      this.scopeTracker.register(task.id, task.scope);
    }

    try {
      let handoff: Handoff;

      // Route complex tasks through subplanner
      if (this.subplanner && shouldDecompose(task, DEFAULT_SUBPLANNER_CONFIG, 0)) {
        logger.info("Task scope is complex — routing through subplanner", {
          taskId: task.id,
          scopeSize: task.scope.length,
        });
        handoff = await this.subplanner.decomposeAndExecute(task, 0, dispatchSpan);
      } else {
        // Atomic task → direct to worker
        handoff = await this.workerPool.assignTask(task);
      }

      // Process handoff and enqueue for merge
      if (handoff.status === "complete") {
        this.mergeQueue.enqueue(task.branch, task.priority);
      }

      this.pendingHandoffs.push({ task, handoff });
    } finally {
      this.scopeTracker.release(task.id);
      this.dispatchLimiter.release();
    }
  })();
}
Decomposition criteria (shouldDecompose):
  • Task scope has ≥4 files (default scopeThreshold)
  • Current depth < 3 (max decomposition depth)

Prompt Engineering

Location: prompts/root-planner.md The root planner system prompt is structured in clear sections:

1. Philosophy and Conversation Model

You are the root planner for a distributed coding system. You decompose a project 
into tasks through **iterative discovery** — not upfront enumeration.

You operate in **sprints**. Each time you are called, you plan one sprint — a focused 
batch of work based on what you know *right now*.

**Your planning philosophy: plan only what you can confidently specify. Discover the rest.**
Key constraint: Agents are told they operate as persistent conversations with memory, not stateless batch calls.

2. Scratchpad System

Every response MUST include a `scratchpad` field. This is your working memory — 
**rewrite it completely each time**, never append.

Your scratchpad MUST contain:

1. **Goals & Specs** — Full goal set from SPEC.md/FEATURES.json, coverage status
2. **Current State** — Iteration number, phase, what's built/broken/in-progress
3. **Sprint Reasoning** — Why this batch of tasks, what's deferred and why
4. **Worker Intelligence** — Patterns from handoffs, unresolved concerns
Purpose: The scratchpad survives context compaction and prevents planning drift across iterations.

3. Sprint Planning Strategy

| Phase | Focus | Guidance |
|-------|-------|----------|
| **Discovery** | Understand the codebase, read specs, identify architecture | Emit 3-8 foundational tasks. Establish the base everything else depends on. |
| **Foundation** | Core infrastructure, shared utilities, database, routing | 5-15 tasks. Foundations landing. Emit backbone — only features with confirmed stable dependencies. |
| **Core build-out** | Primary features, main application logic | 10-30 tasks. Foundations proven. Fan out across features you can now fully specify. |
| **Integration & hardening** | Wiring systems, edge cases, bug fixes | 5-20 tasks. Targeted work based on worker reports. Fix what's broken before adding more. |
Critical rule: “These numbers are ceilings, not targets. If only 5 things can be precisely specified, emit 5.”

4. Definition of Done

The prompt enforces staff-engineer-level acceptance criteria:
Every task must include a clear **definition of done** in its `acceptance` field.

**The bar: code indistinguishable from what a staff engineer wrote.**

Acceptance MUST specify:

1. **Verification** — Build/type-check command and expected result. What tests must exist AND pass.
2. **Integration** — What call sites should work. API contracts: request/response shapes, error formats.
3. **Quality bar** — Existing patterns to follow. Edge cases that must be handled.
Example comparison:
BadGood
”Function works correctly. Tests pass.""createUser() rejects duplicate emails with DuplicateEmailError. Tests cover: valid creation, duplicate email, missing required fields, invalid email format. tsc —noEmit exits 0.”

5. Processing Handoffs

- **NEVER re-assign completed work** — Acknowledge what's done
- **ALWAYS act on concerns** — If a worker flagged a risk, factor it into follow-up tasks
- **NEVER retry a failed task wholesale** — Create a targeted follow-up addressing the specific failure
- **ALWAYS incorporate worker feedback** — Workers discover things the plan didn't anticipate

6. Concern Triage

| Classification | Action | Example |
|---------------|--------|------|
| **Blocking** | Create a targeted fix task in this sprint | "Type mismatch breaks callers in 3 files" |
| **Architectural** | Update scratchpad, adjust future task context | "Auth module doesn't handle token refresh" |
| **Informational** | Note in scratchpad, no immediate action | "Found dead code in utils.ts" |

Your scratchpad MUST track unresolved concerns across iterations. A concern raised in 
sprint 3 that isn't addressed by sprint 5 is a planning failure.

7. Finalization Awareness

After you return `{ "scratchpad": "...", "tasks": [] }`, the system does NOT immediately 
shut down. A **finalization phase** runs:

1. All pending merges are drained.
2. A build + test sweep checks if the project compiles and tests pass.
3. If failures are found, fix tasks are dispatched and the sweep repeats.

Before returning `[]`, run these checks:

1. **Feature depth**: Check whether workers handled only the happy path
2. **Cross-feature integration**: Read the code where features interact
3. **TODO/stub audit**: Grep for `TODO`, `FIXME`, `HACK`
4. **Scratchpad concerns**: Every concern must be resolved or marked out-of-scope
5. **Build/test health**: Passing status OR fix tasks already emitted
6. **Spec compliance**: Re-read SPEC.md and verify architectural constraints followed

Output Format

The planner returns JSON with two fields:
{
  "scratchpad": "Rewritten synthesis of current project state, goals, and priorities.",
  "tasks": [
    {
      "id": "task-001",
      "description": "Detailed description with full context.",
      "scope": ["src/file1.ts", "src/file2.ts"],
      "acceptance": "Verifiable criteria.",
      "branch": "worker/task-001-detailed-description",
      "priority": 1
    }
  ]
}
Parsing implementation:
export function parsePlannerResponse(responseText: string): {
  scratchpad: string;
  tasks: RawTaskInput[];
} {
  // Extract JSON from markdown code fences or raw text
  const jsonMatch = responseText.match(/```json\s*([\s\S]*?)```/)
    || responseText.match(/```\s*([\s\S]*?)```/)
    || responseText.match(/^\{[\s\S]*\}$/);

  const parsed = JSON.parse(jsonMatch ? jsonMatch[1] : responseText);
  
  return {
    scratchpad: parsed.scratchpad || "",
    tasks: parsed.tasks || [],
  };
}

Examples from Prompt

Sprint 1 — Discovery & Foundation (5 tasks)

{
  "scratchpad": "Sprint 1 / Discovery phase.\n\nGOALS (18 features from FEATURES.json):\n- [NOT STARTED] Auth: login, signup, token refresh, role-based access\n- [NOT STARTED] Tasks CRUD: create, read, update, delete, list with pagination\n...\n\nTHIS SPRINT: Foundation only — scaffolding, types, DB connection. I can fully specify these because they depend on nothing. I CANNOT yet plan auth middleware (need to see how Express is bootstrapped)...",
  "tasks": [
    {
      "id": "task-001",
      "description": "Initialize project scaffolding. Create package.json with name 'taskflow', type 'module', and devDependencies: typescript@^5.4, vitest@^1.0. Add scripts: 'dev', 'build', 'test', 'typecheck'. Create tsconfig.json with strict mode, ES2022 target...",
      "scope": ["package.json", "tsconfig.json", "src/index.ts"],
      "acceptance": "npm install exits 0 with no peer dependency errors. npm run build exits 0. npm run typecheck exits 0. tsconfig.json has strict: true...",
      "branch": "worker/task-001-init-project-scaffolding",
      "priority": 1
    }
  ]
}
Notice:
  • Scratchpad explicitly lists all 18 features and marks them [NOT STARTED]
  • Only emits 5 foundational tasks that have zero dependencies
  • Explicitly states what CANNOT yet be planned and why

Sprint 3 — Informed Core Build-out (12 tasks)

{
  "scratchpad": "Sprint 3 / Core build-out.\n\nGOALS:\n- [DONE] Scaffolding, types, DB connection (sprints 1-2)\n- [IN PROGRESS] Tasks CRUD — planning routes this sprint\n...\n\nDISCOVERIES: Workers established a Zod-validation-then-repository pattern I didn't anticipate. All route tasks in this sprint must follow it. Worker from task-008 flagged that error responses are inconsistent — adding a standardization task.",
  "tasks": [/* 12 tasks */]
}
Notice:
  • Scratchpad updated with [DONE] and [IN PROGRESS] status
  • References actual patterns workers established (“Zod-validation-then-repository”)
  • Creates task based on worker concern (“error responses inconsistent”)

Anti-Patterns (from Prompt)

The prompt explicitly warns against:
  1. Upfront enumeration — “Trying to plan all 50 tasks in sprint 1. You don’t have enough information yet.”
  2. Mega-tasks — “‘Build the authentication system’ is not a task. ‘Implement JWT token generation in src/auth/token.ts’ is.”
  3. Premature fan-out — “Emitting 40 feature tasks when the foundation hasn’t landed yet.”
  4. Stale scratchpad — “Copy-pasting your previous scratchpad without updating it. Rewrite from scratch each time.”
  5. Ignoring the spec — “Generating tasks based on general knowledge instead of the project’s SPEC.md.”
  6. Lost goals — “Failing to track features from FEATURES.json across sprints.”

Configuration

export interface PlannerConfig {
  maxIterations: number;  // Default: determined by orchestrator
}

const DEFAULT_CONFIG: PlannerConfig = {
  maxIterations: 50,  // Typical for medium projects
};
Key constants:
const LOOP_SLEEP_MS = 500;  // Main loop tick rate
const MIN_HANDOFFS_FOR_REPLAN = 3;  // Trigger replanning after N handoffs
const MAX_CONSECUTIVE_ERRORS = 10;  // Abort after consecutive planning failures
const MAX_TASK_RETRIES = 1;  // Auto-retry failed tasks once

Tracing and Monitoring

The planner emits structured trace spans:
// Root span for entire planning session
this.rootSpan = this.tracer.startSpan("planner.runLoop", { agentId: "planner" });

// Per-iteration span
const iterationSpan = this.rootSpan.child("planner.iteration", { agentId: "planner" });
iterationSpan.setAttributes({
  isFirstPlan,
  newHandoffs: newHandoffs.length,
  tasksCreated: tasks.length,
});

// Per-task dispatch span
const dispatchSpan = this.rootSpan.child("planner.dispatchTask", {
  taskId: task.id,
  agentId: "planner",
});
Traces are written to ~/.longshot/traces/ for detailed execution analysis.

Best Practices

1. Use Read-Only Tools Extensively

// Before planning auth routes, check what exists:
await session.prompt(`
  Use the read tool to examine src/middleware/ and identify the existing 
  authentication pattern before specifying new auth tasks.
`);

2. Track All Goals in Scratchpad

Every SPEC.md feature and FEATURES.json entry must be tracked:
{
  "scratchpad": "GOALS (from FEATURES.json):\n- [DONE] User auth (login, signup)\n- [IN PROGRESS] Task CRUD (create done, read/update/delete pending)\n- [NOT STARTED] Notifications\n- [BLOCKED] Admin panel (needs RBAC from auth)"
}

3. Process Every Concern

Worker concerns must be triaged and tracked:
Concern from task-042: "Auth middleware doesn't validate token expiry."

Triage: BLOCKING → Emit fix task this sprint
Action: task-055 "Add token expiry validation in src/middleware/auth.ts"

4. Defer Confidently

Explicitly state what you’re deferring and why:
{
  "scratchpad": "DEFERRED: Email notification tasks. Reason: Need to see the task model schema from task-012 before specifying notification triggers. Will plan in sprint 4 after task-012 handoff."
}

Next Steps

Build docs developers (and LLMs) love