@gobing-ai/spur 0.3.99 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/gates.yaml +36 -0
  4. package/config/decisions/history-anatomy.yaml +35 -0
  5. package/config/decisions/idea-pipeline.yaml +38 -0
  6. package/config/decisions/task-pipeline.yaml +55 -0
  7. package/config/plugin-scripts.json +25 -0
  8. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  9. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  10. package/config/rules/quality/coverage-gate.yaml +5 -3
  11. package/config/rules/recommended-pre-check.yaml +8 -3
  12. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  13. package/config/rules/structure/protected-files.yaml +3 -0
  14. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  15. package/config/script-placement-baseline.json +2 -2
  16. package/config/workflows/feature-lifecycle.yaml +9 -2
  17. package/config/workflows/history-anatomy.yaml +22 -0
  18. package/config/workflows/idea-pipeline.yaml +75 -0
  19. package/config/workflows/task-pipeline.yaml +368 -49
  20. package/config/workflows/wrapup-pipeline.yaml +106 -13
  21. package/package.json +9 -9
  22. package/plugins/sp/README.md +25 -2
  23. package/plugins/sp/agents/super-planner.md +11 -0
  24. package/plugins/sp/agents/super-reviewer.md +39 -11
  25. package/plugins/sp/bunfig.toml +8 -0
  26. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  27. package/plugins/sp/commands/dev-changelog.md +5 -4
  28. package/plugins/sp/commands/dev-find-way.md +80 -0
  29. package/plugins/sp/commands/dev-idea.md +6 -0
  30. package/plugins/sp/commands/dev-parallel.md +8 -2
  31. package/plugins/sp/commands/dev-plan.md +6 -0
  32. package/plugins/sp/commands/dev-review-session.md +14 -7
  33. package/plugins/sp/commands/dev-run.md +17 -4
  34. package/plugins/sp/commands/dev-runall.md +21 -10
  35. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  36. package/plugins/sp/hooks/context-session-start.ts +1 -1
  37. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  38. package/plugins/sp/hooks/hooks.json +45 -1
  39. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
  42. package/plugins/sp/lib/inline-run-app.ts +28 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
  44. package/plugins/sp/lib/inline-run.generated.mjs +364 -46
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
  48. package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  50. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  51. package/plugins/sp/lib/run-summary-core.ts +561 -0
  52. package/plugins/sp/lib/transcript.ts +275 -0
  53. package/plugins/sp/plugin.json +1 -1
  54. package/plugins/sp/references/roles.md +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +335 -0
  56. package/plugins/sp/scripts/batch-plan.ts +100 -0
  57. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  58. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  59. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  60. package/plugins/sp/scripts/commit-guard.ts +249 -0
  61. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  62. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  63. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  64. package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
  65. package/plugins/sp/scripts/inline-run-setup.ts +65 -97
  66. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  67. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  68. package/plugins/sp/scripts/quality-gate.mjs +30407 -73
  69. package/plugins/sp/scripts/quality-gate.ts +21 -5
  70. package/plugins/sp/scripts/residual-scan.mjs +88 -38
  71. package/plugins/sp/scripts/residual-scan.ts +35 -38
  72. package/plugins/sp/scripts/run-summary.mjs +693 -0
  73. package/plugins/sp/scripts/run-summary.ts +105 -0
  74. package/plugins/sp/scripts/session-timeline.mjs +331 -0
  75. package/plugins/sp/scripts/session-timeline.ts +204 -0
  76. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  77. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  78. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  79. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  80. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  81. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  82. package/plugins/sp/skills/code-verification/SKILL.md +8 -4
  83. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  84. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  85. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  86. package/plugins/sp/skills/session-review/SKILL.md +77 -24
  87. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  88. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  89. package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
  90. package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
  91. package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
  92. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  93. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
  94. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  95. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  96. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  97. package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
  98. package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
  99. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  103. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
  104. package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
  105. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
  106. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
  107. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  108. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  109. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
  110. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  111. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
  112. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  113. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  114. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  115. package/schemas/decision-catalog.schema.json +226 -0
  116. package/schemas/spur-config.schema.json +25 -0
  117. package/schemas/state-machine-workflow.schema.json +23 -0
  118. package/spur.js +28165 -19051
  119. package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
  120. package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
  121. package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
  122. package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
  123. package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
  124. package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
  125. package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
  126. package/web/_astro/channel.DZVfto-r.js +1 -0
  127. package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
  128. package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
  129. package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
  130. package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
  131. package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
  132. package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
  133. package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
  134. package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
  135. package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
  136. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
  137. package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
  138. package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
  139. package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
  140. package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
  141. package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
  142. package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
  143. package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
  144. package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
  145. package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
  146. package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
  147. package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
  148. package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
  149. package/web/_astro/index.COh_kBxN.css +1 -0
  150. package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
  151. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
  152. package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
  153. package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
  154. package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
  155. package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
  156. package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
  157. package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
  158. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
  159. package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
  160. package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
  161. package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
  162. package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
  163. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
  164. package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
  165. package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
  166. package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
  167. package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
  168. package/web/board-runtime.json +1 -1
  169. package/web/index.html +2 -2
  170. package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
  171. package/web/_astro/channel.6IFIKBhu.js +0 -1
  172. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: execution-parallel-isolation
