@gobing-ai/spur 0.3.96 → 0.3.98
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 +15 -9
- 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 +34 -18
- 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 +5 -5
- package/plugins/sp/scripts/feature-verification-steps.mjs +14 -1
- package/plugins/sp/scripts/feature-verification-steps.ts +14 -1
- package/plugins/sp/scripts/inline-run-setup.mjs +4 -2
- package/plugins/sp/scripts/inline-run-setup.ts +12 -12
- 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/code-implementation/SKILL.md +1 -1
- package/plugins/sp/skills/conflict-finding/SKILL.md +1 -1
- package/plugins/sp/skills/conflict-finding/references/authority-resolution.md +5 -3
- package/plugins/sp/skills/conflict-finding/references/remediation-routing.md +9 -8
- package/plugins/sp/skills/doc-evolve/SKILL.md +4 -3
- package/plugins/sp/skills/source-driven-development/SKILL.md +11 -0
- package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +16 -9
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +7 -4
- 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 +94 -17
- 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 +16 -8
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +46 -12
- package/spur.js +5241 -3762
- package/web/_astro/BoardApp.CYbe-OTb.js +322 -0
- package/web/_astro/{BoardApp.BIjMatT1.js → BoardApp.Dflq5a8x.js} +1 -1
- package/web/_astro/{TaskDetail.BZM3EAFx.js → TaskDetail.CK7oXddP.js} +1 -1
- package/web/_astro/{arc.HkRiZnoI.js → arc.BIqkDATc.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.BDdK-tgZ.js → architectureDiagram-3BPJPVTR.D0B3SvWv.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.CZ9kOBxx.js → blockDiagram-GPEHLZMM.C2YyKQmL.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DXK9qeRD.js → c4Diagram-AAUBKEIU.DmtTYrt_.js} +1 -1
- package/web/_astro/channel.JuKVyuhZ.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BTD2WeX5.js → chunk-2J33WTMH.HsRXbh7l.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.Ci6Qbcvk.js → chunk-4BX2VUAB.obE_P-R3.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.yl3zsj7p.js → chunk-55IACEB6.CjWTF5mz.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.ClXZsfyR.js → chunk-727SXJPM.DMo799Ax.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.BmKWZBcP.js → chunk-AQP2D5EJ.-cE6JOKZ.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.By30fcb7.js → chunk-FMBD7UC4.D6Rw6HIk.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.24NmrD-k.js → chunk-ND2GUHAM.d5rBD73Z.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BN4sKdcS.js → chunk-QZHKN3VN.r3ox40Q3.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CJpoMPb5.js → classDiagram-4FO5ZUOK.CGGbvjjJ.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CJpoMPb5.js → classDiagram-v2-Q7XG4LA2.CGGbvjjJ.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BraxQ2Nt.js → cose-bilkent-S5V4N54A.CmKtPd4l.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.gzoU73oL.js → cynefin-OW5HDTMX.QNg5GXs8.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.wyDfWySU.js → dagre-BM42HDAG.B1SabsjC.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.h2_EbMxV.js → diagram-2AECGRRQ.RtDbNj89.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Db6dcs89.js → diagram-5GNKFQAL.IzZpuM-Y.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.NVwzxu_U.js → diagram-KO2AKTUF.DPTF_fgv.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.B-eNgDQk.js → diagram-LMA3HP47.CCyza39P.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Bsa8ZRbJ.js → diagram-OG6HWLK6.BXAi8zcZ.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BospRE5Q.js → erDiagram-TEJ5UH35.DUDX7_MR.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DMzvHwAA.js → flowDiagram-I6XJVG4X.C63QTh3N.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.Dmdxgd-M.js → ganttDiagram-6RSMTGT7.CnlZJSwG.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CaqdO0rL.js → gitGraphDiagram-PVQCEYII.CTqIrOPC.js} +1 -1
- package/web/_astro/index.CbNz17Sx.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DDVD9Wn7.js → infoDiagram-5YYISTIA.BYMiDFh9.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.EKyCUFsL.js → ishikawaDiagram-YF4QCWOH.CRiQmI-z.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.By2-RkYB.js → journeyDiagram-JHISSGLW.YQgWBsEU.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.ar-WmlyR.js → kanban-definition-UN3LZRKU.1ECnrrK1.js} +1 -1
- package/web/_astro/{linear.BnzMBgo_.js → linear.DzVA71yT.js} +1 -1
- package/web/_astro/{mermaid.core.DSqeFu2Y.js → mermaid.core.BK7P2Sr_.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.1XcF8Wh2.js → mindmap-definition-RKZ34NQL.BvXyWZK7.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.DnItHOdK.js → pieDiagram-4H26LBE5.BiXk1TW3.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.BQQyrnBt.js → quadrantDiagram-W4KKPZXB.krvBH8lh.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.Dmzxun82.js → requirementDiagram-4Y6WPE33.ih1gL25c.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BBIeRKYB.js → sankeyDiagram-5OEKKPKP.CpUxkv4u.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.CUNVdBBf.js → sequenceDiagram-3UESZ5HK.clAfeN7y.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DlFf2sJm.js → stateDiagram-AJRCARHV.DpXWb8-m.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.DXR7I1H5.js → stateDiagram-v2-BHNVJYJU.ClZb9GIH.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BcO-GyX2.js → timeline-definition-PNZ67QCA.BLcX5dh7.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.ChCECmmW.js → vennDiagram-CIIHVFJN.oGgpKMpx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CRaT12mD.js → wardleyDiagram-YWT4CUSO.BPHtMtt-.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.B3lsHVpj.js → xychartDiagram-2RQKCTM6.BNZOPYSk.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') {
|
|
@@ -184,7 +184,7 @@ reproduce → isolate → minimal fix → regression guard.
|
|
|
184
184
|
|
|
185
185
|
- Writing code without having read the immediate callers of what you're changing.
|
|
186
186
|
- A `## Solution` section that lists files but not what changed or why.
|
|
187
|
-
- A `## Solution` section with file references that are not in backtick `` `path:line` `` form (L3 requirement: `` `packages/app/src/foo.ts:123` `` or `` `packages/app/src/bar.ts:10-20` ``; paths from repo root).
|
|
187
|
+
- A `## Solution` section with file references that are not in backtick `` `path:line` `` form (L3 requirement: `` `packages/app/src/foo.ts:123` `` or `` `packages/app/src/bar.ts:10-20` ``; paths from repo root). Non-repo paths (external packages, `@scope/...`) can never resolve repo-relative — cite them in the frozen external-evidence form instead: `Evidence: @gobing-ai/ts-x \`src/persistence.ts\` line 108` (origin and line number OUTSIDE the backticks). Wrong: `` `@gobing-ai/ts-x/src/persistence.ts:108` `` — the L4 gate fails it as an unresolvable anchor (task 1055).
|
|
188
188
|
- A new abstraction with exactly one caller and no second use in sight.
|
|
189
189
|
- The diff touches files unrelated to the task's scope.
|
|
190
190
|
- The diff touches another WBS's surfaces, or the pass reads sibling `todo` / `wip` task files.
|
|
@@ -249,7 +249,7 @@ Thin wrapper: `Skill(skill="sp:conflict-finding", args="$ARGUMENTS")`.
|
|
|
249
249
|
- Silently omitting a selected pillar or a tool failure (no coverage accounting).
|
|
250
250
|
- Claiming "comprehensive" when `coverage.complete` is false.
|
|
251
251
|
- Any mutation in audit mode, or automatic mutation merely because `--resolve` is present.
|
|
252
|
-
- Editing a derived projection before its
|
|
252
|
+
- Editing a derived projection before correcting a defective authority, or changing a correct authority merely to repair its stale projection.
|
|
253
253
|
- Adding a production analyzer, index/cache/database, dependency, or dedicated subagent in v1.
|
|
254
254
|
|
|
255
255
|
## Reference files
|
|
@@ -206,13 +206,15 @@ authority it governs:
|
|
|
206
206
|
| Owner | Repair route | Governs (claim types) |
|
|
207
207
|
|-------|--------------|----------------------|
|
|
208
208
|
| Task/feature corpus | `sp:spur-cli` / `spur task` / `spur feature`; never direct file writes | Task obligation/status, feature goal and AC |
|
|
209
|
-
| Numbered docs and AGENTS projections | `sp:doc-evolve`; authority first,
|
|
209
|
+
| Numbered docs and AGENTS projections | `sp:doc-evolve`; inspect authority first, correct it only if defective, then repair affected projections | Process rules, structural decision, product scope, architecture, command surface |
|
|
210
210
|
| Source/tests | create/use a Spur task and route through `sp:spur-dev` / build competencies | Implementation behavior, architecture mechanism |
|
|
211
211
|
| Command/skill capability source | Superskill command/skill lifecycle in the owning plugin source | Command/API/schema surface |
|
|
212
212
|
| Ambiguous authority | **stop** for an operator decision; do not mutate either side | (all, when unresolved) |
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
Inspect the governing authority before repairing a derived projection. Correct the authority first
|
|
215
|
+
only when it is defective and the correction is authorized; otherwise leave it unchanged and repair
|
|
216
|
+
the projection. The authority column of §2 remains the owner surface's source of truth; a projection
|
|
217
|
+
is brought into line with it, not the reverse.
|
|
216
218
|
|
|
217
219
|
## 7. Worked examples
|
|
218
220
|
|
|
@@ -113,13 +113,13 @@ between "the audit said X" and "the file now says Y."
|
|
|
113
113
|
Each approved repair is dispatched through the artifact's **owner surface** — the harness path that
|
|
114
114
|
owns writes to that artifact — not a direct file write. Authority for a repair is the same
|
|
115
115
|
authority model as the audit ([./authority-resolution.md](./authority-resolution.md)): for a given
|
|
116
|
-
subject + claim type, one artifact is normative
|
|
117
|
-
authority.
|
|
116
|
+
subject + claim type, one artifact is normative. Inspect it first; correct it only if defective and
|
|
117
|
+
authorized, then update affected projections. A correct authority needs no edit for a stale projection.
|
|
118
118
|
|
|
119
119
|
| Owner | Repair route | Never |
|
|
120
120
|
|-------|--------------|-------|
|
|
121
121
|
| Task/feature corpus | `sp:spur-cli` — `spur task` / `spur feature` (with `--section`, `--from-file`, `--json` as needed) | Direct file writes to task/feature files. |
|
|
122
|
-
| Numbered docs and AGENTS projections | `sp:doc-evolve` —
|
|
122
|
+
| Numbered docs and AGENTS projections | `sp:doc-evolve` — inspect authority first; correct it only if defective, then repair affected projections (see below) | Changing correct authority to repair a stale projection, or projecting a defective authority before its authorized correction. |
|
|
123
123
|
| Source / tests | Create or use a **Spur task** and route through `sp:spur-dev` / build competencies (`sp:code-implementation`, `sp:code-testing`), **unless** the active session already has explicit implementation authority for the change. | Bare in-place edits to source/tests without a task or explicit authority. |
|
|
124
124
|
| Command/skill capability source | Superskill command/skill **lifecycle** (`superskill command …` / `superskill skill …` scaffold/validate/evaluate/refine) in the owning plugin source. | Hand-editing capability source outside the Superskill lifecycle. |
|
|
125
125
|
| Ambiguous authority | **Stop for an operator decision.** Do not mutate either side. | Choosing a winner, editing both sides, or forcing a global ranking. |
|
|
@@ -148,12 +148,13 @@ item; never route around it with a direct write.
|
|
|
148
148
|
|
|
149
149
|
### 3.2 Numbered docs and AGENTS projections
|
|
150
150
|
|
|
151
|
-
Documentation repairs
|
|
151
|
+
Documentation repairs **inspect authority first**. If a derived
|
|
152
152
|
projection (`docs/04_DESIGN.md`, `docs/05_FEATURES.md`, `AGENTS.md`, templates) has drifted from its
|
|
153
153
|
authority (`docs/00_ADR.md`, `docs/01_PRD.md`, `docs/03_ARCHITECTURE.md`,
|
|
154
|
-
`docs/99_PROJECT_CONSTITUTION.md`),
|
|
155
|
-
projection via `sp:doc-evolve`.
|
|
156
|
-
|
|
154
|
+
`docs/99_PROJECT_CONSTITUTION.md`), leave a correct authority unchanged and repair the stale
|
|
155
|
+
projection via `sp:doc-evolve`. If the authority itself is defective, correct it first within the
|
|
156
|
+
approved scope, then update affected projections. Incomparable authority still requires an operator
|
|
157
|
+
decision; projection drift alone never authorizes changing the governing contract (see §6).
|
|
157
158
|
|
|
158
159
|
`sp:doc-evolve` owns the numbered-doc/projection lifecycle: sync checks, contract verification,
|
|
159
160
|
frontmatter checks, and derived-doc refresh. A doc repair is dispatched to it; it is not hand-applied.
|
|
@@ -244,7 +245,7 @@ These are the explicit boundaries for remediation. Each is a hard violation, not
|
|
|
244
245
|
| Anti-pattern | Why it is prohibited |
|
|
245
246
|
|--------------|----------------------|
|
|
246
247
|
| **Automatic mutation merely because `--resolve` is present** | `--resolve` only opens the workflow; it never authorizes automatic repair. Confirmation + freshness + owner surface are all required. |
|
|
247
|
-
| **
|
|
248
|
+
| **Changing correct authority for projection drift** | A stale projection is repaired against its governing contract. Correct authority only when defective and authorized; then update affected projections. |
|
|
248
249
|
| **Broad cleanup outside confirmed findings** | Only the artifacts named by confirmed, freshness-checked findings are touched. No opportunistic refactors or doc "improvements" while repairing. |
|
|
249
250
|
| **Ambiguity silently resolved** | Ambiguous/missing authority stops for an operator decision; it is never forced through a global ranking or a guessed owner. |
|
|
250
251
|
| **Partial failure silently continued** | A failed repair is reported, not papered over; siblings are not marked successful and the run does not roll forward silently. |
|
|
@@ -100,9 +100,10 @@ rg --files apps packages # real modules
|
|
|
100
100
|
git log -1 --format='%ci' -- docs/04_DESIGN.md # last touch vs. recent surface changes
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
**Repair protocol (§7):**
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
**Repair protocol (§7):** inspect the governing statement first. Correct it only if defective and
|
|
104
|
+
authorized, then update affected detail/index/entry files; otherwise leave the authority unchanged
|
|
105
|
+
and repair its stale projections. Preserve ADR numbers, original titles/dates and decision history;
|
|
106
|
+
editorial condensation follows §6.1, while actual reversals need a superseding decision. Record findings in a task/report.
|
|
106
107
|
Routine lessons go to existing learning/context storage (§8); no automatic constitution edits.
|
|
107
108
|
|
|
108
109
|
Output a **drift report**: per finding, `{ doc, what code says, what the doc says, authority, repair
|
|
@@ -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
|
|
|
@@ -42,13 +42,16 @@ for `Plan`, `Acceptance Criteria`, hand-authored `Solution`, and any narrative s
|
|
|
42
42
|
spur task update 0040 --section Review --from-file /tmp/review.md
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
3. The whole `### Review` body is now that file's contents. To amend rather than overwrite,
|
|
46
|
-
the current body (`spur task show 0040`), edit the temp file to the full desired state, and
|
|
47
|
-
replace again —
|
|
45
|
+
3. The whole `### Review` body is now that file's contents. To amend rather than overwrite, either
|
|
46
|
+
read the current body (`spur task show 0040`), edit the temp file to the full desired state, and
|
|
47
|
+
replace again — or pass `--append` (task 1057) to write the file's body after the existing
|
|
48
|
+
section content (existing content + one blank line + new body; surrounding whitespace trimmed; an empty/missing section is
|
|
49
|
+
created). `--append` requires `--section` and never replaces on its own.
|
|
48
50
|
|
|
49
51
|
**`Q&A` is the exception.** `--section "Q&A"` APPENDS a timestamped `#### Q&A entry — <ISO>`
|
|
50
52
|
block rather than replacing the section. Start the body with `<!-- qa:replace -->` to replace it
|
|
51
|
-
wholesale.
|
|
53
|
+
wholesale. Explicit `--append` on `Q&A` uses the generic append (no timestamped wrapper); the
|
|
54
|
+
`<!-- qa:replace -->` marker still wins and forces wholesale replace.
|
|
52
55
|
|
|
53
56
|
`--section` **requires** `--from-file` (exit `2` otherwise). Section names match the DD-08 headings
|
|
54
57
|
exactly: `Background`, `Requirements`, `Acceptance Criteria`, `Q&A`, `Design`, `Plan`, `Solution`, `Root Cause`, `Testing`, `Review`, `References`, `History`, `Notes` (universal sections are `History`, `References`, `Notes`; `Root Cause` is carried by the `issue` template variant).
|
|
@@ -58,16 +61,20 @@ exactly: `Background`, `Requirements`, `Acceptance Criteria`, `Q&A`, `Design`, `
|
|
|
58
61
|
`spur task record 0040 --transition testing` reads `.spur/run/0040-verdict.json`, writes
|
|
59
62
|
`Testing` (per-requirement table), and backfills `Review` (P1–P4 findings table) **only when the
|
|
60
63
|
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
|
-
|
|
64
|
+
coordinator's authored Review — optionally backfilling a bare `Solution` from `git diff -U0`.
|
|
65
|
+
Record can also refresh its own earlier Review backfill. The `--transition testing` example stops
|
|
66
|
+
at testing; `--transition done` with a PASS verdict walks the forward lifecycle edges through their
|
|
67
|
+
guards and creates the required pipeline run-link immediately before the done hop.
|
|
63
68
|
|
|
64
69
|
- **Use `record`** when a verify step produced a verdict artifact — it is the pipeline's record step
|
|
65
70
|
and the deterministic `Testing` writer.
|
|
66
71
|
- **Use `update --section`** when you are authoring a section by hand (planning, design, narrative
|
|
67
72
|
solution) or amending one `record` already wrote.
|
|
68
73
|
|
|
69
|
-
`record`
|
|
70
|
-
|
|
74
|
+
`record` re-reads the verdict on each invocation. PASS/PARTIAL/FAIL artifacts refresh Testing;
|
|
75
|
+
UNKNOWN preserves authored Testing and writes a stub only when Testing is bare. Review remains
|
|
76
|
+
fallback-only: bare content or record's own earlier backfill can be replaced, authored Review cannot.
|
|
77
|
+
A later `update --section` overwrites either section (file-wins). Both use the same atomic write.
|
|
71
78
|
|
|
72
79
|
## Which section, when
|
|
73
80
|
|
|
@@ -81,7 +88,7 @@ LLM's job (orchestrated by `sp:spur-dev`); this skill only owns the *mechanism*:
|
|
|
81
88
|
| `Plan` | before `wip` | the step list |
|
|
82
89
|
| `Solution` | during impl (implement step) | the approach actually taken; L3 `file:line` rule fires once it has real content |
|
|
83
90
|
| `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 --
|
|
91
|
+
| `Review` | review phase (`/sp:dev-review` coordinator) | merged SECU findings + verdict — gated at `testing→done` by `check --as done` |
|
|
85
92
|
|
|
86
93
|
Section presence per status is a **runtime contract**, not a table: query
|
|
87
94
|
`spur task sections <wbs> list --json` (permitted present sections) and `spur task check <wbs> --json`
|