@gobing-ai/spur 0.3.98 → 0.3.99

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 (77) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/plugin-scripts.json +5 -0
  3. package/config/templates/task/brainstorm.md +1 -1
  4. package/config/templates/task/feature-impl.md +1 -1
  5. package/config/templates/task/issue.md +1 -1
  6. package/config/templates/task/meta.md +1 -1
  7. package/config/templates/task/review.md +1 -1
  8. package/config/templates/task/standard.md +1 -1
  9. package/package.json +9 -9
  10. package/plugins/sp/lib/idea-handoff.generated.mjs +37 -4
  11. package/plugins/sp/lib/inline-run.generated.mjs +38 -5
  12. package/plugins/sp/lib/residual-scan.generated.d.mts +5 -0
  13. package/plugins/sp/lib/residual-scan.generated.mjs +45 -5
  14. package/plugins/sp/plugin.json +1 -1
  15. package/plugins/sp/scripts/persist-out-check.mjs +164 -0
  16. package/plugins/sp/scripts/persist-out-check.ts +185 -0
  17. package/plugins/sp/scripts/residual-scan.mjs +50 -8
  18. package/plugins/sp/scripts/residual-scan.ts +4 -3
  19. package/plugins/sp/scripts/task-diffstat.mjs +17 -5
  20. package/plugins/sp/scripts/task-diffstat.ts +25 -4
  21. package/plugins/sp/skills/code-verification/SKILL.md +3 -0
  22. package/plugins/sp/skills/code-verification/references/verdict-schema.md +17 -1
  23. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +40 -7
  24. package/plugins/sp/skills/spur-dev/references/execution-batch.md +216 -37
  25. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +3 -1
  26. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +107 -3
  27. package/spur.js +1662 -386
  28. package/web/_astro/{BoardApp.CYbe-OTb.js → BoardApp.BVp-LYi6.js} +101 -101
  29. package/web/_astro/{BoardApp.Dflq5a8x.js → BoardApp.i4eh_fHA.js} +1 -1
  30. package/web/_astro/{TaskDetail.CK7oXddP.js → TaskDetail.C5Cu8LdB.js} +1 -1
  31. package/web/_astro/{arc.BIqkDATc.js → arc.Bu5XpBEz.js} +1 -1
  32. package/web/_astro/{architectureDiagram-3BPJPVTR.D0B3SvWv.js → architectureDiagram-3BPJPVTR.9vHQTvhF.js} +1 -1
  33. package/web/_astro/{blockDiagram-GPEHLZMM.C2YyKQmL.js → blockDiagram-GPEHLZMM.Cn2Xl7D6.js} +1 -1
  34. package/web/_astro/{c4Diagram-AAUBKEIU.DmtTYrt_.js → c4Diagram-AAUBKEIU.7o0P_QDo.js} +1 -1
  35. package/web/_astro/channel.6IFIKBhu.js +1 -0
  36. package/web/_astro/{chunk-2J33WTMH.HsRXbh7l.js → chunk-2J33WTMH.C3zZFsNt.js} +1 -1
  37. package/web/_astro/{chunk-4BX2VUAB.obE_P-R3.js → chunk-4BX2VUAB.CNy_M7fO.js} +1 -1
  38. package/web/_astro/{chunk-55IACEB6.CjWTF5mz.js → chunk-55IACEB6.2TiPnLBL.js} +1 -1
  39. package/web/_astro/{chunk-727SXJPM.DMo799Ax.js → chunk-727SXJPM.5CGSyKAp.js} +1 -1
  40. package/web/_astro/{chunk-AQP2D5EJ.-cE6JOKZ.js → chunk-AQP2D5EJ.w-ZIgfsK.js} +1 -1
  41. package/web/_astro/{chunk-FMBD7UC4.D6Rw6HIk.js → chunk-FMBD7UC4.BkcEyhKa.js} +1 -1
  42. package/web/_astro/{chunk-ND2GUHAM.d5rBD73Z.js → chunk-ND2GUHAM.mxKKbBG4.js} +1 -1
  43. package/web/_astro/{chunk-QZHKN3VN.r3ox40Q3.js → chunk-QZHKN3VN.cyA0z9aT.js} +1 -1
  44. package/web/_astro/{classDiagram-4FO5ZUOK.CGGbvjjJ.js → classDiagram-4FO5ZUOK.CqtUzwhX.js} +1 -1
  45. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CGGbvjjJ.js → classDiagram-v2-Q7XG4LA2.CqtUzwhX.js} +1 -1
  46. package/web/_astro/{cose-bilkent-S5V4N54A.CmKtPd4l.js → cose-bilkent-S5V4N54A.BExPTopI.js} +1 -1
  47. package/web/_astro/{cynefin-OW5HDTMX.QNg5GXs8.js → cynefin-OW5HDTMX.Bq2i10Cn.js} +1 -1
  48. package/web/_astro/{dagre-BM42HDAG.B1SabsjC.js → dagre-BM42HDAG.By_HsOuv.js} +1 -1
  49. package/web/_astro/{diagram-2AECGRRQ.RtDbNj89.js → diagram-2AECGRRQ.BtU0b7hz.js} +1 -1
  50. package/web/_astro/{diagram-5GNKFQAL.IzZpuM-Y.js → diagram-5GNKFQAL.D00ztMdT.js} +1 -1
  51. package/web/_astro/{diagram-KO2AKTUF.DPTF_fgv.js → diagram-KO2AKTUF.DMVJWaUN.js} +1 -1
  52. package/web/_astro/{diagram-LMA3HP47.CCyza39P.js → diagram-LMA3HP47.Cxqrjg5-.js} +1 -1
  53. package/web/_astro/{diagram-OG6HWLK6.BXAi8zcZ.js → diagram-OG6HWLK6.2lz_1B5h.js} +1 -1
  54. package/web/_astro/{erDiagram-TEJ5UH35.DUDX7_MR.js → erDiagram-TEJ5UH35.BVQEuJ3T.js} +1 -1
  55. package/web/_astro/{flowDiagram-I6XJVG4X.C63QTh3N.js → flowDiagram-I6XJVG4X.Qlyo90ky.js} +1 -1
  56. package/web/_astro/{ganttDiagram-6RSMTGT7.CnlZJSwG.js → ganttDiagram-6RSMTGT7.DRwb8HHZ.js} +1 -1
  57. package/web/_astro/{gitGraphDiagram-PVQCEYII.CTqIrOPC.js → gitGraphDiagram-PVQCEYII.YfoPthfS.js} +1 -1
  58. package/web/_astro/{infoDiagram-5YYISTIA.BYMiDFh9.js → infoDiagram-5YYISTIA.DvFREck1.js} +1 -1
  59. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CRiQmI-z.js → ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js} +1 -1
  60. package/web/_astro/{journeyDiagram-JHISSGLW.YQgWBsEU.js → journeyDiagram-JHISSGLW.BDMT_AFb.js} +1 -1
  61. package/web/_astro/{kanban-definition-UN3LZRKU.1ECnrrK1.js → kanban-definition-UN3LZRKU.D6Fh3gj9.js} +1 -1
  62. package/web/_astro/{linear.DzVA71yT.js → linear.Dxw00fGQ.js} +1 -1
  63. package/web/_astro/{mermaid.core.BK7P2Sr_.js → mermaid.core.CsoxUWa0.js} +4 -4
  64. package/web/_astro/{mindmap-definition-RKZ34NQL.BvXyWZK7.js → mindmap-definition-RKZ34NQL.CN8oeHLu.js} +1 -1
  65. package/web/_astro/{pieDiagram-4H26LBE5.BiXk1TW3.js → pieDiagram-4H26LBE5.C7ADqQ_7.js} +1 -1
  66. package/web/_astro/{quadrantDiagram-W4KKPZXB.krvBH8lh.js → quadrantDiagram-W4KKPZXB.Cw9lKyHP.js} +1 -1
  67. package/web/_astro/{requirementDiagram-4Y6WPE33.ih1gL25c.js → requirementDiagram-4Y6WPE33.B1ni-z4q.js} +1 -1
  68. package/web/_astro/{sankeyDiagram-5OEKKPKP.CpUxkv4u.js → sankeyDiagram-5OEKKPKP.BhexdjVr.js} +1 -1
  69. package/web/_astro/{sequenceDiagram-3UESZ5HK.clAfeN7y.js → sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js} +1 -1
  70. package/web/_astro/{stateDiagram-AJRCARHV.DpXWb8-m.js → stateDiagram-AJRCARHV.CkuMt27u.js} +1 -1
  71. package/web/_astro/{stateDiagram-v2-BHNVJYJU.ClZb9GIH.js → stateDiagram-v2-BHNVJYJU.Dc_aazUA.js} +1 -1
  72. package/web/_astro/{timeline-definition-PNZ67QCA.BLcX5dh7.js → timeline-definition-PNZ67QCA.ja7sUFhH.js} +1 -1
  73. package/web/_astro/{vennDiagram-CIIHVFJN.oGgpKMpx.js → vennDiagram-CIIHVFJN.BxIshX_K.js} +1 -1
  74. package/web/_astro/{wardleyDiagram-YWT4CUSO.BPHtMtt-.js → wardleyDiagram-YWT4CUSO.DfHlC0sh.js} +1 -1
  75. package/web/_astro/{xychartDiagram-2RQKCTM6.BNZOPYSk.js → xychartDiagram-2RQKCTM6.DSnhHwCh.js} +1 -1
  76. package/web/index.html +1 -1
  77. package/web/_astro/channel.JuKVyuhZ.js +0 -1
