@gobing-ai/spur 0.3.99 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/gates.yaml +36 -0
  4. package/config/decisions/history-anatomy.yaml +35 -0
  5. package/config/decisions/idea-pipeline.yaml +38 -0
  6. package/config/decisions/task-pipeline.yaml +55 -0
  7. package/config/plugin-scripts.json +25 -0
  8. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  9. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  10. package/config/rules/quality/coverage-gate.yaml +5 -3
  11. package/config/rules/recommended-pre-check.yaml +8 -3
  12. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  13. package/config/rules/structure/protected-files.yaml +3 -0
  14. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  15. package/config/script-placement-baseline.json +2 -2
  16. package/config/workflows/feature-lifecycle.yaml +9 -2
  17. package/config/workflows/history-anatomy.yaml +22 -0
  18. package/config/workflows/idea-pipeline.yaml +75 -0
  19. package/config/workflows/task-pipeline.yaml +368 -49
  20. package/config/workflows/wrapup-pipeline.yaml +106 -13
  21. package/package.json +9 -9
  22. package/plugins/sp/README.md +25 -2
  23. package/plugins/sp/agents/super-planner.md +11 -0
  24. package/plugins/sp/agents/super-reviewer.md +39 -11
  25. package/plugins/sp/bunfig.toml +8 -0
  26. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  27. package/plugins/sp/commands/dev-changelog.md +5 -4
  28. package/plugins/sp/commands/dev-find-way.md +80 -0
  29. package/plugins/sp/commands/dev-idea.md +6 -0
  30. package/plugins/sp/commands/dev-parallel.md +8 -2
  31. package/plugins/sp/commands/dev-plan.md +6 -0
  32. package/plugins/sp/commands/dev-review-session.md +14 -7
  33. package/plugins/sp/commands/dev-run.md +17 -4
  34. package/plugins/sp/commands/dev-runall.md +21 -10
  35. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  36. package/plugins/sp/hooks/context-session-start.ts +1 -1
  37. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  38. package/plugins/sp/hooks/hooks.json +45 -1
  39. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
  42. package/plugins/sp/lib/inline-run-app.ts +28 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
  44. package/plugins/sp/lib/inline-run.generated.mjs +364 -46
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
  48. package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  50. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  51. package/plugins/sp/lib/run-summary-core.ts +561 -0
  52. package/plugins/sp/lib/transcript.ts +275 -0
  53. package/plugins/sp/plugin.json +1 -1
  54. package/plugins/sp/references/roles.md +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +335 -0
  56. package/plugins/sp/scripts/batch-plan.ts +100 -0
  57. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  58. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  59. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  60. package/plugins/sp/scripts/commit-guard.ts +249 -0
  61. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  62. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  63. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  64. package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
  65. package/plugins/sp/scripts/inline-run-setup.ts +65 -97
  66. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  67. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  68. package/plugins/sp/scripts/quality-gate.mjs +30407 -73
  69. package/plugins/sp/scripts/quality-gate.ts +21 -5
  70. package/plugins/sp/scripts/residual-scan.mjs +88 -38
  71. package/plugins/sp/scripts/residual-scan.ts +35 -38
  72. package/plugins/sp/scripts/run-summary.mjs +693 -0
  73. package/plugins/sp/scripts/run-summary.ts +105 -0
  74. package/plugins/sp/scripts/session-timeline.mjs +331 -0
  75. package/plugins/sp/scripts/session-timeline.ts +204 -0
  76. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  77. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  78. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  79. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  80. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  81. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  82. package/plugins/sp/skills/code-verification/SKILL.md +8 -4
  83. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  84. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  85. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  86. package/plugins/sp/skills/session-review/SKILL.md +77 -24
  87. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  88. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  89. package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
  90. package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
  91. package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
  92. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  93. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
  94. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  95. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  96. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  97. package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
  98. package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
  99. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  103. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
  104. package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
  105. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
  106. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
  107. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  108. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  109. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
  110. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  111. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
  112. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  113. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  114. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  115. package/schemas/decision-catalog.schema.json +226 -0
  116. package/schemas/spur-config.schema.json +25 -0
  117. package/schemas/state-machine-workflow.schema.json +23 -0
  118. package/spur.js +28165 -19051
  119. package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
  120. package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
  121. package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
  122. package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
  123. package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
  124. package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
  125. package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
  126. package/web/_astro/channel.DZVfto-r.js +1 -0
  127. package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
  128. package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
  129. package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
  130. package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
  131. package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
  132. package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
  133. package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
  134. package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
  135. package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
  136. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
  137. package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
  138. package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
  139. package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
  140. package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
  141. package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
  142. package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
  143. package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
  144. package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
  145. package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
  146. package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
  147. package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
  148. package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
  149. package/web/_astro/index.COh_kBxN.css +1 -0
  150. package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
  151. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
  152. package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
  153. package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
  154. package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
  155. package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
  156. package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
  157. package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
  158. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
  159. package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
  160. package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
  161. package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
  162. package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
  163. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
  164. package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
  165. package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
  166. package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
  167. package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
  168. package/web/board-runtime.json +1 -1
  169. package/web/index.html +2 -2
  170. package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
  171. package/web/_astro/channel.6IFIKBhu.js +0 -1
  172. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -9,6 +9,23 @@ see_also:
9
9
 
10
10
  # Execution Batch
11
11
 
12
+ ## Driver state checklist (re-read this after a compaction)
13
+
14
+ - **Run id** — the batch id in `.spur/run/worktree-<marker-id>.json`, or the persisted Step 5 report
15
+ filename; every later artifact is keyed by it.
16
+ - **Markers under `.spur/run/`** — `worktree-<marker-id>.json` (identity + status),
17
+ `worktree-<marker-id>-batch-report.md` (frozen plan + outcomes), `<wbs>-verdict.json` (verdict),
18
+ `<wbs>-question.md` (paused on an operator question).
19
+ - **Re-read per state** — this file (Steps 1–5, WT-6/WT-7), [execution-batch-report.md](execution-batch-report.md)
20
+ at Step 5, [execution-worktree-setup.md](execution-worktree-setup.md) for a worktree start,
21
+ [execution-worktree-landing.md](execution-worktree-landing.md) at batch end,
22
+ [execution-parallel-isolation.md](execution-parallel-isolation.md) for `--mode parallel`,
23
+ [execution-batch-continuation.md](execution-batch-continuation.md) for `--continue`.
24
+ - **Never re-derive the plan** — the frozen plan is the persisted report; a new selector match is
25
+ `not-admitted`, not a plan change.
26
+
27
+ ## What this file owns
28
+
12
29
  `/sp:dev-runall` runs a **set** of task files through their pipelines in one operation, in
13
30
  dependency-correct order. This file owns the batch algorithm: selector resolution, set freeze,
14
31
  topological ordering, the per-task run loop, optional parallel fan-out, the failure policy, and the
@@ -76,18 +93,33 @@ dependency resolution, or worktree task execution:
76
93
 
77
94
  ```bash
78
95
  # monorepo source; installed projects use their resolved `spur` binary
96
+ # 1132 R4 (auto-fix-first): repair before checking — machine repairs land first, the
97
+ # strict check then judges the repaired file. `--fix` output is collected as
98
+ # `autoRepairs` in the batch report.
99
+ bun run apps/cli/src/index.ts feature check <id> --fix --json
79
100
  bun run apps/cli/src/index.ts feature check <id> --strict --json
80
101
  ```
81
102
 
82
103
  - **Abort shape.** A non-zero check aborts the batch immediately: verdict `aborted`, zero attempted
83
104
  tasks, and the structured feature findings (the `--json` finding list) reported verbatim. This is
84
- the same abort vocabulary as cycle / unknown selector (Step 4/Step 5). **Scoped to structural
85
- findings:** `L4.scenario-unverified` is the expected state of any not-yet-run feature (its
86
- covering tasks have no PASS verdicts yet) and `--strict` elevates it to error, so it is reported
87
- verbatim but does **not** abort the batch; every other error finding (L1–L3 structural,
88
- `L4.malformed-verdict-artifact`, `L4.uncovered-feature-scenario`) still aborts. The terminal
89
- feature-transition gate enforces scenario verification after the batch runs, so pre-run unverified
90
- scenarios are transient, not defects (dogfood 2026-08-11, feature I2).
105
+ the same abort vocabulary as cycle / unknown selector (Step 4/Step 5). **Expected pre-run
106
+ states are classified in code, not here:** the non-aborting set lives in
107
+ `NON_ABORTING_PREFLIGHT_CODES` (`plugins/sp/scripts/batch-preflight.ts`), consumed through
108
+ `classifyFeaturePreflightFindings`, which splits the finding list into `aborting` and
109
+ `nonAborting` and reports `shouldAbort`. `L4.scenario-unverified` is the expected state of any
110
+ not-yet-run feature (its covering tasks have no PASS verdicts yet) and
111
+ `L4.verifying-incomplete-tasks` is the expected state of a `verifying` feature that gained new
112
+ live tasks (a warning in `FeatureCheckService` until `done`); `--strict` elevates both to error,
113
+ so they are reported verbatim but do **not** abort the batch; every other error finding (L1–L3
114
+ structural, `L4.malformed-verdict-artifact`, `L4.uncovered-feature-scenario`) still aborts. The
115
+ terminal feature-transition gate enforces scenario verification after the batch runs, so pre-run
116
+ unverified scenarios and incomplete tasks are transient, not defects (dogfood 2026-08-11,
117
+ feature I2; 2026-10-08, feature H1). Extend the set by editing that constant, never this prose.
118
+ - **Auto-repairs are reported.** Structural repairs applied by `--fix` are listed under
119
+ `autoRepairs` in the batch report, so a green preflight that only passed after a machine repair is
120
+ visible rather than silent. Two producers: the task-level `task check <wbs> --fix` inside the
121
+ task-pipeline precheck guard (its `--json` output is captured at
122
+ `.spur/run/<wbs>-auto-repairs.json`) and the feature-level `feature check <id> --fix` pass above.
91
123
  - **Exactly once.** The check runs once per batch, before `task list`; it is not re-run per task.
92
124
  - **Non-feature exclusion.** Explicit WBS lists, status pseudo-lists, and `ready` selectors add no
93
125
  feature check — only an effective `feature:<id>` selector is feature-derived. When explicit
@@ -96,12 +128,23 @@ bun run apps/cli/src/index.ts feature check <id> --strict --json
96
128
  **warning**; feature-scoped batches never ran a strict check before freezing, so the late feature
97
129
  transition became the first blocking check. A selector-local strict preflight catches a known
98
130
  strict finding before any task pipeline action without changing corpus-wide severity. A not-yet-run
99
- feature always fails `L4.scenario-unverified` under `--strict`, so that class is
100
- reported-not-aborting per the Abort shape above — the preflight targets defects, not the
131
+ feature always fails `L4.scenario-unverified` under `--strict`, so that class (and
132
+ `L4.verifying-incomplete-tasks`) is reported-not-aborting per the Abort shape above — the preflight targets defects, not the
101
133
  expected pre-run state.
102
134
  - **Scope.** This preflight is advisory to severity policy: it does not alter `FeatureCheckService`,