3
+ description: "Parallel batch isolation (task 1128 split): --mode parallel per-task worktrees, the driver loop, rebase + fast-forward-only integration, conflict retention and generated-region deferral. Read it with --mode parallel."
4
+ see_also:
5
+ - execution-batch
6
+ - parallel-execution
7
+ ---
8
+
9
+ ## Parallel isolation (`--mode parallel`)
10
+
11
+ Under `--mode parallel` (task 0931) every concurrently running task gets its **own create-mode git
12
+ worktree** (`sp/run-<wbs>-<short-id>`, cut from the current base-ref tip). Two task pipelines never
13
+ share a working tree, and the main tree receives no task writes while the batch runs. The per-task
14
+ pipeline (`task-pipeline.yaml`) is unchanged — only where it runs and when dependents may start
15
+ differ. The fan-out decision framework (independence, overlap, budget) stays owned by
16
+ [sp:parallel-execution](../../parallel-execution/SKILL.md); this section owns the isolation +
17
+ integration lifecycle and reuses WT-1…WT-5 by reference — it defines no new worktree mechanics.
18
+
19
+ ### Driver loop
20
+
21
+ ```
22
+ WT-1 once on the main tree (dirty → abort) # one precheck for the whole batch, not per task
23
+ ready = topo frontier; running = {}; integrated = set()
24
+ while ready or running:
25
+ while |running| < CONCURRENCY and ready has t with deps(t) ⊆ integrated:
26
+ WT-2 create sp/run-<wbs>-<short-id> (branch sp/run-<wbs>-<short-id>) from BASE_REF tip
27
+ WT-3 marker {command: dev-runall, selector: <wbs>, batchId: <batchId>}
28
+ # WT-3 schema fields (path, branch, baseRef, baseSha) + batchId shared by every marker
29
+ RUN[t] = (cd "$WT" && spur workflow run task-pipeline.yaml \
30
+ --vars '{"wbs":"<wbs>","deferFeatureSync":"true"[,"deferQualityGate":"true"],...}' --async --json)
31
+ # deferQualityGate is added ONLY under the opt-in --defer-gate flag (1111 R3):
32
+ # the task's `test` hop then runs the light tier and writes DEFERRED.
33
+ wait for any RUN terminal # trace poll --follow --timeout 600000; stale-evidence rule applies
34
+ on done: WT-3b commit on $BRANCH → integrate(t)
35
+ on failed: WT-5 retain (marker retained); failure policy — stop-the-batch default / --keep-going subtree skip
36
+ on timeout/paused: WT-5 retain; run is non-terminal (HITL approve pause or poll bound) — stop-the-batch
37
+ default / --keep-going subtree skip; the report marks the task `non-terminal`, never `done`
38
+ post: integrated full gate on BASE_REF (only under --defer-gate, 1111 R3) -> feature sync + refresh
39
+ per touched feature, one chore(corpus) commit (generated regions, R5)
40
+ emit batch report (per-task outcomes + preflight skips + recovery hints + batch verdict)
41
+ ```
42
+
43
+ - **Bound (R2).** `CONCURRENCY` is the [`--concurrency <n>`](flag-glossary.md#flag-concurrency)
44
+ value: default **2**, `n ≥ 1`; at most that many pipelines run at once.
45
+ - **Eligibility (R2).** A task becomes eligible only when all of its in-set dependencies are
46
+ **integrated** onto the base ref — pipeline-terminal is not enough, because its branch must
47
+ rebase over theirs. Omitting `--mode` stays sequential; this loop is entered only via
48
+ `--mode parallel` on `/sp:dev-runall`.
49
+ - **Workdir.** `spur workflow run` records the launch workdir (0784 R1), so launch with `cwd` = the
50
+ task worktree: resume and `.spur/run/` artifact paths resolve there. Read the verdict from
51
+ `$WT/.spur/run/<wbs>-verdict.json` before integration; WT-4a persists it into the invoking tree.
52
+
53
+ ### Integration — rebase, then fast-forward only (R3)
54
+
55
+ Pipeline-terminal is not terminal under parallel mode. The orchestrator integrates each succeeded
56
+ task from the main tree; integrations are **serialized** (one at a time) in completion order:
57
+
58
+ ```
59
+ integrate(t):
60
+ git -C "$WT" rebase "$BASE_REF" # replay onto the CURRENT base tip (re-read per integration)
61
+ ├─ rebase fails → git -C "$WT" rebase --abort → conflict path (R4) below
62
+ └─ rebase clean → git checkout "$BASE_REF" → zero-commit guard → git merge --ff-only "$BRANCH"
63
+ → WT-4a verdict persist → WT-4b holders → WT-4c registry
64
+ → git worktree remove → existence-guarded git branch -D → marker merged
65
+ ```
66
+
67
+ The merge is always `--ff-only`: a base that will not fast-forward after a clean rebase is a
68
+ conflict (R4). The pipeline never creates a merge commit, and no conflict is ever resolved
69
+ automatically. `BASE_REF` is re-read at each integration so a task that finished late rebases over
70
+ everything integrated before it.
71
+
72
+ ### Conflict — retain, report, block (R4)
73
+
74
+ A failed rebase is aborted (`git -C "$WT" rebase --abort`), which leaves the worktree clean on its
75
+ original branch tip. The worktree and branch are **retained** (WT-5, marker `retained`) and the
76
+ task's outcome is `integration-conflict`. There is no auto-resolution — not for generated paths,
77
+ not for anything. The batch report's row names the worktree path, the branch, and the manual
78
+ commands:
79
+
80
+ ```
81
+ resume: cd <worktree-path> && git rebase <BASE_REF> # the operator resolves, never the driver
82
+ merge: git checkout <BASE_REF> && git merge --ff-only <branch>
83
+ discard: git worktree remove <worktree-path> && git branch -D <branch> && spur projects remove <worktree-path>
84
+ ```
85
+
86
+ The task's dependent subtree is blocked under the normal failure policy (Step 4). Resuming a
87
+ retained parallel batch with `--continue` is future work; today a retained task is resumed
88
+ per-task with `/sp:dev-run <wbs> --worktree <branch>`.
89
+
90
+ ### Generated regions — defer the sync, regenerate once (R5)
91
+
92
+ The only per-task writer of feature files is the `record` step's post-record feature sync
93
+ (`task-pipeline.yaml`). Parallel launches set
94
+ the pipeline var `deferFeatureSync: "true"` (default `"false"`): the record step appends
95
+ `feature sync deferred to batch integration` to the task report and skips the sync, so task
96
+ branches never touch feature files or `docs/features/INDEX.md`. After the last integration, on the
97
+ base ref, the orchestrator runs `spur feature sync <f> --json` (service-level suppression) plus `spur feature refresh --feature <f>`
98
+ once per touched feature and commits the result as one `chore(corpus)` commit. Sequential and
99
+ inline runs keep the default `"false"` and are unchanged. Any rebase conflict — on a generated
100
+ path or any other — is an R4 `integration-conflict`; there is no path-based exception.
101
+
102
+ **Deferred gate (`--defer-gate`, task 1111 R3/R4).** The opt-in flag adds
103
+ `deferQualityGate: "true"` to the same per-task `--vars`, so each task's `test` hop runs the light
104
+ tier and writes `DEFERRED` instead of a full-gate `PASS` (see
105
+ [flag-glossary](flag-glossary.md#flag-defer-gate)). The orchestrator then runs the project
106
+ `qualityGateCmd` **once** on the integrated `BASE_REF` **before** the deferred feature sync:
107
+
108
+ ```
109
+ if deferred:
110
+ run ${qualityGateCmd} on BASE_REF # the ONE full gate for the batch
111
+ PASS -> proceed to feature sync (R5)
112
+ FAIL -> batch verdict FAIL; feature sync SKIPPED; report every affected branch with a
113
+ re-gate command, newest-first (git log order); no automatic bisect, no revert
114
+ ```
115
+
116
+ Every report row of a deferred task carries `gate: deferred`, so a reader can never mistake a light
117
+ receipt for the batch's full-gate evidence. A task with a red light tier follows its normal bounded
118
+ fix lane and never reaches integration.
119
+
120
+ **Report.** Step 5's per-task outcome vocabulary gains the parallel-only `integration-conflict`;
121
+ its row carries `worktree`, `branch`, and the manual commands above. Under parallel mode `done`
122
+ means **integrated onto the base ref**, not merely pipeline-terminal.
123
+
124
+ **`--worktree` is rejected under parallel mode.** `--worktree --mode parallel` fails with
125
+ "parallel mode already isolates each task in its own worktree" — reuse mode (`--worktree <name>`)
126
+ has no per-task meaning (WT-7). Run parallel batches without `--worktree`, or run them sequentially
127
+ with it.
128
+
129
+ **See also:** `sp:parallel-execution` skill (fan-out decision framework), `sp:super-planner` agent
130
+ (parallel mode), `execution-batch.md` § Worktree isolation (WT-1…WT-7 mechanics).
@@ -163,6 +163,26 @@ The pipeline (`kind: state-machine`) runs the work loop:
163
163
  precheck → implement → test [→ test-fix → test-recheck] → review → approve(HITL) → verify → record → done
164
164
  ```
165
165
 
166
+ Two branches reach the bounded repair hop, not one (session finding after 1088):
167
+
168
+ - `test`/`test-recheck` FAIL → `test-fail-triage` (0943's failure-class router) → `test-fix`;
169
+ - `review` without a `Verdict: PASS` in its answer → `review-fail-triage` (the review-side twin) →
170
+ `test-fix`, bounded by the SAME `qualityGateMaxFixAttempts` counter. A FAIL review therefore has
171
+ an on-graph, bounded path to remediation instead of dead-ending the run, and the fix re-enters
172
+ quality → review → verify on a freshly captured proof digest.
173
+
174
+ The completion gate also reads the verifier's confidence (session finding after 1088): the verify
175
+ answer's `Confidence: HIGH|MEDIUM|LOW` line reaches the verdict artifact, and both `verify → record`
176
+ and `record → done` refuse a `LOW` (or absent) level unless the run was launched with
177
+ `--vars '{"ackLowConfidence":"true"}'`. A PASS the verifier would not stand behind is an operator
178
+ risk decision, so it takes an operator acknowledgement — not a silent certification.
179
+ The CLI-layer done gate (`done-transition-guard`, task 1068 R3) separately requires the artifact to *state* a
180
+ level at all: a PASS artifact whose `confidence` is missing is denied there too, and a present but
181
+ out-of-vocabulary value fails the read closed (`invalid confidence field`), so a hand-driven
182
+ `task update <wbs> done` — which is how this pipeline itself reaches `done` (`--no-lifecycle`) —
183
+ cannot certify a verdict that carries no confidence. `LOW` is accepted at that gate; the
184
+ acknowledgement rule above stays the pipeline's.
185
+
166
186
  Agentic steps use the pure slash inputs declared by each YAML `agent.run` action (ADR-043). The
167
187
  workflow engine dispatches them; the interactive driver invokes their backing skills in-session.
168
188
  The `test` hop is primarily
@@ -0,0 +1,471 @@
1
+ ---
2
+ name: execution-worktree-landing
3
+ description: "Worktree batch end (task 1128 split): WT-4 success (merge, remove, delete or retain), WT-5 failure retention, the retained-worktree report template and the conflict-merge recipe. Read it at batch end; setup stays in execution-worktree-setup.md."
4
+ see_also:
5
+ - execution-batch
6
+ - execution-workflow
7
+ ---
8
+
9
+ # Worktree landing (WT-4/WT-5)
10
+
11
+ Read at batch end (task 1128 R1).
12
+
13
+ ### WT-4 — Success path (R4)
14
+
15
+ When the batch completes with **no failed task**, fast-forward-merge the worktree branch onto the
16
+ base ref. The terminal action after the merge differs by mode (ownership rule: *the flag removes
17
+ only what it created*):
18
+
19
+ #### Create mode — merge, remove, delete
20
+
21
+ ```bash
22
+ # Run these from the main tree (not inside the worktree) - you merge the worktree branch
23
+ # back onto the base ref there. Per-call tree pinning (task 1058, § above): every command
24
+ # below selects the INVOKING tree (where the WT-3 marker lives), re-pinned with the canonical
25
+ # per-call pin protocol in its own subshell — never the execution tree. The whole sequence is
26
+ # fail-stop: checkout, guard, ancestry check, merge, landed verify, persistence, and cleanup
27
+ # each stop explicitly. Every tip/ancestry read uses local refs — no fetch runs anywhere:
28
+ MARKER=".spur/run/worktree-<marker-id>.json"
29
+ git checkout "$BASE_REF" \
30
+ || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
31
+ # Guard (task 0701 R1): a zero-commit branch makes the FF-only git merge exit 0
32
+ # ("Already up to date") while merging nothing — the lines below would then delete
33
+ # the worktree holding the only copy of the batch's writes. Refuse instead:
34
+ [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
35
+ || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; } # -> WT-5, marker stays "active"
36
+ # Capture the batch tip BEFORE any cleanup or branch work: every later step (landed verify,
37
+ # mergeCommit, retained report) reads this captured value — a removed branch is never queried.
38
+ BATCH_TIP="$(git rev-parse "$BRANCH")" \
39
+ || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
40
+ # Fresh LOCAL base tip, re-read now; the marker's baseSha may be stale and never governs this
41
+ # decision (an advanced-but-ancestor base still FF-merges, a divergent base halts):
42
+ BASE_TIP="$(git rev-parse "$BASE_REF")" \
43
+ || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
44
+ # Ancestry check immediately before the sole mutation (the FF merge below). Command/read
45
+ # errors are distinguished from ordinary non-ancestry; both fail closed:
46
+ ANCESTRY_RC=0
47
+ git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
48
+ [ "$ANCESTRY_RC" -gt 1 ] \
49
+ && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
50
+ [ "$ANCESTRY_RC" -eq 1 ] && {
51
+ echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
52
+ git log --oneline "$BASE_TIP..$BATCH_TIP" >&2 # batch-side divergent commits
53
+ git log --oneline "$BATCH_TIP..$BASE_TIP" >&2 # base-side divergent commits
54
+ exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
55
+ git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
56
+ [ $? -eq 0 ] \
57
+ || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; } # -> WT-5
58
+ # Landed verification before anything else runs: the captured BATCH_TIP must be an ancestor of
59
+ # the base ref as it now stands (base tips advancing again later stay valid — this binds the
60
+ # merge result, not the end state):
61
+ LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
62
+ || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
63
+ git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
64
+ || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
65
+ # --- Landed from here on. Any halt below is landed-but-incomplete: it records status
66
+ # --- "retained" + mergeCommit "$BATCH_TIP" via write_marker and stops — never a branch query.
67
+ 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
68
+ mkdir -p "$(dirname "$MARKER")"
69
+ if [ -f "$MARKER" ]; then
70
+ TMP_MARKER="$(mktemp)"
71
+ jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
72
+ || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
73
+ mv "$TMP_MARKER" "$MARKER"
74
+ else
75
+ printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
76
+ "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
77
+ fi
78
+ }
79
+ # WT-4a evidence persistence runs FIRST after the landed verify (Step 5, task 0720 R3):
80
+ # persist the batch report + verdict artifacts AND the per-run provenance (task
81
+ # 0975 R1) into the invoking tree before anything below touches the worktree.
82
+ # Any persistence failure routes to WT-5 — landed-but-incomplete: retained marker + mergeCommit.
83
+ WT_PATH="$(cd "../<worktree-dir>" && pwd)" # hoisted: needed by WT-4a AND WT-4b below
84
+ # WT-4a provenance persist-out (task 0975 R1): copy the worktree DB's run rows plus
85
+ # the .spur/memory/runs/<runId>.md + .state.json records into THIS tree. Run from the main
86
+ # tree (cwd = the invoking tree). --task-file (0984 R2) forwards each merged task
87
+ # file (post-merge path) so the cited .spur/run/<file> evidence is copied/verified
88
+ # too. Resolve the merged path(s) BEFORE this block — an empty value exits 2:
89
+ # TASK_FILE="$(spur task show <wbs> --json | jq -r .filePath)" # per done task;
90
+ # build TASK_FILE_ARGS=(--task-file "$TASK_FILE") # one flag each
91
+ # Idempotent; conflicts are reported, never overwritten. A non-zero exit —
92
+ # including an unresolved or divergent citation, or a half-readable worktree — must
93
+ # NOT proceed to WT-4b removal. 1149 R3: after a successful persist-out, the runner
94
+ # automatically reconciles each forwarded terminal task's receiving lifecycle row in
95
+ # the invoking tree (finalizing any stale in-flight or interrupted row left from the
96
+ # task's creation), so a manual DB edit is never required:
97
+ 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)"
98
+ bun "$SETUP_SCRIPT" --persist-out --from "$WT_PATH" "${TASK_FILE_ARGS[@]}" \
99
+ || { echo "halt: worktree run-record persist-out failed - worktree retained (WT-5)" >&2; write_marker retained; exit 1; }
100
+ #
101
+ # WT-4b — bounded CWD-holder cleanup (task 0720 R1). $WT_PATH above is the EXACT
102
+ # absolute worktree path; a relative path or a stale entry matches the wrong processes.
103
+ # Holders = processes with any open fd under the worktree tree (lsof +D walks the
104
+ # tree; CWD holders are the common case but +D also catches open-file holders —
105
+ # over-match errs toward removal success; a plain -t <dir> matches only the
106
+ # directory itself). Orphaned `serve` proof daemons (PPID 1) are exactly this
107
+ # class: they defeat `git worktree remove` (ENOTEMPTY), defeat rm -rf, while
108
+ # `git worktree prune` still deregisters the tree. Note +D is a full-tree walk,
109
+ # so the wait loop below bounds ITERATIONS (6 × 1s ticks + walk cost), not
110
+ # wall-clock.
111
+ HOLDERS="$(lsof -t +D "$WT_PATH" 2>/dev/null | sort -u)"
112
+ if [ -n "$HOLDERS" ]; then
113
+ kill -TERM $HOLDERS 2>/dev/null # 1) TERM first, all holders (unquoted — word-split PID list)
114
+ for _ in 1 2 3 4 5 6; do # 2) bounded wait: 6 × 1s ticks
115
+ sleep 1
116
+ [ -z "$(lsof -t +D "$WT_PATH" 2>/dev/null)" ] && break
117
+ done
118
+ SURVIVORS="$(lsof -t +D "$WT_PATH" 2>/dev/null | sort -u)"
119
+ if [ -n "$SURVIVORS" ]; then
120
+ kill -KILL $SURVIVORS 2>/dev/null # 3) KILL only the survivors (unquoted — one arg per PID)
121
+ sleep 1
122
+ fi
123
+ fi
124
+ # 4) Re-query: only an EMPTY holder set may proceed to remove/prune/branch delete.
125
+ FINAL="$(lsof -t +D "$WT_PATH" 2>/dev/null | sort -u)"
126
+ if [ -n "$FINAL" ]; then
127
+ PORTS="$(lsof -nP -a -p "$(echo "$FINAL" | paste -sd, -)" -iTCP -sTCP:LISTEN 2>/dev/null \
128
+ | awk 'NR>1 {print $9}' | sort -u | paste -sd' ' -)"
129
+ echo "halt: worktree still held by PID(s): $FINAL ${PORTS:+listening: $PORTS}" >&2
130
+ write_marker retained # landed-but-incomplete: retained + mergeCommit
131
+ exit 1 # -> WT-5: retain worktree + branch,
132
+ fi # NO prune/remove/branch delete
133
+ # Pre-removal evidence assertion (task 1067 R1, E71 enforcement): refuse to remove the
134
+ # worktree while it still owns evidence absent from (or divergent from) the invoking tree —
135
+ # the D63 landing skipped persist-out and silently lost verdicts + receipts. WBSes ride the
136
+ # same TASK_FILE_ARGS resolved for WT-4a; run ids are a separate `--run-id <id>` per run and
137
+ # belong to THIS check only — `inline-run-setup.ts --persist-out` accepts `--from` and
138
+ # `--task-file` alone and exits 2 on `--run-id` (1089 driver note).
139
+ 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)"
140
+ bun "$CHECK_SCRIPT" --from "$WT_PATH" "${TASK_FILE_ARGS[@]}" \
141
+ || { echo "WT-4 halt: worktree evidence would be abandoned - persist-out missing or stale (WT-5)" >&2; write_marker retained; exit 1; }
142
+ git worktree remove "../<worktree-dir>" \
143
+ || { echo "WT-4 halt: worktree remove failed after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
144
+ # Branch deletion runs only after the merge landed, persisted, and the worktree is gone —
145
+ # and it is existence-guarded, so a removed branch is never queried. -D over -d: the landed
146
+ # verify already proved the merge landed, so an unmerged-refusal would only block proven-merged cleanup.
147
+ if git rev-parse --verify --quiet "$BRANCH" >/dev/null; then
148
+ git branch -D "$BRANCH" || { echo "WT-4 halt: cannot delete branch $BRANCH after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
149
+ fi
150
+ # WT-4c — clean up registry entry in ~/.config/spur/projects.json (task 0924)
151
+ spur projects remove "$WT_PATH" 2>/dev/null || spur projects clean --json 2>/dev/null || true
152
+ # WT-4d — relink the invoking tree when the landed diff touched a manifest or the lockfile (task 1121).
153
+ # The gate evidence came from the worktree's node_modules; the receiving tree still links the old
154
+ # workspace set until it is re-installed. --ignore-scripts mirrors the worktree convention (0701 R2a).
155
+ if git diff --name-only "$BASE_TIP" "$BATCH_TIP" -- bun.lock '*package.json' | grep -q .; then
156
+ bun install --frozen-lockfile --ignore-scripts \
157
+ || echo "WT-4d warning: invoking tree workspace links are stale — run 'bun install --frozen-lockfile --ignore-scripts'" \
158
+ | tee -a ".spur/run/worktree-<marker-id>-batch-report.md" >&2
159
+ fi
160
+ # Success marker, written once and only here: status "merged" + mergeCommit "$BATCH_TIP" —
161
+ # after landed verification, required persistence, and cleanup:
162
+ write_marker merged
163
+ ```
164
+
165
+ Every halt above stops explicitly and falls through to **WT-5** — zero-commit branch (task 0701
166
+ R1), failed checkout, divergent base, failed FF, persist-out failure (task 0975), or surviving CWD
167
+ holders (task 0720 R1). WT-4d (task 1121) is the one non-halting step: when the landed diff touched
168
+ a manifest or the lockfile it re-installs the invoking tree, and a failed relink only appends a
169
+ stale-links warning to the batch report — the success marker still records `merged`. Pre-merge
170
+ failures leave the marker at `status: active` and the worktree +
171
+ branch untouched. Landed-but-incomplete failures record `status: retained` + `mergeCommit`
172
+ BATCH_TIP: the merge landed, and the retained report reads the captured tip only. While any holder
173
+ remains, do **not** run `git worktree prune`, `git worktree remove`, or branch deletion — only an
174
+ EMPTY holder set may proceed to those steps. The holder
175
+ halt report names every surviving PID; the listening port is best-effort — a CWD holder may own no
176
+ socket, and `lsof` port discovery failing must not hide the PIDs.
177
+
178
+ #### Reuse mode — merge, retain
179
+
180
+ The fail-stop merge sequence runs identically — same guard, captured `BATCH_TIP`/`BASE_TIP`,
181
+ ancestry check immediately before FF, `git merge --ff-only`, landed verification (per-call pinned
182
+ to the invoking tree, task 1058) — but no WT-4a/4b/4c and no removal: the worktree and branch are
183
+ **retained**. Pre-merge halts leave the marker `active`; a persistence failure after the landed
184
+ merge records `retained` + `mergeCommit`:
185
+
186
+ ```bash
187
+ # Same fail-stop sequence as create mode (zero-commit guard -> BATCH_TIP/BASE_TIP -> ancestry
188
+ # rc check -> git merge --ff-only -> landed verify); local refs only — no fetch.
189
+ MARKER=".spur/run/worktree-<marker-id>.json"
190
+ git checkout "$BASE_REF" \
191
+ || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
192
+ [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
193
+ || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; }
194
+ BATCH_TIP="$(git rev-parse "$BRANCH")" || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
195
+ BASE_TIP="$(git rev-parse "$BASE_REF")" || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
196
+ ANCESTRY_RC=0
197
+ git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
198
+ [ "$ANCESTRY_RC" -gt 1 ] \
199
+ && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
200
+ [ "$ANCESTRY_RC" -eq 1 ] && {
201
+ echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
202
+ git log --oneline "$BASE_TIP..$BATCH_TIP" >&2
203
+ git log --oneline "$BATCH_TIP..$BASE_TIP" >&2
204
+ exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
205
+ git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
206
+ [ $? -eq 0 ] \
207
+ || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; }
208
+ LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
209
+ || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
210
+ git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
211
+ || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
212
+ WT_PATH="<resolved-worktree-path>" # reuse mode: the adopted tree's physical path
213
+ 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
214
+ mkdir -p "$(dirname "$MARKER")"
215
+ if [ -f "$MARKER" ]; then
216
+ TMP_MARKER="$(mktemp)"
217
+ jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
218
+ || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
219
+ mv "$TMP_MARKER" "$MARKER"
220
+ else
221
+ printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
222
+ "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
223
+ fi
224
+ }
225
+ # Step 5 evidence persistence (task 0720 R3) — required BEFORE the success marker:
226
+ if printf 'batch report %s\n' "$BATCH_TIP" > ".spur/run/worktree-<marker-id>-batch-report.md"; then :; else
227
+ echo "WT-5 halt: persistence failed after landed merge $BATCH_TIP; recovery reads captured tips only" >&2
228
+ write_marker retained
229
+ exit 1
230
+ fi
231
+ # WT-4d — relink the invoking tree when the landed diff touched a manifest or the lockfile (task 1121).
232
+ # The gate evidence came from the worktree's node_modules; the receiving tree still links the old
233
+ # workspace set until it is re-installed. --ignore-scripts mirrors the worktree convention (0701 R2a).
234
+ if git diff --name-only "$BASE_TIP" "$BATCH_TIP" -- bun.lock '*package.json' | grep -q .; then
235
+ bun install --frozen-lockfile --ignore-scripts \
236
+ || echo "WT-4d warning: invoking tree workspace links are stale — run 'bun install --frozen-lockfile --ignore-scripts'" \
237
+ | tee -a ".spur/run/worktree-<marker-id>-batch-report.md" >&2
238
+ fi
239
+ # Success marker: status "merged" + mergeCommit "$BATCH_TIP" — written once, only after landed verification and required persistence.
240
+ # The worktree and branch are intentionally NOT removed.
241
+ write_marker merged
242
+ ```
243
+
244
+ The operator supplied the tree, so the operator owns its lifetime. After a green reuse batch
245
+ `baseRef == $BRANCH`, so the same worktree keeps fast-forwarding on the next invocation instead of
246
+ having to be rebuilt — the continue-the-work loop is stable. WT-4d (task 1121) relinks the
247
+ invoking tree there too — after the Step 5 persistence, before the success marker; a failed relink
248
+ appends the same stale-links warning to the persisted batch report, and the success marker still
249
+ records `merged`.
250
+
251
+ **Fast-forward only.** `git merge --ff-only` is the sole mutation of the terminal sequence; every
252
+ other step is a read, a check, a persistence write, or a cleanup, and every tip/ancestry read uses
253
+ local refs — no fetch runs anywhere in this lifecycle. If the fresh `BASE_TIP` is not an ancestor of
254
+ `BATCH_TIP`, the ancestry check halts before the merge: the report names both tips and both
255
+ divergent commit ranges (`$BASE_TIP..$BATCH_TIP` and `$BATCH_TIP..$BASE_TIP`) and falls to the
256
+ retention path (WT-5) with the tree and branch retained. A concurrent writer that lands between the
257
+ ancestry check and the merge still trips `--ff-only` — the race survives every precheck, and a
258
+ failed FF leaves the marker at `status: active`. Do **not** rebase, merge-commit, or resolve
259
+ conflicts. The corpus files (`docs/tasks*/`, kanban/index) are
260
+ auto-generated and conflict-prone; automated conflict resolution over generated files is exactly the
261
+ wrong thing to attempt unattended. FF-only means the merge either is trivially correct or does not
262
+ happen.
263
+
264
+ **Auto-decision carve-out.** The create-mode success path runs unattended on a fully-passing
265
+ `--worktree --auto` batch — it does **not** pause even though it performs a merge and a branch
266
+ deletion. That is the explicit single exception to Auto-Decision Principle #6 (`cross-cutting.md`),
267
+ which otherwise pauses any `--merge` / branch-deletion action regardless of `--auto`. The exception
268
+ is safe because `git merge --ff-only` fails closed (it refuses rather than risk losing work), the
269
+ branch deletion runs only after the merge has landed, persisted, and cleaned up — and is
270
+ existence-guarded (`git rev-parse --verify --quiet`), so a removed branch is never queried;
271
+ WT-5 retains the worktree and branch whenever FF is impossible or any task fails.
272
+ Reuse mode is **narrower** than the carve-out (it merges but does not delete the branch), so the
273
+ carve-out text needs no widening.
274
+
275
+ **Lifecycle-DB disposition (task 0701 R2d, amended by 0720).** The worktree has its own `.spur`
276
+ lifecycle DB, and WT-4/WT-5 remove or retain that tree — the DB state does **not** travel with the
277
+ merge. One contract, no alternatives:
278
+
279
+ - **Committed task files own lifecycle state.** The **committed task file is authoritative**: after
280
+ a green merge the branch's task files already read `done`/`testing` in the invoking tree; no
281
+ `spur task update` or `spur task record` replay runs post-merge. Replay is not "one of two
282
+ options" — it is removed: it writes `updated_at`-only churn and can never restore worktree-only
283
+ DB rows.
284
+ - **The persisted invoking-tree artifacts own evidence.** The Step 5 batch report at
285
+ `.spur/run/worktree-<marker-id>-batch-report.md` and the copied verdict JSONs under
286
+ `.spur/run/worktree-<marker-id>-verdicts/` (written before WT-4 removal) are the batch/verdict
287
+ record.
288
+ - **Per-worktree lifecycle DB rows intentionally do not travel.** No `task_run_links` import, no
289
+ cross-database provenance synthesis — the DB is per-tree by design.
290
+ - **No timestamp-only corpus churn.** Post-merge the invoking tree's DB statuses may read stale
291
+ relative to the committed files; that divergence is accepted, not repaired. Do not run
292
+ `task update`/`task record` to "catch up" the DB, and do not repair churn with
293
+ `git checkout -- docs/tasks*/`.
294
+
295
+ This is a deliberate choice over auto-migrating DB state: the committed corpus files are the durable
296
+ record and the persisted run artifacts are the evidence record.
297
+
298
+ ### WT-4r — rebasing a retained branch in a linked worktree
299
+
300
+ When a pre-merge halt is resolved by rebasing the retained branch (rather than landing as-is), two
301
+ things differ from rebasing in the main tree. Getting them wrong silently leaves a **detached HEAD
302
+ holding verified work while the branch ref still points at the old tip** (observed 2026-10-10):
303
+
304
+ - **The rebase state lives under the worktree's metadata, not `.git/`.** In a linked worktree the
305
+ in-progress directories are `.git/worktrees/<worktree-name>/rebase-merge` (or `rebase-apply`);
306
+ `ls .git/rebase-merge` reports nothing even mid-rebase, so its absence is not evidence that none is
307
+ running.
308
+ - **HEAD is the authority, not a directory probe.** Confirm both before continuing or amending:
309
+
310
+ ```bash
311
+ git symbolic-ref -q HEAD || echo DETACHED # a live rebase has a detached HEAD
312
+ git rev-parse HEAD; git rev-parse "$BRANCH" # finished-but-unfinalized replay: the refs disagree
313
+ ```
314
+
315
+ When the replay finished but was never finalized, `git rebase --continue` is the repair — it
316
+ advances the branch to the verified commit and clears the state. Amending (or committing) at that
317
+ moment targets the **detached** commit and leaves the branch behind: it looks successful and the
318
+ work looks landed when it is not.
319
+
320
+ ### WT-5 — Failure path: retain and report (R5)
321
+
322
+ On any per-task failure, batch halt, HITL pause that ends the run, or a WT-4 halt, the
323
+ worktree directory and branch are left **intact**. No destructive automation on this path under
324
+ any flag combination (`--auto`, `--force`, `--keep-going` — all leave the worktree in place). The
325
+ marker records which class of halt occurred (frozen vocabulary):
326
+
327
+ - **Pre-merge failure** (failed checkout, zero-commit guard, divergent base, failed FF): the merge
328
+ never landed, so the marker **stays at `status: active`** — the WT-3 marker stands and WT-5 is
329
+ its disposition. Nothing was merged onto the base ref.
330
+ - **Landed-but-incomplete failure** (persist-out, holder cleanup, or worktree removal failed after
331
+ a landed merge): the marker records `status: retained` + `mergeCommit` BATCH_TIP — a truthful
332
+ report that the merge landed. The retained path reads the captured `BATCH_TIP` only; a removed
333
+ branch is never queried.
334
+
335
+ The worktree's own `.spur` lifecycle DB is retained with the tree,
336
+ so nothing is lost on either path (see the WT-4 lifecycle-DB disposition for the merged case —
337
+ task 0701 R2d). Emit a retention report in the existing halt-report shape:
338
+
339
+ **Persist-out skip recovery (task 1067 R2, E71).** When evidence was lost to a landing that
340
+ skipped persist-out (or the pre-removal assertion fired too late), verdicts remain derivable
341
+ from the task records — `spur task show <wbs> --json` carries the recorded verdict and its
342
+ proof block, so a lost `<wbs>-verdict.json` is regenerated by re-recording or copying from
343
+ the task store. Feature run/latest receipts are re-runnable, not recovered: rerun the
344
+ feature-verification workflow in the invoking tree —
345
+ `spur workflow run feature-verification.yaml --vars '{"featureId":"<id>"}'` — which rewrites
346
+ the receipt files it owns. Divergent files are never auto-overwritten; reconcile by hand.
347
+
348
+ ```
349
+ ## Worktree retained — <command> <selector>
350
+
351
+ **Halt cause:** <one-line cause — batch halted at task <wbs> / non-FF base ref / HITL pause / landed-but-incomplete cleanup failure>
352
+ **Worktree path:** ../<worktree-dir>
353
+ **Branch:** sp/<command>-<selector-slug>-<short-id>
354
+ **Base ref:** <base-ref> (<base-sha>)
355
+ **Merge state:** pre-merge failure — nothing landed | landed-but-incomplete — mergeCommit <batch-tip-sha>
356
+
357
+ The worktree and its branch are intact.
358
+ pre-merge failure: nothing was merged onto the base ref.
359
+ landed-but-incomplete: the batch merge LANDED as <batch-tip-sha> on <base-ref>; retention covers post-merge persistence/cleanup only.
360
+ Resume, merge, or discard:
361
+
362
+ resume: cd <worktree-path> && <command> --continue --worktree <worktree-path>
363
+ merge: git checkout <base-ref> && git merge <branch> # resolve conflicts manually
364
+ discard: git worktree remove <worktree-path> && git branch -D <branch> && spur projects remove <worktree-path>
365
+ ```
366
+
367
+ When the halt cause is `non-FF base ref`, the report replaces the one-line `merge:` hint with this
368
+ ordered divergence recipe, run by the operator — the driver never merges, rebases, or resolves
369
+ conflicts itself. Other halt causes (task failure, HITL pause) keep the hint as printed:
370
+
371
+ ```
372
+ # 1. decide whether conflicts need resolving FIRST, because that decides the merge form.
373
+ # Use the rc of `git merge-tree --write-tree` (git ≥ 2.38; `git --version` here reports 2.55):
374
+ # git merge-tree --write-tree <base-ref> <branch> >/dev/null && echo "clean — use the atomic form" \
375
+ # || echo "conflicts — use the two-phase form below"
376
+ # Do NOT use the legacy three-argument form
377
+ # (`git merge-tree $(git merge-base …) <base-ref> <branch> | grep '^<<<<<<<'`): it reports no
378
+ # conflict markers at all — verified 2026-10-08 on 2.55 against a synthetic repo, where it
379
+ # printed 0 markers for a real same-file conflict and sent this report's own landing into the
380
+ # atomic form on a diverging base (the merge then conflicted and had to be driven two-phase by
381
+ # hand). If `--write-tree` is unavailable on the operator's git, assume conflicts: the
382
+ # two-phase form is always correct, it just costs the resolution window described in 1a.
383
+ #
384
+ # 1a. CLEAN (the common case): integrate as ONE atomic merge commit. Never a rebase — task
385
+ # evidence cites the branch's commit SHAs. This is the file's sanctioned merge commit.
386
+ # Do NOT split this into a two-phase merge + commit when nothing needs resolving: that spans
387
+ # the whole gate run, and anything in that window that touches the repository's git state
388
+ # (a test, a hook, a tool invocation) discards MERGE_HEAD silently. The failure is invisible
389
+ # until the history is read — it produced a single-parent commit in run ada5a36c (task 1090),
390
+ # which then needed an extra empty ancestry merge to repair. A cheap probe could not
391
+ # reproduce it (MERGE_HEAD survived `bun run lint`, the rule preset and a test-file subset),
392
+ # so the cross-command dependency is removed rather than diagnosed.
393
+ git checkout <base-ref> && git merge --no-ff -m "<prepared message>" <branch>
394
+
395
+ # 1b. CONFLICTING: the two-phase form is correct here, because the resolution work must land
396
+ # between the merge and the commit. Everything in steps 2–4 belongs to this branch only.
397
+ # A divergent branch cannot fast-forward, so the merge flag from 1a is not needed here.
398
+ # Fingerprint FIRST — step 3 stages through this guard, so a path that was already dirty
399
+ # before the merge is refused as foreign instead of riding into the merge commit (1129 R2).
400
+ # This commit lands in the INVOKING tree, so the fingerprint is taken there too (`$RUN_ID` is
401
+ # the batch's run id, the same one WT-3b's worktree fingerprint used — the two trees keep two
402
+ # artifacts under the same id):
403
+ # RUN_ID="<marker-id>" # the WT-3 marker's id
404
+ # GUARD=plugins/sp/scripts/commit-guard.ts; [ -f config/plugin-scripts.json -a -f "$GUARD" ] || GUARD="$(superskill script path sp commit-guard.mjs 2>/dev/null)"
405
+ # bun "$GUARD" start --run "$RUN_ID"
406
+ # git checkout <base-ref> && git merge --no-commit <branch>
407
+ # 2. resolve source conflicts by hand; generated files are then regenerated with the project's
408
+ # generator, never hand-merged
409
+ # (this repo: bun run build:plugin-lib && bun run --filter @gobing-ai/spur build:bundle)
410
+ # 3. stage every resolved path and regenerated bundle through the guard — an unmerged or
411
+ # unstaged path makes step 5 abort, and a conflict-marked path exits 3 (resolve, then retry):
412
+ # bun "$GUARD" stage --run "$RUN_ID" -- <resolved paths and regenerated bundles>
413
+ # Forbidden here as in every driver commit step: `git add -A` and `git add .`.
414
+ # 4. run qualityGateCmd once, after ALL conflicts are resolved
415
+ # 5. commit the merge with the prepared message file — CHECK THE PARENTS FIRST (step 5a)
416
+ # git commit -F <message-file>
417
+ #
418
+ # 5a. ALWAYS verify the merge really is a merge before treating it as landed:
419
+ # git log -1 --format='%h parents=%p' # must print TWO parents
420
+ # One parent means MERGE_HEAD was lost somewhere between steps 1 and 5 — the content is on
421
+ # the base ref but the branch is not an ancestor, so `git branch -d` would refuse and the
422
+ # retained worktree's provenance would not be readable from the base ref. Recover by
423
+ # re-running step 1a against the already-landed tree (a content-free merge commit restores
424
+ # ancestry).
425
+
426
+ # 6. persist evidence out (WT-4a), then WT-4b/4c cleanup, and set the marker to merged
427
+ inline-run-setup --persist-out --from <worktree> --task-file …
428
+ ```
429
+
430
+ The report reuses the [`--next` chain contract](flag-glossary.md#--next-chain-contract) halt-report
431
+ shape (halt cause + where + why), not new vocabulary. Retention is the right default: these batches
432
+ are long and already resumable via `--continue`; auto-deleting is data loss, auto-merging is a
433
+ partial result presented as a whole. The answer to "what happens if it fails" is "nothing happens,
434
+ and we tell you where the work is."
435
+
436
+ **Partial worktree-removal recovery is inspection-only.** If a create-mode run halted partway
437
+ through removal (leftover directory, stale registration), the recovery pass gathers facts and
438
+ nothing else: `git worktree list` (is the tree still registered?), `ls` of the path (what remains
439
+ on disk?), and the marker / `~/.config/spur/projects.json` registration state. Before removing
440
+ anything, verify the leftover path is the batch's **owned** path — match it against the marker's
441
+ `path`/`branch` (or the resolved `--worktree <name>` identity), never a guessed sibling directory.
442
+ Recursive deletion of leftover directories that survived `git worktree remove` is **never
443
+ automatic** — it requires explicit operator authorization. No automatic rebase, recovery merge,
444
+ `git worktree prune`, or holder kill runs on this path.
445
+
446
+ ### WT-0 — markerless worktree runs (task 1148 R3)
447
+
448
+ A worktree created by hand (or by a foreign tool) has **no WT-3 marker**, so nothing on disk tells a
449
+ later landing step that a branch is waiting. `.spur/run` is disposable scratch under ADR-131, so a
450
+ marker there could never be the durable obligation anyway.
451
+
452
+ Instead, a run that closes `done` while its cwd is a **linked git worktree** and no WT-3 marker names
453
+ it appends the obligation to its own canonical run record in the **owning tree**:
454
+
455
+ ```
456
+ landing: required branch=<branch> path=<worktree> base=<ref>
457
+ ```
458
+
459
+ appended to `.spur/memory/runs/<runId>.md`. Detection is filesystem-only — `.git` is a file whose
460
+ `gitdir:` line plus the linked `commondir` resolve the owning tree; the step spawns no process, and a
461
+ run in the main working tree writes no line.
462
+
463
+ **The sweep.** Find every run still waiting to be landed:
464
+
465
+ ```bash
466
+ rg -l '^landing: required' .spur/memory/runs
467
+ ```
468
+
469
+ Each hit names the branch, the tree and the base ref, which is everything `--worktree <name>` needs to
470
+ adopt and land it (reuse mode, § WT-2). A later WT-4 merge or retain rewrites the line to
471
+ `landing: merged <sha>` or `landing: retained`; the obligation is append-only until then.