@@ -19,13 +19,17 @@
19
19
  * other probe problem writes `sensitive: true` (and exits 0) so the lane degrades to
20
20
  * safety, never to fast.
21
21
  *
22
+ * The artifact is written atomically (tmp + rename), so redirecting stdout onto the artifact path
23
+ * cannot corrupt it — the rename swaps in the real JSON after the shell's redirect truncates the
24
+ * old inode (1067 R3); that redirect is still refused with a nonzero exit. Normal output is silent.
25
+ *
22
26
  * Node-builtin imports only; the git lookup is a spawnable `GitRunner` so tests can fake
23
27
  * it; `main(argv, env, options)` is the same injection point the workflow wrapper and its
24
28
  * node twin exercise. Only an empty `wbs` (mis-invocation) exits nonzero.
25
29
  */
26
30
 
27
31
  import { spawnSync } from 'node:child_process';
28
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
32
+ import { fstatSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
29
33
  import { join } from 'node:path';
30
34
  import { getEnvVars } from '../lib/env';
31
35
 
@@ -166,10 +170,18 @@ export function runDiffstat(
166
170
  const failSafe = (reason: string): DiffstatResult => {
167
171
  process.stderr.write(`task-diffstat: ${reason} — failing safe (sensitive)\n`);
168
172
  const row: Diffstat = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
169
- writeFileSync(abs(relResult), `${JSON.stringify(row)}\n`);
173
+ writeArtifact(abs(relResult), row);
170
174
  return { ...row, resultFile: relResult, exitCode: 0 };
171
175
  };
172
176
 
177
+ let stdoutIsArtifact = false; // 1067 R3: stdout redirected onto the artifact itself clobbered it once
178
+ try {
179
+ const [out, art] = [fstatSync(1), statSync(abs(relResult))];
180
+ stdoutIsArtifact = out.dev === art.dev && out.ino === art.ino;
181
+ } catch {
182
+ /* artifact absent or stdout not a regular file — nothing to compare */
183
+ }
184
+
173
185
  let base = '';
174
186
  try {
175
187
  base = readFileSync(abs(join(relRun, `${wbs}-base.sha`)), 'utf8').trim();
@@ -203,8 +215,17 @@ export function runDiffstat(
203
215
  const all = [...paths].filter((p) => p.length > 0).sort();
204
216
  const sensitive = all.some((p) => sensitiveReasonForPath(p) !== null);
205
217
  const row: Diffstat = { files: all.length, insertions, deletions, paths: all, sensitive };
206
- writeFileSync(abs(relResult), `${JSON.stringify(row)}\n`);
207
- return { ...row, resultFile: relResult, exitCode: 0 };
218
+ writeArtifact(abs(relResult), row);
219
+ if (stdoutIsArtifact)
220
+ process.stderr.write('task-diffstat: stdout redirected onto the artifact — kept; redirect stdout elsewhere\n');
221
+ return { ...row, resultFile: relResult, exitCode: stdoutIsArtifact ? 1 : 0 };
222
+ }
223
+
224
+ /** Atomic artifact write (tmp + rename): a stdout redirect onto the path cannot interleave (1067 R3). */
225
+ function writeArtifact(absPath: string, row: Diffstat): void {
226
+ const tmp = `${absPath}.tmp`;
227
+ writeFileSync(tmp, `${JSON.stringify(row)}\n`);
228
+ renameSync(tmp, absPath);
208
229
  }
209
230
 
210
231
  export const TASK_DIFFSTAT_USAGE = 'usage: task-diffstat (env: wbs)';
@@ -336,6 +336,7 @@ Verdict: PASS (or PARTIAL / FAIL)
336
336
 
337
337
  ```markdown
338
338
  Verdict: PASS
339
+ Confidence: HIGH
339
340
 
340
341
  ### Per-Requirement Traceability
341
342
  | Req | Status | Evidence |
@@ -354,6 +355,8 @@ Verdict: PASS
354
355
  ```
355
356
 
356
357
  The per-requirement traceability table MUST use `| Req | Status | Evidence |` (exactly this header, no `R#`/`R`/`Requirement` variant — `Status` in column 2 — and no extra columns between Req and Status). The Acceptance Criteria table MUST use `| AC | Status | Evidence Type | Evidence |`.
358
+ The answer MUST carry exactly one `Confidence: HIGH|MEDIUM|LOW` line (task 1068): the verifier's stated confidence in the verdict — HIGH = fresh executable evidence + re-read anchors this run; MEDIUM = partial reliance on prior evidence, external/delegated outcomes, or an unstable tree; LOW = key evidence not re-executed. A missing or out-of-vocabulary line fails lint (`confidence-missing` / `confidence-value`) and blocks derivation; the value lands in the verdict artifact's `confidence` field and the recorded `## Testing` section.
359
+ The answer MUST cite a source for external API/library claims in Requirement/AC evidence cells: a cell asserting a scoped-package (`@scope/name`), `node_modules/`, semver-version (`x.y.z`), or backticked function-call (`` `name(args)` ``) claim is rejected with `evidence-citation` unless the SAME cell carries a citation — a repo `path:line(-end)` (backticked or bare), the external named-origin form (@origin `path` line N), or a URL. Cross-cell references ("receipt above") do not count; marker-free receipts ("bun test 398 pass / 0 fail") need no citation.
357
360
  **MUST NOT:** place a `Severity` column between `Req` and `Status` in the authoring contract.
358
361
  The parser is tolerant of these variants (defense-in-depth), but the authoring contract is
359
362
  canonical.
@@ -115,7 +115,9 @@ AC ids that do not resolve to one accepted identity — a task AC checklist labe
115
115
  (exact/bare title, `Scenario:` prefix, bracket tags, `AC-N`); paraphrases and ambiguous aliases
116
116
  fail — invalid status (`MET | PARTIAL | UNMET` for requirements;
117
117
  `N/A` additionally allowed for AC), invalid evidence type (`test | command | static-ref |
118
- manual-review | llm-judge | n/a`, or a `+` compound), and empty evidence. Interrupted runs keep the
118
+ manual-review | llm-judge | n/a`, or a `+` compound), and empty evidence. Evidence cells that assert an
119
+ external API/library claim without a same-cell citation are rejected with `evidence-citation` (see
120
+ "Source citations for external claims" below). Interrupted runs keep the
119
121
  rows that pass the lint and complete only the missing IDs on retry.
120
122
 
121
123
  **Feature-credited AC ids are narrower than the lint's accepted set (task 0966 finding).** A row keyed
@@ -136,6 +138,20 @@ the run actually verified. Since 0804 R9 the checker ignores complete parsed cit
136
138
  scanning for subjects, so a citation's filename (including snake_case paths) can never become a
137
139
  false subject — a real absent symbol, nonexistent file, or invalid range still reports.
138
140
 
141
+ ## Source citations for external claims (task 1070)
142
+
143
+ A Requirement/AC evidence cell that asserts an external API/library claim must carry a verifiable
144
+ citation in the SAME cell, or lint rejects the row with `evidence-citation` (blocking derivation).
145
+
146
+ - Claim markers (closed set): a scoped-package mention (`@scope/name`), a `node_modules/` reference,
147
+ a semver version claim (`x.y.z`), or a backticked function-call-shaped token (`` `name(args)` ``).
148
+ - Accepted citation forms (any one clears the cell): a repo `path:line(-end)` (backticked or bare),
149
+ the external named-origin form (@origin `path` line N — line number outside the backticks), or an
150
+ http(s) URL.
151
+ - The rule is cell-local: a citation in another row or a "receipt above" cross-reference does not
152
+ satisfy it. Marker-free receipts (test counts, reviewed-log notes) need no citation. The check is
153
+ presence-only — which citation proves which claim is not deterministically decidable.
154
+
139
155
  ## Checks evidence
140
156
 
141
157
  Wave C verification can emit the following additive `checks[]` rows:
@@ -30,10 +30,28 @@ Rules:
30
30
  ## Task-side numbering (`AC<n>`)
31
31
 
32
32
  `R<n>` is the **feature** scenario key and the **task Requirements** key. Task `### Acceptance
33
- Criteria` items therefore use their own namespace: `- [ ] AC1 — <title>` or `Scenario: AC1 — <title>`,
34
- numbered task-locally. Carry a feature scenario by copying its title after the prefix (`normalizeTitle`
35
- strips both `AC<n>` and `R<n>`, so DD-09 matching is unaffected); bind a task requirement with
36
- `(req: R<n>)`. Legacy tasks that wrote `- [ ] R<n> —` / `Scenario: R<n> —` keep working unchanged.
33
+ Criteria` items therefore use their own namespace: `AC1, AC2, …`. Two frontmatter fields split the
34
+ gate work, and neither implies the other:
35
+
36
+ - **`ac_altitude`** owns the **feature subset** (DD-09). `graduating` (or absent) keeps
37
+ normalized-title matching against the linked feature (`normalizeTitle` strips both `AC<n>` and
38
+ `R<n>` prefixes, so copied titles match); `task-local` skips that check — and nothing else.
39
+ - **`ac_numbering: task-local`** owns **task requirement bindings**. It opts the AC section into the
40
+ `L3.ac-requirement-coverage` loop, which reads **`Scenario:` titles only**: bind requirements with
41
+ `(req: R1; R2)` in the title, or ship a legacy `Scenario: R1 — …` title, which still binds by its
42
+ own prefix.
43
+
44
+ **Preferred task-local form** (regression/fix tasks): `Scenario: AC1 — <concrete outcome> (req: R1)`
45
+ with Given/When/Then, declaring **both** `ac_altitude: task-local` and `ac_numbering: task-local`.
46
+
47
+ Parsed checkbox rows (`- [ ] AC1 — <title>`) are a supported form: they join checklist parsing, the
48
+ feature-subset match, and the terminal-status unchecked-box count — but they do **not** enter the
49
+ Scenario-only requirement-binding loop, so a checkbox row never substitutes for a `(req: …)` binding.
50
+
51
+ Raw freeform bullets (`- AC1 — …`: no checkbox, no `Scenario:`) parse as nothing — no subset match,
52
+ no requirement binding, no box counting. They survive in the corpus as **legacy unparsed records**:
53
+ read them, don't author them, and don't reach for them to dodge traceability. Completed tasks keep
54
+ their records; new work uses one of the two parsed forms above.
37
55
 
38
56
  ## Two AC tiers (authoring convention)
39
57
 
@@ -145,9 +163,11 @@ The `spur task verdict` answer lint declares the bold span of a single-line crit
145
163
  (`- **AC2 — The roster runtime is gone (R3).** Given …, when …, then …`) and its head before the
146
164
  first ` — ` or `:` — so answer rows may key `AC2` or `AC2 — The roster runtime is gone (R3).`. Keep
147
165
  at least one answer row keyed to the verbatim feature scenario title the task graduates, and keep
148
- requirement ids in the corpus form they are checked against (`- **R1** — …`): `L3.requirements-format`
149
- matches `R\d+` followed by a space, so a colon after the bold span silently drops the whole section
150
- to a warning.
166
+ requirement ids in the template-taught corpus form, `- [ ] R1. <text>`: that (or an unbolded `R1.`
167
+ variant) is what the task-local binding loop parses — bold wrapping (`**R1**`) passes the
168
+ `L3.requirements-format` ratio but binds nothing — and a colon after the bold span still silently
169
+ drops the whole section to a warning. Bold AC heads are likewise a verdict-table id form, not the AC
170
+ authoring form: author new AC rows in the plain `- [ ] AC1 —` / `Scenario: AC1 —` shapes.
151
171
 
152
172
  ### The id is exactly the scenario title — no Gherkin body appended
153
173
 
@@ -276,3 +296,16 @@ Use `task-local` for a fix/refactor task whose regression criteria are not the f
276
296
  contract. Keep a `graduating` task's scenario titles identical to the feature's so DD-09 stays
277
297
  satisfied. Absent-altitude is `graduating` — set `task-local` explicitly only where the subset rule
278
298
  truly does not apply (do not silently default new tasks to it).
299
+
300
+ ### Choosing an AC form (decision table)
301
+
302
+ | Task situation | Author AC as | Frontmatter | Gate effect |
303
+ | --- | --- | --- | --- |
304
+ | Criteria that graduate the feature's ship contract | `Scenario: AC1 — <feature scenario title>` | `ac_altitude: graduating` (or absent) | DD-09 subset enforced — drifted titles report `L4.uncovered-task-scenario`; `(req: …)` binding only when `ac_numbering: task-local` is also declared |
305
+ | Task-local regression/fix criteria | `Scenario: AC1 — <concrete outcome> (req: R1)` + Given/When/Then | `ac_altitude: task-local` + `ac_numbering: task-local` | subset skipped; every `- [ ] R<n>.` requirement must be bound by some scenario's `(req: …)` |
306
+ | Simple verifiable checklist rows | `- [ ] AC1 — <title>` | either altitude, per the rows above | joins checklist parsing and the subset match; **no** requirement binding (`Scenario:`-only) |
307
+ | Legacy raw bullets | `- AC1 …` (no checkbox, no `Scenario:`) | n/a | unparsed: invisible to subset, binding, and box checks — preserve in place, never author new |
308
+
309
+ `task-local` altitude skips only the feature-subset check. It does not waive the unchecked-box check,
310
+ required sections, the proof-bound verdict, line anchors, or any other done check — AC syntax alone,
311
+ at any altitude, never makes a task done.
@@ -626,6 +626,20 @@ resolution runs, the loop executes with the worktree as process cwd, and a termi
626
626
  retains after the batch report is emitted. Steps 1–5 themselves are not modified — only their cwd
627
627
  differs.
628
628
 
629
+ **Per-call tree pinning (task 1058).** A host shell call starts in an arbitrary directory and
630
+ inherits no cwd from the previous call, so "the loop runs with the worktree as process cwd" is a
631
+ bootstrap convenience, never a carried guarantee. Every host command boundary in this lifecycle —
632
+ corpus reads/writes, the WT-3b commit, WT-4's merge and evidence persistence, the WT-4a
633
+ persist-out — re-selects its intended tree within the same call via the canonical per-call pin
634
+ protocol ([inline-pipeline-driver.md](inline-pipeline-driver.md) § "Per-call execution-tree
635
+ pin"): `cd --` to the quoted absolute tree, verify `pwd -P` against the recorded physical tree,
636
+ verify Git top-level and expected branch, then execute the already-resolved Spur invocation. The
637
+ identity is consumed, not re-derived: it is the WT-3 marker's confirmed `path` + `branch` (or the
638
+ resolved worktree's physical path + checked-out branch in reuse mode). The one deliberate tree
639
+ change is WT-4: the terminal merge actions select the **invoking** tree, not the execution tree —
640
+ and pin it explicitly the same way. FF ancestry, marker and cleanup sequencing remain owned by
641
+ 1059.
642
+
629
643
  **Portability (R10).** Use portable `git worktree` commands only. Do **not** depend on the Claude
630
644
  Code `EnterWorktree`/`ExitWorktree` tools — the `sp` plugin ships to Codex, Gemini CLI, pi, omp, and
631
645
  OpenCode. The underlying git mechanics (create / list / remove / prune, sibling-directory naming,
@@ -765,6 +779,10 @@ the tree's own source:
765
779
 
766
780
  cd "<worktree>" && bun apps/cli/src/index.ts task check <wbs> --json
767
781
 
782
+ Run that as a per-call pin subshell (task 1058) — `cd --` plus the `pwd -P` / git-top-level /
783
+ branch identity checks of the canonical protocol (inline-pipeline-driver.md § "Per-call
784
+ execution-tree pin") — rather than relying on a persistent `cd` between host tool calls.
785
+
768
786
  Confirm isolation by making a distinctive change in the worktree and checking that
769
787
  the command reflects it.
770
788
 
@@ -832,13 +850,19 @@ Reuse mode resolves the marker by the resolved worktree's `path` (not by `comman
832
850
  ### WT-3b — Commit the batch's writes on `$BRANCH` (task 0701 R1)
833
851
 
834
852
  Before any terminal action, commit the batch's corpus writes **on `$BRANCH`, inside the
835
- worktree** — including the generated task files under `docs/tasks*/` and the kanban index:
853
+ worktree** — including the generated task files under `docs/tasks*/` and the kanban index. The
854
+ commit runs as a per-call pinned subshell (task 1058): it selects the execution tree inside the
855
+ same call, verifies the WT-3 identity, and never relies on a persistent `cd` or a trailing
856
+ `cd -` to restore the caller's directory:
836
857
 
837
858
  ```bash
838
- cd "../<worktree-dir>"
839
- git add <files-the-batch-wrote>
840
- git commit -m "<type>(<scope>): <command> <selector> batch writes"
841
- cd - >/dev/null
859
+ (
860
+ cd -- "../<worktree-dir>" || { echo "tree missing: expected ../<worktree-dir>" >&2; exit 1; }
861
+ [ "$(git branch --show-current)" = "$BRANCH" ] \
862
+ || { echo "branch mismatch: expected $BRANCH, got $(git branch --show-current)" >&2; exit 1; }
863
+ git add <files-the-batch-wrote>
864
+ git commit -m "<type>(<scope>): <command> <selector> batch writes"
865
+ )
842
866
  ```
843
867
 
844
868
  The FF-only git merge carries only commits — uncommitted writes in the worktree would be left
@@ -855,18 +879,66 @@ only what it created*):
855
879
 
856
880
  ```bash
857
881
  # Run these from the main tree (not inside the worktree) - you merge the worktree branch
858
- # back onto the base ref there:
859
- git checkout "$BASE_REF"
882
+ # back onto the base ref there. Per-call tree pinning (task 1058, § above): every command
883
+ # below selects the INVOKING tree (where the WT-3 marker lives), re-pinned with the canonical
884
+ # per-call pin protocol in its own subshell — never the execution tree. The whole sequence is
885
+ # fail-stop: checkout, guard, ancestry check, merge, landed verify, persistence, and cleanup
886
+ # each stop explicitly. Every tip/ancestry read uses local refs — no fetch runs anywhere:
887
+ MARKER=".spur/run/worktree-<marker-id>.json"
888
+ git checkout "$BASE_REF" \
889
+ || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
860
890
  # Guard (task 0701 R1): a zero-commit branch makes the FF-only git merge exit 0
861
- # ("Already up to date") while merging nothing — the two lines below would then delete
891
+ # ("Already up to date") while merging nothing — the lines below would then delete
862
892
  # the worktree holding the only copy of the batch's writes. Refuse instead:
863
- [ "$(git rev-list --count "$BASE_SHA..$BRANCH")" -gt 0 ] \
864
- || { echo "halt: branch carries no commits - nothing to merge" >&2; false; } # -> WT-5
893
+ [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
894
+ || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; } # -> WT-5, marker stays "active"
895
+ # Capture the batch tip BEFORE any cleanup or branch work: every later step (landed verify,
896
+ # mergeCommit, retained report) reads this captured value — a removed branch is never queried.
897
+ BATCH_TIP="$(git rev-parse "$BRANCH")" \
898
+ || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
899
+ # Fresh LOCAL base tip, re-read now; the marker's baseSha may be stale and never governs this
900
+ # decision (an advanced-but-ancestor base still FF-merges, a divergent base halts):
901
+ BASE_TIP="$(git rev-parse "$BASE_REF")" \
902
+ || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
903
+ # Ancestry check immediately before the sole mutation (the FF merge below). Command/read
904
+ # errors are distinguished from ordinary non-ancestry; both fail closed:
905
+ ANCESTRY_RC=0
906
+ git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
907
+ [ "$ANCESTRY_RC" -gt 1 ] \
908
+ && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
909
+ [ "$ANCESTRY_RC" -eq 1 ] && {
910
+ echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
911
+ git log --oneline "$BASE_TIP..$BATCH_TIP" >&2 # batch-side divergent commits
912
+ git log --oneline "$BATCH_TIP..$BASE_TIP" >&2 # base-side divergent commits
913
+ exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
865
914
  git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
866
- # if FF succeeded — WT-4a evidence persistence runs FIRST (Step 5, task 0720 R3):
915
+ [ $? -eq 0 ] \
916
+ || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; } # -> WT-5
917
+ # Landed verification before anything else runs: the captured BATCH_TIP must be an ancestor of
918
+ # the base ref as it now stands (base tips advancing again later stay valid — this binds the
919
+ # merge result, not the end state):
920
+ LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
921
+ || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
922
+ git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
923
+ || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
924
+ # --- Landed from here on. Any halt below is landed-but-incomplete: it records status
925
+ # --- "retained" + mergeCommit "$BATCH_TIP" via write_marker and stops — never a branch query.
926
+ write_marker () { # $1 = merged | retained; read-modify-write: preserve WT-3 fields (id, command, selector, createdAt, adopted, adoptedAt); jq already assumed by WT-3/task-resolution flows
927
+ mkdir -p "$(dirname "$MARKER")"
928
+ if [ -f "$MARKER" ]; then
929
+ TMP_MARKER="$(mktemp)"
930
+ jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
931
+ || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
932
+ mv "$TMP_MARKER" "$MARKER"
933
+ else
934
+ printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
935
+ "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
936
+ fi
937
+ }
938
+ # WT-4a evidence persistence runs FIRST after the landed verify (Step 5, task 0720 R3):
867
939
  # persist the batch report + verdict artifacts AND the per-run provenance (task
868
940
  # 0975 R1) into the invoking tree before anything below touches the worktree.
869
- # Any persistence failure routes to WT-5 — the worktree and branch are retained.
941
+ # Any persistence failure routes to WT-5 — landed-but-incomplete: retained marker + mergeCommit.
870
942
  WT_PATH="$(cd "../<worktree-dir>" && pwd)" # hoisted: needed by WT-4a AND WT-4b below
871
943
  # WT-4a provenance persist-out (task 0975 R1): copy the worktree DB's run rows plus
872
944
  # the .spur/memory/runs/<runId>.md + .state.json records into THIS tree. Run from the main
@@ -880,7 +952,7 @@ WT_PATH="$(cd "../<worktree-dir>" && pwd)" # hoisted: needed by WT-4a AND WT-4
880
952
  # NOT proceed to WT-4b removal:
881
953
  SETUP_SCRIPT=plugins/sp/scripts/inline-run-setup.ts; [ -f config/plugin-scripts.json -a -f "$SETUP_SCRIPT" ] || SETUP_SCRIPT="$(superskill script path sp inline-run-setup.mjs 2>/dev/null)"
882
954
  bun "$SETUP_SCRIPT" --persist-out --from "$WT_PATH" "${TASK_FILE_ARGS[@]}" \
883
- || { echo "halt: worktree run-record persist-out failed - worktree retained (WT-5)" >&2; exit 1; }
955
+ || { echo "halt: worktree run-record persist-out failed - worktree retained (WT-5)" >&2; write_marker retained; exit 1; }
884
956
  #
885
957
  # WT-4b — bounded CWD-holder cleanup (task 0720 R1). $WT_PATH above is the EXACT
886
958
  # absolute worktree path; a relative path or a stale entry matches the wrong processes.
@@ -911,39 +983,112 @@ if [ -n "$FINAL" ]; then
911
983
  PORTS="$(lsof -nP -a -p "$(echo "$FINAL" | paste -sd, -)" -iTCP -sTCP:LISTEN 2>/dev/null \
912
984
  | awk 'NR>1 {print $9}' | sort -u | paste -sd' ' -)"
913
985
  echo "halt: worktree still held by PID(s): $FINAL ${PORTS:+listening: $PORTS}" >&2
986
+ write_marker retained # landed-but-incomplete: retained + mergeCommit
914
987
  exit 1 # -> WT-5: retain worktree + branch,
915
988
  fi # NO prune/remove/branch delete
916
- git worktree remove "../<worktree-dir>"
917
- git branch -d "$BRANCH"
989
+ # Pre-removal evidence assertion (task 1067 R1, E71 enforcement): refuse to remove the
990
+ # worktree while it still owns evidence absent from (or divergent from) the invoking tree —
991
+ # the D63 landing skipped persist-out and silently lost verdicts + receipts. Task-file
992
+ # WBSes / run ids ride the same TASK_FILE_ARGS resolved for WT-4a (run ids: --run-id <id>).
993
+ CHECK_SCRIPT=plugins/sp/scripts/persist-out-check.ts; [ -f config/plugin-scripts.json -a -f "$CHECK_SCRIPT" ] || CHECK_SCRIPT="$(superskill script path sp persist-out-check.mjs 2>/dev/null)"
994
+ bun "$CHECK_SCRIPT" --from "$WT_PATH" "${TASK_FILE_ARGS[@]}" \
995
+ || { echo "WT-4 halt: worktree evidence would be abandoned - persist-out missing or stale (WT-5)" >&2; write_marker retained; exit 1; }
996
+ git worktree remove "../<worktree-dir>" \
997
+ || { echo "WT-4 halt: worktree remove failed after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
998
+ # Branch deletion runs only after the merge landed, persisted, and the worktree is gone —
999
+ # and it is existence-guarded, so a removed branch is never queried. -D over -d: the landed
1000
+ # verify already proved the merge landed, so an unmerged-refusal would only block proven-merged cleanup.
1001
+ if git rev-parse --verify --quiet "$BRANCH" >/dev/null; then
1002
+ git branch -D "$BRANCH" || { echo "WT-4 halt: cannot delete branch $BRANCH after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
1003
+ fi
918
1004
  # WT-4c — clean up registry entry in ~/.config/spur/projects.json (task 0924)
919
1005
  spur projects remove "$WT_PATH" 2>/dev/null || spur projects clean --json 2>/dev/null || true
920
- # update marker: status = "merged"
1006
+ # Success marker, written once and only here: status "merged" + mergeCommit "$BATCH_TIP" —
1007
+ # after landed verification, required persistence, and cleanup:
1008
+ write_marker merged
921
1009
  ```
922
1010
 
923
- On either guard firing — zero-commit branch (task 0701 R1), or surviving CWD holders (task 0720 R1)
924
- — fall through to **WT-5**: the worktree and branch are retained, never removed. While any holder
925
- remains, do **not** run `git worktree prune`, `git worktree remove`, or branch deletion. The holder
1011
+ Every halt above stops explicitly and falls through to **WT-5** — zero-commit branch (task 0701
1012
+ R1), failed checkout, divergent base, failed FF, persist-out failure (task 0975), or surviving CWD
1013
+ holders (task 0720 R1). Pre-merge failures leave the marker at `status: active` and the worktree +
1014
+ branch untouched. Landed-but-incomplete failures record `status: retained` + `mergeCommit`
1015
+ BATCH_TIP: the merge landed, and the retained report reads the captured tip only. While any holder
1016
+ remains, do **not** run `git worktree prune`, `git worktree remove`, or branch deletion — only an
1017
+ EMPTY holder set may proceed to those steps. The holder
926
1018
  halt report names every surviving PID; the listening port is best-effort — a CWD holder may own no
927
1019
  socket, and `lsof` port discovery failing must not hide the PIDs.
928
1020
 
929
1021
  #### Reuse mode — merge, retain
930
1022
 
931
- The FF-merge runs identically (same `git checkout "$BASE_REF" && git merge --ff-only "$BRANCH"`),
932
- but then the worktree and branch are **retained**, not removed:
1023
+ The fail-stop merge sequence runs identically — same guard, captured `BATCH_TIP`/`BASE_TIP`,
1024
+ ancestry check immediately before FF, `git merge --ff-only`, landed verification (per-call pinned
1025
+ to the invoking tree, task 1058) — but no WT-4a/4b/4c and no removal: the worktree and branch are
1026
+ **retained**. Pre-merge halts leave the marker `active`; a persistence failure after the landed
1027
+ merge records `retained` + `mergeCommit`:
933
1028
 
934
1029
  ```bash
935
- git checkout "$BASE_REF"
936
- git merge --ff-only "$BRANCH"
937
- # update marker: status = "merged" (worktree and branch are intentionally NOT removed)
1030
+ # Same fail-stop sequence as create mode (zero-commit guard -> BATCH_TIP/BASE_TIP -> ancestry
1031
+ # rc check -> git merge --ff-only -> landed verify); local refs only — no fetch.
1032
+ MARKER=".spur/run/worktree-<marker-id>.json"
1033
+ git checkout "$BASE_REF" \
1034
+ || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
1035
+ [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
1036
+ || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; }
1037
+ BATCH_TIP="$(git rev-parse "$BRANCH")" || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
1038
+ BASE_TIP="$(git rev-parse "$BASE_REF")" || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
1039
+ ANCESTRY_RC=0
1040
+ git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
1041
+ [ "$ANCESTRY_RC" -gt 1 ] \
1042
+ && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
1043
+ [ "$ANCESTRY_RC" -eq 1 ] && {
1044
+ echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
1045
+ git log --oneline "$BASE_TIP..$BATCH_TIP" >&2
1046
+ git log --oneline "$BATCH_TIP..$BASE_TIP" >&2
1047
+ exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
1048
+ git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
1049
+ [ $? -eq 0 ] \
1050
+ || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; }
1051
+ LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
1052
+ || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
1053
+ git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
1054
+ || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
1055
+ WT_PATH="<resolved-worktree-path>" # reuse mode: the adopted tree's physical path
1056
+ write_marker () { # $1 = merged | retained; read-modify-write: preserve WT-3 fields (id, command, selector, createdAt, adopted, adoptedAt); jq already assumed by WT-3/task-resolution flows
1057
+ mkdir -p "$(dirname "$MARKER")"
1058
+ if [ -f "$MARKER" ]; then
1059
+ TMP_MARKER="$(mktemp)"
1060
+ jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
1061
+ || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
1062
+ mv "$TMP_MARKER" "$MARKER"
1063
+ else
1064
+ printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
1065
+ "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
1066
+ fi
1067
+ }
1068
+ # Step 5 evidence persistence (task 0720 R3) — required BEFORE the success marker:
1069
+ if printf 'batch report %s\n' "$BATCH_TIP" > ".spur/run/worktree-<marker-id>-batch-report.md"; then :; else
1070
+ echo "WT-5 halt: persistence failed after landed merge $BATCH_TIP; recovery reads captured tips only" >&2
1071
+ write_marker retained
1072
+ exit 1
1073
+ fi
1074
+ # Success marker: status "merged" + mergeCommit "$BATCH_TIP" — written once, only after landed verification and required persistence.
1075
+ # The worktree and branch are intentionally NOT removed.
1076
+ write_marker merged
938
1077
  ```
939
1078
 
940
1079
  The operator supplied the tree, so the operator owns its lifetime. After a green reuse batch
941
1080
  `baseRef == $BRANCH`, so the same worktree keeps fast-forwarding on the next invocation instead of
942
1081
  having to be rebuilt — the continue-the-work loop is stable.
943
1082
 
944
- **Fast-forward only.** If the base ref has moved since the worktree was created and FF is
945
- impossible, do **not** rebase, merge-commit, or resolve conflicts — fall through to the retention
946
- path (WT-5) and report the divergence. The corpus files (`docs/tasks*/`, kanban/index) are
1083
+ **Fast-forward only.** `git merge --ff-only` is the sole mutation of the terminal sequence; every
1084
+ other step is a read, a check, a persistence write, or a cleanup, and every tip/ancestry read uses
1085
+ local refs — no fetch runs anywhere in this lifecycle. If the fresh `BASE_TIP` is not an ancestor of
1086
+ `BATCH_TIP`, the ancestry check halts before the merge: the report names both tips and both
1087
+ divergent commit ranges (`$BASE_TIP..$BATCH_TIP` and `$BATCH_TIP..$BASE_TIP`) and falls to the
1088
+ retention path (WT-5) with the tree and branch retained. A concurrent writer that lands between the
1089
+ ancestry check and the merge still trips `--ff-only` — the race survives every precheck, and a
1090
+ failed FF leaves the marker at `status: active`. Do **not** rebase, merge-commit, or resolve
1091
+ conflicts. The corpus files (`docs/tasks*/`, kanban/index) are
947
1092
  auto-generated and conflict-prone; automated conflict resolution over generated files is exactly the
948
1093
  wrong thing to attempt unattended. FF-only means the merge either is trivially correct or does not
949
1094
  happen.
@@ -952,8 +1097,10 @@ happen.
952
1097
  `--worktree --auto` batch — it does **not** pause even though it performs a merge and a branch
953
1098
  deletion. That is the explicit single exception to Auto-Decision Principle #6 (`cross-cutting.md`),
954
1099
  which otherwise pauses any `--merge` / branch-deletion action regardless of `--auto`. The exception
955
- is safe because `git merge --ff-only` and `git branch -d` both fail closed (they refuse rather than
956
- risk losing work); WT-5 retains the worktree and branch whenever FF is impossible or any task fails.
1100
+ is safe because `git merge --ff-only` fails closed (it refuses rather than risk losing work), the
1101
+ branch deletion runs only after the merge has landed, persisted, and cleaned up — and is
1102
+ existence-guarded (`git rev-parse --verify --quiet`), so a removed branch is never queried;
1103
+ WT-5 retains the worktree and branch whenever FF is impossible or any task fails.
957
1104
  Reuse mode is **narrower** than the carve-out (it merges but does not delete the branch), so the
958
1105
  carve-out text needs no widening.
959
1106
 
@@ -982,22 +1129,44 @@ record and the persisted run artifacts are the evidence record.
982
1129
 
983
1130
  ### WT-5 — Failure path: retain and report (R5)
984
1131
 
985
- On any per-task failure, batch halt, HITL pause that ends the run, or non-FF merge from WT-4, the
986
- worktree directory and branch are left **intact**. No destructive automation on this path under any
987
- flag combination (`--auto`, `--force`, `--keep-going` — all leave the worktree in place). Update the
988
- marker: `status = "retained"`. The worktree's own `.spur` lifecycle DB is retained with the tree,
989
- so nothing is lost on this path (see the WT-4 lifecycle-DB disposition for the merged case —
1132
+ On any per-task failure, batch halt, HITL pause that ends the run, or a WT-4 halt, the
1133
+ worktree directory and branch are left **intact**. No destructive automation on this path under
1134
+ any flag combination (`--auto`, `--force`, `--keep-going` — all leave the worktree in place). The
1135
+ marker records which class of halt occurred (frozen vocabulary):
1136
+
1137
+ - **Pre-merge failure** (failed checkout, zero-commit guard, divergent base, failed FF): the merge
1138
+ never landed, so the marker **stays at `status: active`** — the WT-3 marker stands and WT-5 is
1139
+ its disposition. Nothing was merged onto the base ref.
1140
+ - **Landed-but-incomplete failure** (persist-out, holder cleanup, or worktree removal failed after
1141
+ a landed merge): the marker records `status: retained` + `mergeCommit` BATCH_TIP — a truthful
1142
+ report that the merge landed. The retained path reads the captured `BATCH_TIP` only; a removed
1143
+ branch is never queried.
1144
+
1145
+ The worktree's own `.spur` lifecycle DB is retained with the tree,
1146
+ so nothing is lost on either path (see the WT-4 lifecycle-DB disposition for the merged case —
990
1147
  task 0701 R2d). Emit a retention report in the existing halt-report shape:
991
1148
 
1149
+ **Persist-out skip recovery (task 1067 R2, E71).** When evidence was lost to a landing that
1150
+ skipped persist-out (or the pre-removal assertion fired too late), verdicts remain derivable
1151
+ from the task records — `spur task show <wbs> --json` carries the recorded verdict and its
1152
+ proof block, so a lost `<wbs>-verdict.json` is regenerated by re-recording or copying from
1153
+ the task store. Feature run/latest receipts are re-runnable, not recovered: rerun the
1154
+ feature-verification workflow in the invoking tree —
1155
+ `spur workflow run feature-verification.yaml --vars '{"featureId":"<id>"}'` — which rewrites
1156
+ the receipt files it owns. Divergent files are never auto-overwritten; reconcile by hand.
1157
+
992
1158
  ```
993
1159
  ## Worktree retained — <command> <selector>
994
1160
 
995
- **Halt cause:** <one-line cause — batch halted at task <wbs> / non-FF base ref / HITL pause>
1161
+ **Halt cause:** <one-line cause — batch halted at task <wbs> / non-FF base ref / HITL pause / landed-but-incomplete cleanup failure>
996
1162
  **Worktree path:** ../<worktree-dir>
997
1163
  **Branch:** sp/<command>-<selector-slug>-<short-id>
998
1164
  **Base ref:** <base-ref> (<base-sha>)
1165
+ **Merge state:** pre-merge failure — nothing landed | landed-but-incomplete — mergeCommit <batch-tip-sha>
999
1166
 
1000
- The worktree and its branch are intact. Nothing was merged onto the base ref.
1167
+ The worktree and its branch are intact.
1168
+ pre-merge failure: nothing was merged onto the base ref.
1169
+ landed-but-incomplete: the batch merge LANDED as <batch-tip-sha> on <base-ref>; retention covers post-merge persistence/cleanup only.
1001
1170
  Resume, merge, or discard:
1002
1171
 
1003
1172
  resume: cd <worktree-path> && <command> --continue --worktree <worktree-path>
@@ -1030,6 +1199,16 @@ are long and already resumable via `--continue`; auto-deleting is data loss, aut
1030
1199
  partial result presented as a whole. The answer to "what happens if it fails" is "nothing happens,
1031
1200
  and we tell you where the work is."
1032
1201
 
1202
+ **Partial worktree-removal recovery is inspection-only.** If a create-mode run halted partway
1203
+ through removal (leftover directory, stale registration), the recovery pass gathers facts and
1204
+ nothing else: `git worktree list` (is the tree still registered?), `ls` of the path (what remains
1205
+ on disk?), and the marker / `~/.config/spur/projects.json` registration state. Before removing
1206
+ anything, verify the leftover path is the batch's **owned** path — match it against the marker's
1207
+ `path`/`branch` (or the resolved `--worktree <name>` identity), never a guessed sibling directory.
1208
+ Recursive deletion of leftover directories that survived `git worktree remove` is **never
1209
+ automatic** — it requires explicit operator authorization. No automatic rebase, recovery merge,
1210
+ `git worktree prune`, or holder kill runs on this path.
1211
+
1033
1212
  ### WT-6 — `--continue` re-entry (R7)
1034
1213
 
1035
1214
  A `--continue` resume of a batch started with `--worktree` must re-enter the existing worktree via
@@ -1172,7 +1351,7 @@ integrate(t):
1172
1351
  ├─ rebase fails → git -C "$WT" rebase --abort → conflict path (R4) below
1173
1352
  └─ rebase clean → git checkout "$BASE_REF" → zero-commit guard → git merge --ff-only "$BRANCH"
1174
1353
  → WT-4a verdict persist → WT-4b holders → WT-4c registry
1175
- → git worktree remove → git branch -d → marker merged
1354
+ → git worktree remove → existence-guarded git branch -D → marker merged
1176
1355
  ```
1177
1356
 
1178
1357
  The merge is always `--ff-only`: a base that will not fast-forward after a clean rebase is a
@@ -203,7 +203,9 @@ When `--next` is set and implementation succeeds:
203
203
  2. **Record provenance** — `spur task run-link <wbs> --source next-auto --json`. Writes a
204
204
  `kind: pipeline` entry into `task_run_links` so the `testing → done` provenance guard
205
205
  (lifecycle-adapter.ts L106-131) accepts the in-session implementation path. Idempotent:
206
- safe to call even when a pipeline link already exists.
206
+ safe to call even when a pipeline link already exists. Run it from the same cwd as the
207
+ `task update` calls in step 1 — per-tree DB isolation makes a link from another tree
208
+ invisible to this tree's guard.
207
209
  3. **On a clean transition:** invoke `/sp:dev-verify <wbs> --auto --next` (`--auto` propagates
208
210
  down the whole chain). The verify step's `--next` transition to `done` now passes the
209
211
  provenance guard because step 2 recorded the link.