@gobing-ai/spur 0.3.68 → 0.3.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/corpus-baseline.json +0 -18
  3. package/config/plugin-scripts.json +8 -0
  4. package/config/workflow-composition-baseline.json +123 -377
  5. package/config/workflows/basic.yaml +1 -1
  6. package/config/workflows/docs-pipeline.yaml +1 -1
  7. package/config/workflows/feature-dev.yaml +1 -1
  8. package/config/workflows/idea-pipeline.yaml +1 -1
  9. package/config/workflows/task-pipeline.yaml +62 -20
  10. package/config/workflows/wayfinder-resolution.yaml +1 -1
  11. package/config/workflows/wrapup-pipeline.yaml +1 -1
  12. package/package.json +9 -9
  13. package/plugins/sp/plugin.json +1 -1
  14. package/plugins/sp/scripts/surface-drift-inventory.ts +2 -15
  15. package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
  16. package/plugins/sp/scripts/task-size-precheck.ts +21 -79
  17. package/plugins/sp/scripts/verify-answer-lint.ts +386 -0
  18. package/plugins/sp/skills/code-verification/SKILL.md +11 -10
  19. package/plugins/sp/skills/code-verification/references/verdict-schema.md +10 -0
  20. package/plugins/sp/skills/spur-cli/references/history.md +1 -0
  21. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +2 -1
  22. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +16 -0
  23. package/spur.js +1275 -292
  24. package/web/_astro/BoardApp.DHj-03Dp.js +1 -0
  25. package/web/_astro/{BoardApp.BQFbkeqq.js → BoardApp.DOadeEJV.js} +77 -77
  26. package/web/_astro/{TaskDetail.Dl2Eaj1w.js → TaskDetail.DKzpkDj5.js} +1 -1
  27. package/web/_astro/{arc.uG14rp8A.js → arc.Bsa0gprH.js} +1 -1
  28. package/web/_astro/{architectureDiagram-3BPJPVTR.Dye6uD_x.js → architectureDiagram-3BPJPVTR.LOUZCDBC.js} +1 -1
  29. package/web/_astro/{blockDiagram-GPEHLZMM.B9Pkh7Hb.js → blockDiagram-GPEHLZMM.CGee4isG.js} +1 -1
  30. package/web/_astro/{c4Diagram-AAUBKEIU.C2x7SC_X.js → c4Diagram-AAUBKEIU.B2CrQJ_O.js} +1 -1
  31. package/web/_astro/channel.BGL7KSHC.js +1 -0
  32. package/web/_astro/{chunk-2J33WTMH.D2p4-nWk.js → chunk-2J33WTMH.DJvf8RiP.js} +1 -1
  33. package/web/_astro/{chunk-4BX2VUAB.S-6jf33o.js → chunk-4BX2VUAB.B_l35O-Y.js} +1 -1
  34. package/web/_astro/{chunk-55IACEB6.DVXj4Fdh.js → chunk-55IACEB6.DqbxMswU.js} +1 -1
  35. package/web/_astro/{chunk-727SXJPM.Dz689FMN.js → chunk-727SXJPM.D_7ZGNjE.js} +1 -1
  36. package/web/_astro/{chunk-AQP2D5EJ.KxYj5TnI.js → chunk-AQP2D5EJ.Cq6yZ0s8.js} +1 -1
  37. package/web/_astro/{chunk-FMBD7UC4.itTQyHQB.js → chunk-FMBD7UC4.BWrlUPYi.js} +1 -1
  38. package/web/_astro/{chunk-ND2GUHAM.euSrbJf5.js → chunk-ND2GUHAM.BPLaXFZH.js} +1 -1
  39. package/web/_astro/{chunk-QZHKN3VN.OWASJRQy.js → chunk-QZHKN3VN.BtmAEAwZ.js} +1 -1
  40. package/web/_astro/{classDiagram-4FO5ZUOK.BLvrlpNO.js → classDiagram-4FO5ZUOK.CWycT7Ia.js} +1 -1
  41. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BLvrlpNO.js → classDiagram-v2-Q7XG4LA2.CWycT7Ia.js} +1 -1
  42. package/web/_astro/{cose-bilkent-S5V4N54A.XBF-rmyD.js → cose-bilkent-S5V4N54A.DyxjSWon.js} +1 -1
  43. package/web/_astro/{cynefin-OW5HDTMX.DlCx762Z.js → cynefin-OW5HDTMX.DnNfLBpX.js} +1 -1
  44. package/web/_astro/{dagre-BM42HDAG.D17Rshxv.js → dagre-BM42HDAG.BFAvnjKD.js} +1 -1
  45. package/web/_astro/{diagram-2AECGRRQ.AhBIVJC8.js → diagram-2AECGRRQ.BAoNCFeB.js} +1 -1
  46. package/web/_astro/{diagram-5GNKFQAL.C9ximjyC.js → diagram-5GNKFQAL.DiiKe7BP.js} +1 -1
  47. package/web/_astro/{diagram-KO2AKTUF.CZb7Ru_9.js → diagram-KO2AKTUF.t_uOQWVP.js} +1 -1
  48. package/web/_astro/{diagram-LMA3HP47.BW7LwqoS.js → diagram-LMA3HP47.SB9997cP.js} +1 -1
  49. package/web/_astro/{diagram-OG6HWLK6.XC025W0V.js → diagram-OG6HWLK6.Bgsxgf5G.js} +1 -1
  50. package/web/_astro/{erDiagram-TEJ5UH35.CpMXmBDP.js → erDiagram-TEJ5UH35.DO9OAOAc.js} +1 -1
  51. package/web/_astro/{flowDiagram-I6XJVG4X.D2ednJWg.js → flowDiagram-I6XJVG4X.CIb41ZyL.js} +1 -1
  52. package/web/_astro/{ganttDiagram-6RSMTGT7.BjL9FGKO.js → ganttDiagram-6RSMTGT7.Dc8Lk0fV.js} +1 -1
  53. package/web/_astro/{gitGraphDiagram-PVQCEYII.B90g1VGk.js → gitGraphDiagram-PVQCEYII.CpPmMWrl.js} +1 -1
  54. package/web/_astro/index.C-t8kB0T.css +1 -0
  55. package/web/_astro/{infoDiagram-5YYISTIA.RqgycKtQ.js → infoDiagram-5YYISTIA.DxBXfQSf.js} +1 -1
  56. package/web/_astro/{ishikawaDiagram-YF4QCWOH.Ctn-zt6a.js → ishikawaDiagram-YF4QCWOH.gA6OKtXq.js} +1 -1
  57. package/web/_astro/{journeyDiagram-JHISSGLW.DJhT8Ctp.js → journeyDiagram-JHISSGLW.CMEyLMmm.js} +1 -1
  58. package/web/_astro/{kanban-definition-UN3LZRKU.BY1QdejI.js → kanban-definition-UN3LZRKU.CKXlUNCr.js} +1 -1
  59. package/web/_astro/{linear.Di7YObSt.js → linear.CpTGdVx3.js} +1 -1
  60. package/web/_astro/{mermaid.core.CbxtJS3Q.js → mermaid.core.c8SSCsE5.js} +4 -4
  61. package/web/_astro/{mindmap-definition-RKZ34NQL.CxvR4g_J.js → mindmap-definition-RKZ34NQL.BH59HDw6.js} +1 -1
  62. package/web/_astro/{pieDiagram-4H26LBE5.jNWqnBHH.js → pieDiagram-4H26LBE5.DmgFXkZR.js} +1 -1
  63. package/web/_astro/{quadrantDiagram-W4KKPZXB.BCp12MbA.js → quadrantDiagram-W4KKPZXB.DyfB7N4J.js} +1 -1
  64. package/web/_astro/{requirementDiagram-4Y6WPE33.Dxhm4TyR.js → requirementDiagram-4Y6WPE33.DosszoFo.js} +1 -1
  65. package/web/_astro/{sankeyDiagram-5OEKKPKP.BtQXp4J9.js → sankeyDiagram-5OEKKPKP.pBkHhvVN.js} +1 -1
  66. package/web/_astro/{sequenceDiagram-3UESZ5HK.BWEM1R_Q.js → sequenceDiagram-3UESZ5HK.BE8tmkNR.js} +1 -1
  67. package/web/_astro/{stateDiagram-AJRCARHV.BG3wUkWB.js → stateDiagram-AJRCARHV.C4n7vDiG.js} +1 -1
  68. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BLtMeFVP.js → stateDiagram-v2-BHNVJYJU.BpgwYlRy.js} +1 -1
  69. package/web/_astro/{timeline-definition-PNZ67QCA.D5fHo0az.js → timeline-definition-PNZ67QCA.DvcVKc-W.js} +1 -1
  70. package/web/_astro/{vennDiagram-CIIHVFJN.0DcuMluU.js → vennDiagram-CIIHVFJN.BLOk-UOl.js} +1 -1
  71. package/web/_astro/{wardleyDiagram-YWT4CUSO.BZ-dxgHm.js → wardleyDiagram-YWT4CUSO.CIzB7M2o.js} +1 -1
  72. package/web/_astro/{xychartDiagram-2RQKCTM6.Bg-XWF7z.js → xychartDiagram-2RQKCTM6.CGC_lZ19.js} +1 -1
  73. package/web/index.html +2 -2
  74. package/web/_astro/BoardApp.CTkqrhWd.js +0 -1
  75. package/web/_astro/channel.Dsvulp7W.js +0 -1
  76. package/web/_astro/index.BVXdIsZV.css +0 -1
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * verify-answer-lint — deterministic pre-verdict answer lint (0726 R3).
4
+ *
5
+ * Runs AFTER the verify agent exits and BEFORE `spur task verdict --from-answer`.
6
+ * The verifier owns the answer file (`.spur/run/<wbs>-verify-answer.txt`): it creates
7
+ * it with `Verdict: PARTIAL`, appends one complete requirement/AC row at a time, and
8
+ * only replaces the first verdict line once every row is certified. Because the file
9
+ * is now append-progress instead of a single captured blob, malformed rows can reach
10
+ * the verdict step — this lint rejects each invalid class with a row-level message:
11
+ *
12
+ * - missing, duplicate, or unknown requirement IDs (vs the task's Requirements)
13
+ * - AC IDs that do not exactly match the task's AC checklist label or a linked
14
+ * feature scenario title
15
+ * - status / evidence-type values the verdict parser would drop
16
+ * - empty evidence
17
+ *
18
+ * Compound evidence types (`test + command`) stay valid — normalization mirrors
19
+ * `packages/app/src/services/task-verdict.ts` exactly, so anything this lint accepts
20
+ * is also accepted by `spur task verdict --from-answer` (and vice versa).
21
+ *
22
+ * Exits non-zero on any finding, with bounded diagnostics (first 10). Writes nothing.
23
+ *
24
+ * Ships with the plugin to arbitrary projects; node-builtin only — no workspace imports.
25
+ *
26
+ * Usage:
27
+ * bun plugins/sp/scripts/verify-answer-lint.ts <wbs> --answer <path> [--spur-bin <path>]
28
+ *
29
+ * Env: SPUR_BIN
30
+ */
31
+
32
+ import { execFileSync } from 'node:child_process';
33
+ import { existsSync, readFileSync } from 'node:fs';
34
+ import { fileURLToPath } from 'node:url';
35
+
36
+ // ─── CLI (same spur-bin chain as task-evidence-precheck.ts) ─────────────────
37
+
38
+ function usage(): never {
39
+ console.error('Usage: bun plugins/sp/scripts/verify-answer-lint.ts <wbs> --answer <path> [--spur-bin <path>]');
40
+ process.exit(1);
41
+ }
42
+
43
+ function defaultSpurBin(): string {
44
+ if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
45
+ const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
46
+ if (existsSync(local)) return `bun ${local}`;
47
+ return 'spur';
48
+ }
49
+
50
+ function parseArgs(argv: string[]): { wbs: string; answer: string; spurBin: string } {
51
+ let spurBin = defaultSpurBin();
52
+ let wbs = '';
53
+ let answer = '';
54
+ let i = 0;
55
+ while (i < argv.length) {
56
+ const arg = argv[i];
57
+ if (arg === '--spur-bin') {
58
+ spurBin = argv[i + 1] ?? defaultSpurBin();
59
+ i += 2;
60
+ } else if (arg === '--answer') {
61
+ answer = argv[i + 1] ?? '';
62
+ i += 2;
63
+ } else if (!arg.startsWith('--')) {
64
+ wbs = arg;
65
+ i++;
66
+ } else {
67
+ i++;
68
+ }
69
+ }
70
+ if (!wbs || !answer) usage();
71
+ return { wbs, answer, spurBin };
72
+ }
73
+
74
+ function runSpur(spurBin: string, args: string[]): string {
75
+ const [file = 'spur', ...lead] = spurBin.split(/\s+/).filter(Boolean);
76
+ return execFileSync(file, [...lead, ...args], {
77
+ encoding: 'utf-8',
78
+ stdio: ['pipe', 'pipe', 'pipe'],
79
+ });
80
+ }
81
+
82
+ // ─── Answer parsing — mirrors packages/app/src/services/task-verdict.ts ─────
83
+
84
+ interface ReqRow {
85
+ id: string;
86
+ status: string;
87
+ evidence: string;
88
+ line: number;
89
+ }
90
+ interface AcRow {
91
+ id: string;
92
+ status: string;
93
+ evidenceType: string;
94
+ evidence: string;
95
+ line: number;
96
+ }
97
+
98
+ function splitTableCells(line: string): string[] {
99
+ return line
100
+ .split(/(?<!\\)\|/)
101
+ .map((c) => c.replace(/\\\|/g, '|').trim())
102
+ .filter(Boolean);
103
+ }
104
+
105
+ function normalizeReqStatus(raw: string): string | null {
106
+ if (/\bMET\b/.test(raw)) return 'MET';
107
+ if (/\bPARTIAL\b/.test(raw)) return 'PARTIAL';
108
+ if (/\bUNMET\b/.test(raw)) return 'UNMET';
109
+ return null;
110
+ }
111
+
112
+ function normalizeAcStatus(raw: string): string | null {
113
+ if (/\bMET\b/.test(raw)) return 'MET';
114
+ if (/\bPARTIAL\b/.test(raw)) return 'PARTIAL';
115
+ if (/\bUNMET\b/.test(raw)) return 'UNMET';
116
+ if (/\bN\/A\b/.test(raw) || /\bNA\b/.test(raw)) return 'N/A';
117
+ return null;
118
+ }
119
+
120
+ function normalizeEvidenceTypeToken(normalized: string): string | null {
121
+ if (normalized === 'test') return 'test';
122
+ if (normalized === 'command') return 'command';
123
+ if (
124
+ normalized === 'static-ref' ||
125
+ normalized === 'static' ||
126
+ normalized === 'doc' ||
127
+ normalized === 'docs' ||
128
+ normalized === 'documentation'
129
+ ) {
130
+ return 'static-ref';
131
+ }
132
+ if (normalized === 'manual-review' || normalized === 'manual') return 'manual-review';
133
+ if (normalized === 'llm-judge' || normalized === 'judge') return 'llm-judge';
134
+ if (normalized === 'n/a' || normalized === 'na') return 'n/a';
135
+ return null;
136
+ }
137
+
138
+ const EVIDENCE_TYPE_PRECEDENCE = ['test', 'command', 'static-ref', 'manual-review', 'llm-judge', 'n/a'] as const;
139
+
140
+ function normalizeEvidenceType(raw: string): string | null {
141
+ const normalized = raw.toLowerCase().trim();
142
+ const single = normalizeEvidenceTypeToken(normalized);
143
+ if (single !== null) return single;
144
+ const parts = normalized.split(/[+,/]/).filter((p) => p.trim());
145
+ const tokens = parts.map((part) => normalizeEvidenceTypeToken(part.trim())).filter((t) => t !== null);
146
+ if (tokens.length < 2 || tokens.length !== parts.length) return null;
147
+ return EVIDENCE_TYPE_PRECEDENCE.find((candidate) => tokens.includes(candidate)) ?? null;
148
+ }
149
+
150
+ interface AnswerTables {
151
+ verdict: { value: string; line: number } | null;
152
+ reqs: ReqRow[];
153
+ acs: AcRow[];
154
+ }
155
+
156
+ function parseAnswer(text: string): AnswerTables {
157
+ const out: AnswerTables = { verdict: null, reqs: [], acs: [] };
158
+ const lines = text.split('\n');
159
+ let reqTable = false;
160
+ let acTable = false;
161
+
162
+ for (let i = 0; i < lines.length; i++) {
163
+ const trimmed = lines[i]?.trim() ?? '';
164
+ const lineNo = i + 1;
165
+
166
+ // A markdown heading closes whichever table is open (mirrors the verdict parser).
167
+ if ((reqTable || acTable) && /^#{1,6}\s/.test(trimmed)) {
168
+ reqTable = false;
169
+ acTable = false;
170
+ continue;
171
+ }
172
+ if (!trimmed.startsWith('|')) continue;
173
+ const cells = splitTableCells(trimmed);
174
+ if (/^[-:]+$/.test(cells[0] ?? '')) continue;
175
+
176
+ const h0 = (cells[0] ?? '').toLowerCase();
177
+ const h1 = (cells[1] ?? '').toLowerCase();
178
+
179
+ // Requirement header: `| Req | Status | Evidence |` (id-like first cell + status column).
180
+ if (!reqTable && !acTable && cells.length >= 2) {
181
+ const idLike = h0.includes('req') || h0 === 'requirement' || h0 === 'r#' || h0 === 'r' || /^r\d+$/.test(h0);
182
+ if (idLike && (h1.includes('status') || h1 === 'verdict')) {
183
+ reqTable = true;
184
+ continue;
185
+ }
186
+ if (
187
+ (h0 === 'ac' || h0.includes('acceptance')) &&
188
+ h1.includes('status') &&
189
+ (cells[2] ?? '').toLowerCase().includes('evidence')
190
+ ) {
191
+ acTable = true;
192
+ continue;
193
+ }
194
+ }
195
+
196
+ if (reqTable && cells.length >= 2) {
197
+ // An AC header following the requirement table closes it (mirrors the parser).
198
+ if ((h0 === 'ac' || h0.includes('acceptance')) && h1.includes('status')) {
199
+ reqTable = false;
200
+ acTable = true;
201
+ continue;
202
+ }
203
+ out.reqs.push({
204
+ id: cells[0] ?? '',
205
+ status: (cells[1] ?? '').toUpperCase(),
206
+ evidence: cells[2] ?? '',
207
+ line: lineNo,
208
+ });
209
+ continue;
210
+ }
211
+ if (acTable && cells.length >= 3) {
212
+ out.acs.push({
213
+ id: cells[0] ?? '',
214
+ status: cells[1] ?? '',
215
+ evidenceType: cells[2] ?? '',
216
+ evidence: cells[3] ?? '',
217
+ line: lineNo,
218
+ });
219
+ }
220
+ }
221
+
222
+ const verdictMatches = [...text.matchAll(/^\s*Verdict:\s*(\S+)\s*$/gim)];
223
+ if (verdictMatches.length === 1) {
224
+ out.verdict = {
225
+ value: verdictMatches[0]?.[1] ?? '',
226
+ line: text.slice(0, verdictMatches[0]?.index ?? 0).split('\n').length,
227
+ };
228
+ }
229
+ return out;
230
+ }
231
+
232
+ // ─── Task-side identity extraction ───────────────────────────────────────────
233
+
234
+ function sectionBetween(text: string, heading: string): string {
235
+ const marker = new RegExp(`^#{1,6}\\s+${heading}\\s*$`, 'im');
236
+ const match = marker.exec(text);
237
+ if (!match || match.index === undefined) return '';
238
+ const rest = text.slice(match.index);
239
+ const lineEnd = rest.indexOf('\n');
240
+ const afterHeading = lineEnd === -1 ? '' : rest.slice(lineEnd + 1);
241
+ const next = afterHeading.search(/^#{1,6}\s/m);
242
+ return next === -1 ? afterHeading : afterHeading.slice(0, next);
243
+ }
244
+
245
+ function extractRequirementIds(taskContent: string): string[] {
246
+ const section = sectionBetween(taskContent, 'Requirements');
247
+ const ids = new Set<string>();
248
+ for (const m of section.matchAll(/\*\*(R\d+(?:\.\d+)*)\s*\./g)) ids.add(m[1] ?? '');
249
+ for (const m of section.matchAll(/\*\*(R\d+(?:\.\d+)*)\*\*/g)) ids.add(m[1] ?? '');
250
+ return [...ids];
251
+ }
252
+
253
+ function extractAcIdentities(taskContent: string, featureContent: string | null): string[] {
254
+ const identities = new Set<string>();
255
+ const section = sectionBetween(taskContent, 'Acceptance Criteria');
256
+ for (const m of section.matchAll(/^[-*]\s+\[[ x]\]\s+(.+?)\s*(?::|$)/gm)) {
257
+ const label = (m[1] ?? '').trim();
258
+ if (!label) continue;
259
+ identities.add(label);
260
+ const leading = label.split(/\s+/)[0] ?? '';
261
+ if (leading && leading !== label) identities.add(leading);
262
+ }
263
+ if (featureContent !== null) {
264
+ for (const m of featureContent.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)) {
265
+ const title = (m[1] ?? '').trim();
266
+ if (title) identities.add(title);
267
+ }
268
+ }
269
+ return [...identities];
270
+ }
271
+
272
+ // ─── Main ────────────────────────────────────────────────────────────────────
273
+
274
+ function main(): void {
275
+ const { wbs, answer, spurBin } = parseArgs(process.argv.slice(2));
276
+ const findings: string[] = [];
277
+ const add = (msg: string): void => {
278
+ if (findings.length < 10) findings.push(msg);
279
+ };
280
+
281
+ if (!existsSync(answer)) {
282
+ console.error(`verify-answer-lint: FAIL — answer file not found: ${answer}`);
283
+ process.exit(1);
284
+ }
285
+ const raw = readFileSync(answer, 'utf8');
286
+ if (!raw.trim()) {
287
+ console.error(`verify-answer-lint: FAIL — answer file is empty: ${answer}`);
288
+ process.exit(1);
289
+ }
290
+
291
+ const tables = parseAnswer(raw);
292
+ if (tables.verdict === null) {
293
+ add('no `Verdict:` line (expected exactly one `Verdict: PASS|PARTIAL|FAIL` line)');
294
+ } else if (!/^(PASS|PARTIAL|FAIL)$/i.test(tables.verdict.value)) {
295
+ add(`line ${tables.verdict.line}: invalid Verdict value "${tables.verdict.value}" (PASS | PARTIAL | FAIL)`);
296
+ }
297
+
298
+ let taskContent = '';
299
+ let featureId = '';
300
+ try {
301
+ const task = JSON.parse(runSpur(spurBin, ['task', 'show', wbs, '--json'])) as {
302
+ content?: string;
303
+ body?: string;
304
+ feature_id?: string;
305
+ frontmatter?: { feature_id?: string };
306
+ };
307
+ taskContent = task.content ?? task.body ?? '';
308
+ featureId = task.feature_id ?? task.frontmatter?.feature_id ?? '';
309
+ } catch {
310
+ console.error(`verify-answer-lint: FAIL — could not fetch task ${wbs} via ${spurBin}`);
311
+ process.exit(1);
312
+ }
313
+ if (!taskContent) {
314
+ console.error(`verify-answer-lint: FAIL — task ${wbs} returned no content via ${spurBin}`);
315
+ process.exit(1);
316
+ }
317
+
318
+ let featureContent: string | null = null;
319
+ if (featureId) {
320
+ try {
321
+ const feature = JSON.parse(runSpur(spurBin, ['feature', 'show', featureId, '--json'])) as {
322
+ content?: string;
323
+ };
324
+ featureContent = feature.content ?? '';
325
+ } catch {
326
+ featureContent = null; // checklist labels still apply; scenario titles unavailable
327
+ }
328
+ }
329
+
330
+ const reqIds = extractRequirementIds(taskContent);
331
+ const acIdentities = extractAcIdentities(taskContent, featureContent);
332
+
333
+ // Requirement rows: completeness, no unknowns, no duplicates, valid status, non-empty evidence.
334
+ const seenReq = new Set<string>();
335
+ for (const row of tables.reqs) {
336
+ if (!reqIds.includes(row.id))
337
+ add(`line ${row.line}: unknown requirement ID "${row.id}" (task declares: ${reqIds.join(', ') || 'none'})`);
338
+ else if (seenReq.has(row.id)) add(`line ${row.line}: duplicate requirement row "${row.id}"`);
339
+ seenReq.add(row.id);
340
+ if (normalizeReqStatus(row.status) === null)
341
+ add(`line ${row.line}: "${row.id}" invalid status "${row.status}" (MET | PARTIAL | UNMET)`);
342
+ if (!row.evidence.trim()) add(`line ${row.line}: "${row.id}" has empty evidence`);
343
+ }
344
+ for (const id of reqIds) {
345
+ if (!seenReq.has(id)) add(`missing requirement row for "${id}"`);
346
+ }
347
+
348
+ // AC rows: identity must exactly match a checklist label/token or a scenario title;
349
+ // status and evidence type must normalize; evidence non-empty. AC completeness is the
350
+ // verifier's authoring contract, not a lint rejection class (0726 R3).
351
+ const seenAc = new Set<string>();
352
+ for (const row of tables.acs) {
353
+ if (!acIdentities.includes(row.id)) {
354
+ add(
355
+ `line ${row.line}: AC ID "${row.id.slice(0, 60)}" matches no task AC checklist label or scenario title`,
356
+ );
357
+ } else if (seenAc.has(row.id)) {
358
+ add(`line ${row.line}: duplicate AC row "${row.id.slice(0, 60)}"`);
359
+ }
360
+ seenAc.add(row.id);
361
+ if (normalizeAcStatus(row.status) === null)
362
+ add(`line ${row.line}: invalid AC status "${row.status}" (MET | PARTIAL | UNMET | N/A)`);
363
+ if (normalizeEvidenceType(row.evidenceType) === null)
364
+ add(
365
+ `line ${row.line}: invalid evidence type "${row.evidenceType}" (test | command | static-ref | manual-review | llm-judge | n/a, or a + compound)`,
366
+ );
367
+ if (!row.evidence.trim()) add(`line ${row.line}: AC "${row.id.slice(0, 40)}" has empty evidence`);
368
+ }
369
+
370
+ if (findings.length > 0) {
371
+ console.error(
372
+ `verify-answer-lint: FAIL — ${findings.length}${findings.length >= 10 ? '+' : ''} finding(s) in ${answer}`,
373
+ );
374
+ for (const f of findings) console.error(` ${f}`);
375
+ process.exit(1);
376
+ }
377
+
378
+ const reqCount = tables.reqs.length;
379
+ const acCount = tables.acs.length;
380
+ console.error(
381
+ `verify-answer-lint: PASS — ${reqCount} requirement row(s), ${acCount} AC row(s), verdict ${tables.verdict?.value ?? '?'}`,
382
+ );
383
+ process.exit(0);
384
+ }
385
+
386
+ main();
@@ -247,9 +247,10 @@ completion gate (`PARTIAL`/`FAIL` route the pipeline to `failed`).
247
247
  ### Step 10 — Emit the verdict artifact (the only verify output)
248
248
 
249
249
  Assemble the evidence and **emit the canonical verdict artifact** — verification writes no task
250
- section (F92 0593 R1). Under the pipeline, the output is captured as
251
- `.spur/run/<wbs>-verify-answer.txt`; a deterministic shell step derives
252
- `.spur/run/<wbs>-verdict.json`, and the `record` step transcribes `## Testing` from it.
250
+ section (F92 0593 R1). Under the pipeline **you** write
251
+ `.spur/run/<wbs>-verify-answer.txt` (0726 R3: host `expectFile`, never captured/overwritten); a
252
+ deterministic shell step lints it and derives `.spur/run/<wbs>-verdict.json`, and `record`
253
+ transcribes `## Testing` from it.
253
254
 
254
255
  **Standalone** (`/sp:dev-verify` outside the pipeline), write the artifact yourself, then invoke
255
256
  the deterministic Testing writer `spur task record` (section authorship never happens here):
@@ -306,14 +307,14 @@ The per-requirement traceability table MUST use `| Req | Status | Evidence |` (e
306
307
  The parser is tolerant of these variants (defense-in-depth), but the authoring contract is
307
308
  canonical.
308
309
 
309
- **Under the pipeline** (`task-pipeline.yaml`), `agent.run answerFile` captures this whole output to
310
- `.spur/run/<wbs>-verify-answer.txt`. A deterministic shell step then derives
311
- `.spur/run/<wbs>-verdict.json` from it plus an independent `spur task check` (R9; the agent
312
- reporting PASS in prose is necessary but not sufficient the artifact is never left to the agent's
313
- discretion). Section transcription follows the Step 10 contract (record `## Testing`; bare-only
314
- Review fallback; verify never writes sections).
310
+ Under the pipeline, the verifier owns the answer file `.spur/run/<wbs>-verify-answer.txt` (0726
311
+ R3): `Verdict: PARTIAL` first, append one row at a time, replace the verdict line only when all
312
+ rows are certified interruptions leave lintable partial rows; retries fill only missing IDs.
313
+ Host: `expectFile` `verify-answer-lint.ts` verdict + `spur task check` (R9). Vocabularies,
314
+ rejection classes, and the AC identity rule: `references/verdict-schema.md`. Sections follow the
315
+ Step 10 contract.
315
316
 
316
- **Standalone** (`/sp:dev-verify` outside the pipeline — no answer-file capture exists), write the
317
+ **Standalone** (outside the pipeline — no host lint/derive step), write the answer file and
317
318
  artifact yourself; shape and field-by-field contract in
318
319
  [references/verdict-schema.md](references/verdict-schema.md):
319
320
 
@@ -106,6 +106,16 @@ For answer files, emit a matching parseable table:
106
106
  | Scenario: CLI emits JSON | MET | test | `apps/cli/tests/foo.test.ts:42` |
107
107
  ```
108
108
 
109
+ **Authoring contract under the pipeline (0726 R3).** The verifier owns the answer file: write
110
+ `Verdict: PARTIAL` first, append one complete row at a time, and replace the first verdict line only
111
+ after every row is certified. `verify-answer-lint.ts` gates the file before `spur task verdict
112
+ --from-answer` and rejects, with row-level diagnostics: missing/duplicate/unknown requirement IDs,
113
+ AC ids that do not exactly match a task AC checklist label (or its leading token, e.g. `AC1`) or a
114
+ linked feature scenario title, invalid status (`MET | PARTIAL | UNMET` for requirements;
115
+ `N/A` additionally allowed for AC), invalid evidence type (`test | command | static-ref |
116
+ manual-review | llm-judge | n/a`, or a `+` compound), and empty evidence. Interrupted runs keep the
117
+ rows that pass the lint and complete only the missing IDs on retry.
118
+
109
119
  ## Checks evidence
110
120
 
111
121
  Wave C verification can emit the following additive `checks[]` rows:
@@ -22,6 +22,7 @@ live in `packages/app/src/services/history-service.ts` (`FanOutResult`, `DailyRe
22
22
  | `analyze` | Aggregate imported rows and write a versioned forensic artifact | `--since <iso>` `--until <iso>` `--source <source>` `--session <id>` `--run <runId>` `--task <wbs>` `--top <n>` `--out <path>` `--json` |
23
23
  | `report [path]` | Purely render an existing artifact; default to `latest.json` | `--mode <name>` `--task <wbs>` `--top <n>` `--json` |
24
24
  | `daily` | Run import-all → analyze → artifact → 90-day report pruning once | `--since <iso>` `--until <iso>` `--root <path>` `--source-timeout <ms>` `--mode <name>` `--json` |
25
+ | `reset` | Destructively wipe every `history_*` table for a clean re-import; refuses without `--yes` | `--yes` `--json` |
25
26
 
26
27
  Every JSON-capable verb also advertises `--json-envelope`; use the facade's machine-output contract.
27
28
 
@@ -139,7 +139,8 @@ resolved in this order; first match wins:
139
139
  2. **`agent.default`** from `.spur/config.yaml` (project layer, then `~/.config/spur/config.yaml`) —
140
140
  `spur workflow run` injects it as the `agent` var when `vars.agent` was not set by the caller.
141
141
  3. **YAML literal `agent:` in the pipeline file** — the last-resort fallback declared in the
142
- workflow YAML (e.g. `agent: "omp"` in `task-pipeline.yaml`). This fires only when no
142
+ workflow YAML. Every shipped pipeline declares `agent: "auto"`, so this rung resolves through
143
+ the role/tier ladder instead of pinning an executor name; it fires only when no
143
144
  `agent.default` is configured anywhere.
144
145
 
145
146
  `--agent auto` tier-resolves an executor (stage `model_policy` → `agent.default` → tier priority)
@@ -72,6 +72,16 @@ Entered before `task-pipeline.yaml` `precheck` state runs `spur task check <wbs>
72
72
  - [ ] The `## Plan` section is an ordered checklist (not prose).
73
73
  - [ ] The `## Design` section, if present, does not contradict the parent feature's design.
74
74
  - [ ] No `TODO`, `TBD`, or `???` placeholders in Requirements, AC, Design, or Plan.
75
+ - [ ] The evidence-channel precheck (0726 R2) status file is consulted by the
76
+ pipeline guard: `plugins/sp/scripts/task-evidence-precheck.ts` parses the task
77
+ content for an exact `evidence-channel: history_tool_call.args_raw[pi]`
78
+ declaration and, when present, counts live pi rows with `args_raw` on
79
+ `.spur/spur.db` via bun:sqlite. Tasks without a declaration pass without opening
80
+ SQLite; unknown declarations, a missing database/table, and a zero count write
81
+ FAIL. Both precheck guard conjuncts (`precheck-size.status` and
82
+ `precheck-evidence.status`) must read PASS — tasks declaring a live-data
83
+ evidence channel must import real history (safe importer, non-dry-run) before
84
+ implementation begins.
75
85
 
76
86
  ## review gate
77
87
 
@@ -90,6 +100,12 @@ Entered before `task-pipeline.yaml` `review` state dispatches `sp:code-verificat
90
100
 
91
101
  Entered before `task-pipeline.yaml` `verify` state produces a task verdict.
92
102
 
103
+ - [ ] The verify answer file (`.spur/run/<wbs>-verify-answer.txt`) is lint-clean before
104
+ verdict derivation: `plugins/sp/scripts/verify-answer-lint.ts <wbs>` (0726 R3)
105
+ rejects missing/duplicate/unknown R IDs, AC identities that are not an exact task
106
+ checklist label or linked-feature scenario title, invalid status/evidence-type
107
+ values, and empty evidence on any row. A lint failure fails the verify
108
+ step (fail-closed) before `spur task verdict` runs.
93
109
  - [ ] `spur task check <wbs> --strict-core --json` returns PASS.
94
110
  - [ ] Every AC scenario has a corresponding verify command that exited 0.
95
111
  - [ ] The `## Solution` section is filled (not the placeholder comment).