F2 slice 2: worktree-per-track reuse, serialization, multi-track concurrency (#13) - #20
Conversation
…rack concurrency (F2 slice 2, #13) Make the track abstraction real on top of slice 1's worktree-backed bridge. - Parse a `track: <key>` spec hint (mirrors parseAllowStaleBaseFromSpec) and strip it from the worker preamble; default when unset = the task's own id, so the slice-1 per-task behavior is preserved. - Same-track worktree reuse via an in-memory track map: the first dispatch of a track lazily creates the child worktree (existing slice-1 path); later same-track tasks reuse it (review continues implement's branch → one PR). On a reuse miss-of-terminal, the cached worker-agent terminal is reused when idle, else a fresh terminal is opened in the same checkout. - Per-track serialization: one active dispatch per track. A ready task on a busy track waits (stays ready, retried next tick), so two agents never edit one checkout. Tracks claimed earlier in a tick count too. - Multi-track concurrency under maxConcurrent: effective parallelism = min(maxConcurrent, #distinct ready tracks with no in-flight dispatch). Honors the slice-1 hard constraints: default-off byte-for-byte (all new logic under worktreeBacked; legacy specs without a track line are unchanged), tui-idle readiness gate before the preamble (including reused agent terminals), and breaker accounting on every provisioning/dispatch failure so retries converge. Reuse failures do NOT tear down the shared worktree (it holds the predecessor's work). Cross-track base-ref-from-predecessor and F3 resume map seeding are out of scope (TODOs reference F3 #14). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h path (#13) Drive a real Coordinator against the real OrcaRuntimeService.createWorktree adapter (only createManagedWorktree / terminal I/O stubbed) so the reuse path is exercised through the actual adapter, not the coordinator's fully-mocked createWorktree. Asserts two same-track tasks produce ONE adapter worktree, the second is serialized behind the first, and its preamble lands in the first's interactive terminal with no recreate and no extra terminal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review panel verdict — not merge-ready (2 must-fix)4-lens adversarial panel + synthesis, findings then verified against source by the coordinator. The two serious ones hit the canonical 🔴 Must-fix
🟠 Should-fix
Clean / over-flagged (dropped)Default-off path confirmed unchanged; F1 isolation respected; the "serialize-wait doesn't burn the breaker" and "default=task-id preserved" items were positive confirmations, not findings. |
…lice 2 round 2, #13) `created_at` is second-granularity, so two tasks seeded in the same second sorted non-deterministically — a same-track `review` could dispatch before `implement` into an empty worktree (per-track serialization gives mutual exclusion, not ordering). Add an `id` tie-break (`ORDER BY created_at, id`) to listTasks and listTasksWithDispatch for a stable total order. Cross-task ordering (review runs after implement FINISHES) is enforced separately via deps:[predecessor]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hell; fence-aware track hint (F2 slice 2 round 2, #13) Two round-2 fixes on the track-reuse path: - must-fix #1: when a same-track successor reuses a worktree but the cached agent terminal is gone (implement's agent exits after its grace window), the reuse path previously opened a bare shell and fired the agent preamble into it — no worker_done ever arrives, so per-track serialization keeps the track busy on a dead dispatch forever. Now resolveTrackTerminal RELAUNCHES the worker agent in the SAME worktree (createTerminal launchAgent — agent terminal in the existing checkout, predecessor's commits preserved), deciding agent-vs-shell from the run config so a prior fallback can't permanently downgrade. If the terminal can't be obtained it returns null → the caller breaker-accounts it and leaves the shared worktree intact (never torn down, never hangs). Adds launchAgent to the CoordinatorRuntime.createTerminal interface (optional/additive; the real adapter already supports it). - should-fix #3: parseTrackFromSpec now scans line-by-line and skips fenced code blocks, so a `track:` line inside a worker-instruction example is not parsed as the real key (which would mis-route the track and strip an example line). Tests: reuse tui-idle gate, agent relaunch (launchAgent asserted), breaker-fail convergence with the shared worktree preserved, two no-track tasks → distinct worktrees, same-track deps:[predecessor] implement-first, and fenced-hint parsing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… CLI worktree-backed flags (F2 slice 2 round 2, #13) - SKILL.md: document worktree-backed runs and the track model — the `track:` hint (own line, ignored in fenced code), distinct tracks vs same-track sharing, and (must-fix #2) that same-track successors must declare deps:[predecessor] so review runs after implement FINISHES (serialization gives mutual exclusion, not ordering). - CLI: forward --worktree-backed and --worker-agent on `orchestration run` (the RPC already validated them but the run handler never passed them, so the documented workflow was unreachable from the CLI). Default-off preserved. - Real-adapter test: the reuse RELAUNCH path drives the real Coordinator against the real runtime's createTerminal({ launchAgent }) when the cached agent terminal is gone — asserts an agent (not a shell) is relaunched into the reused worktree. Plus a CLI test for the new flag forwarding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round 2 — all panel findings closedPushed 4 commits ( 🔴 Must-fix #1 — reuse no longer downgrades an agent dispatch to a bare shell
🔴 Must-fix #2 — within-track order is now deterministic
🟠 Should-fix #3 —
|
Round-2 re-review — must-fix #1 NOT closed (blocker), verified against sourceA scoped re-review panel + the coordinator's own read of the adapter agree: 🔴 BLOCKER — the reuse-relaunch still spawns a bare shell
🔴 BLOCKER — the "real-adapter" reuse test masks the defect
🟠 Should-fix
No merge. Round 3 dispatched: relaunch via |
…l); refuse unordered same-track DAGs (F2 slice 2 round 3, #13) Round-2's reuse-relaunch was still a bare shell: createTerminal({ launchAgent }) only spawns its `command` (unset → a plain shell) and treats launchAgent as a metadata tag — it does NOT run an agent (orca-runtime.ts:14612/14644). The agent preamble went into a shell → no worker_done → the track hung. Must-fix #1 was not actually closed; its test (asserting only launchAgent==='claude') masked it. - must-fix #1: resolveTrackTerminal now relaunches via runtime.launchAgentTerminal — the real agent spawn (builds a launch command via buildStartupForAgent) — in the EXISTING worktree (predecessor's commits preserved). Added launchAgentTerminal to CoordinatorRuntime (optional/additive); removed the obsolete createTerminal launchAgent option and the false "createTerminal already supports launchAgent" comment. A runtime without launchAgentTerminal, or a failed relaunch, returns null → caller breaker-accounts it, shared worktree intact. Never dispatches an agent preamble into a non-agent terminal. - should-fix #3: same-track tasks share one worktree and must be totally ordered, else two can be ready at once and review races implement into an empty branch (id is random, so the sort can't guarantee implement-first). decompose() now refuses (when worktreeBacked) a DAG where a track has tasks not ordered by deps — safe by default, not reliant on the operator remembering deps. Legacy path unaffected. - nits: corrected the ORDER BY comment (stabilizes read order, does NOT by itself guarantee implement-first); documented the fence parser's safe degradation on an unclosed fence (→ task-id default). Tests rewritten to FAIL against the bare-shell code: reuse relaunch now asserts launchAgentTerminal is the invoked spawn (not createTerminal); same-track tests declare deps:[predecessor]; added the refusal guard test and a legacy-path no-guard test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… slice 2 round 3, #13) should-fix #4: --worktree-backed without an agent is a worktree-backed bare-shell mode — workers run in a plain shell that can never emit worker_done, so every track hangs. The run RPC now refuses --worktree-backed unless --worker-agent is set, with an actionable error. Enforced at the RPC boundary (authoritative for the CLI and any other caller). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gent (F2 slice 2 round 3, #13) - launchAgentTerminal builds a real agent launch command (asserts a non-empty command reaches the spawn primitive) — proves the relaunch is a real agent, not a tagged shell. - the reuse path drives the real Coordinator and asserts it invokes launchAgentTerminal (the real spawn), never the bare-shell createTerminal. - existing real-adapter reuse test updated to declare deps:[predecessor] (the round-3 safe-by-default ordering). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round 3 — the reuse-relaunch is now a REAL agent (confirmed against the adapter)You were right: round-2's relaunch was still a bare shell. Confirmed against source — 🔴 Must-fix #1 — relaunch a real agent
🔴 Must-fix #2 — the test now fails against the bare-shell codeThe reuse-relaunch tests now assert the coordinator invokes 🟠 Should-fix #3 — same-track ordering safe by default
🟠 Should-fix #4 — couple the flags
Nits
Verification — greentypecheck (node/cli/web) · electron-vite build · oxlint · full vitest 20391 passed (the lone failure is |
Part of #5 / #13. Builds on F2 slice 1 (lineage-visible worktrees) which shipped the single-task, opt-in, default-off
worktreeBackedbridge. Slice 2 makes the track abstraction (design §3.3) real.What this does
All under the existing default-off
worktreeBackedpath:track:spec hint — a task may declaretrack: <key>in its spec text (same low-friction channel as F1'sallow-stale-base). Parsed byparseTrackFromSpecand stripped from the worker's--- TASK ---block. Default when unset = the task's own id → per-task (slice-1) behavior preserved.Map<trackKey, …>. The first dispatch of a track lazily creates the child worktree (slice-1 path); later same-track tasks reuse it (dispatch a terminal into the existing checkout). This is theimplement → reviewhandoff: areviewtask declaringtrack: <implement-key>runs in implement's worktree and sees its commits → one branch → one PR.ready, re-evaluated next tick — same shape as the legacy "no idle terminal" wait), so two agents never edit one checkout. Tracks claimed earlier in a tick count too.maxConcurrent— distinct tracks run concurrently. Effective parallelism =min(maxConcurrent, #distinct ready tracks with no in-flight dispatch).Hard constraints honored (these bit us in slice 1)
worktreeBacked; a legacy spec with notrack:line strips to itself, so the bare-terminal path is unchanged. Covered by tests.waitForTerminal(tui-idle)gate is applied to reused agent terminals too (same race).Coordinatoragainst the realOrcaRuntimeService.createWorktreeadapter (not the coordinator mock) to prove the reuse path.Tests (fail without the fix)
maxConcurrent(plus amaxConcurrent=1bound check).track:hint parsed + stripped from the preamble (worktree-backed and legacy paths).parseTrackFromSpecunit tests.Deferred (out of scope)
Verification
typecheck(node/cli/web): green.electron-vitebuild: green.oxlint: clean.Do not merge — review first.