@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/plugin-scripts.json +5 -0
- package/config/templates/task/brainstorm.md +1 -1
- package/config/templates/task/feature-impl.md +1 -1
- package/config/templates/task/issue.md +1 -1
- package/config/templates/task/meta.md +1 -1
- package/config/templates/task/review.md +1 -1
- package/config/templates/task/standard.md +1 -1
- package/package.json +9 -9
- package/plugins/sp/lib/idea-handoff.generated.mjs +37 -4
- package/plugins/sp/lib/inline-run.generated.mjs +38 -5
- package/plugins/sp/lib/residual-scan.generated.d.mts +5 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +45 -5
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/persist-out-check.mjs +164 -0
- package/plugins/sp/scripts/persist-out-check.ts +185 -0
- package/plugins/sp/scripts/residual-scan.mjs +50 -8
- package/plugins/sp/scripts/residual-scan.ts +4 -3
- package/plugins/sp/scripts/task-diffstat.mjs +17 -5
- package/plugins/sp/scripts/task-diffstat.ts +25 -4
- package/plugins/sp/skills/code-verification/SKILL.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +17 -1
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +40 -7
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +216 -37
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +3 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +107 -3
- package/spur.js +1662 -386
- package/web/_astro/{BoardApp.CYbe-OTb.js → BoardApp.BVp-LYi6.js} +101 -101
- package/web/_astro/{BoardApp.Dflq5a8x.js → BoardApp.i4eh_fHA.js} +1 -1
- package/web/_astro/{TaskDetail.CK7oXddP.js → TaskDetail.C5Cu8LdB.js} +1 -1
- package/web/_astro/{arc.BIqkDATc.js → arc.Bu5XpBEz.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.D0B3SvWv.js → architectureDiagram-3BPJPVTR.9vHQTvhF.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.C2YyKQmL.js → blockDiagram-GPEHLZMM.Cn2Xl7D6.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DmtTYrt_.js → c4Diagram-AAUBKEIU.7o0P_QDo.js} +1 -1
- package/web/_astro/channel.6IFIKBhu.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.HsRXbh7l.js → chunk-2J33WTMH.C3zZFsNt.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.obE_P-R3.js → chunk-4BX2VUAB.CNy_M7fO.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CjWTF5mz.js → chunk-55IACEB6.2TiPnLBL.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DMo799Ax.js → chunk-727SXJPM.5CGSyKAp.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.-cE6JOKZ.js → chunk-AQP2D5EJ.w-ZIgfsK.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.D6Rw6HIk.js → chunk-FMBD7UC4.BkcEyhKa.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.d5rBD73Z.js → chunk-ND2GUHAM.mxKKbBG4.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.r3ox40Q3.js → chunk-QZHKN3VN.cyA0z9aT.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CGGbvjjJ.js → classDiagram-4FO5ZUOK.CqtUzwhX.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CGGbvjjJ.js → classDiagram-v2-Q7XG4LA2.CqtUzwhX.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CmKtPd4l.js → cose-bilkent-S5V4N54A.BExPTopI.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.QNg5GXs8.js → cynefin-OW5HDTMX.Bq2i10Cn.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.B1SabsjC.js → dagre-BM42HDAG.By_HsOuv.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.RtDbNj89.js → diagram-2AECGRRQ.BtU0b7hz.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.IzZpuM-Y.js → diagram-5GNKFQAL.D00ztMdT.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DPTF_fgv.js → diagram-KO2AKTUF.DMVJWaUN.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.CCyza39P.js → diagram-LMA3HP47.Cxqrjg5-.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.BXAi8zcZ.js → diagram-OG6HWLK6.2lz_1B5h.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DUDX7_MR.js → erDiagram-TEJ5UH35.BVQEuJ3T.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.C63QTh3N.js → flowDiagram-I6XJVG4X.Qlyo90ky.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.CnlZJSwG.js → ganttDiagram-6RSMTGT7.DRwb8HHZ.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CTqIrOPC.js → gitGraphDiagram-PVQCEYII.YfoPthfS.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.BYMiDFh9.js → infoDiagram-5YYISTIA.DvFREck1.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CRiQmI-z.js → ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.YQgWBsEU.js → journeyDiagram-JHISSGLW.BDMT_AFb.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.1ECnrrK1.js → kanban-definition-UN3LZRKU.D6Fh3gj9.js} +1 -1
- package/web/_astro/{linear.DzVA71yT.js → linear.Dxw00fGQ.js} +1 -1
- package/web/_astro/{mermaid.core.BK7P2Sr_.js → mermaid.core.CsoxUWa0.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BvXyWZK7.js → mindmap-definition-RKZ34NQL.CN8oeHLu.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.BiXk1TW3.js → pieDiagram-4H26LBE5.C7ADqQ_7.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.krvBH8lh.js → quadrantDiagram-W4KKPZXB.Cw9lKyHP.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.ih1gL25c.js → requirementDiagram-4Y6WPE33.B1ni-z4q.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.CpUxkv4u.js → sankeyDiagram-5OEKKPKP.BhexdjVr.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.clAfeN7y.js → sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DpXWb8-m.js → stateDiagram-AJRCARHV.CkuMt27u.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.ClZb9GIH.js → stateDiagram-v2-BHNVJYJU.Dc_aazUA.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BLcX5dh7.js → timeline-definition-PNZ67QCA.ja7sUFhH.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.oGgpKMpx.js → vennDiagram-CIIHVFJN.BxIshX_K.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.BPHtMtt-.js → wardleyDiagram-YWT4CUSO.DfHlC0sh.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.BNZOPYSk.js → xychartDiagram-2RQKCTM6.DSnhHwCh.js} +1 -1
- package/web/index.html +1 -1
- 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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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.
|
|
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:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
git
|
|
841
|
-
|
|
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
|
-
|
|
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
|
|
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 "$
|
|
864
|
-
|| { echo "halt: branch carries no commits - nothing to merge" >&2;
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
917
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
|
932
|
-
|
|
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
|
-
|
|
936
|
-
git merge --ff-only
|
|
937
|
-
|
|
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.**
|
|
945
|
-
|
|
946
|
-
|
|
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`
|
|
956
|
-
|
|
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
|
|
986
|
-
worktree directory and branch are left **intact**. No destructive automation on this path under
|
|
987
|
-
flag combination (`--auto`, `--force`, `--keep-going` — all leave the worktree in place).
|
|
988
|
-
marker
|
|
989
|
-
|
|
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.
|
|
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 -
|
|
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.
|