feat(orchestration): token-free recipe director — launchRecipeDirector + implement_then_review (#9) - #23
Conversation
Add the built-in Recipe/RecipeTask types and the one canonical recipe this PR ships, `implement_then_review` — implement + review on the SAME track (one worktree → one PR) with review.dependsOn=[implement] so review runs after implement finishes (a real handoff that also satisfies the coordinator's same-track safe-ordering guard). `compileRecipe` lowers a recipe into dependency-ordered taskCreate inputs: it topo-sorts by deps, prepends the coordinator's `track:` spec hint, and fails fast on unknown/self/cyclic deps. Pure + deterministic so the launch path can walk it, calling taskCreate per task and resolving dependsOn keys to created ids as it goes. No director LLM tokens involved. Tests assert the implement_then_review compilation (2 tasks, same track, review after implement) against the coordinator's real parseTrackFromSpec. Deferred: full recipe set (#10), DirectorBackend abstraction (#8), picker UI (#11). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ors (#9) orchestration.taskCreate stamps a task's target_key from the creating terminal's worktree, but the renderer recipe director has the director worktree id, not a live terminal handle. Add an optional `targetWorktree` selector that stamps target_key through the SAME resolveOrchestrationTargetKey that orchestration.run uses for `worktree` — so a pre-created task's key matches the run's target and run-start adoptUnownedTasks claims it. Precedence: an explicit targetWorktree wins over callerTerminalHandle; it fails closed (refuses the task) when the selector doesn't resolve, mirroring run's guard. Plumbed through the shared preload binding type; preload/web forward params verbatim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#9) Extract the hidden director worktree-shell creation shared by both director kinds into createDirectorWorktreeShell (no agent, no prompt → token-free by construction); launchOrchestratorForProject now layers the coordinator agent + /orcastrate on top of it. Add launchRecipeDirector(project, recipe): create the no-LLM shell, activate it with NO startup payload (blank terminal — the token-free invariant), then compile the recipe and create one task per recipe task (track hint in the spec, deps wired key→created id, targetWorktree stamping each to the shell so run-start adoption claims them), then start a worktree-backed coordinator run anchored on the shell with the user's default coding agent as the worker. The run's `from` is bound to the shell's terminal handle (best-effort) so the live Control Panel keys the DAG on the director pane; on a miss the run still starts. The only LLM cost is the worker agent(s) doing implement/review. Tests: recipe→taskCreate compilation (ordered, same-track, review deps implement), token-free invariant (activation gets no agent startup), the launch issues the right taskCreate + worktree-backed run calls, blank-agent fallback to claude, and graceful shell-create/handle-resolve failure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ee guard (#9) Pre-merge hardenings from the PR #23 review panel (no blockers; both load-bearing properties confirmed). Tighten the seams that have clash history. End-to-end target_key adoption test (the F1 clash seam): prove the task path (taskCreate --target-worktree) and the run path (run --worktree) resolve the SAME key through the REAL resolveOrchestrationTargetKey + REAL adoptUnownedTasks — a real in-memory OrchestrationDb, only the filesystem worktree lookup stubbed (id:X→worktree X). A task stamped to id:W is adopted by a run on id:W; a negative control (task on id:OTHER) is NOT adopted, so the test fails if the two paths ever diverge. Token-free invariant guard: comment at createDirectorWorktreeShell's createWorktree call documenting that it MUST NOT pass createdWithAgent (else activation's buildCreatedAgentReopenStartup fallback relaunches an LLM in the director pane), plus a test exercising the REAL shell helper that asserts the createdWithAgent arg stays undefined and setup is 'skip'. Nits: drop the dead per-task agent plumbing from Recipe/CompiledRecipeTask (re-add when #10/#11 need heterogeneous agents); throw on an unmapped dependsOn key in the launch loop instead of silently dropping it; document that the #11 picker MUST gate launchRecipeDirector on experimentalOrchestrators; document the shell-handle anchoring as opportunistic (no readiness await, fail-safe to a derived coordinator handle). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hardening round — panel pre-merge items closedPushed MUST 🟠 — e2e MUST 🟡 — token-free invariant guarded. Comment at Nits
Green: typecheck (3 projects), oxlint, electron-vite build, and vitest (new e2e + token-free tests; full orchestration + renderer-lib sweep 2152). Not merging. |
#9 — RecipeDirectorBackend: the token-free recipe director
A director that runs a fixed recipe with ZERO director LLM tokens. The "director" is deterministic dispatch — the worker agents do all the coding. Built entirely on the merged bridge runtime (F1–F4, O1/O2); no worktree/dispatch logic is forked.
Flow —
launchRecipeDirector(project, recipe)ORCASTRATOR ·,'skip'setup) is extracted fromlaunchOrchestratorForProjectinto a sharedcreateDirectorWorktreeShell. The recipe director creates that same shell but activates it with no startup payload — a blank terminal, no agent, no/orcastrate.compileRecipetopo-sorts by deps, prepends the coordinator'strack:spec hint, and fails fast on unknown/self/cyclic deps. The launch path then callswindow.api.orchestration.taskCreateper task, resolving eachdependsOnkey to the previously-created task id and stamping every task to the shell worktree (see plumbing below).window.api.orchestration.run({ worktree: id:<shell>, worktreeBacked: true, workerAgent: <default coding agent> }). The merged coordinator creates per-track child worktrees (lineage parent = the director shell) and launches the real worker agents. The live Control Panel renders the DAG automatically.The director shell runs no agent → no director LLM tokens. The only LLM cost is the worker agent(s) doing implement/review.
The one recipe this PR ships —
implement_then_reviewTwo tasks,
implementandreview, on the same track → one worktree, one branch, one PR.review.dependsOn = [implement]so review runs after implement finishes — a real handoff that also satisfies the coordinator's same-track safe-ordering guard (it refuses a worktree-backed run whose same-track tasks aren't ordered by deps).Plumbing —
taskCreatetargetWorktreeFor run-start
adoptUnownedTasksto claim a pre-created task, the task'starget_keymust equal the run's. The coordinator stampstarget_keyfrom the creating terminal's worktree, but the renderer recipe director has a worktree id, not a live terminal handle. Soorchestration.taskCreategains an optionaltargetWorktreeselector that stampstarget_keythrough the sameresolveOrchestrationTargetKeythatrunuses forworktree. It takes precedence overcallerTerminalHandleand fails closed when the selector doesn't resolve. The run'sfromis bound to the shell's terminal handle (best-effort, viaterminal.resolveActive) so the Control Panel keys the DAG on the director pane; on a miss the run still starts.Tests
implement_then_review→ 2 tasks, same track, review depends on implement; topo ordering; rejects unknown/cyclic/self/duplicate deps./orcastrate).taskCreate(deps wired,targetWorktreestamped) + a worktree-backedrun({ worktreeBacked, workerAgent }); tasks created before the run starts; blank-agent → claude fallback; graceful shell-create / handle-resolve failure.taskCreate --target-worktreestampstarget_keyvia the shared resolver and fails closed.Full vitest + typecheck + electron-vite/web build + oxlint green.
Gating & scope
Behind
experimentalOrchestrators. Per-run isolation (F1) intact; reuses the merged worktree-backed coordinator + F4 run binding.Deferred (not this PR): the full starter-recipe set (#10), the
DirectorBackendabstraction (#8), the director-type picker UI (#11), hybrid mode. Wiring the recipe name into the run-DAGrecipefield (currentlynull) needs a coordinator-run column and is left to the abstraction work in #8/#10.Do not merge.
🤖 Generated with Claude Code