103
135
  `L3.scope-delineation` severity, `feature sync`, or batch-create.
104
136
 
137
+ **Semantic precheck failure under `--auto` (1132 R5).** Per-task precheck runs
138
+ `task check <wbs> --fix` before `--precheck` (the task-pipeline precheck guard), so lossless
139
+ format defects are repaired in place and never reach the strict verdict. A finding that survives
140
+ `--fix` is semantic, and the two profiles diverge:
141
+
142
+ - **`--auto`:** the task gets **one** `/sp:dev-refineall --auto` refinement pass (exactly one — never
143
+ a loop). If the re-run precheck still fails, the task is marked **skipped** with its findings
144
+ recorded in the batch report, and independent tasks keep running. The batch does not abort.
145
+ - **without `--auto`:** unchanged halt behavior — the batch stops on the failing task (`--keep-going`
146
+ still skips only its in-batch dependents).
147
+
105
148
  `--tasks <value>` (or the effective value after normalization) resolves to a frozen set of task WBS numbers. Resolution happens **once, at
106
149
  kickoff** — the driver never re-queries `spur task list` to recompute membership mid-batch (R2.1).
107
150
 
@@ -201,17 +244,58 @@ remain SSOT in `next-router/references/routing-table.md`. Step 2.3 already pre-b
201
244
  out-of-set deps; 2.6 is belt-and-braces for status STOP rows and a uniform report shape
202
245
  (`dev-next:`-style reasons). Parallel mode: preflight each WBS before fan-out.
203
246
 
247
+ ### 2.7 Visible batch plan (1105 R3)
248
+
249
+ The batch publishes a generated two-layer plan in the host's native todo list — letters first, digits
250
+ per task only at task start. Rows come from `batch-plan.mjs` (ADR-130 glue over
251
+ `packages/app/src/workflow/plan-projection.ts`), reached like `batch-preflight`:
252
+ `node "$(superskill script path sp batch-plan.mjs)"`. The visible plan is a projection of the frozen
253
+ plan, never a second plan: labels carry identity, updates happen at the boundaries below only, and no
254
+ per-task letter is ever hand-assigned.
255
+
256
+ 1. **Kickoff (before the first task):** publish `A Prepare batch` (A1 Resolve and freeze task set, A2
257
+ Order by dependencies, A3 Prepare Git, A4 Publish plan) and `Z Batch report`. The task set is
258
+ unknown before A1, so task letters do not exist yet.
259
+ 2. **After freeze and ordering (Step 2 complete):** write the frozen ordered list to a temp file as a
260
+ JSON array of `{wbs, name}` and render the waves:
261
+
262
+ ```bash
263
+ node "$(superskill script path sp batch-plan.mjs)" waves --tasks <tasks.json>
264
+ ```
265
+
266
+ Publish wave 1's rows — one letter (B…Y) per task with no digit children, then `Z Batch report`;
267
+ a batch past 24 tasks publishes the next wave on rollover (same A/Z bookends).
268
+ 3. **When a task starts:** mark its letter in_progress and add its phase digits, rendered from a
269
+ `spur workflow show task-pipeline.yaml --no-logo --format todo --json` payload:
270
+
271
+ ```bash
272
+ node "$(superskill script path sp batch-plan.mjs)" task-children --letter <task-letter> --plan <plan.json>
273
+ ```
274
+
275
+ 4. **When a task ends:** mark the task's letter (and its published digits) with the task's terminal
276
+ outcome — `done` renders completed; `failed`/`skipped`/`blocked` render pending + ` [<outcome>]`
277
+ per the frozen status mapping in
278
+ [inline-pipeline-driver.md](inline-pipeline-driver.md) § Host todo update styles.
279
+
204
280
  ## Step 3 — The driver loop (R3, R4)
205
281
 
206
282
  ```
207
283
  plan = resolve(--tasks) → freeze → order(deps) # may abort (cycle) or pre-block (unmet dep)
208
284
  report = []
285
+ publish visible plan §2.7: A/Z rows now; one letter per task after this freeze (batch-plan.mjs waves)
286
+ RUN_ID = WT-3 marker id under --worktree, else "batch-$(date -u +%Y%m%dT%H%M%SZ)" # fixed ONCE here; the close-out reuses it
287
+ commit-guard start --run "$RUN_ID" # 1129 R1: fingerprint before the first task writes; the close-out commit stages through it
209
288
  for wbs in plan: # default sequential mode
210
289
  if any dependency of wbs failed earlier in THIS batch:
211
290
  report += skipped(wbs, reason); continue # only relevant under --keep-going
291
+ task check <wbs> --fix; task check <wbs> --precheck # 1132 R4/R5 — repair first, then judge
292
+ if precheck fails on semantic findings:
293
+ if --auto: one /sp:dev-refineall --auto pass; still failing → report += skipped(wbs, findings); continue
294
+ else: HALT (unchanged non-auto behavior)
212
295
  preflight = batch-preflight(wbs) # Step 2.6 — TABLE A STOP
213
296
  if preflight.action == skip:
214
297
  report += preflight-skip(wbs, preflight); continue
298
+ visible plan (§2.7): letter in_progress + task-children digits at start; letter ← outcome at end
215
299
  run: if interactive sequential omit/inline:
216
300
  inline-pipeline-driver(task-pipeline.yaml, wbs)
217
301
  else:
@@ -233,7 +317,7 @@ for wbs in plan: # default sequential mode
233
317
  if terminal == failed:
234
318
  if --keep-going: mark wbs + in-batch dependents as failed/skipped; continue
235
319
  else: HALT; remaining → not-attempted; break # stop-the-batch default (R3.1)
236
- emit batch report (per-task outcome + preflight skips + recovery hints + batch verdict)
320
+ emit batch report (per-task outcome + preflight skips + recovery hints + autoRepairs + batch verdict)
237
321
  ```
238
322
 
239
323
  Parallel mode keeps the same lifecycle but swaps the inner loop for the per-task-worktree fan-out
@@ -332,13 +416,12 @@ node "$(superskill script path sp batch-preflight.mjs)" --wbs <wbs> --status <st
332
416
  Helper: `recoveryHint(status, wbs)` in `plugins/sp/scripts/batch-preflight.ts`. Tables remain SSOT
333
417
  in next-router; this only maps status → primary TABLE A hop for recovery.
334
418
 
335
- ### 3.3c Feature-sync retry suppression (task 0411; 1004 R3 moved it into the sync service)
419
+ ### 3.3c Feature-sync retry suppression (task 0411; 1004 R3)
336
420
 
337
421
  During a batch, the per-task `record` step and the wrap-up `feature-transition` step each invoke
338
422
  feature status sync. When a feature is L4-gate-blocked (e.g. not all linked tasks are `done`), the
339
- identical blocked proposal repeats on every call with no intervening input change — in the H9
340
- dogfood, 4 redundant sync calls produced the same blocked result. The service fixes this, not the
341
- engine.
423
+ identical blocked proposal repeats on every call with no intervening input change. The service fixes
424
+ this, not the engine.
342
425
 
343
426
  Both `task-pipeline.yaml` (`record` step) and `wrapup-pipeline.yaml` (`feature-transition` step)
344
427
  invoke `spur feature sync <feature-id> --json` directly — retry suppression lives inside the
@@ -419,802 +502,22 @@ derivable from the same dependency graph built in Step 2.
419
502
 
420
503
  ## Step 5 — Batch report (R5.2)
421
504
 
