@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.global.yaml +17 -0
- package/config/decisions/gates.yaml +36 -0
- package/config/decisions/history-anatomy.yaml +35 -0
- package/config/decisions/idea-pipeline.yaml +38 -0
- package/config/decisions/task-pipeline.yaml +55 -0
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/sp-runtime-path.yaml +2 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/quality/coverage-gate.yaml +5 -3
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/strict/runtime-boundaries.yaml +11 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/workflows/feature-lifecycle.yaml +9 -2
- package/config/workflows/history-anatomy.yaml +22 -0
- package/config/workflows/idea-pipeline.yaml +75 -0
- package/config/workflows/task-pipeline.yaml +368 -49
- package/config/workflows/wrapup-pipeline.yaml +106 -13
- package/package.json +9 -9
- package/plugins/sp/README.md +25 -2
- package/plugins/sp/agents/super-planner.md +11 -0
- package/plugins/sp/agents/super-reviewer.md +39 -11
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-changelog.md +5 -4
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-parallel.md +8 -2
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +14 -7
- package/plugins/sp/commands/dev-run.md +17 -4
- package/plugins/sp/commands/dev-runall.md +21 -10
- package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
- package/plugins/sp/hooks/context-session-start.ts +1 -1
- package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
- package/plugins/sp/hooks/hooks.json +45 -1
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
- package/plugins/sp/lib/inline-run.generated.mjs +364 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
- package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
- package/plugins/sp/lib/run-summary-core.ts +561 -0
- package/plugins/sp/lib/transcript.ts +275 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +335 -0
- package/plugins/sp/scripts/batch-plan.ts +100 -0
- package/plugins/sp/scripts/batch-preflight.mjs +51 -3
- package/plugins/sp/scripts/batch-preflight.ts +72 -1
- package/plugins/sp/scripts/commit-guard.mjs +216 -0
- package/plugins/sp/scripts/commit-guard.ts +249 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
- package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
- package/plugins/sp/scripts/inline-run-setup.ts +65 -97
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/quality-gate.mjs +30407 -73
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +88 -38
- package/plugins/sp/scripts/residual-scan.ts +35 -38
- package/plugins/sp/scripts/run-summary.mjs +693 -0
- package/plugins/sp/scripts/run-summary.ts +105 -0
- package/plugins/sp/scripts/session-timeline.mjs +331 -0
- package/plugins/sp/scripts/session-timeline.ts +204 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
- package/plugins/sp/scripts/wrapup-steps.ts +203 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -4
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
- package/plugins/sp/skills/session-review/SKILL.md +77 -24
- package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
- package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
- package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
- package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
- package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/decision-catalog.schema.json +226 -0
- package/schemas/spur-config.schema.json +25 -0
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +28165 -19051
- package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
- package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
- package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
- package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
- package/web/_astro/channel.DZVfto-r.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
- package/web/_astro/index.COh_kBxN.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
- package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
- package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
- package/web/board-runtime.json +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
- package/web/_astro/channel.6IFIKBhu.js +0 -1
- 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.
|