F2 slice 1: coordinator creates lineage-visible worktrees (#13) - #18
Conversation
…worktrees (#13) Design-first proposal for the bridge keystone (#13, part of #5). Resolves the task→worktree mapping decision (recommend worktree-per-track, of which per-run and per-task are degenerate cases), specifies the additive `createWorktree` on CoordinatorRuntime/OrcaRuntimeService as a thin adapter over the existing `createManagedWorktree`, and carves the smallest opt-in first slice that makes a coordinator worker visible in Mission Control with no MC change. No production code changed. Refs #13 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…2 slice 1, #13) Bridge keystone, slice 1 — the single-task, opt-in, default-off proof that a coordinator run's worker shows up in Mission Control. - Add optional `createWorktree` to the CoordinatorRuntime interface and implement it on OrcaRuntimeService as a thin adapter over the existing `createManagedWorktree`, stamping lineage parent = the director worktree via orchestrationContext. No git logic forked (base-ref/SSH/relay parity and lineage recording inherited); no DB schema change. - Add opt-in `worktreeBacked` coordinator option (default OFF). When off, the legacy bare-terminal dispatch path is byte-for-byte unchanged. When on, `dispatchReadyTasks` creates one child worktree per task (trackKey = task id), launches the worker agent in it via the startup option, and sends the existing preamble unchanged; the drift pre-flight + dispatch target move to the new track worktree. Lineage parent = director so selectSpawnedWorktreeIds finds the worker with no Mission Control change. - Wire `--worktree-backed` / `--worker-agent` through orchestration.run (additive, default off). Deferred to slice 2: track: spec hint, same-track worktree reuse (implement→review), per-track serialization, multi-track concurrency, resume. Refs #13 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#13) - Coordinator (main) test: worktreeBacked ON creates a child worktree whose lineage parent is the director, dispatches the preamble into it, and drift-probes the new track worktree (not the director). Plus a default-off test proving the legacy bare-terminal path is unchanged, and a guard test that no parentless worktrees are created when the director worktree is unset. - Mission Control (renderer) test: selectSpawnedWorktreeIds discovers a coordinator-created worktree-backed worker given the exact lineage shape the adapter emits — the consumer half of the bridge, kept in the renderer project to respect the main↔renderer boundary. - Unit test for the deterministic worktreeNameForTask slug. Refs #13 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review — F2 slice 1 (worktree-backed dispatch)Reviewed only the slice-1 delta ( Attack #1 — default-off "byte-for-byte unchanged" claim: HOLDS. ✅Verified against 🔴 BLOCKER — startup-agent → preamble cold-boot race (attack #4)
🟠 SHOULD-FIX — no circuit-breaker + unbounded retry + worktree leak on creation failure (attack #2)When
🟠 SHOULD-FIX — adapter↔createManagedWorktree integration is entirely untested (attack #3)The real adapter (
🟡 NIT —
|
Round 2 on PR #18, addressing the adversarial review. BLOCKER — preamble cold-boot race: the worktree-backed path sent the preamble immediately after launching the agent, so it was fired into a still-booting TUI and dropped. Now gate the send on agent readiness by reusing the runtime's existing signal — waitForTerminal(handle, {condition:'tui-idle', timeoutMs}) — before sendTerminal, only when an agent was launched (legacy path unchanged). A readiness timeout is routed through the circuit breaker like a send failure. SHOULD-FIX — no breaker + infinite retry + orphan worktrees on create failure: worktree/terminal provisioning failures now burn the same F1 circuit breaker (via a per-task sentinel dispatch context) so an unprovisionable task gives up after 3 strikes instead of retrying every tick forever; and a worktree that fails a post-create step (no usable terminal, readiness timeout, send failure) is torn down via a new optional removeWorktree capability so failures don't accumulate orphans. SHOULD-FIX — double-terminal in the no-agent path: the adapter now reuses the plain initial terminal createManagedWorktree already opened (found via listTerminals) instead of the coordinator creating a second one; a missing handle is surfaced so the coordinator tears the worktree down. NITs: validate --worker-agent against the TuiAgent set at the RPC boundary (reject fast); log the worktreeBacked→legacy downgrade once. dispatchTask now returns whether it dispatched (drift-skip → false) so the worktree-backed caller can tear down a created-but-undispatched worktree. Refs #13 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Readiness (BLOCKER): the happy path now asserts the tui-idle wait happens BEFORE the send; a new test proves that when the agent never becomes ready the preamble is NEVER sent and the task is breaker-failed (would fail without the readiness gate). - Failure path / breaker: createWorktree-throws and no-usable-terminal cases give up after the breaker's strikes (task → failed, no infinite retry) and tear down any created worktree (no orphan leak). - No-agent path: dispatches without a second terminal and without a readiness wait (plain shell). - Downgrade nit: logs exactly once when worktreeBacked is set but the runtime lacks createWorktree. The mock is now faithful to the real adapter: it can throw, can return no handle, and returns a (possibly different) resolved branch rather than the raw name — so it can't hide the paths above. Refs #13 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round 2 — review blocker + should-fixes closedThanks for the adversarial pass. All three substantive findings + both nits are addressed; the default-off path the review confirmed clean is untouched. Pushed in 🔴 BLOCKER — preamble cold-boot race → fixed
🟠 No breaker + infinite retry + orphan leak → fixed
🟠 Adapter mock divergence / untested failure path → fixedThe mock is now faithful to the real adapter: it can throw, can return no 🟡 Nits
Verification
Still slice 1 only — track reuse/serialization/multi-track/resume remain deferred to slice 2. Not merged. |
Round-2 re-reviewRe-reviewed only the round-2 delta ( Round-1 BLOCKER (preamble cold-boot race): CLOSED. ✅
Mechanism 1 — sentinel dispatch context: SOUND. ✅
Mechanism 2 —
|
Round 3 on PR #18, addressing the round-2 re-review. Mechanism 1 (sentinel breaker) and the default-off path are untouched. SHOULD-FIX #1 — drift-skip teardown churn loop: in worktree-backed mode the drift pre-flight ran on the freshly-created child inside dispatchTask, so a child branched from a stale local base reported behind>threshold on every tick → create→skip→teardown forever (unbounded git worktree add/remove). Now resolve drift on the DIRECTOR worktree BEFORE createWorktree (extracted into resolveDispatchDrift) and thread the result into dispatchTask so the fresh child is never re-probed. A stale base now skips without creating (or removing) a worktree — recoverable, exactly like the legacy path, zero churn. The legacy callsite keeps computing drift internally → byte-for-byte unchanged. SHOULD-FIX #2 — positional terminal pick could grab the Setup terminal: the no-agent adapter path re-discovered a terminal via listTerminals(...)[0], which on a repo with a setup script could be the "Setup" runner rather than the interactive shell. createManagedWorktree now surfaces the plain initial terminal's handle on its result (initialTerminal); the adapter uses startupTerminal?.handle ?? initialTerminal?.handle and no longer re-discovers positionally. Refs #13 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drift-skip churn (fix #1): a worktree-backed run on a stale base creates and removes ZERO worktrees (skip happens before createWorktree), sends nothing, probes drift on the director (not a child), and the task stays ready — fails without the pre-create probe (old path would create→teardown every tick). Updated the happy-path probe assertion accordingly (director, not child). - Adapter terminal selection (fix #2): real OrcaRuntimeService.createWorktree tests (createManagedWorktree stubbed) assert it returns the known initial interactive terminal (not a positionally-discovered "Setup" terminal) and never calls listTerminals, returns the startup-agent terminal when launched, and returns no handle when none was opened. Updated the setup-policy test to expect the new initialTerminal field (spawn #1 = interactive, #2 = Setup). Refs #13 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round 3 — both re-review should-fixes closedThanks for the re-review. Mechanism 1 (sentinel breaker) and the default-off path are untouched, as asked. Both should-fixes closed with real-adapter coverage. Pushed in 🟠 #1 — drift-skip teardown churn loop → fixed (probe before create)Took the clean option: the drift pre-flight is extracted into
🟠 #2 — positional terminal pick could grab the Setup terminal → fixed (return the known handle)Took the clean option:
Untouched
Verification
Still slice 1 only. Not merged. |
F2 slice 1 — the bridge proof (#13)
Implements slice 1 of the green-lit
F2-DESIGN.md§7: the single-task, opt-in, default-off proof that a coordinator run's worker shows up in Mission Control. Part of #5, builds on F1 (#12).Problem
The coordinator dispatched tasks to bare terminals in one shared worktree and never created worktrees, while Mission Control discovers workers only by lineage (
selectSpawnedWorktreeIds:parentWorktreeId === directorWorktreeId). So coordinator-driven work was invisible — "No worktrees yet" forever.What this does
createWorktreeonCoordinatorRuntime(optional method) implemented onOrcaRuntimeServiceas a thin adapter over the existingcreateManagedWorktree, stamping lineage parent = the director worktree viaorchestrationContext.parentWorktreeId. No git logic forked (base-ref handling, SSH/relay parity, lineage recording all inherited). No DB schema change.worktreeBackedcoordinator option, defaultfalse. When off, the legacy bare-terminal dispatch path is byte-for-byte unchanged (verified by test). When on,dispatchReadyTaskscreates one child worktree per task (trackKey = task id), launches the worker agent in it via the startup option, and sends the existing preamble unchanged; the drift pre-flight + dispatch target move to the new track worktree.selectSpawnedWorktreeIdsfinds the worker with no Mission Control change.--worktree-backed/--worker-agentthroughorchestration.run(additive, default off).Upstream-friendliness (stablyai#6201)
Additive by construction: optional interface method, opt-in default-off flag, no schema change, no forked git logic. The one behavior change (worker-in-worktree vs bare-terminal) is gated behind the default-off flag, so existing coordinator semantics are preserved unless a caller opts in.
Deferred to slice 2 (explicitly NOT built here)
track:spec hint parsing, same-track worktree reuse (the implement→review handoff), per-track serialization lock, multi-track concurrency undermaxConcurrent, base-ref-from-predecessor-tip, and resume/adopt-existing-worktree (F3).Test evidence
worktreeBackedON: asserts the created worker worktree'slineage.parentWorktreeId === directorWorktreeId, the preamble is dispatched into the child worktree's terminal, and the drift probe targets the new track worktree (not the director). Plus a default-off test proving the legacy bare-terminal path is unchanged, and a guard test that no parentless worktrees are created when the director worktree is unset.selectSpawnedWorktreeIdsdiscovers a coordinator-created worktree-backed worker given the exact lineage shape the adapter emits (consumer half of the bridge; kept in the renderer project to respect the main↔renderer boundary).worktreeNameForTaskslug.All green locally:
vitestorchestration + MC suites: 247 passedelectron-vite build: ✓ builtoxlint: clean🤖 Generated with Claude Code