422
- When the batch finishes — clean (all `done`), halted (default failure policy), parked (`paused`:
423
- one or more tasks sit on an operator question (escalate, 0933) or an approval gate), or aborted
424
- (cycle / unknown selector) — emit a structured report. The report is the orchestrator's sole
425
- output; it does not mutate the corpus (the pipeline's `record` step already wrote per-task
426
- results). A `paused` task is non-terminal — the report marks it `paused` (never `done`) and names
427
- the resume command.
428
-
429
- ```
430
- ## Batch Report — <selector>
431
-
432
- **Selector:** <value>
433
- **Plan:** <n> tasks (ordered: <wbs-list>) · <m> blocked · <p> not-attempted
434
- **Mode:** stop-the-batch | --keep-going | --auto
435
- **Verdict:** clean | halted | aborted
436
-
437
- | WBS | Status | Reason |
438
- |-----|--------|--------|
439
- | 0040 | done | — |
440
- | 0042 | failed | verify verdict PARTIAL (see .spur/run/0042-verdict.json) |
441
- | 0050 | not-attempted | batch halted after 0042 (stop-the-batch) |
442
- | 0051 | skipped | dependency 0040 failed (--keep-going) |
443
- | 0060 | blocked | unmet out-of-set dep: 0099 is wip |
444
-
445
- **Next:** <one-line action — pick up halted run / resolve 0099 / all green, feature H1 complete>
446
-
447
- **Excluded from wrap** (only under `--wrap`/`--next`, when some batch tasks are not `done`):
448
-
449
- ```
450
- | WBS | Status | Recovery |
451
- |-----|--------|--------|
452
- | 0042 | failed | C6 recovery: /sp:dev-run 0042 (residual report: .spur/run/0042-residual-report.md) |
453
- | 0050 | not-attempted | /sp:dev-next 0050 |
454
- ```
455
-
456
- A task with a residual report (failing `residual-sweep` check in its verdict artifact) uses the
457
- C6 recovery line (re-run the pipeline); every other excluded task uses the next-router A-row
458
- command for its status (F96 task 0952 R2).
459
-
460
- The per-task outcome vocabulary: `done` | `failed` | `blocked` | `skipped` | `not-attempted`,
461
- plus the resume-only `recheck` (stale/mismatched evidence — pipeline re-run), `not-admitted`
462
- (post-freeze selector match, never executed — task 0919 BC-1/BC-2), and the parallel-only
463
- `integration-conflict` (rebase conflict retained for manual integration — task 0931 R4,
464
- [§ Parallel isolation](#parallel-isolation---mode-parallel)).
465
- The batch verdict: `clean` (all attempted tasks `done`) | `halted` (a failure stopped the batch) |
466
- `aborted` (cycle or selector error before any run).
467
-
468
- **Zero-task rule (task 0701 R7b).** A selector that resolves to an **empty set after the status
469
- filter** is an `aborted` verdict (`aborted (empty set after filter)`), matching dev-operations.md
470
- §5a. Under `--worktree`, **WT-2 is skipped entirely**: no worktree is cut and no WT-3 marker is
471
- written for a batch with nothing to run. The early-exit report carries zero per-task rows,
472
- `Steps: 0 derived, 0 executed`, and the `aborted` verdict; no WT-3b commit step and no WT-4/WT-5
473
- terminal action runs. A contract test pins this
474
- (`plugins/sp/tests/dogfood-testing/execution-batch-contract.test.ts`).
475
-
476
- **Evidence persistence (worktree batches — task 0720 R3).** Copy-out is mandatory.
477
- A worktree batch's Step 5 report, verdict artifacts, and the batch's own
478
- `.spur/run` records live in the worktree while the batch runs — exactly the tree
479
- create-mode WT-4 deletes. Before any WT-4 removal, persist them into the **invoking** tree, which
480
- survives removal:
481
-
482
- - Write the emitted batch report to `.spur/run/worktree-<marker-id>-batch-report.md`.
483
- - Copy each attempted task's `.spur/run/<wbs>-verdict.json` from the worktree to
484
- `.spur/run/worktree-<marker-id>-verdicts/<wbs>-verdict.json`.
485
- - Make the report's per-task verdict references use those persisted invoking-tree paths, not the
486
- worktree-local paths that removal deletes.
487
-
488
- Evidence persistence precedes destructive cleanup: a persistence failure (unreadable verdict file,
489
- disk-full, missing directory) routes to **WT-5** — the worktree and branch are retained so a green
490
- batch can never destroy its own evidence. Reuse mode retains its operator-owned tree but still
491
- persists the Step 5 report under the invoking tree; the reused tree's `.spur/run/` remains the live
492
- copy while that tree lives on. The per-run provenance — the worktree DB's run/action rows and the
493
- `.spur/memory/runs/<runId>.md` + `.state.json` records — is persisted mechanically by WT-4a's
494
- `inline-run-setup.ts --persist-out --from <worktree> [--task-file <merged-task>]...` call,
495
- not by hand.
496
-
497
- **Stage records are worktree-local too (0948 R9, E7 Finding 5; persisted by 0975 R1).** Each task's
498
- own per-stage run record (`.spur/memory/runs/<runId>.md` + `.state.json`) and the worktree DB's run rows are
499
- written inside the worktree and **are removed with it** in create mode — the E7 batch lost exactly
500
- this evidence. Copying them out is no longer a manual audit-time duty: WT-4a (create-mode block
501
- below) runs `inline-run-setup.ts --persist-out --from "$WT_PATH"` **before** WT-4b holder cleanup,
502
- which copies the run/action/phase/transition/workflow-state rows and both record files into the
503
- invoking tree.
504
-
505
- **Cited evidence rides the same call (0984 R1/R2).** The driver forwards each merged task file with
506
- repeatable `--task-file <path>` (paths resolved in the invoking tree after the FF merge, e.g.
507
- `spur task show <wbs> --json` → `.filePath`), and persist-out then copies/verifies every literal
508
- `.spur/run/<file>` that file cites — WBS verdicts, check receipts, test-gate logs, run-ID records —
509
- so a merged task file never anchors a path that died with the tree. Abbreviated references
510
- (`fadca099-…`, `run-*-ac87.log`, `{batch-report.md,…}`) are not literal files and carry no
511
- obligation; neither do root-qualified paths (`knowledge-kit/.spur/run/…`, `/abs/.spur/run/…`),
512
- which cite another project's evidence — cite foreign run artifacts that way, never bare. A citation missing in BOTH trees, a divergent cited file (never overwritten — reconcile
513
- by hand), an unreadable task file, or more than 64 distinct cited files fails the pass → WT-5.
514
-
515
- **Durable planes ride it too (E71).** Persist-out copies canonical task verdicts, feature run/latest receipts, nested run artifacts and owned sessions before teardown. Artifact and task-run-link rows are exported with run rows, and stored path references are redirected to the invoking tree. Conflicting or unreadable retained evidence refuses teardown.
516
-
517
- **Owned evidence rides it too (1012).** With at least one `--task-file`, persist-out also treats as
518
- copy obligations the worktree's `.spur/run/` direct children named `<wbs>-…` (the WBS is each
519
- forwarded task file's leading four digits before `_`) or `<runId>-…` (every run row in the worktree
520
- DB, whichever task it ran) — `<wbs>-verdict.json`, check receipts, route reasons — whether or not
521
- the task file cites them. They join the cited set: same copy / byte-identical no-op /
522
- divergent-refuse handling. The 64-file cap bounds citations alone; owned names are bounded per
523
- owner (each `<wbs>-` / `<runId>-` prefix gets its own 64-file budget, task 1034), so the bound
524
- scales with the batch and one runaway owner refuses by name before any write. `<runId>.md` /
525
- `<runId>.state.json` stay with the record copy (a conflict is reported and the delegate refuses teardown). Files
526
- matching neither a citation nor an ownership prefix are left behind. An absent worktree `.spur/run/`
527
- means nothing is owned; any other listing failure (not a directory, permission denied) fails the
528
- pass before the invoking tree is written → WT-5. Without `--task-file` nothing is enumerated.
529
-
530
- The shapes are pinned (task 0975 R1; `record-missing` and citation behavior per 0984): idempotent on re-persist;
531
- success exits 0 printing
532
- `{"ok":true,"persisted":<n>,"skipped":[{"id":<run-id>,"reason":"id-exists"|"external-key-conflict"|"record-conflict:<file>"|"record-missing:<file>"|"cited-directory:<name>"|"cited-symlink:<name>"|"cited-non-file:<name>"}]}`
533
- — an `external-key-conflict` skip leaves the target run unchanged **and fails the pass (1049)**: the delegate exits 1 printing `{"ok":false,"error":…}` naming the skipped source run ids, because that (workflow, external key) identity already belongs to a different receiving run — the batch was not persisted, so reconcile the source worktree by hand (it stays the provenance owner of record) before any teardown: auditable reconciliation names the original archived DB snapshot and the skipped source run identities, verifies the archive by file hash and by merged-commit ancestry of the source branch, and treats any residual deletion as explicit operator-authorized cleanup under the strict canonical evidence rules above. An `id-exists` replay repairs missing owned artifacts and task links without duplicating them. A
534
- `record-conflict:<file>` never overwrites a divergent invoking-tree record and causes the delegate to exit 1, retaining the worktree. A
535
- `record-missing:<file>` skip is a known `task-lifecycle`/`feature-lifecycle` row with no record file
536
- at all (its inserted DB row still counts in `persisted` — 0984 R5). Any failure
537
- exits 1 printing `{"ok":false,"error":<message>}` (a worktree DB run id that is not a single safe
538
- filename component is rejected before any target write). Any persist-out failure
539
- routes to **WT-5** — worktree and branch retained — the same copy-out-first contract as the
540
- verdict persistence above. After a green persist-out, `spur workflow progress --json` in the
541
- invoking tree shows the merged run `done` with its per-action rows.
542
-
543
- ## Step 6 — Batch wrap (`--wrap` / `--next`) (F96 task 0952 R1)
544
-
545
- Run **once for the batch**, after the Step 5 report — never per task (dev-operations.md §runall
546
- agrees; the old "per task" flag-row wording in dev-runall.md was a contradiction, now fixed).
547
-
548
- The wrap receives only what it would accept:
549
-
550
- 1. `vars.tasks` = the JSON-encoded array of batch WBS whose terminal status is `done`. A task that
551
- is `failed`/`blocked`/`skipped`/`not-attempted`/`recheck`/`not-admitted` is **excluded** —
552
- `wrapup task-resolve` refuses non-done members (wrapup-steps.ts task-resolve), so the driver
553
- simply stops handing the wrap tasks it would refuse.
554
- 2. `vars.feature` = the batch feature **only when every task in the frozen batch is `done` or
555
- `cancelled`**. Otherwise omit it and print `feature lifecycle not advanced: <n> task(s)
556
- unfinished` — advancing a feature while some of its batch tasks are unfinished would overstate
557
- completion. Learnings/metrics capture still happens for the done subset.
558
- 3. When the done subset is **empty**, skip the wrap entirely with the reason (e.g. `batch wrap
559
- skipped: no done tasks`) instead of invoking wrapup-pipeline on an empty set.
560
-
561
- **Repo-wide tripwire (1037).** After the doc-sync exits converge, the pipeline's `doc-tripwire` hop
562
- runs the TRUSTED CONFIG ONLY `docTripwireCmd` over the still-uncommitted wrap diff before
563
- metrics-record. The default probes `package.json` for a `test-repo-wide` script and runs
564
- `bun run test-repo-wide` only when it is declared (a no-op in other projects), so the batch driver
565
- passes no extra vars. Batch callers override it like any wrap var (`docTripwireCmd` in `--vars`);
566
- an empty string disables the check while still recording PASS. A FAIL routes the wrap to `failed`
567
- with already-written learnings/docs preserved — fix the flagged working-diff violation and re-run
568
- the wrap.
569
-
570
- Filtering lives here, in the batch driver — no change to wrapup-pipeline.yaml or wrapup-steps.ts;
571
- the wrap's refusal of non-done tasks remains the hard invariant.
505
+ Read [execution-batch-report.md](execution-batch-report.md) at Step 5 — the structured Batch Report
506
+ template, the worktree evidence-persistence rules and Step 6 Batch wrap (`--wrap` / `--next`).
572
507
 
573
508
  ## Worktree isolation (`--worktree [<name>]`)
574
509
 
575
- When a batch command (`dev-runall`, `dev-refineall`, `dev-verifyall`) is invoked with
576
- [`--worktree [<name>]`](flag-glossary.md#flag-worktree), the entire driver loop runs inside an
577
- isolated git worktree instead of the operator's working directory. This section owns the worktree
578
- lifecycle for the sequential batch loop. `--mode parallel` has its own per-task isolation — see
579
- [§ Parallel isolation](#parallel-isolation---mode-parallel); `--worktree --mode parallel` is
580
- rejected there (parallel mode already isolates each task in its own worktree).
581
-
582
- **Startup ordering (task 0814 R3).** Resolve the selector/status filter and run the quick
583
- command-aware readiness (the `quickReadiness` contract in `batch-preflight.ts`) **before** creating
584
- or adopting the tree. The admission decision is what determines whether a tree should be cut at all;
585
- all subsequent tools, agents, task/feature writes, and run artifacts use the confirmed execution
586
- tree's cwd. A stale or empty selector, an unsupported mode, or an invalid target creates no tree and
587
- no marker (WT-2/WT-7), and the required Git safety checks (WT-1) still precede creation.
588
-
589
- > **Command wiring (task 0814 R3).** The four task-set worktree commands (`dev-run`, `dev-runall`,
590
- > `dev-refineall`, `dev-verifyall`) each call `quickReadiness` with their operation (`run`/`refine`/
591
- > `verify`), the resolved selector/status, and the filtered-set size **before** WT-1/WT-2. The
592
- > admission outcome gates the tree: an invalid/empty selector, unsupported mode, or a target that
593
- > quickReadiness marks `blocked`/`invalid` creates no tree and no marker (WT-2/WT-7); a
594
- > `needs-refinement` refine batch is still work to do (the tree is created, the gaps are the work).
595
- > The required Git safety checks (WT-1) still precede creation, and ownership/identity is confirmed
596
- > before any tool, agent, corpus write, or run artifact. A later failure retains the tree with
597
- > recovery information (WT-5).
598
-
599
- **Single-task `dev-run` (batch of one).** `/sp:dev-run <wbs> --worktree [<name>]` runs this same
600
- lifecycle with a one-task loop: WT-1…WT-6 apply unchanged, the marker's `command` is `dev-run` and
601
- its `selector` is the `<wbs>` (so WT-6's command+selector fallback resolves the resume), and the
602
- derived branch/directory slug is the WBS — `sp/run-<wbs>-<short-id>`. The WT-4 success condition
603
- "no failed task" reads as "the task reached terminal `done` with no failed stage"; a failing gate, a
604
- non-PASS verify verdict, or a HITL pause that ends the run take the WT-5 retention path. Only the
605
- full pipeline is eligible — `--worktree --mode implement` is rejected (WT-7), because that mode is
606
- the pipeline's implement stage and already runs in the driver's tree.
607
-
608
- **Review triage `dev-review` (run of one).** `/sp:dev-review [--tasks <selector> | --feature <id>[,<id>] | --scope <path>[,<path>]] --triage --worktree [<name>]`
609
- runs this lifecycle around one review-plus-triage pass: WT-1…WT-6 apply unchanged, the marker's
610
- `command` is `dev-review` and its `selector` records the full normalized target list, and the slug
611
- is `sp/review-<first>-and-<N>-<short-id>` for a multi-target run (N = target count) or
612
- `sp/review-<slug>-<short-id>` for a single target (the WBS or the path's basename). It skips
613
- `quickReadiness` (there is no task set; admission is "every target resolves" — each WBS/path must
614
- resolve before the tree is cut). Under `--triage` the findings are bucketed across all targets once
615
- (identical `file:line` findings deduped). WT-4 success reads as "every direct fix passed its check and the
616
- project gate is green"; anything else takes WT-5. Contract: [dev-operations.md § 2. review](dev-operations.md#2-review).
617
-
618
- One flag, two modes (see the glossary entry for the ownership rule). Bare `--worktree` is **create
619
- mode** (cut a fresh branch + sibling tree). `--worktree <name>` is **reuse mode** (attach to a tree
620
- that already exists); name resolution (§ WT-2 below) runs before WT-1. The deltas each mode applies
621
- are stated inline in WT-1…WT-4. `--continue` re-entry (WT-6) resolves by explicit name first, then
622
- falls back to the command+selector marker scan.
623
-
624
- The lifecycle wraps Steps 1–5 unchanged: a precheck creates or adopts the worktree before selector
625
- resolution runs, the loop executes with the worktree as process cwd, and a terminal action merges or
626
- retains after the batch report is emitted. Steps 1–5 themselves are not modified — only their cwd
627
- differs.
628
-
629
- **Per-call tree pinning (task 1058).** A host shell call starts in an arbitrary directory and
630
- inherits no cwd from the previous call, so "the loop runs with the worktree as process cwd" is a
631
- bootstrap convenience, never a carried guarantee. Every host command boundary in this lifecycle —
632
- corpus reads/writes, the WT-3b commit, WT-4's merge and evidence persistence, the WT-4a
633
- persist-out — re-selects its intended tree within the same call via the canonical per-call pin
634
- protocol ([inline-pipeline-driver.md](inline-pipeline-driver.md) § "Per-call execution-tree
635
- pin"): `cd --` to the quoted absolute tree, verify `pwd -P` against the recorded physical tree,
636
- verify Git top-level and expected branch, then execute the already-resolved Spur invocation. The
637
- identity is consumed, not re-derived: it is the WT-3 marker's confirmed `path` + `branch` (or the
638
- resolved worktree's physical path + checked-out branch in reuse mode). The one deliberate tree
639
- change is WT-4: the terminal merge actions select the **invoking** tree, not the execution tree —
640
- and pin it explicitly the same way. FF ancestry, marker and cleanup sequencing remain owned by
641
- 1059.
642
-
643
- **Portability (R10).** Use portable `git worktree` commands only. Do **not** depend on the Claude
644
- Code `EnterWorktree`/`ExitWorktree` tools — the `sp` plugin ships to Codex, Gemini CLI, pi, omp, and
645
- OpenCode. The underlying git mechanics (create / list / remove / prune, sibling-directory naming,
646
- disk-space awareness) are reused from [worktree-patterns.md](../../branch-workflow/references/worktree-patterns.md);
647
- this section does not re-author them.
648
-
649
- ### WT-1 — Dirty-tree precheck (R3)
650
-
651
- `git worktree add` branches from a ref, so uncommitted changes in the main tree do **not** carry
652
- into the worktree — a batch would silently run against different tree state than the operator sees.
653
- Before creating the worktree, check the main tree:
654
-
655
- ```bash
656
- git status --porcelain
657
- ```
658
-
659
- - **Clean tree** → proceed to WT-2.
660
- - **Dirty tree** → **abort** before any worktree is created. Name the offending files (from
661
- `git status --porcelain`) and instruct the operator to commit or stash. No task work has run.
662
- - **`--force`** → proceed past a dirty tree with a divergence warning that names the uncommitted
663
- files. The worktree is created and the batch proceeds against the committed base ref, not the
664
- operator's working-directory state.
665
-
666
- **Reuse-mode delta.** The main-tree precheck is unchanged — the divergence hazard is identical (the
667
- batch still runs somewhere the operator is not standing). The *target* worktree being dirty is
668
- **expected** (it holds retained partial work from a prior halt) and must **not** abort: report the
669
- file list once and proceed. The batch runs against the target worktree's working-directory state, not
670
- a clean checkout — which is exactly the point of reuse mode.
671
-
672
- ### WT-2 — Worktree creation or adoption
673
-
674
- **Name resolution runs first** for both modes — bare `--worktree` skips it (no name to resolve);
675
- `--worktree <name>` must resolve before WT-1's precheck touches anything. Resolution is specified
676
- in [§ Name resolution](#name-resolution---worktree-name) below; this section covers creation
677
- (create mode) and adoption (reuse mode).
678
-
679
- #### Create mode (bare `--worktree`)
680
-
681
- Create one worktree on a new branch cut from the current HEAD's ref (the **base ref** — often a
682
- `feat/…` branch, not literally `main`). Location follows the sibling-directory convention in
683
- [worktree-patterns.md](../../branch-workflow/references/worktree-patterns.md):
684
-
685
- **Location rule (0948 R9) — never under `.spur/`.** The default root is the **sibling** directory
686
- (`../<repo>-<command>-<selector-slug>-<short-id>`). A worktree nested under `.spur/` breaks Biome's
687
- vcs-root detection: `bunx biome check .` inside it reports `Checked 0 files`, so lint/format silently
688
- no-op for the whole run. Verified in both directions — a sibling worktree reports a non-zero file
689
- count (`Checked 1076 files` at the time of the fix) while a `.spur/`-nested one reports
690
- `Checked 0 files`. If a project's tooling needs a custom root, keep it **outside** any path that a
691
- VCS-root-detecting tool treats as ignorable.
692
-
693
- ```bash
694
- BASE_REF=$(git rev-parse --abbrev-ref HEAD)
695
- BASE_SHA=$(git rev-parse HEAD)
696
- BRANCH="sp/<command>-<selector-slug>-<short-id>" # e.g. sp/runall-h1-a3f2
697
- # `git worktree add -b` creates the branch BEFORE the directory, so a failed create leaves a
698
- # dangling branch and the natural retry dies on "a branch named ... already exists"
699
- # (task 0701 R2b). Wrap the create: on failure, delete the branch — or derive a fresh
700
- # short-id per attempt — before surfacing the error.
701
- git worktree add "../<repo>-<command>-<selector-slug>-<short-id>" -b "$BRANCH" "$BASE_REF" \
702
- || { git branch -D "$BRANCH"; false; }
703
- ```
704
-
705
- **Worktree root is outside `.spur/` (0948 R9).** The default create path is that
706
- sibling directory, which sits next to the repository and not under it. Do not
707
- put the default root at `.spur/worktrees` or any other gitignored path: Biome's
708
- `vcs.useIgnoreFile` then treats the checkout as empty ("Checked 0 files") and
709
- the quality gate cannot see the tree. Same class as the eval-pipeline worktree
710
- move off `.spur/tmp/` (task 0610).
711
-
712
- Branch and directory names are derived (command + selector slug + short id); the create path never
713
- takes an operator-supplied name (R8.3 — no create-with-name; `--worktree <name>` where `<name>` does
714
- not resolve is an error, not a create).
715
-
716
- A fresh worktree has no `node_modules` (gitignored), so the first `bun test` or
717
- typecheck fails on the first workspace import. Install before any task work:
718
-
719
- cd "../<worktree-dir>" && bun install --frozen-lockfile --ignore-scripts
720
-
721
- `--frozen-lockfile` pins the worktree to `bun.lock` rather than re-resolving,
722
- so the worktree's dependency tree matches the base ref's. `--ignore-scripts` is required, not
723
- stylistic (task 0701 R2a): worktrees share the main tree's `.git`, and this repo's `prepare`
724
- script is `lefthook install` (`package.json`) — a bare install rewrites the operator's
725
- main-repo hooks from inside the "isolated" tree. Scripts are skipped only at this call site;
726
- a normal clone keeps `prepare`. The worktree still gets a usable dependency tree — the install
727
- exists so the first `bun test` resolves workspace imports.
728
-
729
- #### Reuse mode (`--worktree <name>`)
730
-
731
- Creation is **skipped entirely**. `$BRANCH` is the resolved worktree's already-checked-out branch; a
732
- detached HEAD aborts (no branch can serve as `$BRANCH` for WT-4's FF-merge). `BASE_REF` is the
733
- invoking tree's current HEAD ref (not the worktree's branch) and `BASE_SHA` is
734
- `git merge-base <BASE_REF> <BRANCH>` — so WT-4's FF-merge lands the worktree's accumulated commits
735
- onto the invoking tree's base ref, exactly as create mode does.
736
-
737
- `bun install --frozen-lockfile --ignore-scripts` runs **only when `node_modules` is absent** in
738
- the resolved worktree (same `--ignore-scripts` rationale as create mode — task 0701 R2a). A warm reused tree does not re-pay the install; a cold one (hand-made, or a retained tree
739
- whose deps were removed) installs exactly once before the first task. This is the R3 conditional
740
- install rule (source: task 0481) — create mode always installs because a fresh tree is always cold.
741
-
742
- After creation or adoption, immediately write/adopt the state marker (WT-3), then run the
743
- existing batch loop (Steps 1–5) with the worktree as process cwd. `spur workflow run` resolves cwd
744
- from the process (`apps/cli/src/commands/workflow.ts:124`), so no CLI change is needed — `cd` into
745
- the worktree directory before launching the loop.
746
-
747
- #### Name resolution (`--worktree <name>`)
748
-
749
- <a id="name-resolution---worktree-name"></a>
750
-
751
- Authority: **git**, not the marker store — a marker may be stale, git is not. A foreign worktree
752
- with no marker is a valid target (R3 synthesizes one). Resolve `<name>` against
753
- `git worktree list --porcelain`, matching in this order and stopping at the first tier that yields
754
- ≥1 hit:
755
-
756
- 1. **exact `worktree <path>`** (after path normalization against the invoking tree)
757
- 2. **`basename(<path>)`**
758
- 3. **checked-out branch** — accept both `<name>` and the full `refs/heads/<name>` form
759
-
760
- Then require exactly one survivor across the tiers:
761
-
762
- - **0 hits** → abort before any task work. Print each candidate worktree as
763
- `<basename> <branch> <path>`, and the line: *"`--worktree <name>` selects an existing worktree;
764
- it never creates one. Use bare `--worktree` to create."*
765
- - **≥2 hits** → abort naming the candidates and require the path form (tier 1).
766
- - **1 hit, but** the worktree is `locked`, `prunable`, or belongs to a different repo → abort naming
767
- the condition.
768
-
769
- ### `spur` on PATH is not this checkout
770
-
771
- `spur` (`~/.bun/bin/spur`) resolves to a *published* bundle in `~/node_modules/`,
772
- not to the repo you are standing in and not to the worktree. `resolveSpurBin()`
773
- propagates whichever binary you entered through into `vars.spurBin`, which the
774
- `task-lifecycle.yaml` guards run as `$spurBin task check` — so one wrong entry
775
- point silently gate-checks against the published bundle.
776
-
777
- Inside a worktree, and in the monorepo whenever CLI behavior is under test, invoke
778
- the tree's own source:
779
-
780
- cd "<worktree>" && bun apps/cli/src/index.ts task check <wbs> --json
781
-
782
- Run that as a per-call pin subshell (task 1058) — `cd --` plus the `pwd -P` / git-top-level /
783
- branch identity checks of the canonical protocol (inline-pipeline-driver.md § "Per-call
784
- execution-tree pin") — rather than relying on a persistent `cd` between host tool calls.
785
-
786
- Confirm isolation by making a distinctive change in the worktree and checking that
787
- the command reflects it.
788
-
789
- **General rule — every path-resolving tool, not just `spur`.** The same failure class is not
790
- limited to the CLI on PATH. A host-agent file-edit or hash/`hashline` tool may resolve main-repo
791
- paths while the shell `cwd` is the worktree, silently acting on the wrong tree. Before relying on any
792
- path-resolving tool inside a `--worktree` batch, verify it is acting on the worktree — for example
793
- by making a distinctive change and confirming the path the tool reports matches the worktree. When a
794
- tool cannot be pointed at the worktree, fall back to `perl -i` in-place edits (or the agent's `write`
795
- verb) whose path argument you control.
796
-
797
- ### WT-3 — Crash-safe state marker (R6)
798
-
799
- Worktree identity lives on disk under `.spur/run/`, not only in the orchestrator's memory, so a
800
- session that dies mid-batch is recoverable. Write the marker at creation and update it at the
801
- terminal transition (merged / retained). The marker is written to the **invoking** tree's
802
- `.spur/run/` (task 0701 R2c) — the tree where the driver process started, not the worktree's own
803
- `.spur/run/` — so WT-6's resume scan finds it regardless of where the operator stands. Schema:
804
-
805
- ```json
806
- {
807
- "id": "<marker-id>",
808
- "path": "../<repo>-<command>-<selector-slug>-<short-id>",
809
- "branch": "sp/<command>-<selector-slug>-<short-id>",
810
- "baseRef": "feat/example",
811
- "baseSha": "<sha-at-creation>",
812
- "command": "dev-runall",
813
- "selector": "feature:H1",
814
- "createdAt": "<iso-8601>",
815
- "status": "active"
816
- }
817
- ```
818
-
819
- `status` transitions: `active` → `merged` (WT-4 success) | `retained` (WT-5 failure/halt/non-FF),
820
- and `retained` → `active` on a reuse re-entry (WT-6). The marker file is named
821
- `.spur/run/worktree-<marker-id>.json`. It is the authority for WT-6 resume and for operator recovery
822
- after a crash: a killed session leaves the marker at `status: active`, which the operator reads to
823
- find the worktree path, branch, and base ref.
824
-
825
- **Reuse-mode marker adoption.** Two new optional fields record when a run did not create the tree:
826
-
827
- ```json
828
- {
829
- "adopted": true,
830
- "adoptedAt": "<iso-8601>"
831
- }
832
- ```
833
-
834
- `adopted` is what WT-4 reads to decide retain-vs-remove, so it is set whenever the current run did
835
- not create the tree — including when reuse mode adopts a marker that create mode originally wrote.
836
- It records "this run did not create this tree", not "this tree was never created by the flag".
837
-
838
- Reuse mode resolves the marker by the resolved worktree's `path` (not by `command`+`selector`):
839
-
840
- - **Existing marker for that path** → adopt it in place: update `command`/`selector` to the current
841
- invocation, set `status` to `active`, set `adopted: true` + `adoptedAt`, **preserve `baseRef` and
842
- `baseSha`**. This makes cross-command resume — `dev-runall` halted, now `dev-verifyall` over the
843
- same tree — a supported path (R5.2).
844
- - **No marker** (hand-made or foreign worktree) → synthesize one with `baseRef` = the invoking
845
- tree's current HEAD ref, `baseSha` = `git merge-base <baseRef> <BRANCH>`, `adopted: true`.
846
- - **Marker already at `status: active`** → **abort** — another session may own that tree
847
- (AGENTS.md one-writer-per-tree; task 0487 R5). Overridable with `--force` (the operator can tell
848
- a crashed-session marker from a live-session one; the harness cannot).
849
-
850
- ### WT-3b — Commit the batch's writes on `$BRANCH` (task 0701 R1)
851
-
852
- Before any terminal action, commit the batch's corpus writes **on `$BRANCH`, inside the
853
- worktree** — including the generated task files under `docs/tasks*/` and the kanban index. The
854
- commit runs as a per-call pinned subshell (task 1058): it selects the execution tree inside the
855
- same call, verifies the WT-3 identity, and never relies on a persistent `cd` or a trailing
856
- `cd -` to restore the caller's directory:
857
-
858
- ```bash
859
- (
860
- cd -- "../<worktree-dir>" || { echo "tree missing: expected ../<worktree-dir>" >&2; exit 1; }
861
- [ "$(git branch --show-current)" = "$BRANCH" ] \
862
- || { echo "branch mismatch: expected $BRANCH, got $(git branch --show-current)" >&2; exit 1; }
863
- git add <files-the-batch-wrote>
864
- git commit -m "<type>(<scope>): <command> <selector> batch writes"
865
- )
866
- ```
867
-
868
- The FF-only git merge carries only commits — uncommitted writes in the worktree would be left
869
- behind by the merge and then destroyed by create mode's `git worktree remove`. WT-3b exists so
870
- that can never happen.
871
-
872
- ### WT-4 — Success path (R4)
873
-
874
- When the batch completes with **no failed task**, fast-forward-merge the worktree branch onto the
875
- base ref. The terminal action after the merge differs by mode (ownership rule: *the flag removes
876
- only what it created*):
877
-
878
- #### Create mode — merge, remove, delete
879
-
880
- ```bash
881
- # Run these from the main tree (not inside the worktree) - you merge the worktree branch
882
- # back onto the base ref there. Per-call tree pinning (task 1058, § above): every command
883
- # below selects the INVOKING tree (where the WT-3 marker lives), re-pinned with the canonical
884
- # per-call pin protocol in its own subshell — never the execution tree. The whole sequence is
885
- # fail-stop: checkout, guard, ancestry check, merge, landed verify, persistence, and cleanup
886
- # each stop explicitly. Every tip/ancestry read uses local refs — no fetch runs anywhere:
887
- MARKER=".spur/run/worktree-<marker-id>.json"
888
- git checkout "$BASE_REF" \
889
- || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
890
- # Guard (task 0701 R1): a zero-commit branch makes the FF-only git merge exit 0
891
- # ("Already up to date") while merging nothing — the lines below would then delete
892
- # the worktree holding the only copy of the batch's writes. Refuse instead:
893
- [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
894
- || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; } # -> WT-5, marker stays "active"
895
- # Capture the batch tip BEFORE any cleanup or branch work: every later step (landed verify,
896
- # mergeCommit, retained report) reads this captured value — a removed branch is never queried.
897
- BATCH_TIP="$(git rev-parse "$BRANCH")" \
898
- || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
899
- # Fresh LOCAL base tip, re-read now; the marker's baseSha may be stale and never governs this
900
- # decision (an advanced-but-ancestor base still FF-merges, a divergent base halts):
901
- BASE_TIP="$(git rev-parse "$BASE_REF")" \
902
- || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
903
- # Ancestry check immediately before the sole mutation (the FF merge below). Command/read
904
- # errors are distinguished from ordinary non-ancestry; both fail closed:
905
- ANCESTRY_RC=0
906
- git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
907
- [ "$ANCESTRY_RC" -gt 1 ] \
908
- && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
909
- [ "$ANCESTRY_RC" -eq 1 ] && {
910
- echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
911
- git log --oneline "$BASE_TIP..$BATCH_TIP" >&2 # batch-side divergent commits
912
- git log --oneline "$BATCH_TIP..$BASE_TIP" >&2 # base-side divergent commits
913
- exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
914
- git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
915
- [ $? -eq 0 ] \
916
- || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; } # -> WT-5
917
- # Landed verification before anything else runs: the captured BATCH_TIP must be an ancestor of
918
- # the base ref as it now stands (base tips advancing again later stay valid — this binds the
919
- # merge result, not the end state):
920
- LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
921
- || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
922
- git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
923
- || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
924
- # --- Landed from here on. Any halt below is landed-but-incomplete: it records status
925
- # --- "retained" + mergeCommit "$BATCH_TIP" via write_marker and stops — never a branch query.
926
- write_marker () { # $1 = merged | retained; read-modify-write: preserve WT-3 fields (id, command, selector, createdAt, adopted, adoptedAt); jq already assumed by WT-3/task-resolution flows
927
- mkdir -p "$(dirname "$MARKER")"
928
- if [ -f "$MARKER" ]; then
929
- TMP_MARKER="$(mktemp)"
930
- jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
931
- || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
932
- mv "$TMP_MARKER" "$MARKER"
933
- else
934
- printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
935
- "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
936
- fi
937
- }
938
- # WT-4a evidence persistence runs FIRST after the landed verify (Step 5, task 0720 R3):
939
- # persist the batch report + verdict artifacts AND the per-run provenance (task
940
- # 0975 R1) into the invoking tree before anything below touches the worktree.
941
- # Any persistence failure routes to WT-5 — landed-but-incomplete: retained marker + mergeCommit.
942
- WT_PATH="$(cd "../<worktree-dir>" && pwd)" # hoisted: needed by WT-4a AND WT-4b below
943
- # WT-4a provenance persist-out (task 0975 R1): copy the worktree DB's run rows plus
944
- # the .spur/memory/runs/<runId>.md + .state.json records into THIS tree. Run from the main
945
- # tree (cwd = the invoking tree). --task-file (0984 R2) forwards each merged task
946
- # file (post-merge path) so the cited .spur/run/<file> evidence is copied/verified
947
- # too. Resolve the merged path(s) BEFORE this block — an empty value exits 2:
948
- # TASK_FILE="$(spur task show <wbs> --json | jq -r .filePath)" # per done task;
949
- # build TASK_FILE_ARGS=(--task-file "$TASK_FILE") # one flag each
950
- # Idempotent; conflicts are reported, never overwritten. A non-zero exit —
951
- # including an unresolved or divergent citation, or a half-readable worktree — must
952
- # NOT proceed to WT-4b removal:
953
- SETUP_SCRIPT=plugins/sp/scripts/inline-run-setup.ts; [ -f config/plugin-scripts.json -a -f "$SETUP_SCRIPT" ] || SETUP_SCRIPT="$(superskill script path sp inline-run-setup.mjs 2>/dev/null)"
954
- bun "$SETUP_SCRIPT" --persist-out --from "$WT_PATH" "${TASK_FILE_ARGS[@]}" \
955
- || { echo "halt: worktree run-record persist-out failed - worktree retained (WT-5)" >&2; write_marker retained; exit 1; }
956
- #
957
- # WT-4b — bounded CWD-holder cleanup (task 0720 R1). $WT_PATH above is the EXACT
958
- # absolute worktree path; a relative path or a stale entry matches the wrong processes.
959
- # Holders = processes with any open fd under the worktree tree (lsof +D walks the
960
- # tree; CWD holders are the common case but +D also catches open-file holders —
961
- # over-match errs toward removal success; a plain -t <dir> matches only the
962
- # directory itself). Orphaned `serve` proof daemons (PPID 1) are exactly this
963
- # class: they defeat `git worktree remove` (ENOTEMPTY), defeat rm -rf, while
964
- # `git worktree prune` still deregisters the tree. Note +D is a full-tree walk,
965
- # so the wait loop below bounds ITERATIONS (6 × 1s ticks + walk cost), not
966
- # wall-clock.
967
- HOLDERS="$(lsof -t +D "$WT_PATH" 2>/dev/null | sort -u)"
968
- if [ -n "$HOLDERS" ]; then
969
- kill -TERM $HOLDERS 2>/dev/null # 1) TERM first, all holders (unquoted — word-split PID list)
970
- for _ in 1 2 3 4 5 6; do # 2) bounded wait: 6 × 1s ticks
971
- sleep 1
972
- [ -z "$(lsof -t +D "$WT_PATH" 2>/dev/null)" ] && break
973
- done
974
- SURVIVORS="$(lsof -t +D "$WT_PATH" 2>/dev/null | sort -u)"
975
- if [ -n "$SURVIVORS" ]; then
976
- kill -KILL $SURVIVORS 2>/dev/null # 3) KILL only the survivors (unquoted — one arg per PID)
977
- sleep 1
978
- fi
979
- fi
980
- # 4) Re-query: only an EMPTY holder set may proceed to remove/prune/branch delete.
981
- FINAL="$(lsof -t +D "$WT_PATH" 2>/dev/null | sort -u)"
982
- if [ -n "$FINAL" ]; then
983
- PORTS="$(lsof -nP -a -p "$(echo "$FINAL" | paste -sd, -)" -iTCP -sTCP:LISTEN 2>/dev/null \
984
- | awk 'NR>1 {print $9}' | sort -u | paste -sd' ' -)"
985
- echo "halt: worktree still held by PID(s): $FINAL ${PORTS:+listening: $PORTS}" >&2
986
- write_marker retained # landed-but-incomplete: retained + mergeCommit
987
- exit 1 # -> WT-5: retain worktree + branch,
988
- fi # NO prune/remove/branch delete
989
- # Pre-removal evidence assertion (task 1067 R1, E71 enforcement): refuse to remove the
990
- # worktree while it still owns evidence absent from (or divergent from) the invoking tree —
991
- # the D63 landing skipped persist-out and silently lost verdicts + receipts. Task-file
992
- # WBSes / run ids ride the same TASK_FILE_ARGS resolved for WT-4a (run ids: --run-id <id>).
993
- CHECK_SCRIPT=plugins/sp/scripts/persist-out-check.ts; [ -f config/plugin-scripts.json -a -f "$CHECK_SCRIPT" ] || CHECK_SCRIPT="$(superskill script path sp persist-out-check.mjs 2>/dev/null)"
994
- bun "$CHECK_SCRIPT" --from "$WT_PATH" "${TASK_FILE_ARGS[@]}" \
995
- || { echo "WT-4 halt: worktree evidence would be abandoned - persist-out missing or stale (WT-5)" >&2; write_marker retained; exit 1; }
996
- git worktree remove "../<worktree-dir>" \
997
- || { echo "WT-4 halt: worktree remove failed after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
998
- # Branch deletion runs only after the merge landed, persisted, and the worktree is gone —
999
- # and it is existence-guarded, so a removed branch is never queried. -D over -d: the landed
1000
- # verify already proved the merge landed, so an unmerged-refusal would only block proven-merged cleanup.
1001
- if git rev-parse --verify --quiet "$BRANCH" >/dev/null; then
1002
- git branch -D "$BRANCH" || { echo "WT-4 halt: cannot delete branch $BRANCH after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
1003
- fi
1004
- # WT-4c — clean up registry entry in ~/.config/spur/projects.json (task 0924)
1005
- spur projects remove "$WT_PATH" 2>/dev/null || spur projects clean --json 2>/dev/null || true
1006
- # Success marker, written once and only here: status "merged" + mergeCommit "$BATCH_TIP" —
1007
- # after landed verification, required persistence, and cleanup:
1008
- write_marker merged
1009
- ```
1010
-
1011
- Every halt above stops explicitly and falls through to **WT-5** — zero-commit branch (task 0701
1012
- R1), failed checkout, divergent base, failed FF, persist-out failure (task 0975), or surviving CWD
1013
- holders (task 0720 R1). Pre-merge failures leave the marker at `status: active` and the worktree +
1014
- branch untouched. Landed-but-incomplete failures record `status: retained` + `mergeCommit`
1015
- BATCH_TIP: the merge landed, and the retained report reads the captured tip only. While any holder
1016
- remains, do **not** run `git worktree prune`, `git worktree remove`, or branch deletion — only an
1017
- EMPTY holder set may proceed to those steps. The holder
1018
- halt report names every surviving PID; the listening port is best-effort — a CWD holder may own no
1019
- socket, and `lsof` port discovery failing must not hide the PIDs.
1020
-
1021
- #### Reuse mode — merge, retain
1022
-
1023
- The fail-stop merge sequence runs identically — same guard, captured `BATCH_TIP`/`BASE_TIP`,
1024
- ancestry check immediately before FF, `git merge --ff-only`, landed verification (per-call pinned
1025
- to the invoking tree, task 1058) — but no WT-4a/4b/4c and no removal: the worktree and branch are
1026
- **retained**. Pre-merge halts leave the marker `active`; a persistence failure after the landed
1027
- merge records `retained` + `mergeCommit`:
1028
-
1029
- ```bash
1030
- # Same fail-stop sequence as create mode (zero-commit guard -> BATCH_TIP/BASE_TIP -> ancestry
1031
- # rc check -> git merge --ff-only -> landed verify); local refs only — no fetch.
1032
- MARKER=".spur/run/worktree-<marker-id>.json"
1033
- git checkout "$BASE_REF" \
1034
- || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
1035
- [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
1036
- || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; }
1037
- BATCH_TIP="$(git rev-parse "$BRANCH")" || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
1038
- BASE_TIP="$(git rev-parse "$BASE_REF")" || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
1039
- ANCESTRY_RC=0
1040
- git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
1041
- [ "$ANCESTRY_RC" -gt 1 ] \
1042
- && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
1043
- [ "$ANCESTRY_RC" -eq 1 ] && {
1044
- echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
1045
- git log --oneline "$BASE_TIP..$BATCH_TIP" >&2
1046
- git log --oneline "$BATCH_TIP..$BASE_TIP" >&2
1047
- exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
1048
- git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
1049
- [ $? -eq 0 ] \
1050
- || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; }
1051
- LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
1052
- || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
1053
- git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
1054
- || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
1055
- WT_PATH="<resolved-worktree-path>" # reuse mode: the adopted tree's physical path
1056
- write_marker () { # $1 = merged | retained; read-modify-write: preserve WT-3 fields (id, command, selector, createdAt, adopted, adoptedAt); jq already assumed by WT-3/task-resolution flows
1057
- mkdir -p "$(dirname "$MARKER")"
1058
- if [ -f "$MARKER" ]; then
1059
- TMP_MARKER="$(mktemp)"
1060
- jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
1061
- || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
1062
- mv "$TMP_MARKER" "$MARKER"
1063
- else
1064
- printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
1065
- "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
1066
- fi
1067
- }
1068
- # Step 5 evidence persistence (task 0720 R3) — required BEFORE the success marker:
1069
- if printf 'batch report %s\n' "$BATCH_TIP" > ".spur/run/worktree-<marker-id>-batch-report.md"; then :; else
1070
- echo "WT-5 halt: persistence failed after landed merge $BATCH_TIP; recovery reads captured tips only" >&2
1071
- write_marker retained
1072
- exit 1
1073
- fi
1074
- # Success marker: status "merged" + mergeCommit "$BATCH_TIP" — written once, only after landed verification and required persistence.
1075
- # The worktree and branch are intentionally NOT removed.
1076
- write_marker merged
1077
- ```
1078
-
1079
- The operator supplied the tree, so the operator owns its lifetime. After a green reuse batch
1080
- `baseRef == $BRANCH`, so the same worktree keeps fast-forwarding on the next invocation instead of
1081
- having to be rebuilt — the continue-the-work loop is stable.
1082
-
1083
- **Fast-forward only.** `git merge --ff-only` is the sole mutation of the terminal sequence; every
1084
- other step is a read, a check, a persistence write, or a cleanup, and every tip/ancestry read uses
1085
- local refs — no fetch runs anywhere in this lifecycle. If the fresh `BASE_TIP` is not an ancestor of
1086
- `BATCH_TIP`, the ancestry check halts before the merge: the report names both tips and both
1087
- divergent commit ranges (`$BASE_TIP..$BATCH_TIP` and `$BATCH_TIP..$BASE_TIP`) and falls to the
1088
- retention path (WT-5) with the tree and branch retained. A concurrent writer that lands between the
1089
- ancestry check and the merge still trips `--ff-only` — the race survives every precheck, and a
1090
- failed FF leaves the marker at `status: active`. Do **not** rebase, merge-commit, or resolve
1091
- conflicts. The corpus files (`docs/tasks*/`, kanban/index) are
1092
- auto-generated and conflict-prone; automated conflict resolution over generated files is exactly the
1093
- wrong thing to attempt unattended. FF-only means the merge either is trivially correct or does not
1094
- happen.
1095
-
1096
- **Auto-decision carve-out.** The create-mode success path runs unattended on a fully-passing
1097
- `--worktree --auto` batch — it does **not** pause even though it performs a merge and a branch
1098
- deletion. That is the explicit single exception to Auto-Decision Principle #6 (`cross-cutting.md`),
1099
- which otherwise pauses any `--merge` / branch-deletion action regardless of `--auto`. The exception
1100
- is safe because `git merge --ff-only` fails closed (it refuses rather than risk losing work), the
1101
- branch deletion runs only after the merge has landed, persisted, and cleaned up — and is
1102
- existence-guarded (`git rev-parse --verify --quiet`), so a removed branch is never queried;
1103
- WT-5 retains the worktree and branch whenever FF is impossible or any task fails.
1104
- Reuse mode is **narrower** than the carve-out (it merges but does not delete the branch), so the
1105
- carve-out text needs no widening.
1106
-
1107
- **Lifecycle-DB disposition (task 0701 R2d, amended by 0720).** The worktree has its own `.spur`
1108
- lifecycle DB, and WT-4/WT-5 remove or retain that tree — the DB state does **not** travel with the
1109
- merge. One contract, no alternatives:
1110
-
1111
- - **Committed task files own lifecycle state.** The **committed task file is authoritative**: after
1112
- a green merge the branch's task files already read `done`/`testing` in the invoking tree; no
1113
- `spur task update` or `spur task record` replay runs post-merge. Replay is not "one of two
1114
- options" — it is removed: it writes `updated_at`-only churn and can never restore worktree-only
1115
- DB rows.
1116
- - **The persisted invoking-tree artifacts own evidence.** The Step 5 batch report at
1117
- `.spur/run/worktree-<marker-id>-batch-report.md` and the copied verdict JSONs under
1118
- `.spur/run/worktree-<marker-id>-verdicts/` (written before WT-4 removal) are the batch/verdict
1119
- record.
1120
- - **Per-worktree lifecycle DB rows intentionally do not travel.** No `task_run_links` import, no
1121
- cross-database provenance synthesis — the DB is per-tree by design.
1122
- - **No timestamp-only corpus churn.** Post-merge the invoking tree's DB statuses may read stale
1123
- relative to the committed files; that divergence is accepted, not repaired. Do not run
1124
- `task update`/`task record` to "catch up" the DB, and do not repair churn with
1125
- `git checkout -- docs/tasks*/`.
1126
-
1127
- This is a deliberate choice over auto-migrating DB state: the committed corpus files are the durable
1128
- record and the persisted run artifacts are the evidence record.
1129
-
1130
- ### WT-5 — Failure path: retain and report (R5)
1131
-
1132
- On any per-task failure, batch halt, HITL pause that ends the run, or a WT-4 halt, the
1133
- worktree directory and branch are left **intact**. No destructive automation on this path under
1134
- any flag combination (`--auto`, `--force`, `--keep-going` — all leave the worktree in place). The
1135
- marker records which class of halt occurred (frozen vocabulary):
1136
-
1137
- - **Pre-merge failure** (failed checkout, zero-commit guard, divergent base, failed FF): the merge
1138
- never landed, so the marker **stays at `status: active`** — the WT-3 marker stands and WT-5 is
1139
- its disposition. Nothing was merged onto the base ref.
1140
- - **Landed-but-incomplete failure** (persist-out, holder cleanup, or worktree removal failed after
1141
- a landed merge): the marker records `status: retained` + `mergeCommit` BATCH_TIP — a truthful
1142
- report that the merge landed. The retained path reads the captured `BATCH_TIP` only; a removed
1143
- branch is never queried.
1144
-
1145
- The worktree's own `.spur` lifecycle DB is retained with the tree,
1146
- so nothing is lost on either path (see the WT-4 lifecycle-DB disposition for the merged case —
1147
- task 0701 R2d). Emit a retention report in the existing halt-report shape:
1148
-
1149
- **Persist-out skip recovery (task 1067 R2, E71).** When evidence was lost to a landing that
1150
- skipped persist-out (or the pre-removal assertion fired too late), verdicts remain derivable
1151
- from the task records — `spur task show <wbs> --json` carries the recorded verdict and its
1152
- proof block, so a lost `<wbs>-verdict.json` is regenerated by re-recording or copying from
1153
- the task store. Feature run/latest receipts are re-runnable, not recovered: rerun the
1154
- feature-verification workflow in the invoking tree —
1155
- `spur workflow run feature-verification.yaml --vars '{"featureId":"<id>"}'` — which rewrites
1156
- the receipt files it owns. Divergent files are never auto-overwritten; reconcile by hand.
1157
-
1158
- ```
1159
- ## Worktree retained — <command> <selector>
1160
-
1161
- **Halt cause:** <one-line cause — batch halted at task <wbs> / non-FF base ref / HITL pause / landed-but-incomplete cleanup failure>
1162
- **Worktree path:** ../<worktree-dir>
1163
- **Branch:** sp/<command>-<selector-slug>-<short-id>
1164
- **Base ref:** <base-ref> (<base-sha>)
1165
- **Merge state:** pre-merge failure — nothing landed | landed-but-incomplete — mergeCommit <batch-tip-sha>
1166
-
1167
- The worktree and its branch are intact.
1168
- pre-merge failure: nothing was merged onto the base ref.
1169
- landed-but-incomplete: the batch merge LANDED as <batch-tip-sha> on <base-ref>; retention covers post-merge persistence/cleanup only.
1170
- Resume, merge, or discard:
1171
-
1172
- resume: cd <worktree-path> && <command> --continue --worktree <worktree-path>
1173
- merge: git checkout <base-ref> && git merge <branch> # resolve conflicts manually
1174
- discard: git worktree remove <worktree-path> && git branch -D <branch> && spur projects remove <worktree-path>
1175
- ```
1176
-
1177
- When the halt cause is `non-FF base ref`, the report replaces the one-line `merge:` hint with this
1178
- ordered divergence recipe, run by the operator — the driver never merges, rebases, or resolves
1179
- conflicts itself. Other halt causes (task failure, HITL pause) keep the hint as printed:
1180
-
1181
- ```
1182
- # 1. integrate as a merge commit — never a rebase; task evidence cites the branch's commit SHAs
1183
- git checkout <base-ref> && git merge --no-ff --no-commit <branch>
1184
- # 2. resolve source conflicts by hand; generated files are then regenerated with the project's
1185
- # generator, never hand-merged
1186
- # (this repo: bun run build:plugin-lib && bun run --filter @gobing-ai/spur build:bundle)
1187
- # 3. stage every resolved path and regenerated bundle (git add …) — an unmerged or
1188
- # unstaged path makes step 5 abort
1189
- # 4. run qualityGateCmd once, after ALL conflicts are resolved
1190
- # 5. commit the merge with the prepared message file
1191
- git commit -F <message-file>
1192
- # 6. persist evidence out (WT-4a), then WT-4b/4c cleanup, and set the marker to merged
1193
- inline-run-setup --persist-out --from <worktree> --task-file …
1194
- ```
1195
-
1196
- The report reuses the [`--next` chain contract](flag-glossary.md#--next-chain-contract) halt-report
1197
- shape (halt cause + where + why), not new vocabulary. Retention is the right default: these batches
1198
- are long and already resumable via `--continue`; auto-deleting is data loss, auto-merging is a
1199
- partial result presented as a whole. The answer to "what happens if it fails" is "nothing happens,
1200
- and we tell you where the work is."
1201
-
1202
- **Partial worktree-removal recovery is inspection-only.** If a create-mode run halted partway
1203
- through removal (leftover directory, stale registration), the recovery pass gathers facts and
1204
- nothing else: `git worktree list` (is the tree still registered?), `ls` of the path (what remains
1205
- on disk?), and the marker / `~/.config/spur/projects.json` registration state. Before removing
1206
- anything, verify the leftover path is the batch's **owned** path — match it against the marker's
1207
- `path`/`branch` (or the resolved `--worktree <name>` identity), never a guessed sibling directory.
1208
- Recursive deletion of leftover directories that survived `git worktree remove` is **never
1209
- automatic** — it requires explicit operator authorization. No automatic rebase, recovery merge,
1210
- `git worktree prune`, or holder kill runs on this path.
510
+ Read [execution-worktree-setup.md](execution-worktree-setup.md) when `--worktree` is passed at batch
511
+ start (the lifecycle, WT-1…WT-3b). Read [execution-worktree-landing.md](execution-worktree-landing.md)
512
+ at batch end (WT-4/WT-5, the retained-worktree report and the conflict-merge recipe). WT-6/WT-7 and
513
+ the corpus-visibility note stay below.
1211
514
 
1212
515
  ### WT-6 — `--continue` re-entry (R7)
1213
516
 
1214
517
  A `--continue` resume of a batch started with `--worktree` must re-enter the existing worktree via
1215
518
  its WT-3 marker rather than creating a second one. Marker lookup tries two paths in order:
1216
519
 
1217
- 1. **Name-resolution path (when `--worktree <name>` is present)** — run the [§ Name resolution](#name-resolution---worktree-name)
520
+ 1. **Name-resolution path (when `--worktree <name>` is present)** — run the [§ Name resolution](execution-worktree-setup.md#name-resolution---worktree-name)
1218
521
  algorithm against `<name>`. The resolved worktree's path identifies the marker file to adopt.
1219
522
  This path covers the common resume shapes: the operator remembers the name used last time, or
1220
523
  passes the path (tier-1 match).
@@ -1258,12 +561,11 @@ While the batch runs, corpus writes (`spur task update`, `spur feature update`)
1258
561
  the merge (WT-4) or manual integration (WT-5) propagates the writes back. Worth one line in each
1259
562
  command doc so it does not read as a bug.
1260
563
 
1261
- ## Gate preflight (dogfood 2026-08-21, feature A3)
564
+ ## Gate preflight
1262
565
 
1263
- The A3 batch burned multiple full `spur-check-new` runs (~2 min each) that failed only at the tail
1264
- gates. The cheap rule gates fail fast when run first — the full-gate run is dominated by the ~65 s
1265
- test suite, so a gate run that dies at `test-post-check` or `corpus-check` wasted most of its wall
1266
- time. Before launching a full `spur-check-new`:
566
+ The cheap rule gates fail fast when run first — the full-gate run is dominated by the ~65 s test
567
+ suite, so a gate run that dies at `test-post-check` or `corpus-check` wastes most of its wall time.
568
+ Before launching a full `spur-check-new`:
1267
569
 
1268
570
  1. **Run the two rule gates first** — `bun run test-pre-check` (43 rules: `no-console-output`,
1269
571
  `no-direct-process-spawn`, `cli-*`, `require-corresponding-test`) and `bun run test-post-check`
@@ -1301,105 +603,9 @@ time. Before launching a full `spur-check-new`:
1301
603
 
1302
604
  ## Parallel isolation (`--mode parallel`)
1303
605
 
1304
- Under `--mode parallel` (task 0931) every concurrently running task gets its **own create-mode git
1305
- worktree** (`sp/run-<wbs>-<short-id>`, cut from the current base-ref tip). Two task pipelines never
1306
- share a working tree, and the main tree receives no task writes while the batch runs. The per-task
1307
- pipeline (`task-pipeline.yaml`) is unchanged — only where it runs and when dependents may start
1308
- differ. The fan-out decision framework (independence, overlap, budget) stays owned by
1309
- [sp:parallel-execution](../../parallel-execution/SKILL.md); this section owns the isolation +
1310
- integration lifecycle and reuses WT-1…WT-5 by reference — it defines no new worktree mechanics.
1311
-
1312
- ### Driver loop
1313
-
1314
- ```
1315
- WT-1 once on the main tree (dirty → abort) # one precheck for the whole batch, not per task
1316
- ready = topo frontier; running = {}; integrated = set()
1317
- while ready or running:
1318
- while |running| < CONCURRENCY and ready has t with deps(t) ⊆ integrated:
1319
- WT-2 create sp/run-<wbs>-<short-id> (branch sp/run-<wbs>-<short-id>) from BASE_REF tip
1320
- WT-3 marker {command: dev-runall, selector: <wbs>, batchId: <batchId>}
1321
- # WT-3 schema fields (path, branch, baseRef, baseSha) + batchId shared by every marker
1322
- RUN[t] = (cd "$WT" && spur workflow run task-pipeline.yaml \
1323
- --vars '{"wbs":"<wbs>","deferFeatureSync":"true",...}' --async --json)
1324
- wait for any RUN terminal # trace poll --follow --timeout 600000; stale-evidence rule applies
1325
- on done: WT-3b commit on $BRANCH → integrate(t)
1326
- on failed: WT-5 retain (marker retained); failure policy — stop-the-batch default / --keep-going subtree skip
1327
- on timeout/paused: WT-5 retain; run is non-terminal (HITL approve pause or poll bound) — stop-the-batch
1328
- default / --keep-going subtree skip; the report marks the task `non-terminal`, never `done`
1329
- post: feature sync + refresh per touched feature, one chore(corpus) commit (generated regions, R5)
1330
- emit batch report (per-task outcomes + preflight skips + recovery hints + batch verdict)
1331
- ```
1332
-
1333
- - **Bound (R2).** `CONCURRENCY` is the [`--concurrency <n>`](flag-glossary.md#flag-concurrency)
1334
- value: default **2**, `n ≥ 1`; at most that many pipelines run at once.
1335
- - **Eligibility (R2).** A task becomes eligible only when all of its in-set dependencies are
1336
- **integrated** onto the base ref — pipeline-terminal is not enough, because its branch must
1337
- rebase over theirs. Omitting `--mode` stays sequential; this loop is entered only via
1338
- `--mode parallel` on `/sp:dev-runall`.
1339
- - **Workdir.** `spur workflow run` records the launch workdir (0784 R1), so launch with `cwd` = the
1340
- task worktree: resume and `.spur/run/` artifact paths resolve there. Read the verdict from
1341
- `$WT/.spur/run/<wbs>-verdict.json` before integration; WT-4a persists it into the invoking tree.
1342
-
1343
- ### Integration — rebase, then fast-forward only (R3)
1344
-
1345
- Pipeline-terminal is not terminal under parallel mode. The orchestrator integrates each succeeded
1346
- task from the main tree; integrations are **serialized** (one at a time) in completion order:
1347
-
1348
- ```
1349
- integrate(t):
1350
- git -C "$WT" rebase "$BASE_REF" # replay onto the CURRENT base tip (re-read per integration)
1351
- ├─ rebase fails → git -C "$WT" rebase --abort → conflict path (R4) below
1352
- └─ rebase clean → git checkout "$BASE_REF" → zero-commit guard → git merge --ff-only "$BRANCH"
1353
- → WT-4a verdict persist → WT-4b holders → WT-4c registry
1354
- → git worktree remove → existence-guarded git branch -D → marker merged
1355
- ```
1356
-
1357
- The merge is always `--ff-only`: a base that will not fast-forward after a clean rebase is a
1358
- conflict (R4). The pipeline never creates a merge commit, and no conflict is ever resolved
1359
- automatically. `BASE_REF` is re-read at each integration so a task that finished late rebases over
1360
- everything integrated before it.
1361
-
1362
- ### Conflict — retain, report, block (R4)
1363
-
1364
- A failed rebase is aborted (`git -C "$WT" rebase --abort`), which leaves the worktree clean on its
1365
- original branch tip. The worktree and branch are **retained** (WT-5, marker `retained`) and the
1366
- task's outcome is `integration-conflict`. There is no auto-resolution — not for generated paths,
1367
- not for anything. The batch report's row names the worktree path, the branch, and the manual
1368
- commands:
1369
-
1370
- ```
1371
- resume: cd <worktree-path> && git rebase <BASE_REF> # the operator resolves, never the driver
1372
- merge: git checkout <BASE_REF> && git merge --ff-only <branch>
1373
- discard: git worktree remove <worktree-path> && git branch -D <branch> && spur projects remove <worktree-path>
1374
- ```
1375
-
1376
- The task's dependent subtree is blocked under the normal failure policy (Step 4). Resuming a
1377
- retained parallel batch with `--continue` is future work; today a retained task is resumed
1378
- per-task with `/sp:dev-run <wbs> --worktree <branch>`.
1379
-
1380
- ### Generated regions — defer the sync, regenerate once (R5)
1381
-
1382
- The only per-task writer of feature files is the `record` step's post-record feature sync
1383
- (`task-pipeline.yaml`). Parallel launches set
1384
- the pipeline var `deferFeatureSync: "true"` (default `"false"`): the record step appends
1385
- `feature sync deferred to batch integration` to the task report and skips the sync, so task
1386
- branches never touch feature files or `docs/features/INDEX.md`. After the last integration, on the
1387
- base ref, the orchestrator runs `spur feature sync <f> --json` (service-level suppression) plus `spur feature refresh --feature <f>`
1388
- once per touched feature and commits the result as one `chore(corpus)` commit. Sequential and
1389
- inline runs keep the default `"false"` and are unchanged. Any rebase conflict — on a generated
1390
- path or any other — is an R4 `integration-conflict`; there is no path-based exception.
1391
-
1392
- **Report.** Step 5's per-task outcome vocabulary gains the parallel-only `integration-conflict`;
1393
- its row carries `worktree`, `branch`, and the manual commands above. Under parallel mode `done`
1394
- means **integrated onto the base ref**, not merely pipeline-terminal.
1395
-
1396
- **`--worktree` is rejected under parallel mode.** `--worktree --mode parallel` fails with
1397
- "parallel mode already isolates each task in its own worktree" — reuse mode (`--worktree <name>`)
1398
- has no per-task meaning (WT-7). Run parallel batches without `--worktree`, or run them sequentially
1399
- with it.
1400
-
1401
- **See also:** `sp:parallel-execution` skill (fan-out decision framework), `sp:super-planner` agent
1402
- (parallel mode), `execution-batch.md` § Worktree isolation (WT-1…WT-7 mechanics).
606
+ Read [execution-parallel-isolation.md § Parallel isolation](execution-parallel-isolation.md#parallel-isolation---mode-parallel)
607
+ with `--mode parallel` — per-task worktrees, the rebase + fast-forward integration, conflict
608
+ retention and the generated-region deferral.
1403
609
 
1404
610
  ## Subagent execution disciplines
1405
611
 
@@ -1413,61 +619,5 @@ Parallel fan-out and any subagent dispatch obey the four disciplines owned by
1413
619
 
1414
620
  ## Batch continuation (`--continue`) — identity binding + checkpoint reconciliation (task 0919)
1415
621
 
1416
- `--continue` resumes an interrupted batch against the batch's **original identity** — never
1417
- whatever a fresh selector resolution or a newer unrelated checkpoint would suggest.
1418
-
1419
- ### BC-1 — Re-bind the original frozen identity (R1; AC1/AC5)
1420
-
1421
- Resume identity comes from persisted batch artifacts, in priority order:
1422
-
1423
- 1. **WT-3 marker** (worktree batches): the marker's `command` + `selector` and worktree
1424
- name/branch re-derive the original launch (the WT-6 fallback already resolves this way).
1425
- 2. **Persisted batch report** (`.spur/run/worktree-<marker-id>-batch-report.md`, or the
1426
- invoking-tree Step 5 report of a non-worktree batch): its `Plan:` row IS the frozen ordered
1427
- membership — the resumed loop iterates exactly those WBS rows.
1428
-
1429
- Re-running the selector is a **validation, not a re-definition**: tasks that newly match the
1430
- selector but are absent from the frozen plan are reported `not-admitted` and never executed —
1431
- freshly listed tasks do not join a resumed batch implicitly. A frozen task whose dependencies
1432
- changed after freeze may invalidate its admission: report `blocked (admission invalidated —
1433
- re-plan required)` and stop for operator decision. The authorized frozen set is never silently
1434
- rewritten (Design).
1435
-
1436
- ### BC-2 — Checkpoints are hints (R2; AC2)
1437
-
1438
- Do **not** pick the resume point by mtime alone: `ls -t … | head -1` over `.spur/memory/sessions/`
1439
- is identity-blind, so an unrelated later session (different feature or task) must not become
1440
- authoritative. Select candidate checkpoints by identity first, newest first within the match:
1441
-
1442
- - frontmatter `workflow` is `task-pipeline` (or the batch's own workflow), and
1443
- - `feature_id` equals the batch's feature selector, or `task_wbs` is a member of the frozen plan.
1444
-
1445
- Checkpoints matching neither rule are ignored regardless of recency. A matched checkpoint only
1446
- **hints** where the loop left off (`phase`, `next_action` — surface them to the operator); the
1447
- driver then reconciles against authoritative state before skipping or repeating any task:
1448
-
1449
- - **Skip** requires ALL of: task file status `done` AND a persisted PASS verdict artifact
1450
- (`.spur/run/<wbs>-verdict.json`, or the worktree-persisted
1451
- `.spur/run/worktree-<marker-id>-verdicts/<wbs>-verdict.json`) consistent with the task's
1452
- current metadata. Anything less is not a valid skip.
1453
- - **Stale or mismatched evidence** — checkpoint claims done but the verdict artifact is missing,
1454
- the task file moved backwards, or the recorded `source_commit`/`digest` no longer matches —
1455
- yields outcome `recheck`: re-run that task's pipeline. When reconciliation cannot proceed
1456
- safely (lost worktree, unresolvable marker), report `blocked` with the reason. Never treat an
1457
- unverified claim as done.
1458
-
1459
- ### BC-3 — Ordering, write ownership, terminal mix, evidence (R3/R4; AC3/AC4)
1460
-
1461
- Sequential dependency-correct execution remains the default; opt-in `--mode parallel` keeps the
1462
- existing proven-independence requirements with one writer per tree (Step 3). Mixed terminal
1463
- results resume under the unchanged failure policy (Step 4). The Step 5 report keeps per-task
1464
- outcomes distinct — `done | failed | blocked | skipped | not-attempted` plus the resume-only
1465
- `recheck` and `not-admitted` — and the batch verdict stays `halted`/`aborted`: a resumed,
1466
- partially-complete batch is never reported `clean`. Worktree evidence survives cleanup via the
1467
- invoking-tree persistence in Step 5 (task 0720 R3); a persistence failure routes to **WT-5**
1468
- (retain tree + branch), so partial outcomes are never silently lost and never read as a completed
1469
- batch.
1470
-
1471
- See [cross-cutting.md](cross-cutting.md) § "Session Checkpoint Convention" for the canonical
1472
- frontmatter fields and the per-task owner-mismatch semantics
1473
- (`packages/app/src/workflow/checkpoint-contract.ts`).
622
+ Read [execution-batch-continuation.md](execution-batch-continuation.md) with `--continue` — original
623
+ identity re-binding, checkpoints-as-hints reconciliation and the resumed-run terminal rules.