@gobing-ai/spur 0.3.93 → 0.3.95
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/README.md +42 -0
- package/board/index.d.ts +38 -0
- package/config/plugin-scripts.json +5 -0
- package/config/rules/boundary/test-subpath-boundary.yaml +48 -0
- package/config/rules/strict/runtime-boundaries.yaml +2 -1
- package/config/rules/structure/protected-files.yaml +4 -0
- package/config/workflows/feature-verification.yaml +7 -4
- package/config/workflows/idea-pipeline.yaml +10 -4
- package/config/workflows/task-pipeline.yaml +71 -52
- package/config/workflows/wrapup-pipeline.yaml +92 -17
- package/package.json +15 -11
- package/plugins/sp/README.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +4 -4
- package/plugins/sp/commands/dev-review.md +16 -5
- package/plugins/sp/commands/dev-verify.md +7 -4
- package/plugins/sp/commands/dev-verifyall.md +4 -3
- package/plugins/sp/commands/dev-wrap.md +1 -1
- package/plugins/sp/hooks/context-post-tool.ts +2 -2
- package/plugins/sp/hooks/context-session-start.ts +2 -1
- package/plugins/sp/hooks/context-session-stop.ts +22 -14
- package/plugins/sp/hooks/pi/guard-extension.ts +112 -172
- package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
- package/plugins/sp/lib/inline-run.generated.d.mts +4 -2
- package/plugins/sp/lib/inline-run.generated.mjs +35 -14
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
- package/plugins/sp/scripts/feature-sync-bounded.mjs +5 -2
- package/plugins/sp/scripts/feature-sync-bounded.ts +3 -1
- package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
- package/plugins/sp/scripts/history-anatomy-cache.mjs +3 -3
- package/plugins/sp/scripts/history-anatomy-cache.ts +4 -4
- package/plugins/sp/scripts/inline-run-setup.mjs +60 -12
- package/plugins/sp/scripts/inline-run-setup.ts +122 -15
- package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
- package/plugins/sp/scripts/pr-reviewing.ts +1 -0
- package/plugins/sp/scripts/quality-gate.mjs +15 -6
- package/plugins/sp/scripts/quality-gate.ts +30 -9
- package/plugins/sp/scripts/residual-scan.mjs +4 -4
- package/plugins/sp/scripts/residual-scan.ts +5 -4
- package/plugins/sp/scripts/script-contract-check.ts +175 -46
- package/plugins/sp/scripts/script-root.mjs +125 -0
- package/plugins/sp/scripts/script-root.ts +195 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1 -1
- package/plugins/sp/scripts/task-evidence-precheck.ts +4 -2
- package/plugins/sp/scripts/task-size-precheck.ts +3 -1
- package/plugins/sp/scripts/validate-commands.ts +12 -12
- package/plugins/sp/scripts/validate-flag-contracts.ts +42 -30
- package/plugins/sp/scripts/verify-answer-lint.ts +3 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +5 -2
- package/plugins/sp/scripts/workflow-step-profile.ts +3 -1
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +5 -2
- package/plugins/sp/scripts/wrapup-drift-probe.ts +4 -2
- package/plugins/sp/scripts/wrapup-steps.mjs +32 -8
- package/plugins/sp/scripts/wrapup-steps.ts +56 -10
- package/plugins/sp/skills/code-verification/SKILL.md +12 -7
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -0
- package/plugins/sp/skills/functional-review/SKILL.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/session-review/SKILL.md +11 -9
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +9 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +8 -2
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +65 -17
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -3
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +22 -5
- package/plugins/sp/tsconfig.json +8 -0
- package/spur.js +20934 -21087
- package/web/_astro/{BoardApp.CerSBgis.js → BoardApp.BxJuwD7I.js} +151 -152
- package/web/_astro/BoardApp.Cpxntzad.js +1 -0
- package/web/_astro/TaskDetail.C5bW4WGV.js +1 -0
- package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
- package/web/_astro/arc.CyjRvNMY.js +1 -0
- package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
- package/web/_astro/{blockDiagram-GPEHLZMM.DXVIiv0p.js → blockDiagram-GPEHLZMM.Be38USQ2.js} +1 -1
- package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
- package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
- package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
- package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
- package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
- package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
- package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
- package/web/_astro/{c4Diagram-AAUBKEIU.BbF_zCxW.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
- package/web/_astro/channel.CI6N_tCg.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CAgQHpPC.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.BN-5tpw4.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CnPkEEr0.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.BQzQeMVm.js → chunk-727SXJPM.Bi-TUb_V.js} +4 -4
- package/web/_astro/{chunk-AQP2D5EJ.B6xNyDnL.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.C7f9Ih78.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
- package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
- package/web/_astro/{chunk-ND2GUHAM.CNV1dFXT.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.Cudn2TkJ.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
- package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
- package/web/_astro/{classDiagram-4FO5ZUOK.D1NwP50q.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.D1NwP50q.js → classDiagram-v2-Q7XG4LA2.D_apfHS7.js} +1 -1
- package/web/_astro/client.BhluLQqe.js +1 -0
- package/web/_astro/client.DBpSRLnx.js +9 -0
- package/web/_astro/cose-bilkent-S5V4N54A.DNLU_L-x.js +1 -0
- package/web/_astro/{cynefin-OW5HDTMX.BmK52w8G.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.Bfy5CTDT.js → dagre-BM42HDAG.Uc0lvjcV.js} +2 -2
- package/web/_astro/diagram-2AECGRRQ.DDIxtDBo.js +43 -0
- package/web/_astro/diagram-5GNKFQAL.DVoDnD3H.js +10 -0
- package/web/_astro/{diagram-KO2AKTUF.CW_vMJ4z.js → diagram-KO2AKTUF.CRubTJ-0.js} +3 -3
- package/web/_astro/{diagram-LMA3HP47.B_8ZGF67.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.BppnHsdS.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BEuHXcjJ.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +5 -5
- package/web/_astro/{flowDiagram-I6XJVG4X.CH-UlnGr.js → flowDiagram-I6XJVG4X.DrZcys16.js} +4 -4
- package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
- package/web/_astro/{gitGraphDiagram-PVQCEYII.XnPxPPZN.js → gitGraphDiagram-PVQCEYII.BWc9oJ4l.js} +1 -1
- package/web/_astro/index.5dn-Zm-2.js +1 -0
- package/web/_astro/index.C-IqPpW2.js +1 -0
- package/web/_astro/index.CENnIEqT.css +1 -0
- package/web/_astro/index.Cyc_GA4f.js +1 -0
- package/web/_astro/index.tZfqwSut.js +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.JyjYRu_T.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +5 -5
- package/web/_astro/{journeyDiagram-JHISSGLW.C_iymSyp.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
- package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
- package/web/_astro/{kanban-definition-UN3LZRKU.DdfW-Oqt.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +7 -7
- package/web/_astro/{linear.C2_IkbZT.js → linear.zRsuuDTE.js} +1 -1
- package/web/_astro/mermaid.core.jAJTcMKc.js +301 -0
- package/web/_astro/{mindmap-definition-RKZ34NQL.DAZIxQSK.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +2 -2
- package/web/_astro/{pieDiagram-4H26LBE5.CN8sIhKM.js → pieDiagram-4H26LBE5.CacwWaE3.js} +3 -3
- package/web/_astro/{quadrantDiagram-W4KKPZXB.3dGcX5GP.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.BV2y4dd6.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +3 -3
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Cqo15Tvo.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +4 -4
- package/web/_astro/{sequenceDiagram-3UESZ5HK.CROCPMJB.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.RfXZrkFE.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CPXmbBs9.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.DdgKTiO8.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +3 -3
- package/web/_astro/{vennDiagram-CIIHVFJN.CPNVSHF1.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CQhA0Jyr.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +3 -3
- package/web/_astro/{xychartDiagram-2RQKCTM6.n61BWyy4.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
- package/web/board-runtime.json +69 -0
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.eoTz0pZs.js +0 -1
- package/web/_astro/TaskDetail.DCqiC-OZ.js +0 -1
- package/web/_astro/arc.CPwg6Rw0.js +0 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.DM_vp_hO.js +0 -36
- package/web/_astro/channel.MYZLKNwy.js +0 -1
- package/web/_astro/client.yhYJvxCU.js +0 -9
- package/web/_astro/cose-bilkent-S5V4N54A.B1wSL-Xb.js +0 -1
- package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +0 -43
- package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +0 -10
- package/web/_astro/ganttDiagram-6RSMTGT7.BO81S85v.js +0 -292
- package/web/_astro/index.De90oHcH.js +0 -1
- package/web/_astro/index.Hjbr15fG.css +0 -1
- package/web/_astro/mermaid.core.GAOYeSR0.js +0 -303
|
@@ -66,7 +66,7 @@ function parseCommand(filePath: string, name: string): ParsedCommand {
|
|
|
66
66
|
// Extract frontmatter between --- delimiters
|
|
67
67
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
68
68
|
const body = fmMatch ? raw.slice(fmMatch[0].length).trim() : raw;
|
|
69
|
-
const fm = fmMatch
|
|
69
|
+
const fm = fmMatch?.[1] ?? '';
|
|
70
70
|
|
|
71
71
|
const description = extractYamlField(fm, 'description');
|
|
72
72
|
const argumentHint = extractYamlField(fm, 'argument-hint');
|
|
@@ -124,9 +124,9 @@ function parseCommand(filePath: string, name: string): ParsedCommand {
|
|
|
124
124
|
/** Extract a plain YAML string field (handles quoted and unquoted). */
|
|
125
125
|
function extractYamlField(fm: string, key: string): string | undefined {
|
|
126
126
|
const re = new RegExp(`^${key}:\\s*(.+)$`, 'm');
|
|
127
|
-
const m = fm.match(re);
|
|
128
|
-
if (
|
|
129
|
-
const value = m
|
|
127
|
+
const m = fm.match(re)?.[1];
|
|
128
|
+
if (m === undefined) return undefined;
|
|
129
|
+
const value = m.trim();
|
|
130
130
|
// Unquote double-quoted YAML scalars
|
|
131
131
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
132
132
|
return value.slice(1, -1).replace(/\\"/g, '"');
|
|
@@ -137,9 +137,9 @@ function extractYamlField(fm: string, key: string): string | undefined {
|
|
|
137
137
|
/** Extract a YAML flow list ["A", "B"]. */
|
|
138
138
|
function extractYamlList(fm: string, key: string): string[] | undefined {
|
|
139
139
|
const re = new RegExp(`^${key}:\\s*\\[(.*)\\]$`, 'm');
|
|
140
|
-
const m = fm.match(re);
|
|
141
|
-
if (
|
|
142
|
-
return [...m
|
|
140
|
+
const m = fm.match(re)?.[1];
|
|
141
|
+
if (m === undefined) return undefined;
|
|
142
|
+
return [...m.matchAll(/"([^"]*)"/g)].map((q) => q[1] ?? '');
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
const ALLOWED_HEADINGS: Record<string, true> = { Usage: true, Implementation: true };
|
|
@@ -262,7 +262,7 @@ function checkTargetResolution(cmd: ParsedCommand, skillsDir: string, root: stri
|
|
|
262
262
|
|
|
263
263
|
// Extract ## Implementation section content
|
|
264
264
|
const implPart = cmd.body.split(/^## Implementation\n/m)[1] ?? '';
|
|
265
|
-
const implSection = implPart.split(/\n## (?!Usage)/m)[0].trim();
|
|
265
|
+
const implSection = (implPart.split(/\n## (?!Usage)/m)[0] ?? '').trim();
|
|
266
266
|
|
|
267
267
|
// Skill() calls: Skill(skill="sp:NAME",
|
|
268
268
|
// The name charset must stay at least as wide as any name a skill directory can
|
|
@@ -270,7 +270,7 @@ function checkTargetResolution(cmd: ParsedCommand, skillsDir: string, root: stri
|
|
|
270
270
|
// would pass a genuinely unresolved reference rather than report it.
|
|
271
271
|
const skillCalls = [...implSection.matchAll(/Skill\(skill="sp:([a-z][a-z0-9-]*)"/g)];
|
|
272
272
|
for (const ref of skillCalls) {
|
|
273
|
-
const skillName = ref[1];
|
|
273
|
+
const skillName = ref[1] ?? '';
|
|
274
274
|
const skillPath = join(skillsDir, skillName, 'SKILL.md');
|
|
275
275
|
if (!existsSync(skillPath)) {
|
|
276
276
|
violations.push({
|
|
@@ -304,7 +304,7 @@ function checkTargetResolution(cmd: ParsedCommand, skillsDir: string, root: stri
|
|
|
304
304
|
// Procedure reference + anchor — markdown links with anchors in Implementation
|
|
305
305
|
const anchorRefs = [...implSection.matchAll(/\[([^\]]*)\]\(([^)]*#([^)]*))\)/g)];
|
|
306
306
|
for (const ref of anchorRefs) {
|
|
307
|
-
const filePart = ref[2].split('#')[0];
|
|
307
|
+
const filePart = (ref[2] ?? '').split('#')[0] ?? '';
|
|
308
308
|
const anchor = ref[3];
|
|
309
309
|
// Resolve relative paths from commands/ directory
|
|
310
310
|
const resolved = resolve(join(root, 'plugins', 'sp', 'commands'), filePart);
|
|
@@ -414,7 +414,7 @@ function parseMarkdownTable(section: string): MarkdownTable | null {
|
|
|
414
414
|
.replace(/\|$/, '')
|
|
415
415
|
.split(/(?<!\\)\|/)
|
|
416
416
|
.map((c) => c.trim().replace(/\\\|/g, '|'));
|
|
417
|
-
const header = split(tableLines[0]);
|
|
417
|
+
const header = split(tableLines[0] ?? '');
|
|
418
418
|
// Skip the separator row (--- cells).
|
|
419
419
|
const dataRows = tableLines
|
|
420
420
|
.slice(1)
|
|
@@ -430,7 +430,7 @@ function parseMarkdownTable(section: string): MarkdownTable | null {
|
|
|
430
430
|
function extractHintTokens(hint: string): { positionals: string[]; flags: string[] } {
|
|
431
431
|
const positionals = [...hint.matchAll(/<[^>]+>/g)].map((m) => m[0]);
|
|
432
432
|
// Flags: capture the `--flag` literal, ignoring the value placeholder that follows.
|
|
433
|
-
const flags = [...hint.matchAll(/(--[a-z][a-z0-9-]*)/g)].map((m) => m[1]);
|
|
433
|
+
const flags = [...hint.matchAll(/(--[a-z][a-z0-9-]*)/g)].map((m) => m[1] ?? '');
|
|
434
434
|
// Deduplicate while preserving order.
|
|
435
435
|
return {
|
|
436
436
|
positionals: [...new Set(positionals)],
|
|
@@ -71,7 +71,7 @@ function sectionBetween(raw: string, startRe: RegExp, nextHeading: '## ' | '###
|
|
|
71
71
|
function extractFlags(text: string): Set<string> {
|
|
72
72
|
const flags = new Set<string>();
|
|
73
73
|
for (const m of text.matchAll(/(--[a-z][a-z-]*)/g)) {
|
|
74
|
-
flags.add(m[1]);
|
|
74
|
+
flags.add(m[1] ?? '');
|
|
75
75
|
}
|
|
76
76
|
return flags;
|
|
77
77
|
}
|
|
@@ -80,7 +80,7 @@ function extractFlags(text: string): Set<string> {
|
|
|
80
80
|
function deprecatedFlagSet(hint: string): Set<string> {
|
|
81
81
|
const out = new Set<string>();
|
|
82
82
|
for (const m of hint.matchAll(/(--[a-z][a-z-]*)\s*\(deprecated\)/g)) {
|
|
83
|
-
out.add(m[1]);
|
|
83
|
+
out.add(m[1] ?? '');
|
|
84
84
|
}
|
|
85
85
|
return out;
|
|
86
86
|
}
|
|
@@ -104,7 +104,7 @@ function glossaryEntries(glossaryRaw: string): GlossaryEntry[] {
|
|
|
104
104
|
const entryRe = /^### `(--[a-z][a-z-]*)[^\n]*$/gm;
|
|
105
105
|
for (const m of glossaryRaw.matchAll(entryRe)) {
|
|
106
106
|
const flag = m[1];
|
|
107
|
-
if (m.index === undefined) continue;
|
|
107
|
+
if (flag === undefined || m.index === undefined) continue;
|
|
108
108
|
const bodyStart = m.index + m[0].length;
|
|
109
109
|
const next = glossaryRaw.indexOf('\n### ', bodyStart);
|
|
110
110
|
const body = next === -1 ? glossaryRaw.slice(bodyStart) : glossaryRaw.slice(bodyStart, next);
|
|
@@ -132,8 +132,8 @@ function glossaryDeclaringClaims(glossaryRaw: string): Map<string, Set<string>>
|
|
|
132
132
|
for (const { flag, firstParagraph } of glossaryEntries(glossaryRaw)) {
|
|
133
133
|
const names = new Set<string>();
|
|
134
134
|
for (const paren of firstParagraph.matchAll(/\(([^)]*)\)/g)) {
|
|
135
|
-
for (const n of paren[1].matchAll(/`(dev-[a-z-]+)`/g)) {
|
|
136
|
-
names.add(n[1]);
|
|
135
|
+
for (const n of (paren[1] ?? '').matchAll(/`(dev-[a-z-]+)`/g)) {
|
|
136
|
+
names.add(n[1] ?? '');
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
if (names.size >= 2) claims.set(flag, names);
|
|
@@ -210,7 +210,7 @@ function commandTableDefaults(commandRaw: string): Map<string, string> {
|
|
|
210
210
|
if (!start || start.index === undefined) return out;
|
|
211
211
|
const from = start.index + start[0].length;
|
|
212
212
|
const next = commandRaw.indexOf('\n## ', from);
|
|
213
|
-
const table = (next === -1 ? commandRaw.slice(from) : commandRaw.slice(from, next)).split(/\n\s*\n/)[0];
|
|
213
|
+
const table = (next === -1 ? commandRaw.slice(from) : commandRaw.slice(from, next)).split(/\n\s*\n/)[0] ?? '';
|
|
214
214
|
// Split on unescaped pipes only (same as validate-commands.ts parseMarkdownTable),
|
|
215
215
|
// then unescape `\|` → `|` per cell.
|
|
216
216
|
const split = (row: string): string[] =>
|
|
@@ -223,10 +223,11 @@ function commandTableDefaults(commandRaw: string): Map<string, string> {
|
|
|
223
223
|
const trimmed = line.trim();
|
|
224
224
|
if (!trimmed.startsWith('|')) continue;
|
|
225
225
|
const cells = split(trimmed);
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
const first = cells[0] ?? '';
|
|
227
|
+
if (cells.length < 3 || first.startsWith('---') || first === 'Flag') continue;
|
|
228
|
+
const flag = first.replace(/`/g, '').split(/\s/)[0] ?? '';
|
|
228
229
|
if (!flag.startsWith('--')) continue;
|
|
229
|
-
out.set(flag, cells[2].replace(/[`*]/g, '').trim());
|
|
230
|
+
out.set(flag, (cells[2] ?? '').replace(/[`*]/g, '').trim());
|
|
230
231
|
}
|
|
231
232
|
return out;
|
|
232
233
|
}
|
|
@@ -237,7 +238,7 @@ function commandToOpHeader(opsRaw: string): Map<string, string> {
|
|
|
237
238
|
const rowRe = /^\|\s*(\d+[a-z]?)\s*\|\s*([a-z-]+)\s*\|\s*`(dev-[a-z-]+)`/;
|
|
238
239
|
for (const line of opsRaw.split('\n')) {
|
|
239
240
|
const m = line.match(rowRe);
|
|
240
|
-
if (m) map.set(m[3], `${m[1]}. ${m[2]}`);
|
|
241
|
+
if (m) map.set(m[3] ?? '', `${m[1] ?? ''}. ${m[2] ?? ''}`);
|
|
241
242
|
}
|
|
242
243
|
return map;
|
|
243
244
|
}
|
|
@@ -249,17 +250,18 @@ function opsSectionDefaults(sectionBody: string): Map<string, { value: string; c
|
|
|
249
250
|
// Pair each default marker with the flag mentioned immediately before it on the line.
|
|
250
251
|
const flags: Array<{ name: string; index: number }> = [];
|
|
251
252
|
for (const m of line.matchAll(/`(--[a-z][a-z-]*)(?: <[^>]+>)?`/g)) {
|
|
252
|
-
flags.push({ name: m[1], index: m.index });
|
|
253
|
+
flags.push({ name: m[1] ?? '', index: m.index });
|
|
253
254
|
}
|
|
254
255
|
for (const m of line.matchAll(/\(default:?\s*\*?\*?`?([^`);]+?)\s*`?[;)]/g)) {
|
|
255
|
-
const value = m[1].replace(/[`*]/g, '').trim();
|
|
256
|
+
const value = (m[1] ?? '').replace(/[`*]/g, '').trim();
|
|
256
257
|
const preceding = flags.filter((f) => f.index < m.index).pop();
|
|
257
258
|
if (!preceding) continue;
|
|
258
259
|
out.set(preceding.name, { value, claim: line.trim() });
|
|
259
260
|
}
|
|
260
261
|
// `X (default)` trailing form (e.g. "**`full`** (default):" in Modes prose).
|
|
261
262
|
for (const m of line.matchAll(/`([^`]+)`\s*\(default\)/g)) {
|
|
262
|
-
|
|
263
|
+
const v = m[1] ?? '';
|
|
264
|
+
out.set(v, { value: v, claim: line.trim() });
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
return out;
|
|
@@ -369,11 +371,12 @@ export function extractValueBehaviorTable(
|
|
|
369
371
|
.replace(/^\||\|$/g, '')
|
|
370
372
|
.split('|')
|
|
371
373
|
.map((c) => c.trim());
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
374
|
+
const first = cells[0] ?? '';
|
|
375
|
+
if (cells.length < 3 || first.startsWith('---') || first === 'Value') continue;
|
|
376
|
+
const valueCell = first.replace(/`/g, '').trim();
|
|
377
|
+
const value = valueCell.split(/\s/)[0] ?? '';
|
|
375
378
|
if (value !== 'inline' && value !== 'auto' && value !== '<name>') continue;
|
|
376
|
-
const behavior = normalizeSurfaceCell(cells[2]);
|
|
379
|
+
const behavior = normalizeSurfaceCell(cells[2] ?? '');
|
|
377
380
|
out.set(value, { ...behavior, defaultWhenOmitted: /default when omitted/i.test(valueCell) });
|
|
378
381
|
}
|
|
379
382
|
return out.size === 3 ? out : null;
|
|
@@ -395,9 +398,10 @@ export function extractTriggerTable(crossCuttingRaw: string): string[] | null {
|
|
|
395
398
|
.replace(/^\||\|$/g, '')
|
|
396
399
|
.split('|')
|
|
397
400
|
.map((c) => c.trim());
|
|
398
|
-
|
|
399
|
-
if (cells
|
|
400
|
-
|
|
401
|
+
const first = cells[0] ?? '';
|
|
402
|
+
if (cells.length < 3 || first.startsWith('---')) continue;
|
|
403
|
+
if (first === 'Trigger') continue;
|
|
404
|
+
rows.push(first.replace(/\*\*/g, '').replace(/\.\s*$/, ''));
|
|
401
405
|
}
|
|
402
406
|
return rows.length >= 4 ? rows : null;
|
|
403
407
|
}
|
|
@@ -414,8 +418,12 @@ function adrAgentClaims(adrRaw: string): Map<string, SurfaceBehavior> | null {
|
|
|
414
418
|
conditional: false,
|
|
415
419
|
defaultWhenOmitted: false,
|
|
416
420
|
});
|
|
417
|
-
out.set('auto', { surfaces: new Set(['subprocess']), conditional: false });
|
|
418
|
-
out.set('<name>', {
|
|
421
|
+
out.set('auto', { surfaces: new Set(['subprocess']), conditional: false, defaultWhenOmitted: false });
|
|
422
|
+
out.set('<name>', {
|
|
423
|
+
surfaces: new Set(['inline', 'subprocess']),
|
|
424
|
+
conditional: true,
|
|
425
|
+
defaultWhenOmitted: false,
|
|
426
|
+
});
|
|
419
427
|
return out;
|
|
420
428
|
}
|
|
421
429
|
// Slice from the `## ADR-041` heading to the next `## ADR-` heading or end of input
|
|
@@ -429,7 +437,11 @@ function adrAgentClaims(adrRaw: string): Map<string, SurfaceBehavior> | null {
|
|
|
429
437
|
const out = new Map<string, SurfaceBehavior>();
|
|
430
438
|
// Rule sentence: "if the named executor is … inline; otherwise … subprocess" → <name> conditional.
|
|
431
439
|
if (/the work happens inline;\s*otherwise it dispatches a subprocess/.test(section)) {
|
|
432
|
-
out.set('<name>', {
|
|
440
|
+
out.set('<name>', {
|
|
441
|
+
surfaces: new Set(['inline', 'subprocess']),
|
|
442
|
+
conditional: true,
|
|
443
|
+
defaultWhenOmitted: false,
|
|
444
|
+
});
|
|
433
445
|
}
|
|
434
446
|
// Collapse mapping: `--subprocess` → `--agent <target>`. The target value's behavior is
|
|
435
447
|
// claimed by the mapping itself — parse the target generically so a drifted mapping
|
|
@@ -438,12 +450,12 @@ function adrAgentClaims(adrRaw: string): Map<string, SurfaceBehavior> | null {
|
|
|
438
450
|
// `--subprocess` is matched explicitly (`` `--subprocess` `` in the source).
|
|
439
451
|
const subprocessTarget = section.match(/--subprocess`\s*→\s*`--agent ([a-z]+)/)?.[1];
|
|
440
452
|
if (subprocessTarget === 'auto') {
|
|
441
|
-
out.set('auto', { surfaces: new Set(['subprocess']), conditional: false });
|
|
453
|
+
out.set('auto', { surfaces: new Set(['subprocess']), conditional: false, defaultWhenOmitted: false });
|
|
442
454
|
} else if (subprocessTarget === 'inline') {
|
|
443
|
-
out.set('auto', { surfaces: new Set(['inline']), conditional: false });
|
|
455
|
+
out.set('auto', { surfaces: new Set(['inline']), conditional: false, defaultWhenOmitted: false });
|
|
444
456
|
}
|
|
445
457
|
if (/--inline`\s*→\s*`--agent inline/.test(section)) {
|
|
446
|
-
out.set('inline', { surfaces: new Set(['inline']), conditional: false });
|
|
458
|
+
out.set('inline', { surfaces: new Set(['inline']), conditional: false, defaultWhenOmitted: false });
|
|
447
459
|
}
|
|
448
460
|
return out.size === 0 ? null : out;
|
|
449
461
|
}
|
|
@@ -637,8 +649,8 @@ export function checkAgentSsotIntegrity(files: ReadonlyMap<string, string>): Fla
|
|
|
637
649
|
for (const block of tableBlocks) {
|
|
638
650
|
const valuesFound = new Set<string>();
|
|
639
651
|
for (const row of block) {
|
|
640
|
-
const firstCell = row.replace(/^\|/, '').split('|')[0].replace(/`/g, '').trim();
|
|
641
|
-
const token = firstCell.split(/\s/)[0].toLowerCase();
|
|
652
|
+
const firstCell = (row.replace(/^\|/, '').split('|')[0] ?? '').replace(/`/g, '').trim();
|
|
653
|
+
const token = (firstCell.split(/\s/)[0] ?? '').toLowerCase();
|
|
642
654
|
if (token === 'inline' || token === 'auto' || token === '<name>') {
|
|
643
655
|
valuesFound.add(token);
|
|
644
656
|
}
|
|
@@ -684,7 +696,7 @@ export function headingAnchors(raw: string): Set<string> {
|
|
|
684
696
|
const anchors = new Set<string>();
|
|
685
697
|
for (const line of raw.split('\n')) {
|
|
686
698
|
const m = line.match(/^#{1,6}\s+(.*)$/);
|
|
687
|
-
if (m) anchors.add(headingSlug(m[1]));
|
|
699
|
+
if (m) anchors.add(headingSlug(m[1] ?? ''));
|
|
688
700
|
}
|
|
689
701
|
return anchors;
|
|
690
702
|
}
|
|
@@ -704,7 +716,7 @@ export function checkSsotAnchorsResolve(crossCuttingRaw: string, files: Readonly
|
|
|
704
716
|
for (const [filename, raw] of files) {
|
|
705
717
|
const seen = new Set<string>();
|
|
706
718
|
for (const m of raw.matchAll(/cross-cutting\.md#([\w-]+)/g)) {
|
|
707
|
-
const anchor = m[1];
|
|
719
|
+
const anchor = m[1] ?? '';
|
|
708
720
|
if (anchors.has(anchor) || seen.has(anchor)) continue;
|
|
709
721
|
seen.add(anchor);
|
|
710
722
|
violations.push({
|
|
@@ -42,7 +42,8 @@ function usage(): never {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function defaultSpurBin(): string {
|
|
45
|
-
|
|
45
|
+
const fromEnv = getEnvVar('SPUR_BIN');
|
|
46
|
+
if (fromEnv) return fromEnv;
|
|
46
47
|
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
47
48
|
if (existsSync(local)) return `bun ${local}`;
|
|
48
49
|
return 'spur';
|
|
@@ -55,6 +56,7 @@ function parseArgs(argv: string[]): { wbs: string; answer: string; spurBin: stri
|
|
|
55
56
|
let i = 0;
|
|
56
57
|
while (i < argv.length) {
|
|
57
58
|
const arg = argv[i];
|
|
59
|
+
if (arg === undefined) break;
|
|
58
60
|
if (arg === '--spur-bin') {
|
|
59
61
|
spurBin = argv[i + 1] ?? defaultSpurBin();
|
|
60
62
|
i += 2;
|
|
@@ -176,8 +176,9 @@ function formatStepProfileHuman(profile) {
|
|
|
176
176
|
`;
|
|
177
177
|
}
|
|
178
178
|
function defaultSpurBin() {
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
const fromEnv = getEnvVar("SPUR_BIN");
|
|
180
|
+
if (fromEnv)
|
|
181
|
+
return fromEnv;
|
|
181
182
|
const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
182
183
|
if (existsSync(local))
|
|
183
184
|
return `bun ${local}`;
|
|
@@ -196,6 +197,8 @@ function parseStepProfileCliArgs(argv) {
|
|
|
196
197
|
let help = false;
|
|
197
198
|
for (let i = 0;i < argv.length; i++) {
|
|
198
199
|
const a = argv[i];
|
|
200
|
+
if (a === undefined)
|
|
201
|
+
continue;
|
|
199
202
|
if (a === "--help" || a === "-h")
|
|
200
203
|
help = true;
|
|
201
204
|
else if (a === "--json")
|
|
@@ -322,7 +322,8 @@ export interface StepProfileCliArgs {
|
|
|
322
322
|
* --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
|
|
323
323
|
*/
|
|
324
324
|
export function defaultSpurBin(): string {
|
|
325
|
-
|
|
325
|
+
const fromEnv = getEnvVar('SPUR_BIN');
|
|
326
|
+
if (fromEnv) return fromEnv;
|
|
326
327
|
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
327
328
|
if (existsSync(local)) return `bun ${local}`;
|
|
328
329
|
return 'spur';
|
|
@@ -343,6 +344,7 @@ export function parseStepProfileCliArgs(argv: string[]): StepProfileCliArgs {
|
|
|
343
344
|
|
|
344
345
|
for (let i = 0; i < argv.length; i++) {
|
|
345
346
|
const a = argv[i];
|
|
347
|
+
if (a === undefined) continue;
|
|
346
348
|
if (a === '--help' || a === '-h') help = true;
|
|
347
349
|
else if (a === '--json') json = true;
|
|
348
350
|
else if (a === '--last') last = positiveInt(argv[++i], last);
|
|
@@ -57,6 +57,8 @@ function changedPathsOf(section) {
|
|
|
57
57
|
const paths = [];
|
|
58
58
|
for (const match of section.matchAll(CHANGE_ENTRY)) {
|
|
59
59
|
const path = match[1];
|
|
60
|
+
if (path === undefined)
|
|
61
|
+
continue;
|
|
60
62
|
if (/\s/.test(path))
|
|
61
63
|
continue;
|
|
62
64
|
if (!path.includes("/") && !path.includes("."))
|
|
@@ -73,7 +75,8 @@ var SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfaceR
|
|
|
73
75
|
function driftReasonForPath(path) {
|
|
74
76
|
if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix)))
|
|
75
77
|
return null;
|
|
76
|
-
|
|
78
|
+
const top = path.split("/")[0];
|
|
79
|
+
if (path.includes("/") && top !== undefined && !KNOWN_TOP_LEVEL.has(top)) {
|
|
77
80
|
return "new top-level workspace directory";
|
|
78
81
|
}
|
|
79
82
|
const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
|
|
@@ -82,7 +85,7 @@ function driftReasonForPath(path) {
|
|
|
82
85
|
function defaultSpurRunner(env, cwd) {
|
|
83
86
|
const parts = (env.spurBin ?? "spur").split(/\s+/).filter((part) => part.length > 0);
|
|
84
87
|
return (args) => {
|
|
85
|
-
const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
|
|
88
|
+
const run = spawnSync(parts[0] ?? "spur", [...parts.slice(1), ...args], {
|
|
86
89
|
cwd,
|
|
87
90
|
encoding: "utf8",
|
|
88
91
|
env: getEnvVars()
|
|
@@ -125,6 +125,7 @@ export function changedPathsOf(section: string | null): string[] {
|
|
|
125
125
|
const paths: string[] = [];
|
|
126
126
|
for (const match of section.matchAll(CHANGE_ENTRY)) {
|
|
127
127
|
const path = match[1];
|
|
128
|
+
if (path === undefined) continue;
|
|
128
129
|
// Path-like: no whitespace and at least one separator or dotted suffix, so prose
|
|
129
130
|
// backticks never masquerade as change-map entries.
|
|
130
131
|
if (/\s/.test(path)) continue;
|
|
@@ -148,7 +149,8 @@ const SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfac
|
|
|
148
149
|
/** 0944 R2 surface match: doc-owned glob, corpus exclusion, new top-level directory. */
|
|
149
150
|
export function driftReasonForPath(path: string): string | null {
|
|
150
151
|
if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) return null;
|
|
151
|
-
|
|
152
|
+
const top = path.split('/')[0];
|
|
153
|
+
if (path.includes('/') && top !== undefined && !KNOWN_TOP_LEVEL.has(top)) {
|
|
152
154
|
return 'new top-level workspace directory';
|
|
153
155
|
}
|
|
154
156
|
const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
|
|
@@ -158,7 +160,7 @@ export function driftReasonForPath(path: string): string | null {
|
|
|
158
160
|
function defaultSpurRunner(env: DriftProbeEnv, cwd?: string): SpurRunner {
|
|
159
161
|
const parts = (env.spurBin ?? 'spur').split(/\s+/).filter((part) => part.length > 0);
|
|
160
162
|
return (args: string[]): SpurShowResult => {
|
|
161
|
-
const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
|
|
163
|
+
const run = spawnSync(parts[0] ?? 'spur', [...parts.slice(1), ...args], {
|
|
162
164
|
cwd,
|
|
163
165
|
encoding: 'utf8',
|
|
164
166
|
env: getEnvVars(),
|
|
@@ -172,6 +172,31 @@ function readFileSyncSafe(path) {
|
|
|
172
172
|
return null;
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
+
function verdictFromTestingSection(content) {
|
|
176
|
+
const heading = /^#{1,6}\s+Testing\s*$/m.exec(content);
|
|
177
|
+
if (!heading)
|
|
178
|
+
return null;
|
|
179
|
+
const level = heading[0].match(/^#+/)?.[0]?.length ?? 2;
|
|
180
|
+
const rest = content.slice(heading.index + heading[0].length);
|
|
181
|
+
const next = new RegExp(`^#{1,${level}}\\s+\\S`, "m").exec(rest);
|
|
182
|
+
const section = next ? rest.slice(0, next.index) : rest;
|
|
183
|
+
for (const line of section.split(`
|
|
184
|
+
`)) {
|
|
185
|
+
const m = /^(?:-\s*|\*\*)?Verdict:\s*(PASS|PARTIAL|FAIL|UNKNOWN)\b/i.exec(line.trim());
|
|
186
|
+
if (m?.[1] !== undefined)
|
|
187
|
+
return m[1].toUpperCase();
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
function verdictOfArtifact(verdictPath) {
|
|
192
|
+
try {
|
|
193
|
+
const raw = jqPick(JSON.parse(readFileSync(verdictPath, "utf8")).verdict, "");
|
|
194
|
+
const text = jqText(raw);
|
|
195
|
+
return text.length > 0 ? text : null;
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
175
200
|
function runMetrics(env, options = {}) {
|
|
176
201
|
const cwd = options.cwd;
|
|
177
202
|
const runId = env.__runId ?? "";
|
|
@@ -212,15 +237,13 @@ function runMetrics(env, options = {}) {
|
|
|
212
237
|
const frontmatter = parsed.frontmatter !== null && typeof parsed.frontmatter === "object" ? parsed.frontmatter : {};
|
|
213
238
|
const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ""));
|
|
214
239
|
const status2 = String(jqPick(frontmatter.status, parsed.status, "unknown"));
|
|
215
|
-
let verdict = "UNKNOWN";
|
|
216
240
|
const verdictPath = join(".spur", "run", `${wbs}-verdict.json`);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
} catch {}
|
|
241
|
+
const artifactVerdict = verdictOfArtifact(abs(verdictPath));
|
|
242
|
+
const trackedVerdict = verdictFromTestingSection(typeof parsed.content === "string" ? parsed.content : "");
|
|
243
|
+
const verdict = artifactVerdict ?? trackedVerdict ?? "UNKNOWN";
|
|
244
|
+
if (verdict === "UNKNOWN") {
|
|
245
|
+
process.stderr.write(`metrics-record: task ${wbs} has no certifying verdict \u2014 ${verdictPath}: ${artifactVerdict ?? "missing or carries none"}, tracked Testing: ${trackedVerdict ?? "no Verdict: line"} \u2014 recording UNKNOWN telemetry
|
|
246
|
+
`);
|
|
224
247
|
}
|
|
225
248
|
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
226
249
|
const row = { wbs, feature_id: featureId, status: status2, verdict, timestamp };
|
|
@@ -403,6 +426,7 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
403
426
|
}
|
|
404
427
|
export {
|
|
405
428
|
writeRouteReason,
|
|
429
|
+
verdictFromTestingSection,
|
|
406
430
|
taskStatusOf,
|
|
407
431
|
spurCommand,
|
|
408
432
|
runMetrics,
|
|
@@ -269,6 +269,49 @@ function readFileSyncSafe(path: string): string | null {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Verdict from a task record's tracked `Testing` section — the durable copy that outlives a
|
|
274
|
+
* worktree teardown or clone (F93). Local copy of `parseVerdictLine` and of the `Testing` slice
|
|
275
|
+
* in `extractTestingSection` (`packages/app/src/services/task-record.ts`) because ADR-065 keeps
|
|
276
|
+
* plugin scripts on builtin and relative imports only; keep the three literals in step by hand
|
|
277
|
+
* (the parity guard in `plugins/sp/tests/wrapup-steps.test.ts` reads the canonical literals).
|
|
278
|
+
*
|
|
279
|
+
* The slice rule is the canonical one verbatim — `#{1,6}` heading, end at the next
|
|
280
|
+
* same-or-higher heading — so an `# Testing` or `#### Testing` heading, and an h4 subheading
|
|
281
|
+
* inside a `## Testing` section, read the same here and in the app. One deliberate difference:
|
|
282
|
+
* with no `Testing` heading at all this returns null instead of falling back to the whole
|
|
283
|
+
* document, so a `Verdict:` token in another section cannot be misread as this task's verdict.
|
|
284
|
+
*/
|
|
285
|
+
export function verdictFromTestingSection(content: string): string | null {
|
|
286
|
+
const heading = /^#{1,6}\s+Testing\s*$/m.exec(content);
|
|
287
|
+
if (!heading) return null;
|
|
288
|
+
const level = heading[0].match(/^#+/)?.[0]?.length ?? 2;
|
|
289
|
+
const rest = content.slice(heading.index + heading[0].length);
|
|
290
|
+
const next = new RegExp(`^#{1,${level}}\\s+\\S`, 'm').exec(rest);
|
|
291
|
+
const section = next ? rest.slice(0, next.index) : rest;
|
|
292
|
+
for (const line of section.split('\n')) {
|
|
293
|
+
// Line-anchored (optionally after `- ` bullet or `**` bold) so evidence text
|
|
294
|
+
// containing a mid-line "Verdict:" token cannot be misread as the section verdict.
|
|
295
|
+
const m = /^(?:-\s*|\*\*)?Verdict:\s*(PASS|PARTIAL|FAIL|UNKNOWN)\b/i.exec(line.trim());
|
|
296
|
+
if (m?.[1] !== undefined) return m[1].toUpperCase();
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* `verdict` field of a verdict artifact; null when the file is absent, unreadable, malformed or
|
|
303
|
+
* carries no usable verdict (jq `//` semantics: null, undefined and false all count as missing).
|
|
304
|
+
*/
|
|
305
|
+
function verdictOfArtifact(verdictPath: string): string | null {
|
|
306
|
+
try {
|
|
307
|
+
const raw = jqPick((JSON.parse(readFileSync(verdictPath, 'utf8')) as { verdict?: unknown }).verdict, '');
|
|
308
|
+
const text = jqText(raw);
|
|
309
|
+
return text.length > 0 ? text : null;
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
272
315
|
export interface MetricsResult {
|
|
273
316
|
status: 'PASS' | 'FAIL';
|
|
274
317
|
statusFile: string;
|
|
@@ -323,17 +366,20 @@ export function runMetrics(env: WrapupStepsEnv, options: WrapupStepsOptions = {}
|
|
|
323
366
|
const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ''));
|
|
324
367
|
const status = String(jqPick(frontmatter.status, parsed.status, 'unknown'));
|
|
325
368
|
|
|
326
|
-
//
|
|
327
|
-
|
|
369
|
+
// The verdict artifact stays the first source (F93 R2); when it is gone — a worktree run
|
|
370
|
+
// fast-forwards and removes the tree, taking the gitignored artifact with it — the tracked
|
|
371
|
+
// `Testing` line `task record` already wrote into the task file is the durable copy. Honest
|
|
372
|
+
// UNKNOWN is the last resort, and it is telemetry, never proof of completion.
|
|
328
373
|
const verdictPath = join('.spur', 'run', `${wbs}-verdict.json`);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
374
|
+
const artifactVerdict = verdictOfArtifact(abs(verdictPath));
|
|
375
|
+
const trackedVerdict = verdictFromTestingSection(typeof parsed.content === 'string' ? parsed.content : '');
|
|
376
|
+
const verdict = artifactVerdict ?? trackedVerdict ?? 'UNKNOWN';
|
|
377
|
+
if (verdict === 'UNKNOWN') {
|
|
378
|
+
// R3: name the task, the artifact path and the tracked state. An explicit UNKNOWN on
|
|
379
|
+
// either source is still an uncertified row, so it reports the same way as a miss.
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
`metrics-record: task ${wbs} has no certifying verdict — ${verdictPath}: ${artifactVerdict ?? 'missing or carries none'}, tracked Testing: ${trackedVerdict ?? 'no Verdict: line'} — recording UNKNOWN telemetry\n`,
|
|
382
|
+
);
|
|
337
383
|
}
|
|
338
384
|
|
|
339
385
|
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
@@ -259,17 +259,22 @@ the deterministic Testing writer `spur task record` (section authorship never ha
|
|
|
259
259
|
|
|
260
260
|
```bash
|
|
261
261
|
# write .spur/run/<wbs>-verdict.json (shape in references/verdict-schema.md), then:
|
|
262
|
-
|
|
262
|
+
spur task record <wbs> --verdict-file .spur/run/<wbs>-verdict.json # renders ## Testing, flips proven boxes
|
|
263
|
+
# F96 residual sweep (observe-only): scan + fold AFTER record, under every --fix mode (0987).
|
|
263
264
|
RESIDUAL=$(superskill script path sp residual-scan.mjs)
|
|
264
265
|
node "$RESIDUAL" scan <wbs>
|
|
265
266
|
node "$RESIDUAL" fold <wbs>
|
|
266
|
-
|
|
267
|
+
# Downgrade only: re-record so Testing carries the final (folded) verdict.
|
|
268
|
+
[ "$(jq -r .verdict .spur/run/<wbs>-verdict.json)" = PASS ] \
|
|
269
|
+
|| spur task record <wbs> --verdict-file .spur/run/<wbs>-verdict.json
|
|
267
270
|
```
|
|
268
271
|
|
|
269
|
-
> **Residual fold (F96).** `scan` reads `.spur/run/` artifacts and
|
|
270
|
-
> `residual-report.md`; `fold` rewrites the
|
|
271
|
-
> check in place (PASS → PARTIAL when a blocking residual exists),
|
|
272
|
-
>
|
|
272
|
+
> **Residual fold (F96).** `scan` reads `.spur/run/` artifacts and the post-record task file and
|
|
273
|
+
> writes `residuals.json` + `residual-report.md`; `fold` rewrites the verdict artifact's
|
|
274
|
+
> `residual-sweep` check in place (PASS → PARTIAL when a blocking residual exists), and the
|
|
275
|
+
> downgrade-only re-record transcribes it — a manual verify cannot certify what the pipeline would
|
|
276
|
+
> reject. The sweep runs after `record` (same order as the pipeline, 0983/0987): before it, a
|
|
277
|
+
> verdict-proven box is still unticked and a clean PASS would fold to PARTIAL. Resolve the
|
|
273
278
|
> script via `superskill script path sp residual-scan.mjs`; shipped surfaces never reference
|
|
274
279
|
> `plugins/sp/scripts/` directly (script-contract-check rule 4). When the task reaches `done`
|
|
275
280
|
> through `--next`, run `residual-scan settle` (links follow-up tasks; best-effort).
|
|
@@ -322,7 +327,7 @@ Verdict: PASS
|
|
|
322
327
|
### SECUA Review
|
|
323
328
|
| Priority | Dimension | Location | Finding |
|
|
324
329
|
| --- | --- | --- | --- |
|
|
325
|
-
| P4 | — | — | No
|
|
330
|
+
| P4 | — | — | No findings (verify verdict PASS) |
|
|
326
331
|
```
|
|
327
332
|
|
|
328
333
|
The per-requirement traceability table MUST use `| Req | Status | Evidence |` (exactly this header, no `R#`/`R`/`Requirement` variant — `Status` in column 2 — and no extra columns between Req and Status). The Acceptance Criteria table MUST use `| AC | Status | Evidence Type | Evidence |`.
|
|
@@ -118,6 +118,17 @@ fail — invalid status (`MET | PARTIAL | UNMET` for requirements;
|
|
|
118
118
|
manual-review | llm-judge | n/a`, or a `+` compound), and empty evidence. Interrupted runs keep the
|
|
119
119
|
rows that pass the lint and complete only the missing IDs on retry.
|
|
120
120
|
|
|
121
|
+
**Feature-credited AC ids are narrower than the lint's accepted set (task 0966 finding).** A row keyed
|
|
122
|
+
by a bare checklist ordinal (`AC1`) passes `verify-answer-lint` — it resolves to the task's AC
|
|
123
|
+
checklist label — but `feature-check.rowMatchesScenario` credits a linked feature scenario only by the
|
|
124
|
+
scenario's normalized title or its `AC-N` ordinal alias. Such a row therefore carries a PASS verdict
|
|
125
|
+
while crediting no scenario, and the feature's `verifying → done` gate denies with
|
|
126
|
+
`L4.scenario-unverified` / `L4.verdict-rows-match-no-scenario`. When the task has a linked feature,
|
|
127
|
+
key each AC row `AC-N` (1-based scenario ordinal, hyphenated) or by the exact scenario title.
|
|
128
|
+
A scenario-keyed row (`R3 — <title>`) ticks the task AC box whose line aliases that scenario
|
|
129
|
+
(`- [ ] AC1 — R3 — <title>`) during `spur task record` (0996); a scenario key no AC line aliases
|
|
130
|
+
ticks nothing, so alias every graduated scenario on its AC line.
|
|
131
|
+
|
|
121
132
|
**Concrete anchors only (task 0804 R9).** An evidence anchor must be a concrete existing `file:line`
|
|
122
133
|
(or `file:start-end`) path. A glob or directory summary (`src/services/*.ts`, `the retry
|
|
123
134
|
classifiers in task-pipeline.yaml`) is not an anchor: expand it into the specific cited files/ranges
|
|
@@ -253,7 +253,7 @@ shape, e.g. `| Req | Status | Evidence |` alone, is structurally rejected and de
|
|
|
253
253
|
```markdown
|
|
254
254
|
| Priority | Dimension | Location | Finding |
|
|
255
255
|
| --- | --- | --- | --- |
|
|
256
|
-
| P4 | — | — | No
|
|
256
|
+
| P4 | — | — | No findings (functional verdict PASS) |
|
|
257
257
|
|
|
258
258
|
| Req | Status | Evidence |
|
|
259
259
|
| --- | --- | --- |
|
|
@@ -113,7 +113,7 @@ token, deterministic).
|
|
|
113
113
|
| C3 | A5/A6 when Testing empty/N/A **and** verify would fail for missing tests — only if prior implement claims code exists | Coverage/test signal: `bun test` fail attributed to task paths OR explicit "insufficient tests" in prior verify verdict artifact `.spur/run/<wbs>-verdict.json` | test fail / coverage gap | `/sp:dev-unit <wbs> --auto` | continue |
|
|
114
114
|
| C4 | A3/A5/A6 when operator or task tags mention rules, OR `spur rule run` last report dirty in `.spur/` if present | `spur rule run` (default project preset) non-zero with findings | rule findings | **HITL STOP** — print rule summary; suggest `/sp:rule-scan` or `rule-add`/`rule-refine` (do not auto-author rules) | continue |
|
|
115
115
|
| C5 | A6 only | Existing `.spur/run/<wbs>-verdict.json` with FAIL and findings pointing at coverage | verdict artifact | `/sp:dev-unit <wbs>` then re-verify on next invocation (`--once` friendly) | `/sp:dev-verify …` |
|
|
116
|
-
| C6 | A4/A5 | `.spur/run/<wbs>-verdict.json` has a failing (PARTIAL/FAIL) `residual-sweep` check —
|
|
116
|
+
| C6 | A4/A5 | `.spur/run/<wbs>-verdict.json` has a failing (PARTIAL/FAIL) `residual-sweep` check — residual blockers survived the pipeline sweep (F96) | folded verdict artifact | **HITL STOP** — print `.spur/run/<wbs>-residual-report.md` and the recovery command `/sp:dev-run <wbs>`; never auto-dispatch a fix (repeating the loop unattended burns quota without new information) | continue |
|
|
117
117
|
|
|
118
118
|
**C-row precedence note (F96):** C6 outranks C2/C3/C5 for the same task — a residual-sweep failure
|
|
119
119
|
means the workspace holds unfinished task residue, so fixall/unit reruns would either clean it
|