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