@gobing-ai/spur 0.3.96 → 0.3.97
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/rules/strict/runtime-boundaries.yaml +1 -0
- package/config/templates/AGENTS.md +3 -3
- package/config/workflows/wrapup-pipeline.yaml +2 -1
- package/package.json +2 -2
- package/plugins/sp/README.md +6 -0
- package/plugins/sp/commands/dev-job-dump.md +29 -0
- package/plugins/sp/commands/dev-job-resume.md +29 -0
- package/plugins/sp/lib/inline-run.generated.mjs +25 -16
- package/plugins/sp/lib/quality-gate.generated.mjs +12 -5
- package/plugins/sp/lib/residual-scan.generated.d.mts +1 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +16 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +2 -2
- package/plugins/sp/scripts/feature-verification-steps.mjs +14 -1
- package/plugins/sp/scripts/feature-verification-steps.ts +14 -1
- package/plugins/sp/scripts/quality-gate.mjs +13 -6
- package/plugins/sp/scripts/quality-gate.ts +1 -1
- package/plugins/sp/scripts/residual-scan.mjs +64 -38
- package/plugins/sp/scripts/residual-scan.ts +39 -34
- package/plugins/sp/scripts/wrapup-steps.mjs +64 -3
- package/plugins/sp/scripts/wrapup-steps.ts +122 -3
- package/plugins/sp/skills/source-driven-development/SKILL.md +11 -0
- package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +9 -5
- package/plugins/sp/skills/spur-cli/references/workflows.md +17 -14
- package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +3 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +79 -5
- package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +4 -2
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +8 -6
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +3 -3
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +9 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +43 -12
- package/spur.js +4246 -2996
- package/web/_astro/{BoardApp.BIjMatT1.js → BoardApp.Bmv5WkJ9.js} +1 -1
- package/web/_astro/BoardApp.vveLOdDq.js +322 -0
- package/web/_astro/{TaskDetail.BZM3EAFx.js → TaskDetail.IKc3uDYV.js} +1 -1
- package/web/_astro/{arc.HkRiZnoI.js → arc.C63Ufg35.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.BDdK-tgZ.js → architectureDiagram-3BPJPVTR.CP8Hyldk.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.CZ9kOBxx.js → blockDiagram-GPEHLZMM.Bo_48MRA.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DXK9qeRD.js → c4Diagram-AAUBKEIU.Ch61sw-O.js} +1 -1
- package/web/_astro/channel.BKCmJpWM.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BTD2WeX5.js → chunk-2J33WTMH.CHpzS7xf.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.Ci6Qbcvk.js → chunk-4BX2VUAB.DzMNNWIw.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.yl3zsj7p.js → chunk-55IACEB6.BqPrDw_Q.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.ClXZsfyR.js → chunk-727SXJPM.ZzBHhNw-.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.BmKWZBcP.js → chunk-AQP2D5EJ.1_O_LLkx.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.By30fcb7.js → chunk-FMBD7UC4.GS4UHg7_.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.24NmrD-k.js → chunk-ND2GUHAM.BnqotN4g.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BN4sKdcS.js → chunk-QZHKN3VN.Ghuh8zoJ.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CJpoMPb5.js → classDiagram-4FO5ZUOK.BW9vC3zg.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CJpoMPb5.js → classDiagram-v2-Q7XG4LA2.BW9vC3zg.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BraxQ2Nt.js → cose-bilkent-S5V4N54A.D6jNC0ti.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.gzoU73oL.js → cynefin-OW5HDTMX.BOGoCs4L.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.wyDfWySU.js → dagre-BM42HDAG.8iU5VcWi.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.h2_EbMxV.js → diagram-2AECGRRQ.CVt88HwH.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Db6dcs89.js → diagram-5GNKFQAL.C9HxUUMl.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.NVwzxu_U.js → diagram-KO2AKTUF.4ywkQN3t.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.B-eNgDQk.js → diagram-LMA3HP47.C48A6gpz.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Bsa8ZRbJ.js → diagram-OG6HWLK6.DxFieQW4.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BospRE5Q.js → erDiagram-TEJ5UH35.CtjtQqYs.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DMzvHwAA.js → flowDiagram-I6XJVG4X.C5zAiahU.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.Dmdxgd-M.js → ganttDiagram-6RSMTGT7.D_L6zsny.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CaqdO0rL.js → gitGraphDiagram-PVQCEYII.Ignnrk3V.js} +1 -1
- package/web/_astro/index.CbNz17Sx.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DDVD9Wn7.js → infoDiagram-5YYISTIA.DLv8-P5u.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.EKyCUFsL.js → ishikawaDiagram-YF4QCWOH.p0OTpwFO.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.By2-RkYB.js → journeyDiagram-JHISSGLW.CIaBt-DD.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.ar-WmlyR.js → kanban-definition-UN3LZRKU.DGySIntq.js} +1 -1
- package/web/_astro/{linear.BnzMBgo_.js → linear.BATV4RXu.js} +1 -1
- package/web/_astro/{mermaid.core.DSqeFu2Y.js → mermaid.core.DzkwM3VX.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.1XcF8Wh2.js → mindmap-definition-RKZ34NQL.3piBRRzA.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.DnItHOdK.js → pieDiagram-4H26LBE5.DUNueLub.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.BQQyrnBt.js → quadrantDiagram-W4KKPZXB.9bihVbrt.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.Dmzxun82.js → requirementDiagram-4Y6WPE33.CkjsC-YT.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BBIeRKYB.js → sankeyDiagram-5OEKKPKP.Dbq4vaoQ.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.CUNVdBBf.js → sequenceDiagram-3UESZ5HK.BivrH99P.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DlFf2sJm.js → stateDiagram-AJRCARHV.CqLQ1Vzz.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.DXR7I1H5.js → stateDiagram-v2-BHNVJYJU.DvvoLD3b.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BcO-GyX2.js → timeline-definition-PNZ67QCA.B-KR78gv.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.ChCECmmW.js → vennDiagram-CIIHVFJN.Ck0Eieb5.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CRaT12mD.js → wardleyDiagram-YWT4CUSO.CRfdd_do.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.B3lsHVpj.js → xychartDiagram-2RQKCTM6.Ck_Jxk9C.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.GvjIe9Z6.js +0 -191
- package/web/_astro/channel.DlhX2MEt.js +0 -1
- package/web/_astro/index.EoZlzLK-.css +0 -1
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* residual-scan — deterministic task-leftover scanner behind feature F96 (ADR-071).
|
|
4
|
-
*
|
|
5
|
-
* Contract owner: docs/design/task-residual-sweep.md. IO glue only (task 1003 R5): argv, git/spur spawning, file
|
|
6
|
-
* IO, and the four modes; parsing/classification/folding/reporting live in the generated standalone bundle
|
|
7
|
-
* `plugins/sp/lib/residual-scan.generated.mjs` (source: packages/app/src/services/residual-scan.ts), re-exported via
|
|
8
|
-
* `export *`; the local `scanResiduals` wrapper (old 5-arg IO signature) shadows the core of the same name.
|
|
9
|
-
* Modes: scan <wbs> (write `.spur/run/<wbs>-residuals.json`); fold <wbs> (fold blocking residuals into
|
|
10
|
-
* `<wbs>-verdict.json` with PASS→PARTIAL downgrade + `<wbs>-test-gate.findings`); settle <wbs> (file one follow-up
|
|
11
|
-
* task for deferrables, delete `/tmp/<wbs>-*` residue); report <wbs> (write `<wbs>-residual-report.md` when the residual-sweep check failed).
|
|
12
|
-
*/
|
|
2
|
+
/** F96 residual IO glue. Pure logic: residual-scan.generated.mjs; contract: task-residual-sweep.md. */
|
|
13
3
|
import { spawnSync } from 'node:child_process';
|
|
14
|
-
import
|
|
4
|
+
import * as fs from 'node:fs';
|
|
15
5
|
import { tmpdir } from 'node:os';
|
|
16
6
|
import { join } from 'node:path';
|
|
17
7
|
import { getEnvVars } from '../lib/env';
|
|
@@ -40,7 +30,7 @@ function spur(env: ScanEnv, spurBinFlag: string | undefined, args: string[], cwd
|
|
|
40
30
|
}
|
|
41
31
|
function isRegularFile(path: string): boolean {
|
|
42
32
|
try {
|
|
43
|
-
return statSync(path).isFile();
|
|
33
|
+
return fs.statSync(path).isFile();
|
|
44
34
|
} catch {
|
|
45
35
|
return false;
|
|
46
36
|
}
|
|
@@ -59,7 +49,7 @@ export function collectAddedLines(root: string, base: string): Array<{ file: str
|
|
|
59
49
|
const untracked = run('git', ['ls-files', '--others', '--exclude-standard'], root);
|
|
60
50
|
for (const f of untracked.stdout.split('\n')) {
|
|
61
51
|
if (f.length === 0 || !isRegularFile(join(root, f))) continue;
|
|
62
|
-
for (const [idx, text] of readFileSync(join(root, f), 'utf8').split('\n').entries())
|
|
52
|
+
for (const [idx, text] of fs.readFileSync(join(root, f), 'utf8').split('\n').entries())
|
|
63
53
|
out.push({ file: f, line: idx + 1, text });
|
|
64
54
|
}
|
|
65
55
|
return out;
|
|
@@ -67,7 +57,7 @@ export function collectAddedLines(root: string, base: string): Array<{ file: str
|
|
|
67
57
|
export function listStagingResidue(tmpDir: string, wbs: string): string[] {
|
|
68
58
|
let names: string[];
|
|
69
59
|
try {
|
|
70
|
-
names = readdirSync(tmpDir);
|
|
60
|
+
names = fs.readdirSync(tmpDir);
|
|
71
61
|
} catch {
|
|
72
62
|
return [];
|
|
73
63
|
}
|
|
@@ -75,7 +65,7 @@ export function listStagingResidue(tmpDir: string, wbs: string): string[] {
|
|
|
75
65
|
}
|
|
76
66
|
function readDeferrals(runDir: string, wbs: string): core.Deferral[] {
|
|
77
67
|
const path = join(runDir, `${wbs}-residual-deferrals.json`);
|
|
78
|
-
if (!existsSync(path)) return [];
|
|
68
|
+
if (!fs.existsSync(path)) return [];
|
|
79
69
|
const isDeferral = (e: unknown): e is core.Deferral =>
|
|
80
70
|
typeof e === 'object' &&
|
|
81
71
|
e !== null &&
|
|
@@ -83,7 +73,7 @@ function readDeferrals(runDir: string, wbs: string): core.Deferral[] {
|
|
|
83
73
|
typeof (e as core.Deferral).reason === 'string' &&
|
|
84
74
|
(e as core.Deferral).reason.trim().length > 0;
|
|
85
75
|
try {
|
|
86
|
-
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
76
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
87
77
|
return (Array.isArray(parsed) ? parsed : []).filter(isDeferral);
|
|
88
78
|
} catch {
|
|
89
79
|
return [];
|
|
@@ -92,7 +82,7 @@ function readDeferrals(runDir: string, wbs: string): core.Deferral[] {
|
|
|
92
82
|
export function scanResiduals(root: string, wbs: string, tmpDir: string, taskContent: string, _env: ScanEnv) {
|
|
93
83
|
const runDir = join(root, '.spur', 'run');
|
|
94
84
|
const basePath = join(runDir, `${wbs}-base.sha`);
|
|
95
|
-
const base = existsSync(basePath) ? readFileSync(basePath, 'utf8').trim() : null;
|
|
85
|
+
const base = fs.existsSync(basePath) ? fs.readFileSync(basePath, 'utf8').trim() : null;
|
|
96
86
|
const stagingResidue = listStagingResidue(tmpDir, wbs);
|
|
97
87
|
const deferrals = readDeferrals(runDir, wbs);
|
|
98
88
|
const addedLines = base === null ? [] : collectAddedLines(root, base);
|
|
@@ -126,14 +116,14 @@ function loadTask(env: ScanEnv, spurBinFlag: string | undefined, wbs: string, ro
|
|
|
126
116
|
return { content: typeof parsed.content === 'string' ? parsed.content : '', featureId };
|
|
127
117
|
}
|
|
128
118
|
function loadVerdict(runDir: string, wbs: string): VerdictFile {
|
|
129
|
-
return JSON.parse(readFileSync(
|
|
119
|
+
return JSON.parse(fs.readFileSync(core.recordedVerdictPath(runDir, wbs, fs), 'utf8'));
|
|
130
120
|
}
|
|
131
121
|
function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
132
122
|
const runDir = join(opts.root, '.spur', 'run');
|
|
133
|
-
mkdirSync(runDir, { recursive: true });
|
|
123
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
134
124
|
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
135
125
|
const artifact = scanResiduals(opts.root, opts.wbs, opts.tmpDir, content, env);
|
|
136
|
-
writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(artifact, null, 2)}\n`);
|
|
126
|
+
fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(artifact, null, 2)}\n`);
|
|
137
127
|
io.out(
|
|
138
128
|
`residual-scan: ${opts.wbs} blocking=${artifact.counts.blocking} deferrable=${artifact.counts.deferrable} advisory=${artifact.counts.advisory} housekeeping=${artifact.counts.housekeeping}\n`,
|
|
139
129
|
);
|
|
@@ -141,16 +131,29 @@ function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
141
131
|
}
|
|
142
132
|
function foldMode(opts: ParsedArgs, _env: ScanEnv, io: ScanIo): number {
|
|
143
133
|
const runDir = join(opts.root, '.spur', 'run');
|
|
144
|
-
const scan = JSON.parse(
|
|
145
|
-
|
|
134
|
+
const scan = JSON.parse(
|
|
135
|
+
fs.readFileSync(join(runDir, `${opts.wbs}-residuals.json`), 'utf8'),
|
|
136
|
+
) as core.ResidualArtifact;
|
|
137
|
+
const target = core.recordedVerdictPath(runDir, opts.wbs, fs);
|
|
146
138
|
const verdict = loadVerdict(runDir, opts.wbs);
|
|
147
139
|
const findingsPath = join(runDir, `${opts.wbs}-test-gate.findings`);
|
|
148
|
-
const fold = core.foldVerdict(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
140
|
+
const fold = core.foldVerdict(
|
|
141
|
+
verdict,
|
|
142
|
+
scan,
|
|
143
|
+
fs.existsSync(findingsPath) ? fs.readFileSync(findingsPath, 'utf8') : '',
|
|
152
144
|
);
|
|
153
|
-
|
|
145
|
+
const bytes = `${JSON.stringify({ ...verdict, verdict: fold.verdict, checks: fold.checks }, null, 2)}\n`;
|
|
146
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
147
|
+
try {
|
|
148
|
+
fs.writeFileSync(temporary, bytes, { flag: 'wx' });
|
|
149
|
+
fs.renameSync(temporary, target);
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(temporary, { force: true });
|
|
152
|
+
}
|
|
153
|
+
// The pipeline still consumes the attempt copy after folding; the recorded copy is authoritative.
|
|
154
|
+
const scratch = join(runDir, `${opts.wbs}-verdict.json`);
|
|
155
|
+
if (target !== scratch) fs.writeFileSync(scratch, bytes);
|
|
156
|
+
fs.writeFileSync(findingsPath, fold.findings);
|
|
154
157
|
io.out(
|
|
155
158
|
`residual-fold: ${opts.wbs} verdict=${fold.verdict} residual-sweep=${fold.checks.find((c) => c.name === 'residual-sweep')?.status}\n`,
|
|
156
159
|
);
|
|
@@ -163,7 +166,7 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
163
166
|
const scan = scanResiduals(opts.root, wbs, opts.tmpDir, task.content, env);
|
|
164
167
|
const residualsPath = join(runDir, `${wbs}-residuals.json`);
|
|
165
168
|
let prior: Partial<core.ResidualArtifact & { followUp?: string }> = {};
|
|
166
|
-
if (existsSync(residualsPath)) prior = JSON.parse(readFileSync(residualsPath, 'utf8'));
|
|
169
|
+
if (fs.existsSync(residualsPath)) prior = JSON.parse(fs.readFileSync(residualsPath, 'utf8'));
|
|
167
170
|
const deferred = scan.items.filter((i) => i.class === 'deferrable');
|
|
168
171
|
if (deferred.length > 0 && prior.followUp === undefined) {
|
|
169
172
|
if (task.featureId === '') {
|
|
@@ -189,7 +192,7 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
189
192
|
const rows = deferred.map((i) => `- ${i.id} — ${i.location}: ${i.text}`);
|
|
190
193
|
const head = `Source task: ${wbs} (feature ${task.featureId}) — deferred residuals filed by residual-scan settle.`;
|
|
191
194
|
const bgFile = join(runDir, `${wbs}-residual-background.md`);
|
|
192
|
-
writeFileSync(bgFile, `${[head, '', ...rows].join('\n')}\n`);
|
|
195
|
+
fs.writeFileSync(bgFile, `${[head, '', ...rows].join('\n')}\n`);
|
|
193
196
|
const updArgs = ['task', 'update', wbsNew, '--section', 'Background', '--from-file', bgFile];
|
|
194
197
|
if (spur(env, opts.spurBin, updArgs, opts.root).status !== 0) {
|
|
195
198
|
io.err(`residual-settle: background write failed; re-run: residual-scan settle ${wbs}\n`);
|
|
@@ -200,13 +203,13 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
200
203
|
}
|
|
201
204
|
for (const path of listStagingResidue(opts.tmpDir, wbs)) {
|
|
202
205
|
try {
|
|
203
|
-
rmSync(path, { force: true });
|
|
206
|
+
fs.rmSync(path, { force: true });
|
|
204
207
|
} catch {
|
|
205
208
|
io.err(`residual-settle: could not remove ${path}; re-run: residual-scan settle ${wbs}\n`);
|
|
206
209
|
return 0;
|
|
207
210
|
}
|
|
208
211
|
}
|
|
209
|
-
writeFileSync(residualsPath, `${JSON.stringify({ ...scan, ...prior }, null, 2)}\n`);
|
|
212
|
+
fs.writeFileSync(residualsPath, `${JSON.stringify({ ...scan, ...prior }, null, 2)}\n`);
|
|
210
213
|
return 0;
|
|
211
214
|
}
|
|
212
215
|
function reportMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
@@ -217,8 +220,10 @@ function reportMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
217
220
|
const scan = scanResiduals(opts.root, opts.wbs, opts.tmpDir, content, env);
|
|
218
221
|
const blocking = scan.items.filter((i) => i.class === 'blocking');
|
|
219
222
|
const attemptFile = join(runDir, `${opts.wbs}-test-fix-attempt`);
|
|
220
|
-
const attempts = existsSync(attemptFile)
|
|
221
|
-
|
|
223
|
+
const attempts = fs.existsSync(attemptFile)
|
|
224
|
+
? Number.parseInt(fs.readFileSync(attemptFile, 'utf8').trim() || '0', 10)
|
|
225
|
+
: 0;
|
|
226
|
+
fs.writeFileSync(
|
|
222
227
|
join(runDir, `${opts.wbs}-residual-report.md`),
|
|
223
228
|
core.renderReport(opts.wbs, blocking, Number.isNaN(attempts) ? 0 : attempts),
|
|
224
229
|
);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// plugins/sp/scripts/wrapup-steps.ts
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
|
-
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
|
|
9
9
|
// plugins/sp/lib/env.ts
|
|
@@ -114,10 +114,68 @@ function resolveTasks(env, options = {}) {
|
|
|
114
114
|
if (unresolved) {
|
|
115
115
|
return writeFail(`failed:unresolved or non-completed task (see ${relTasksFile})`);
|
|
116
116
|
}
|
|
117
|
+
if ((env.feature ?? "") !== "") {
|
|
118
|
+
const pf = preflightFeature(env, cwd);
|
|
119
|
+
if (pf)
|
|
120
|
+
return writeFail(pf);
|
|
121
|
+
}
|
|
117
122
|
writeFileSync(abs(relStatusFile), `PASS
|
|
118
123
|
`);
|
|
119
124
|
return { status: "PASS", statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
120
125
|
}
|
|
126
|
+
function isFindingList(value) {
|
|
127
|
+
return Array.isArray(value) && value.every((f) => f !== null && typeof f === "object" && typeof f.severity === "string" && typeof f.code === "string" && f.code.length > 0);
|
|
128
|
+
}
|
|
129
|
+
function sortedUniqueCodes(findings) {
|
|
130
|
+
const codes = findings.filter((f) => f.severity === "error").map((f) => String(f.code ?? "")).filter((c) => c.length > 0);
|
|
131
|
+
return [...new Set(codes)].sort();
|
|
132
|
+
}
|
|
133
|
+
function preflightFeature(env, cwd) {
|
|
134
|
+
const feature = env.feature ?? "";
|
|
135
|
+
const abs = (p) => cwd ? join(cwd, p) : p;
|
|
136
|
+
const syncOut = spur(env, ["feature", "sync", feature, "--dry-run", "--json"], { cwd });
|
|
137
|
+
let sync;
|
|
138
|
+
if (syncOut.status === 0) {
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(syncOut.stdout);
|
|
141
|
+
if (parsed !== null && typeof parsed === "object")
|
|
142
|
+
sync = parsed;
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
if (sync === undefined)
|
|
146
|
+
return "failed:preflight:sync-unreadable";
|
|
147
|
+
const proposal = sync.proposal;
|
|
148
|
+
if (proposal === null || typeof proposal !== "object" || Array.isArray(proposal) || proposal.gateBlocked !== undefined && typeof proposal.gateBlocked !== "boolean" || proposal.gateFindings !== undefined && !isFindingList(proposal.gateFindings) || proposal.hops !== undefined && (!Array.isArray(proposal.hops) || !proposal.hops.every((hop) => typeof hop === "string")) || proposal.gateBlocked !== true && (typeof proposal.from !== "string" || typeof proposal.to !== "string")) {
|
|
149
|
+
return "failed:preflight:sync-unreadable";
|
|
150
|
+
}
|
|
151
|
+
if (proposal.gateBlocked === true) {
|
|
152
|
+
return `failed:preflight:gate-blocked ${sortedUniqueCodes(proposal.gateFindings ?? []).join(",")}`;
|
|
153
|
+
}
|
|
154
|
+
const reachesDone = proposal.to === "done" || (proposal.hops ?? []).includes("done");
|
|
155
|
+
if (!reachesDone)
|
|
156
|
+
return null;
|
|
157
|
+
const checkOut = spur(env, ["feature", "check", feature, "--strict", "--as", "done", "--json"], { cwd });
|
|
158
|
+
let check;
|
|
159
|
+
try {
|
|
160
|
+
check = JSON.parse(checkOut.stdout);
|
|
161
|
+
} catch {
|
|
162
|
+
return "failed:preflight:check-unreadable";
|
|
163
|
+
}
|
|
164
|
+
const payload = Array.isArray(check) && check.length === 1 ? check[0] : check;
|
|
165
|
+
const findings = payload !== null && typeof payload === "object" ? payload.findings : undefined;
|
|
166
|
+
if (!isFindingList(findings) || checkOut.status !== 0 && (checkOut.status !== 1 || !findings.some((f) => f.severity === "error"))) {
|
|
167
|
+
return "failed:preflight:check-unreadable";
|
|
168
|
+
}
|
|
169
|
+
const eligible = proposal.from === "verifying" ? findings : findings.filter((f) => !String(f.code ?? "").startsWith("L4.feature-receipt-"));
|
|
170
|
+
const runId = env.__runId ?? "";
|
|
171
|
+
if (runId.length > 0) {
|
|
172
|
+
mkdirSync(abs(join(".spur", "run")), { recursive: true });
|
|
173
|
+
writeFileSync(abs(join(".spur", "run", `${runId}-wrapup-preflight.json`)), `${JSON.stringify(check)}
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
const codes = sortedUniqueCodes(eligible);
|
|
177
|
+
return codes.length > 0 ? `failed:preflight:done-gate ${codes.join(",")}` : null;
|
|
178
|
+
}
|
|
121
179
|
var ROUTE_REASON_TABLE = {
|
|
122
180
|
fast: "fast:evidence complete+consistent",
|
|
123
181
|
"": "safety:missing evidence (mode empty)",
|
|
@@ -239,8 +297,10 @@ function runMetrics(env, options = {}) {
|
|
|
239
297
|
const frontmatter = parsed.frontmatter !== null && typeof parsed.frontmatter === "object" ? parsed.frontmatter : {};
|
|
240
298
|
const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ""));
|
|
241
299
|
const status2 = String(jqPick(frontmatter.status, parsed.status, "unknown"));
|
|
242
|
-
const
|
|
243
|
-
const
|
|
300
|
+
const durable = join(".spur", "memory", "evidence", `${wbs}-verdict.json`);
|
|
301
|
+
const hasDurable = existsSync(abs(durable));
|
|
302
|
+
const verdictPath = hasDurable ? durable : join(".spur", "run", `${wbs}-verdict.json`);
|
|
303
|
+
const artifactVerdict = verdictOfArtifact(abs(verdictPath)) ?? (hasDurable ? "UNKNOWN" : null);
|
|
244
304
|
const trackedVerdict = verdictFromTestingSection(typeof parsed.content === "string" ? parsed.content : "");
|
|
245
305
|
const verdict = artifactVerdict ?? trackedVerdict ?? "UNKNOWN";
|
|
246
306
|
if (verdict === "UNKNOWN") {
|
|
@@ -410,6 +470,7 @@ export {
|
|
|
410
470
|
runMetrics,
|
|
411
471
|
runFeatureTransition,
|
|
412
472
|
resolveTasks,
|
|
473
|
+
preflightFeature,
|
|
413
474
|
main,
|
|
414
475
|
jqPick,
|
|
415
476
|
classifySync,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* the shell-program caps while writing the same `.spur/run` artifacts:
|
|
10
10
|
* - `<runId>-wrapup-tasks.json` normalized, deduplicated WBS capture (resolve)
|
|
11
11
|
* - `<runId>-wrapup-resolve.status` `PASS`/`FAIL` (resolve)
|
|
12
|
+
* - `<runId>-wrapup-preflight.json` full feature-check JSON (resolve pre-flight, when `feature` is set)
|
|
12
13
|
* - `<runId>-route-reason.txt` route reason (route-reason subcommand)
|
|
13
14
|
* - `.spur/memory/wrapup-metrics.jsonl` one row per task (metrics)
|
|
14
15
|
* - `<runId>-wrapup-metrics.status` `PASS`/`FAIL` (metrics)
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
import { spawnSync } from 'node:child_process';
|
|
33
|
-
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
34
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
34
35
|
import { join } from 'node:path';
|
|
35
36
|
import { getEnvVars } from '../lib/env';
|
|
36
37
|
import { spurCommand } from '../lib/spur-bin';
|
|
@@ -174,10 +175,126 @@ export function resolveTasks(env: WrapupStepsEnv, options: WrapupStepsOptions =
|
|
|
174
175
|
if (unresolved) {
|
|
175
176
|
return writeFail(`failed:unresolved or non-completed task (see ${relTasksFile})`);
|
|
176
177
|
}
|
|
178
|
+
if ((env.feature ?? '') !== '') {
|
|
179
|
+
const pf = preflightFeature(env, cwd);
|
|
180
|
+
if (pf) return writeFail(pf);
|
|
181
|
+
}
|
|
177
182
|
writeFileSync(abs(relStatusFile), 'PASS\n');
|
|
178
183
|
return { status: 'PASS', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
179
184
|
}
|
|
180
185
|
|
|
186
|
+
interface PreflightFinding {
|
|
187
|
+
severity: string;
|
|
188
|
+
code: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isFindingList(value: unknown): value is PreflightFinding[] {
|
|
192
|
+
return (
|
|
193
|
+
Array.isArray(value) &&
|
|
194
|
+
value.every(
|
|
195
|
+
(f) =>
|
|
196
|
+
f !== null &&
|
|
197
|
+
typeof f === 'object' &&
|
|
198
|
+
typeof f.severity === 'string' &&
|
|
199
|
+
typeof f.code === 'string' &&
|
|
200
|
+
f.code.length > 0,
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface PreflightSyncPayload {
|
|
206
|
+
proposal?: {
|
|
207
|
+
gateBlocked?: boolean;
|
|
208
|
+
from?: string;
|
|
209
|
+
to?: string;
|
|
210
|
+
hops?: string[];
|
|
211
|
+
gateFindings?: PreflightFinding[];
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function sortedUniqueCodes(findings: PreflightFinding[]): string[] {
|
|
216
|
+
const codes = findings
|
|
217
|
+
.filter((f) => f.severity === 'error')
|
|
218
|
+
.map((f) => String(f.code ?? ''))
|
|
219
|
+
.filter((c) => c.length > 0);
|
|
220
|
+
return [...new Set(codes)].sort();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 1033 R2 feature pre-flight for wrapup resolve — runs after the task list resolves and
|
|
225
|
+
* before any corpus-mutating wrapup state. Returns a `failed:preflight:*` reason on the
|
|
226
|
+
* first blocking condition, or null when wrapup may proceed. Never writes PASS, never
|
|
227
|
+
* relaxes the later feature-transition / feature-verify gates, and uses the same `spurBin`
|
|
228
|
+
* env as `runFeatureTransition`.
|
|
229
|
+
*/
|
|
230
|
+
export function preflightFeature(env: WrapupStepsEnv, cwd?: string): string | null {
|
|
231
|
+
const feature = env.feature ?? '';
|
|
232
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
233
|
+
|
|
234
|
+
const syncOut = spur(env, ['feature', 'sync', feature, '--dry-run', '--json'], { cwd });
|
|
235
|
+
let sync: PreflightSyncPayload | undefined;
|
|
236
|
+
if (syncOut.status === 0) {
|
|
237
|
+
try {
|
|
238
|
+
const parsed: unknown = JSON.parse(syncOut.stdout);
|
|
239
|
+
if (parsed !== null && typeof parsed === 'object') sync = parsed as PreflightSyncPayload;
|
|
240
|
+
} catch {
|
|
241
|
+
// fall through to the unreadable branch below
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (sync === undefined) return 'failed:preflight:sync-unreadable';
|
|
245
|
+
const proposal = sync.proposal;
|
|
246
|
+
if (
|
|
247
|
+
proposal === null ||
|
|
248
|
+
typeof proposal !== 'object' ||
|
|
249
|
+
Array.isArray(proposal) ||
|
|
250
|
+
(proposal.gateBlocked !== undefined && typeof proposal.gateBlocked !== 'boolean') ||
|
|
251
|
+
(proposal.gateFindings !== undefined && !isFindingList(proposal.gateFindings)) ||
|
|
252
|
+
(proposal.hops !== undefined &&
|
|
253
|
+
(!Array.isArray(proposal.hops) || !proposal.hops.every((hop) => typeof hop === 'string'))) ||
|
|
254
|
+
(proposal.gateBlocked !== true && (typeof proposal.from !== 'string' || typeof proposal.to !== 'string'))
|
|
255
|
+
) {
|
|
256
|
+
return 'failed:preflight:sync-unreadable';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (proposal.gateBlocked === true) {
|
|
260
|
+
return `failed:preflight:gate-blocked ${sortedUniqueCodes(proposal.gateFindings ?? []).join(',')}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const reachesDone = proposal.to === 'done' || (proposal.hops ?? []).includes('done');
|
|
264
|
+
if (!reachesDone) return null;
|
|
265
|
+
|
|
266
|
+
const checkOut = spur(env, ['feature', 'check', feature, '--strict', '--as', 'done', '--json'], { cwd });
|
|
267
|
+
let check: unknown;
|
|
268
|
+
try {
|
|
269
|
+
check = JSON.parse(checkOut.stdout);
|
|
270
|
+
} catch {
|
|
271
|
+
return 'failed:preflight:check-unreadable';
|
|
272
|
+
}
|
|
273
|
+
const payload = Array.isArray(check) && check.length === 1 ? check[0] : check;
|
|
274
|
+
const findings =
|
|
275
|
+
payload !== null && typeof payload === 'object' ? (payload as { findings?: unknown }).findings : undefined;
|
|
276
|
+
if (
|
|
277
|
+
!isFindingList(findings) ||
|
|
278
|
+
(checkOut.status !== 0 && (checkOut.status !== 1 || !findings.some((f) => f.severity === 'error')))
|
|
279
|
+
) {
|
|
280
|
+
return 'failed:preflight:check-unreadable';
|
|
281
|
+
}
|
|
282
|
+
// The verifying onEnter will produce the receipt, so receipt-only errors on a feature
|
|
283
|
+
// that is not yet verifying would fail every wrapup forever.
|
|
284
|
+
const eligible =
|
|
285
|
+
proposal.from === 'verifying'
|
|
286
|
+
? findings
|
|
287
|
+
: findings.filter((f) => !String(f.code ?? '').startsWith('L4.feature-receipt-'));
|
|
288
|
+
|
|
289
|
+
const runId = env.__runId ?? '';
|
|
290
|
+
if (runId.length > 0) {
|
|
291
|
+
mkdirSync(abs(join('.spur', 'run')), { recursive: true });
|
|
292
|
+
writeFileSync(abs(join('.spur', 'run', `${runId}-wrapup-preflight.json`)), `${JSON.stringify(check)}\n`);
|
|
293
|
+
}
|
|
294
|
+
const codes = sortedUniqueCodes(eligible);
|
|
295
|
+
return codes.length > 0 ? `failed:preflight:done-gate ${codes.join(',')}` : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
181
298
|
/**
|
|
182
299
|
* 0944 R3 route-reason map — mirrors the former inline jq object one-for-one, plus the
|
|
183
300
|
* `safety` entry (operator-forced doc-sync). `fast:drift-probe-clean` is NOT in the map:
|
|
@@ -362,8 +479,10 @@ export function runMetrics(env: WrapupStepsEnv, options: WrapupStepsOptions = {}
|
|
|
362
479
|
// fast-forwards and removes the tree, taking the gitignored artifact with it — the tracked
|
|
363
480
|
// `Testing` line `task record` already wrote into the task file is the durable copy. Honest
|
|
364
481
|
// UNKNOWN is the last resort, and it is telemetry, never proof of completion.
|
|
365
|
-
const
|
|
366
|
-
const
|
|
482
|
+
const durable = join('.spur', 'memory', 'evidence', `${wbs}-verdict.json`);
|
|
483
|
+
const hasDurable = existsSync(abs(durable));
|
|
484
|
+
const verdictPath = hasDurable ? durable : join('.spur', 'run', `${wbs}-verdict.json`);
|
|
485
|
+
const artifactVerdict = verdictOfArtifact(abs(verdictPath)) ?? (hasDurable ? 'UNKNOWN' : null);
|
|
367
486
|
const trackedVerdict = verdictFromTestingSection(typeof parsed.content === 'string' ? parsed.content : '');
|
|
368
487
|
const verdict = artifactVerdict ?? trackedVerdict ?? 'UNKNOWN';
|
|
369
488
|
if (verdict === 'UNKNOWN') {
|
|
@@ -91,6 +91,16 @@ Generate the code and **cite the source** (doc URL + date, or `pkg@version` + th
|
|
|
91
91
|
point of use or in the task's notes. A claim about external behavior without a citation is
|
|
92
92
|
unverified — flag it as such rather than presenting it as fact.
|
|
93
93
|
|
|
94
|
+
### Step 5 — Close with a verification verdict
|
|
95
|
+
|
|
96
|
+
Before finishing, verify the citations for the API/library claims you emitted and close with one
|
|
97
|
+
line: `Verification: HIGH|MEDIUM|LOW — <first failing check and deviation, or 'all checks passed'>`.
|
|
98
|
+
|
|
99
|
+
- **Citation coverage** — every API/library claim in the output carries a source citation (doc URL + date, or `pkg@version` + file/symbol); no uncited claim is asserted as fact.
|
|
100
|
+
- **Source authority** — each citation points at the pinned version's primary source (installed signature/source, or official docs for the lockfile version), not the latest docs or memory.
|
|
101
|
+
- **Honest confidence** — the stated HIGH/MEDIUM/LOW matches the evidence: HIGH requires a check against the pinned source this session; MEDIUM/LOW items are labeled, and LOW is never presented as fact.
|
|
102
|
+
- **HIGH** — all three checks pass. **MEDIUM** — coverage intact but some citations are off-version (latest docs for a pinned dependency) or secondary-source only. **LOW** — an API/library claim lacks any citation, or a LOW-confidence claim was asserted as fact.
|
|
103
|
+
|
|
94
104
|
## Common Rationalizations
|
|
95
105
|
|
|
96
106
|
| Rationalization | Reality |
|
|
@@ -116,6 +126,7 @@ unverified — flag it as such rather than presenting it as fact.
|
|
|
116
126
|
- [ ] Both questions answered — the API exists **and** is called within its contract.
|
|
117
127
|
- [ ] Claims about external behavior carry a citation (source + date, or `pkg@version` + symbol); unverifiable ones are flagged, not asserted.
|
|
118
128
|
- [ ] Confidence is stated honestly (HIGH/MEDIUM/LOW); nothing at LOW was presented as fact.
|
|
129
|
+
- [ ] Run closes with the Step 5 verdict line: `Verification: HIGH|MEDIUM|LOW — <first failing check and deviation, or 'all checks passed'>`.
|
|
119
130
|
|
|
120
131
|
## See also
|
|
121
132
|
|
|
@@ -58,16 +58,20 @@ exactly: `Background`, `Requirements`, `Acceptance Criteria`, `Q&A`, `Design`, `
|
|
|
58
58
|
`spur task record 0040 --transition testing` reads `.spur/run/0040-verdict.json`, writes
|
|
59
59
|
`Testing` (per-requirement table), and backfills `Review` (P1–P4 findings table) **only when the
|
|
60
60
|
section is bare** — a standalone compatibility fallback, never an overwrite of the review
|
|
61
|
-
coordinator's authored Review — optionally backfilling a bare `Solution` from `git diff -U0`.
|
|
62
|
-
|
|
61
|
+
coordinator's authored Review — optionally backfilling a bare `Solution` from `git diff -U0`.
|
|
62
|
+
Record can also refresh its own earlier Review backfill. The `--transition testing` example stops
|
|
63
|
+
at testing; `--transition done` with a PASS verdict walks the forward lifecycle edges through their
|
|
64
|
+
guards and creates the required pipeline run-link immediately before the done hop.
|
|
63
65
|
|
|
64
66
|
- **Use `record`** when a verify step produced a verdict artifact — it is the pipeline's record step
|
|
65
67
|
and the deterministic `Testing` writer.
|
|
66
68
|
- **Use `update --section`** when you are authoring a section by hand (planning, design, narrative
|
|
67
69
|
solution) or amending one `record` already wrote.
|
|
68
70
|
|
|
69
|
-
`record`
|
|
70
|
-
|
|
71
|
+
`record` re-reads the verdict on each invocation. PASS/PARTIAL/FAIL artifacts refresh Testing;
|
|
72
|
+
UNKNOWN preserves authored Testing and writes a stub only when Testing is bare. Review remains
|
|
73
|
+
fallback-only: bare content or record's own earlier backfill can be replaced, authored Review cannot.
|
|
74
|
+
A later `update --section` overwrites either section (file-wins). Both use the same atomic write.
|
|
71
75
|
|
|
72
76
|
## Which section, when
|
|
73
77
|
|
|
@@ -81,7 +85,7 @@ LLM's job (orchestrated by `sp:spur-dev`); this skill only owns the *mechanism*:
|
|
|
81
85
|
| `Plan` | before `wip` | the step list |
|
|
82
86
|
| `Solution` | during impl (implement step) | the approach actually taken; L3 `file:line` rule fires once it has real content |
|
|
83
87
|
| `Testing` | testing phase (`record` — deterministic writer) | what was verified and how — gated at `wip→testing` by `check` |
|
|
84
|
-
| `Review` | review phase (`/sp:dev-review` coordinator) | merged SECU findings + verdict — gated at `testing→done` by `check --
|
|
88
|
+
| `Review` | review phase (`/sp:dev-review` coordinator) | merged SECU findings + verdict — gated at `testing→done` by `check --as done` |
|
|
85
89
|
|
|
86
90
|
Section presence per status is a **runtime contract**, not a table: query
|
|
87
91
|
`spur task sections <wbs> list --json` (permitted present sections) and `spur task check <wbs> --json`
|
|
@@ -204,7 +204,7 @@ spur workflow run ./workflows/approval.yaml --silent # errors only
|
|
|
204
204
|
spur workflow run ./workflows/approval.yaml --verbose # transitions + correlation diagnostics
|
|
205
205
|
spur workflow run ./workflows/approval.yaml --detail minimal # tersest human output
|
|
206
206
|
spur workflow run ./workflows/approval.yaml --trace-file # persist redacted JSONL trace
|
|
207
|
-
spur workflow run ./workflows/approval.yaml --no-log # opt out of the run record .spur/
|
|
207
|
+
spur workflow run ./workflows/approval.yaml --no-log # opt out of the run record .spur/memory/runs/<RUNID>.md + .state.json
|
|
208
208
|
spur workflow run ./workflows/approval.yaml --steer # interactive steering on stdin
|
|
209
209
|
```
|
|
210
210
|
|
|
@@ -214,7 +214,7 @@ spur workflow run ./workflows/approval.yaml --steer # interactive
|
|
|
214
214
|
per-step headers), `full` (transitions + correlation). `--verbose` is shorthand for `--detail full`.
|
|
215
215
|
- **`--trace-file`** appends a redacted, schema-versioned JSONL trace under `.spur/workflow/`
|
|
216
216
|
for post-run analysis - independent of human/JSON output.
|
|
217
|
-
- **`--no-log`** opts out of writing the two-file run record (`.spur/
|
|
217
|
+
- **`--no-log`** opts out of writing the two-file run record (`.spur/memory/runs/<RUNID>.md` + `.state.json`).
|
|
218
218
|
By default the record is written **and retained** after the run ends; this flag skips it entirely
|
|
219
219
|
(propagates to the `--async` detached worker). No `--keep-log` / delete-by-default exists.
|
|
220
220
|
- **`--steer`** is synchronous and in-process: it cannot combine with `--json` or `--async` (exit `2`).
|
|
@@ -285,7 +285,7 @@ not advertise `--json-envelope` because its JSON projection is a kept-raw docume
|
|
|
285
285
|
| `--verbose` | Include transitions and correlation diagnostics in human progress (implies `--detail full`). |
|
|
286
286
|
| `--detail <level>` | Human detail level: `minimal`, `invocation` (default), or `full`. |
|
|
287
287
|
| `--trace-file` | Append a redacted schema-versioned JSONL trace under `.spur/workflow/`. |
|
|
288
|
-
| `--no-log` | Opt out of writing the two-file run record `.spur/
|
|
288
|
+
| `--no-log` | Opt out of writing the two-file run record `.spur/memory/runs/<RUNID>.md` + `.state.json` (retained by default; propagates to `--async` workers). |
|
|
289
289
|
| `--steer` | Accept in-process steering commands on stdin at declared action boundaries (sync only; incompatible with `--json`/`--async`). |
|
|
290
290
|
|
|
291
291
|
`validate` and `run` exit non-zero on failure (`run` exits non-zero when the final status is not
|
|
@@ -302,7 +302,7 @@ Follow a live run to terminal (human streaming mode):
|
|
|
302
302
|
```bash
|
|
303
303
|
spur workflow trace <run-id> --follow # stream until terminal (default 1000ms poll)
|
|
304
304
|
spur workflow trace <run-id> --follow --poll 500 # poll every 500ms
|
|
305
|
-
spur workflow trace <run-id> --follow --output # stream .spur/
|
|
305
|
+
spur workflow trace <run-id> --follow --output # stream .spur/memory/runs/<RUNID>.md instead of the DB timeline
|
|
306
306
|
spur workflow trace <run-id> --follow --timeout 600000 # bound the watch; timeout → one checkpoint, run continues, exit 1
|
|
307
307
|
```
|
|
308
308
|
|
|
@@ -316,7 +316,7 @@ spur workflow trace <run-id> --follow --timeout 600000 # bound the watch; timeou
|
|
|
316
316
|
status and exits `1`. A watch timeout never cancels or relaunches the run; resume by re-running the same
|
|
317
317
|
follow command.
|
|
318
318
|
- **`--output`** swaps the follow source from the structured DB timeline to the human run record
|
|
319
|
-
(`.spur/
|
|
319
|
+
(`.spur/memory/runs/<RUNID>.md`, tail -f equivalent), streaming new lines as they land and exiting at
|
|
320
320
|
terminal status. A pre-0925 run with only a legacy `<RUNID>.log` is followed in place (read-only
|
|
321
321
|
fallback). It requires `--follow` and a `run-id`, is a human stream (rejects `--json`), and is
|
|
322
322
|
a **distinct source** — it never interleaves with the DB timeline. If no record file appears (e.g. the
|
|
@@ -360,15 +360,18 @@ redirecting `agent.run` stages (ADR-047).
|
|
|
360
360
|
|
|
361
361
|
- **Stale-run finalization** (existing): bulk-finalizes orphaned `running`/`pending` runs as `failed`.
|
|
362
362
|
`--older-than <minutes>` (default 30) and `--force` (all non-terminal regardless of age) apply here only.
|
|
363
|
-
- **Run-log reclamation** (0429): removes
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
363
|
+
- **Run-log reclamation** (0429): removes expired `.log` files from `.spur/memory/runs/`
|
|
364
|
+
and legacy `.spur/run/`, using `workflow.logRetentionDays` (default 30 days). Non-terminal
|
|
365
|
+
runs, including paused/interrupted runs, are protected; ownership lookup failures preserve logs.
|
|
366
|
+
The two-file record (`.md` + `.state.json`) and partial handoffs are retained.
|
|
367
|
+
- **Checkpoint reclamation**: removes expired terminal checkpoints only when confinement,
|
|
368
|
+
metadata and run-ownership guards pass. Recoverable runs remain protected.
|
|
369
|
+
- **Migration** (E71): verified lasting scratch data is preserved and references redirected before
|
|
370
|
+
reclamation. Unsettled imports, live dependencies, malformed files and conflicts retain their sources.
|
|
371
|
+
- **`--logs`** scopes to log reclamation and its migration (skips stale-run finalization and
|
|
372
|
+
checkpoint reclamation). `--dry-run` reports without copying or deleting. `--json` returns
|
|
373
|
+
`{ olderThanMinutes, dryRun, cleaned, logs, checkpoints, migration }`; with `--logs`, it returns
|
|
374
|
+
`{ retentionDays, dryRun, reclaimed, failures, migration }`.
|
|
372
375
|
|
|
373
376
|
## Behavior
|
|
374
377
|
|
|
@@ -206,6 +206,8 @@ section adds what that table has no room for: per-file content summaries and ite
|
|
|
206
206
|
step (glossary, config companions).** Read Step routing to find "which file for step X"; read below
|
|
207
207
|
for "what's actually in file Y" or for resources that sit outside the step sequence.
|
|
208
208
|
|
|
209
|
+
- Job transfer: [dump/resume](references/dev-operations.md#11a-job-dump).
|
|
210
|
+
|
|
209
211
|
- [references/glossary.md](references/glossary.md) — sp's own vocabulary: spine, competency, facade,
|
|
210
212
|
corpus, gate, verdict, noun/verb, half, HITL, WBS, section-write contract — canonical term +
|
|
211
213
|
Avoid list. Owns term definitions only; `cross-cutting.md` owns the process rules that use them.
|
|
@@ -135,7 +135,9 @@ workflow run` and never redirect silently to `agent.default`. Interactive **omit
|
|
|
135
135
|
**host-controlled and non-subprocess**, but no longer guarantees host-context execution for every
|
|
136
136
|
model stage (task 0508): an eligible `agent.run` stage — pure-slash input, non-interactive state,
|
|
137
137
|
native subagent with shared-worktree read/write/shell capability, and a task above the
|
|
138
|
-
`estimate_hours` dispatch floor
|
|
138
|
+
`estimate_hours` dispatch floor — a small, non-sensitive triage diffstat
|
|
139
|
+
(`.spur/run/<wbs>-diffstat.json`, verify only) keeps that stage host-inline (2026-09-15
|
|
140
|
+
subagent-dispatch evaluation; the floor and the
|
|
139
141
|
resume-over-re-dispatch rule for worker-role continuation stages are owned by
|
|
140
142
|
[inline-pipeline-driver.md](inline-pipeline-driver.md)) — dispatches **once** to that
|
|
141
143
|
native subagent and joins before the driver continues; any pre-dispatch eligibility failure falls
|