Context
Incident #602: stale repoRoot residue was swept into a merged PR because for N=1 workstreams runBranch sets worktrees = {default: ctx.repoRoot} (work-driver.ts:1274-1277) — development happens directly at repoRoot. The ops branch dispatch also mutates the repoRoot checkout (git fetch/checkout <mainline>/pull --ff-only, inlineBranchPrompt work-driver.ts:4459-4461), and mechanizedCommitPr does git checkout <branch> at repoRoot (:1866) with an in-place stagePorcelainPaths case (:1886). These are the hard serial-only assumptions blocking parallel groups, and the residue cause #283 only detects.
Target architecture: ALL development happens in worktrees (even N=1); repoRoot is the integration point only — worktree patches are applied onto the group's feature branch at repoRoot, committed, pushed, PR'd (mirrors the old PM-driven flow).
Latent bug fixed structurally: inlineLensFixPrompt says "Do NOT commit" (:4696) and nothing pushes after lens-fix — in worktree mode lens-fix edits would never reach the PR.
This work must ship as its own separate PR, independent of any other open issue. Note: #277 touches verifyStepOutcome (:2385-2516); land #277 first and rebase over it.
What to build
A. Mechanized branch setup (runBranch, work-driver.ts:1261-1296, behind mechanizeOpsEnabled() :1769-1771): replace the ops branch dispatch with: git fetch origin <mainline>; baseSha = git rev-parse origin/<mainline> (replacing repoRoot-HEAD read at :1287-1290); branch name = deterministic slug from cached issue title (feature/issue-N-<slug>); create one worktree per workstream via worktreeCreate (worktree.ts:19-27, extended to accept absolute repo path + --detach fromRef, detached at baseSha — no scratch branches). Delete the {default: ctx.repoRoot} N=1 case (:1274-1277): N=1 gets .worktrees/issue-N/default. No git command mutates repoRoot in this step.
B. Integration helper — extract integrate(ctx, patches, opts): preflight git status --porcelain clean at repoRoot (#283's gate relocated) → git checkout -B <branch> <baseSha> (replacing plain checkout at :1866) → per-workstream git -C <worktree> diff --cached/diff HEAD → git apply --index at repoRoot (:1891-1901; delete the in-place repoRoot case :1886 — N=1 uses the sibling-patch path too) → commit → push → optional gh pr create. mechanizedCommitPr (:1843-1990) uses it fully.
C. Lens-fix re-integrate: on adversarial-approved from lens-fix (route table :466), before lens-review, apply the worktree's new diff as a follow-up commit + push via integrate. Update inlinePlanPrompt/inlineLensFixPrompt wording accordingly.
D. Read paths off repoRoot HEAD: fetchAllMergedDiffs (:3047-3070) diffs origin/<base>..origin/<branch>; captureWorktreeSnapshot (:3646-3685) snapshots the group's worktrees + git ls-remote origin <branch>. Delete ?? repoRoot cwd fallbacks at :1386, :1611, :2657.
E. Cleanup: runMerged best-effort worktreeRemove(…, force) + git worktree prune.
Schema (additive): pipelineState.integration?: { integratedAt?: number; reintegrations?: number }.
Knobs: rides PI_ENSEMBLE_MECHANIZE_OPS=0; new PI_ENSEMBLE_ALWAYS_WORKTREE=0 restores old N=1 behavior.
Failure routing: worktree-create failure → dispatch-failed on branch → handoff; git apply conflict → cap-hit → handoff with patch path in scratchDir named in the comment.
Acceptance criteria
Out of scope
Parallel group execution (separate issue); spawn caps/widget multiplexing (separate issue); plan decomposition (separate issue); worktree dep-bootstrap optimization (node_modules hardlinking — follow-up issue); grouping rules R1-R5.
Test plan
Offline: smoke-test fixtures per acceptance criteria via verifyExecFn/dispatchFn injection; N=1 and N=3 workstream fixtures; apply-conflict fixture → handoff. Live on nessie: single-issue /work cycle end-to-end; verify PR contains only worktree-authored changes with a deliberately dirtied repoRoot file left untouched; verify lens-fix commit lands on the PR; verify worktree cleanup after merge.
Context
Incident #602: stale repoRoot residue was swept into a merged PR because for N=1 workstreams
runBranchsetsworktrees = {default: ctx.repoRoot}(work-driver.ts:1274-1277) — development happens directly at repoRoot. The ops branch dispatch also mutates the repoRoot checkout (git fetch/checkout <mainline>/pull --ff-only, inlineBranchPrompt work-driver.ts:4459-4461), andmechanizedCommitPrdoesgit checkout <branch>at repoRoot (:1866) with an in-placestagePorcelainPathscase (:1886). These are the hard serial-only assumptions blocking parallel groups, and the residue cause #283 only detects.Target architecture: ALL development happens in worktrees (even N=1); repoRoot is the integration point only — worktree patches are applied onto the group's feature branch at repoRoot, committed, pushed, PR'd (mirrors the old PM-driven flow).
Latent bug fixed structurally:
inlineLensFixPromptsays "Do NOT commit" (:4696) and nothing pushes after lens-fix — in worktree mode lens-fix edits would never reach the PR.This work must ship as its own separate PR, independent of any other open issue. Note: #277 touches
verifyStepOutcome(:2385-2516); land #277 first and rebase over it.What to build
A. Mechanized branch setup (
runBranch, work-driver.ts:1261-1296, behindmechanizeOpsEnabled():1769-1771): replace the ops branch dispatch with:git fetch origin <mainline>;baseSha = git rev-parse origin/<mainline>(replacing repoRoot-HEAD read at :1287-1290); branch name = deterministic slug from cached issue title (feature/issue-N-<slug>); create one worktree per workstream viaworktreeCreate(worktree.ts:19-27, extended to accept absolute repo path +--detach fromRef, detached at baseSha — no scratch branches). Delete the{default: ctx.repoRoot}N=1 case (:1274-1277): N=1 gets.worktrees/issue-N/default. No git command mutates repoRoot in this step.B. Integration helper — extract
integrate(ctx, patches, opts): preflightgit status --porcelainclean at repoRoot (#283's gate relocated) →git checkout -B <branch> <baseSha>(replacing plain checkout at :1866) → per-workstreamgit -C <worktree> diff --cached/diff HEAD→git apply --indexat repoRoot (:1891-1901; delete the in-place repoRoot case :1886 — N=1 uses the sibling-patch path too) → commit → push → optionalgh pr create.mechanizedCommitPr(:1843-1990) uses it fully.C. Lens-fix re-integrate: on
adversarial-approvedfrom lens-fix (route table :466), before lens-review, apply the worktree's new diff as a follow-up commit + push viaintegrate. UpdateinlinePlanPrompt/inlineLensFixPromptwording accordingly.D. Read paths off repoRoot HEAD:
fetchAllMergedDiffs(:3047-3070) diffsorigin/<base>..origin/<branch>;captureWorktreeSnapshot(:3646-3685) snapshots the group's worktrees +git ls-remote origin <branch>. Delete?? repoRootcwd fallbacks at :1386, :1611, :2657.E. Cleanup:
runMergedbest-effortworktreeRemove(…, force)+git worktree prune.Schema (additive):
pipelineState.integration?: { integratedAt?: number; reintegrations?: number }.Knobs: rides
PI_ENSEMBLE_MECHANIZE_OPS=0; newPI_ENSEMBLE_ALWAYS_WORKTREE=0restores old N=1 behavior.Failure routing: worktree-create failure →
dispatch-failedon branch → handoff;git applyconflict → cap-hit → handoff with patch path in scratchDir named in the comment.Acceptance criteria
runWorkDrivertest (injectedverifyExecFnrecording command+cwd): no git command withcwd === repoRootbetween branch and commit-pr..worktrees/issue-N/defaultpath;worktrees[id]can never resolve to repoRoot.checkout -B <branch> <baseSha>ordering asserted; preflight-dirty repoRoot → handoff.fetchAllMergedDiffs/captureWorktreeSnapshotfixtures show no repoRoot-HEAD reads.PI_ENSEMBLE_ALWAYS_WORKTREE=0restores current N=1 behavior.Out of scope
Parallel group execution (separate issue); spawn caps/widget multiplexing (separate issue); plan decomposition (separate issue); worktree dep-bootstrap optimization (node_modules hardlinking — follow-up issue); grouping rules R1-R5.
Test plan
Offline: smoke-test fixtures per acceptance criteria via
verifyExecFn/dispatchFninjection; N=1 and N=3 workstream fixtures; apply-conflict fixture → handoff. Live on nessie: single-issue/workcycle end-to-end; verify PR contains only worktree-authored changes with a deliberately dirtied repoRoot file left untouched; verify lens-fix commit lands on the PR; verify worktree cleanup after merge.