@gobing-ai/spur 0.3.94 → 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/workflows/feature-verification.yaml +7 -4
- package/config/workflows/idea-pipeline.yaml +10 -4
- package/config/workflows/task-pipeline.yaml +54 -26
- package/config/workflows/wrapup-pipeline.yaml +92 -17
- package/package.json +15 -11
- 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 +109 -150
- package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
- package/plugins/sp/lib/inline-run.generated.d.mts +2 -2
- package/plugins/sp/lib/inline-run.generated.mjs +10 -10
- 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 +23 -13
- package/plugins/sp/scripts/inline-run-setup.ts +60 -27
- 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 +4 -2
- package/plugins/sp/scripts/quality-gate.ts +8 -2
- package/plugins/sp/scripts/residual-scan.mjs +3 -3
- package/plugins/sp/scripts/residual-scan.ts +3 -3
- 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/spur-cli/references/tasks/verbs.md +9 -1
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +30 -10
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +6 -3
- package/plugins/sp/tsconfig.json +8 -0
- package/spur.js +5194 -4457
- package/web/_astro/{BoardApp.FTEs3-N8.js → BoardApp.BxJuwD7I.js} +150 -153
- 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.uuAf51IT.js → arc.CyjRvNMY.js} +1 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
- package/web/_astro/{blockDiagram-GPEHLZMM.D1mGCq3p.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.CMsolcde.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
- package/web/_astro/channel.CI6N_tCg.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CrGA3fik.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DsLVVla0.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.Dxc59Tfi.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.CaEVE2Wy.js → chunk-727SXJPM.Bi-TUb_V.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.BiJ4HXeI.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Mxf1fru5.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
- package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
- package/web/_astro/{chunk-ND2GUHAM.pyOWQixH.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BmEsg4vr.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
- package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
- package/web/_astro/{classDiagram-4FO5ZUOK.BHhhMFTO.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BHhhMFTO.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.C2j1_lKL.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.hZ2NCTdT.js → dagre-BM42HDAG.Uc0lvjcV.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.Bfw5_EzK.js → diagram-2AECGRRQ.DDIxtDBo.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Bt1V_Tmk.js → diagram-5GNKFQAL.DVoDnD3H.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DEk-YFwp.js → diagram-KO2AKTUF.CRubTJ-0.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.CDjndtQm.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.cFnUHScG.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DlhYp7NV.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DNTpsxfx.js → flowDiagram-I6XJVG4X.DrZcys16.js} +1 -1
- package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
- package/web/_astro/{gitGraphDiagram-PVQCEYII.DkEqNI0P.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.CrjioCTG.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BcK0CN8n.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.p0CbBDX1.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
- package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
- package/web/_astro/{kanban-definition-UN3LZRKU.puHIFt6J.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +1 -1
- package/web/_astro/{linear.Bb-3a1d7.js → linear.zRsuuDTE.js} +1 -1
- package/web/_astro/{mermaid.core.CJDgXOJs.js → mermaid.core.jAJTcMKc.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BoAZEI9t.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.CJNSdqY7.js → pieDiagram-4H26LBE5.CacwWaE3.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.DtTy7_0Z.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.BNYV98Xg.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.DRq9DGHp.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.B7KdBFCm.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CD62ZJ2H.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CeVk1TAj.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.C_ZPA5tT.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.CrThO_FQ.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DSZCA5nl.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.KI7baTuk.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
- package/web/board-runtime.json +69 -0
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.C02hAHPO.js +0 -1
- package/web/_astro/TaskDetail.C-GdsS-t.js +0 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.CGe629A1.js +0 -36
- package/web/_astro/channel.fsgl7o5j.js +0 -1
- package/web/_astro/client.yhYJvxCU.js +0 -9
- package/web/_astro/cose-bilkent-S5V4N54A.2fH4YOlp.js +0 -1
- package/web/_astro/ganttDiagram-6RSMTGT7.DC_p36PI.js +0 -292
- package/web/_astro/index.De90oHcH.js +0 -1
- package/web/_astro/index.Hjbr15fG.css +0 -1
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.95",
|
|
4
4
|
"description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
|
|
5
5
|
"extensions": {
|
|
6
6
|
"pi": ["./hooks/pi/guard-extension.ts"]
|
|
@@ -44,7 +44,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
44
44
|
for (let i = 0;i < args.length; i++) {
|
|
45
45
|
const arg = args[i];
|
|
46
46
|
if (arg === "--date" && i + 1 < args.length) {
|
|
47
|
-
options.date = args[++i];
|
|
47
|
+
options.date = args[++i] ?? options.date;
|
|
48
48
|
} else if (arg === "--dry-run") {
|
|
49
49
|
options.dryRun = true;
|
|
50
50
|
} else if (arg === "--output" && i + 1 < args.length) {
|
|
@@ -270,8 +270,8 @@ async function getGitCommits(date) {
|
|
|
270
270
|
} else if (currentCommit && line.includes("\t")) {
|
|
271
271
|
const parts = line.split("\t");
|
|
272
272
|
if (parts.length >= 3) {
|
|
273
|
-
const insertions = parseInt(parts[0], 10) || 0;
|
|
274
|
-
const deletions = parseInt(parts[1], 10) || 0;
|
|
273
|
+
const insertions = parseInt(parts[0] ?? "", 10) || 0;
|
|
274
|
+
const deletions = parseInt(parts[1] ?? "", 10) || 0;
|
|
275
275
|
currentCommit.filesChanged = (currentCommit.filesChanged ?? 0) + 1;
|
|
276
276
|
currentCommit.insertions = (currentCommit.insertions ?? 0) + insertions;
|
|
277
277
|
currentCommit.deletions = (currentCommit.deletions ?? 0) + deletions;
|
|
@@ -130,7 +130,7 @@ export function parseArgs(argv: string[] = process.argv.slice(2)): CliOptions {
|
|
|
130
130
|
for (let i = 0; i < args.length; i++) {
|
|
131
131
|
const arg = args[i];
|
|
132
132
|
if (arg === '--date' && i + 1 < args.length) {
|
|
133
|
-
options.date = args[++i];
|
|
133
|
+
options.date = args[++i] ?? options.date;
|
|
134
134
|
} else if (arg === '--dry-run') {
|
|
135
135
|
options.dryRun = true;
|
|
136
136
|
} else if (arg === '--output' && i + 1 < args.length) {
|
|
@@ -451,8 +451,8 @@ export async function getGitCommits(date: string): Promise<GitCommit[]> {
|
|
|
451
451
|
// This is a numstat line (files changed)
|
|
452
452
|
const parts = line.split('\t');
|
|
453
453
|
if (parts.length >= 3) {
|
|
454
|
-
const insertions = parseInt(parts[0], 10) || 0;
|
|
455
|
-
const deletions = parseInt(parts[1], 10) || 0;
|
|
454
|
+
const insertions = parseInt(parts[0] ?? '', 10) || 0;
|
|
455
|
+
const deletions = parseInt(parts[1] ?? '', 10) || 0;
|
|
456
456
|
currentCommit.filesChanged = (currentCommit.filesChanged ?? 0) + 1;
|
|
457
457
|
currentCommit.insertions = (currentCommit.insertions ?? 0) + insertions;
|
|
458
458
|
currentCommit.deletions = (currentCommit.deletions ?? 0) + deletions;
|
|
@@ -22,8 +22,8 @@ function countLedgerDataRows(markdown) {
|
|
|
22
22
|
return Math.max(rows.length - 1, 0);
|
|
23
23
|
}
|
|
24
24
|
function declaredExecutedSteps(markdown) {
|
|
25
|
-
const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/);
|
|
26
|
-
return match ? Number.parseInt(match
|
|
25
|
+
const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/)?.[1];
|
|
26
|
+
return match ? Number.parseInt(match, 10) : null;
|
|
27
27
|
}
|
|
28
28
|
var UNKNOWN_CELLS = new Set(["~unknown", "unknown", "—", "-", "?", "n/a", "~n/a"]);
|
|
29
29
|
var CACHE_SHARE_TOLERANCE = 1;
|
|
@@ -46,15 +46,15 @@ function parseTokenCell(cell) {
|
|
|
46
46
|
const t = cell.trim();
|
|
47
47
|
if (UNKNOWN_CELLS.has(t))
|
|
48
48
|
return null;
|
|
49
|
-
const m = t.match(/~?(\d+)/);
|
|
50
|
-
return m ? Number.parseInt(m
|
|
49
|
+
const m = t.match(/~?(\d+)/)?.[1];
|
|
50
|
+
return m ? Number.parseInt(m, 10) : null;
|
|
51
51
|
}
|
|
52
52
|
function parsePctCell(cell) {
|
|
53
53
|
const t = cell.trim();
|
|
54
54
|
if (UNKNOWN_CELLS.has(t))
|
|
55
55
|
return null;
|
|
56
|
-
const m = t.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
57
|
-
return m ? Number.parseFloat(m
|
|
56
|
+
const m = t.match(/(\d+(?:\.\d+)?)\s*%/)?.[1];
|
|
57
|
+
return m ? Number.parseFloat(m) : null;
|
|
58
58
|
}
|
|
59
59
|
function ledgerCostRows(markdown) {
|
|
60
60
|
const body = sectionBody(markdown, /^### 3\. Monitor Ledger\s*$/m);
|
|
@@ -104,7 +104,7 @@ function validateCostEvidence(markdown, errors) {
|
|
|
104
104
|
}
|
|
105
105
|
for (const scope of pctScopes) {
|
|
106
106
|
for (const m of scope.matchAll(/(\d+(?:\.\d+)?)\s*%/g)) {
|
|
107
|
-
const pct = Number.parseFloat(m[1]);
|
|
107
|
+
const pct = Number.parseFloat(m[1] ?? "");
|
|
108
108
|
if (pct < 0 || pct > 100)
|
|
109
109
|
errors.push(`impossible_percentage:${m[0].trim()}`);
|
|
110
110
|
}
|
|
@@ -194,6 +194,8 @@ function parseValidateCliArgs(argv) {
|
|
|
194
194
|
let help = false;
|
|
195
195
|
for (let i = 0;i < argv.length; i++) {
|
|
196
196
|
const a = argv[i];
|
|
197
|
+
if (a === undefined)
|
|
198
|
+
continue;
|
|
197
199
|
if (a === "--help" || a === "-h")
|
|
198
200
|
help = true;
|
|
199
201
|
else if (a === "--json")
|
|
@@ -50,8 +50,8 @@ function countLedgerDataRows(markdown: string): number | null {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function declaredExecutedSteps(markdown: string): number | null {
|
|
53
|
-
const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/);
|
|
54
|
-
return match ? Number.parseInt(match
|
|
53
|
+
const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/)?.[1];
|
|
54
|
+
return match ? Number.parseInt(match, 10) : null;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// ── Cost evidence contract (task 0913, R2) ──────────────────────────────────
|
|
@@ -97,15 +97,15 @@ interface LedgerCostRow {
|
|
|
97
97
|
function parseTokenCell(cell: string): number | null {
|
|
98
98
|
const t = cell.trim();
|
|
99
99
|
if (UNKNOWN_CELLS.has(t)) return null;
|
|
100
|
-
const m = t.match(/~?(\d+)/);
|
|
101
|
-
return m ? Number.parseInt(m
|
|
100
|
+
const m = t.match(/~?(\d+)/)?.[1];
|
|
101
|
+
return m ? Number.parseInt(m, 10) : null;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
function parsePctCell(cell: string): number | null {
|
|
105
105
|
const t = cell.trim();
|
|
106
106
|
if (UNKNOWN_CELLS.has(t)) return null;
|
|
107
|
-
const m = t.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
108
|
-
return m ? Number.parseFloat(m
|
|
107
|
+
const m = t.match(/(\d+(?:\.\d+)?)\s*%/)?.[1];
|
|
108
|
+
return m ? Number.parseFloat(m) : null;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function ledgerCostRows(markdown: string): LedgerCostRow[] {
|
|
@@ -169,7 +169,7 @@ function validateCostEvidence(markdown: string, errors: string[]): void {
|
|
|
169
169
|
}
|
|
170
170
|
for (const scope of pctScopes) {
|
|
171
171
|
for (const m of scope.matchAll(/(\d+(?:\.\d+)?)\s*%/g)) {
|
|
172
|
-
const pct = Number.parseFloat(m[1]);
|
|
172
|
+
const pct = Number.parseFloat(m[1] ?? '');
|
|
173
173
|
if (pct < 0 || pct > 100) errors.push(`impossible_percentage:${m[0].trim()}`);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
@@ -291,6 +291,7 @@ export function parseValidateCliArgs(argv: string[]): ValidateCliArgs {
|
|
|
291
291
|
let help = false;
|
|
292
292
|
for (let i = 0; i < argv.length; i++) {
|
|
293
293
|
const a = argv[i];
|
|
294
|
+
if (a === undefined) continue;
|
|
294
295
|
if (a === '--help' || a === '-h') help = true;
|
|
295
296
|
else if (a === '--json') json = true;
|
|
296
297
|
else if (a === '--file') file = argv[++i] ?? null;
|
|
@@ -103,8 +103,9 @@ through unchanged.
|
|
|
103
103
|
|
|
104
104
|
Exit: 0 = sync handled (applied / no-op / suppressed-blocked / live-blocked).`;
|
|
105
105
|
function defaultSpurBin() {
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const fromEnv = getEnvVar("SPUR_BIN");
|
|
107
|
+
if (fromEnv)
|
|
108
|
+
return fromEnv;
|
|
108
109
|
const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
109
110
|
if (existsSync(local))
|
|
110
111
|
return `bun ${local}`;
|
|
@@ -118,6 +119,8 @@ function parseBoundedSyncCliArgs(argv) {
|
|
|
118
119
|
let help = false;
|
|
119
120
|
for (let i = 0;i < argv.length; i++) {
|
|
120
121
|
const a = argv[i];
|
|
122
|
+
if (a === undefined)
|
|
123
|
+
continue;
|
|
121
124
|
if (a === "--help" || a === "-h")
|
|
122
125
|
help = true;
|
|
123
126
|
else if (a === "--json")
|
|
@@ -251,7 +251,8 @@ Exit: 0 = sync handled (applied / no-op / suppressed-blocked / live-blocked).`;
|
|
|
251
251
|
* keeps ad-hoc invocations from silently hitting a stale PATH install.
|
|
252
252
|
*/
|
|
253
253
|
export function defaultSpurBin(): string {
|
|
254
|
-
|
|
254
|
+
const fromEnv = getEnvVar('SPUR_BIN');
|
|
255
|
+
if (fromEnv) return fromEnv;
|
|
255
256
|
// scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw pathname breaks
|
|
256
257
|
// on %-encoded paths, e.g. spaces in the checkout directory)
|
|
257
258
|
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
@@ -268,6 +269,7 @@ export function parseBoundedSyncCliArgs(argv: string[]): BoundedSyncCliArgs {
|
|
|
268
269
|
|
|
269
270
|
for (let i = 0; i < argv.length; i++) {
|
|
270
271
|
const a = argv[i];
|
|
272
|
+
if (a === undefined) continue;
|
|
271
273
|
if (a === '--help' || a === '-h') help = true;
|
|
272
274
|
else if (a === '--json') json = true;
|
|
273
275
|
else if (a === '--spur-bin') spurBin = argv[++i] ?? spurBin;
|
|
@@ -45,12 +45,12 @@ const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
45
45
|
|
|
46
46
|
/** Shape of the generated inline bundle / repo app source this script consumes. */
|
|
47
47
|
interface VerificationModule {
|
|
48
|
-
startFeatureVerificationReceipt: typeof import('
|
|
49
|
-
completeFeatureVerificationReceipt: typeof import('
|
|
50
|
-
captureFeatureReceiptDigest: typeof import('
|
|
51
|
-
resolveWorkflowDefinition: typeof import('
|
|
52
|
-
splitLaunchCommand: typeof import('
|
|
53
|
-
openInlineRunProjectDb: typeof import('
|
|
48
|
+
startFeatureVerificationReceipt: typeof import('../../../packages/app/src/workflow/feature-verification-receipt')['startFeatureVerificationReceipt'];
|
|
49
|
+
completeFeatureVerificationReceipt: typeof import('../../../packages/app/src/workflow/feature-verification-receipt')['completeFeatureVerificationReceipt'];
|
|
50
|
+
captureFeatureReceiptDigest: typeof import('../../../packages/app/src/workflow/feature-verification-receipt')['captureFeatureReceiptDigest'];
|
|
51
|
+
resolveWorkflowDefinition: typeof import('../../../packages/app/src/workflow/workflow-resolver')['resolveWorkflowDefinition'];
|
|
52
|
+
splitLaunchCommand: typeof import('../../../packages/app/src/workflow/split-launch-command')['splitLaunchCommand'];
|
|
53
|
+
openInlineRunProjectDb: typeof import('../../../packages/app/src/services/inline-run-setup')['openInlineRunProjectDb'];
|
|
54
54
|
ArtifactDao: new (
|
|
55
55
|
adapter: never,
|
|
56
56
|
) => { record: (input: { runId: string; path: string; kind: string }) => Promise<unknown> };
|
|
@@ -290,9 +290,9 @@ function checkReportStructure(reportMarkdown) {
|
|
|
290
290
|
const knownRows = findingsBody.match(new RegExp(`^\\|\\s*(?:${catAlt}):[^|]+`, "gm")) ?? [];
|
|
291
291
|
const invalidRows = findingsBody.match(/^\|\s*([^|:\s][^|:]*):[^|]+/gm) ?? [];
|
|
292
292
|
for (const row of [...new Set([...knownRows, ...invalidRows])]) {
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
problems.push(`finding-invalid-key-category:${
|
|
293
|
+
const seg = row.match(/^\|\s*([^:|]+):/)?.[1];
|
|
294
|
+
if (seg !== undefined && !FINDING_CATEGORIES.includes(seg.trim())) {
|
|
295
|
+
problems.push(`finding-invalid-key-category:${seg.trim()}`);
|
|
296
296
|
}
|
|
297
297
|
const rowLower = row.toLowerCase();
|
|
298
298
|
for (const field of FINDING_FIELDS) {
|
|
@@ -375,9 +375,9 @@ export function checkReportStructure(reportMarkdown: string): { ok: boolean; pro
|
|
|
375
375
|
for (const row of [...new Set([...knownRows, ...invalidRows])]) {
|
|
376
376
|
// 0686/I9: a pipe row outside the closed first-segment set fails by name instead of
|
|
377
377
|
// being skipped silently by the legacy regex.
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
380
|
-
problems.push(`finding-invalid-key-category:${
|
|
378
|
+
const seg = row.match(/^\|\s*([^:|]+):/)?.[1];
|
|
379
|
+
if (seg !== undefined && !(FINDING_CATEGORIES as readonly string[]).includes(seg.trim())) {
|
|
380
|
+
problems.push(`finding-invalid-key-category:${seg.trim()}`);
|
|
381
381
|
}
|
|
382
382
|
// Case-insensitive on BOTH sides — camelCase triage fields otherwise never match a
|
|
383
383
|
// lowercased row.
|
|
@@ -630,7 +630,7 @@ export function validateSelector(opts: {
|
|
|
630
630
|
output?: string;
|
|
631
631
|
}): SelectorValidation {
|
|
632
632
|
const errors: string[] = [];
|
|
633
|
-
const has = (v?: string) => v !== undefined && v !== '';
|
|
633
|
+
const has = (v?: string): v is string => v !== undefined && v !== '';
|
|
634
634
|
const val = (v?: string) => (has(v) ? v : null);
|
|
635
635
|
const mode = has(opts.mode) ? opts.mode : 'daily';
|
|
636
636
|
if (mode !== 'daily' && mode !== 'ad-hoc') {
|
|
@@ -30,7 +30,7 @@ function usage() {
|
|
|
30
30
|
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]");
|
|
31
31
|
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> " + "--status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]");
|
|
32
32
|
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]");
|
|
33
|
-
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--spur-bin <path>]");
|
|
33
|
+
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]");
|
|
34
34
|
console.error(" terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, " + "failed-timeout, failed-guard, cancelled, interrupted, retry-exhausted");
|
|
35
35
|
console.error(" bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]");
|
|
36
36
|
process.exit(2);
|
|
@@ -154,6 +154,9 @@ async function printFingerprint(taskFile, featureFile, spurBin) {
|
|
|
154
154
|
return 0;
|
|
155
155
|
}
|
|
156
156
|
var CLOSE_STATUSES = new Set(["done", "failed", "paused"]);
|
|
157
|
+
function isCloseStatus(status) {
|
|
158
|
+
return CLOSE_STATUSES.has(status);
|
|
159
|
+
}
|
|
157
160
|
var TERMINAL_REASONS = new Set([
|
|
158
161
|
"done",
|
|
159
162
|
"paused-operator",
|
|
@@ -166,6 +169,9 @@ var TERMINAL_REASONS = new Set([
|
|
|
166
169
|
"retry-exhausted"
|
|
167
170
|
]);
|
|
168
171
|
var ACTION_STATUSES = new Set(["done", "failed"]);
|
|
172
|
+
function isActionStatus(status) {
|
|
173
|
+
return ACTION_STATUSES.has(status);
|
|
174
|
+
}
|
|
169
175
|
function runRecordLogPath(runDir, runId) {
|
|
170
176
|
const markdownPath = join(runDir, `${runId}.md`);
|
|
171
177
|
const legacyLogPath = join(runDir, `${runId}.log`);
|
|
@@ -258,7 +264,7 @@ async function runDecideMode(input) {
|
|
|
258
264
|
}
|
|
259
265
|
if (!outcome.ok)
|
|
260
266
|
return decideFailed(outcome.error ?? "decide failed without an error message");
|
|
261
|
-
process.stdout.write(`${JSON.stringify({
|
|
267
|
+
process.stdout.write(`${JSON.stringify({ runId: input.runId, node: input.node, ...outcome, ok: true })}
|
|
262
268
|
`);
|
|
263
269
|
appendRunLogLine(input.runId, `decide node=${input.node} value=${outcome.value ?? ""} source=${outcome.source ?? "default"} reason=${outcome.reason ?? ""}`);
|
|
264
270
|
return await runTraceMode({
|
|
@@ -276,7 +282,11 @@ async function runPersistOutMode(input) {
|
|
|
276
282
|
try {
|
|
277
283
|
const { entry } = resolveAppEntry(input.spurBin);
|
|
278
284
|
const app = await import(entry);
|
|
279
|
-
const result = await app.persistWorktreeRuns({
|
|
285
|
+
const result = await app.persistWorktreeRuns({
|
|
286
|
+
fromWorkdir: input.from,
|
|
287
|
+
toWorkdir: process.cwd(),
|
|
288
|
+
...input.taskFiles.length > 0 ? { taskFiles: input.taskFiles } : {}
|
|
289
|
+
});
|
|
280
290
|
process.stdout.write(`${JSON.stringify({ ok: true, persisted: result.persisted, skipped: result.skipped })}
|
|
281
291
|
`);
|
|
282
292
|
return 0;
|
|
@@ -299,7 +309,7 @@ async function main() {
|
|
|
299
309
|
let runId = "";
|
|
300
310
|
let file = "";
|
|
301
311
|
let fingerprint = false;
|
|
302
|
-
|
|
312
|
+
const taskFiles = [];
|
|
303
313
|
let featureFile = "";
|
|
304
314
|
let action = false;
|
|
305
315
|
let close = false;
|
|
@@ -323,7 +333,7 @@ async function main() {
|
|
|
323
333
|
else if (argv[i] === "--fingerprint")
|
|
324
334
|
fingerprint = true;
|
|
325
335
|
else if (argv[i] === "--task-file")
|
|
326
|
-
|
|
336
|
+
taskFiles.push(argv[++i] ?? "");
|
|
327
337
|
else if (argv[i] === "--feature-file")
|
|
328
338
|
featureFile = argv[++i] ?? "";
|
|
329
339
|
else if (argv[i] === "--action")
|
|
@@ -354,12 +364,12 @@ async function main() {
|
|
|
354
364
|
spurBin = argv[++i] ?? spurBin;
|
|
355
365
|
}
|
|
356
366
|
if (fingerprint) {
|
|
357
|
-
if (runId !== "" || file !== "" ||
|
|
367
|
+
if (runId !== "" || file !== "" || taskFiles.length !== 1 || (taskFiles[0] ?? "").trim() === "")
|
|
358
368
|
usage();
|
|
359
|
-
process.exit(await printFingerprint(
|
|
369
|
+
process.exit(await printFingerprint(taskFiles[0] ?? "", featureFile, spurBin));
|
|
360
370
|
}
|
|
361
371
|
if (decide) {
|
|
362
|
-
if (action || close || fingerprint || file !== "" ||
|
|
372
|
+
if (action || close || fingerprint || file !== "" || taskFiles.length > 0)
|
|
363
373
|
usage();
|
|
364
374
|
if (runId.trim() === "" || node.trim() === "" || optionsJson.trim() === "")
|
|
365
375
|
usage();
|
|
@@ -368,11 +378,11 @@ async function main() {
|
|
|
368
378
|
process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
|
|
369
379
|
}
|
|
370
380
|
if (persistOut) {
|
|
371
|
-
if (fingerprint || decide || action || close || runId !== "" || file !== ""
|
|
381
|
+
if (fingerprint || decide || action || close || runId !== "" || file !== "")
|
|
372
382
|
usage();
|
|
373
|
-
if (from.trim() === "")
|
|
383
|
+
if (from.trim() === "" || taskFiles.some((taskFile) => taskFile.trim() === ""))
|
|
374
384
|
usage();
|
|
375
|
-
process.exit(await runPersistOutMode({ from, spurBin }));
|
|
385
|
+
process.exit(await runPersistOutMode({ from, taskFiles, spurBin }));
|
|
376
386
|
}
|
|
377
387
|
if (action || close) {
|
|
378
388
|
if (action && close)
|
|
@@ -382,7 +392,7 @@ async function main() {
|
|
|
382
392
|
if (!SAFE_RUN_ID_RE.test(runId))
|
|
383
393
|
refuseUnsafeRunId(runId);
|
|
384
394
|
if (close) {
|
|
385
|
-
if (!
|
|
395
|
+
if (!isCloseStatus(status))
|
|
386
396
|
usage();
|
|
387
397
|
if (reason.trim() === "") {
|
|
388
398
|
if (status === "failed")
|
|
@@ -404,7 +414,7 @@ async function main() {
|
|
|
404
414
|
}
|
|
405
415
|
if (node.trim() === "" || kind.trim() === "")
|
|
406
416
|
usage();
|
|
407
|
-
if (!
|
|
417
|
+
if (!isActionStatus(status))
|
|
408
418
|
usage();
|
|
409
419
|
if (okRaw !== "true" && okRaw !== "false")
|
|
410
420
|
usage();
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]
|
|
35
35
|
* --status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]
|
|
36
36
|
* bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]
|
|
37
|
-
* bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--spur-bin <path>]
|
|
37
|
+
* bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]
|
|
38
38
|
*
|
|
39
39
|
* The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
|
|
40
40
|
* creates nothing (task 0862 R5).
|
|
@@ -56,8 +56,12 @@
|
|
|
56
56
|
* `--worktree` run executes BEFORE teardown: it copies the worktree's run provenance — the
|
|
57
57
|
* `runs`/`action_runs`/`phase_runs`/`transition_runs`/`workflow_states` rows and the two-file
|
|
58
58
|
* run records — into THIS tree's project DB and `.spur/run/` (cwd = the invoking tree).
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* Repeatable `--task-file <path>` (0984 R2) forwards each merged task file so the app service
|
|
60
|
+
* also copies/verifies the `.spur/run/<file>` evidence that file cites; citation selection,
|
|
61
|
+
* safe-path validation and conflict behavior live entirely in the app service. Idempotent;
|
|
62
|
+
* collisions are reported as `skipped[{id,reason}]`, never overwrites. Exit 0 = persisted
|
|
63
|
+
* (possibly with skips); exit 1 = fail closed — an unreadable/unsafe/over-cap/unresolved
|
|
64
|
+
* citation or a divergent cited file fails the pass too — the driver routes to WT-5 and
|
|
61
65
|
* retains the worktree; usage errors exit 2.
|
|
62
66
|
*
|
|
63
67
|
* Exit 2 is reserved for usage errors.
|
|
@@ -112,7 +116,7 @@ function usage(): never {
|
|
|
112
116
|
' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
|
|
113
117
|
);
|
|
114
118
|
console.error(
|
|
115
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--spur-bin <path>]',
|
|
119
|
+
' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]',
|
|
116
120
|
);
|
|
117
121
|
console.error(
|
|
118
122
|
' terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, ' +
|
|
@@ -329,7 +333,12 @@ async function printFingerprint(taskFile: string, featureFile: string, spurBin:
|
|
|
329
333
|
}
|
|
330
334
|
|
|
331
335
|
/** Terminal statuses the inline driver may declare when closing its run row. */
|
|
332
|
-
const CLOSE_STATUSES = new Set(['done', 'failed', 'paused']);
|
|
336
|
+
const CLOSE_STATUSES: ReadonlySet<string> = new Set<'done' | 'failed' | 'paused'>(['done', 'failed', 'paused']);
|
|
337
|
+
|
|
338
|
+
/** Narrow an argv `--status` to the close vocabulary (`CLOSE_STATUSES`). */
|
|
339
|
+
function isCloseStatus(status: string): status is 'done' | 'failed' | 'paused' {
|
|
340
|
+
return CLOSE_STATUSES.has(status);
|
|
341
|
+
}
|
|
333
342
|
|
|
334
343
|
/**
|
|
335
344
|
* 0937 R2: closed terminal-reason vocabulary the driver may declare on `--close`.
|
|
@@ -350,7 +359,12 @@ const TERMINAL_REASONS = new Set([
|
|
|
350
359
|
]);
|
|
351
360
|
|
|
352
361
|
/** Finalize statuses — a finish emission is terminal, so only done|failed are valid (0868 #4). */
|
|
353
|
-
const ACTION_STATUSES = new Set(['done', 'failed']);
|
|
362
|
+
const ACTION_STATUSES: ReadonlySet<string> = new Set<'done' | 'failed'>(['done', 'failed']);
|
|
363
|
+
|
|
364
|
+
/** Narrow an argv `--status` to the finalize vocabulary (`ACTION_STATUSES`). */
|
|
365
|
+
function isActionStatus(status: string): status is 'done' | 'failed' {
|
|
366
|
+
return ACTION_STATUSES.has(status);
|
|
367
|
+
}
|
|
354
368
|
|
|
355
369
|
/** Input for the ADR-117 emission modes (`--action` / `--close`). */
|
|
356
370
|
import type { WorkflowActionTraceWriter } from '@gobing-ai/spur-app';
|
|
@@ -362,7 +376,12 @@ interface TraceModeInput {
|
|
|
362
376
|
readonly kind: string;
|
|
363
377
|
/** Declared terminal reason (0937 R2) — validated against TERMINAL_REASONS before this point. */
|
|
364
378
|
readonly reason?: string;
|
|
365
|
-
|
|
379
|
+
/**
|
|
380
|
+
* Trace status: the finalize vocabulary plus the close-only `paused` (`CLOSE_STATUSES`).
|
|
381
|
+
* Both are assignable to the engine's `WorkflowStatus`; the plugin cannot import that
|
|
382
|
+
* type (standalone contract), so the literal union stands in for it.
|
|
383
|
+
*/
|
|
384
|
+
readonly status: 'done' | 'failed' | 'paused';
|
|
366
385
|
readonly ok: boolean;
|
|
367
386
|
readonly durationMs: number;
|
|
368
387
|
readonly spurBin: string;
|
|
@@ -553,7 +572,7 @@ async function runDecideMode(input: {
|
|
|
553
572
|
return decideFailed(error instanceof Error ? error.message : String(error));
|
|
554
573
|
}
|
|
555
574
|
if (!outcome.ok) return decideFailed(outcome.error ?? 'decide failed without an error message');
|
|
556
|
-
process.stdout.write(`${JSON.stringify({
|
|
575
|
+
process.stdout.write(`${JSON.stringify({ runId: input.runId, node: input.node, ...outcome, ok: true })}\n`);
|
|
557
576
|
// 0976 R2: the run log names the decision's provenance, so a declared-default fallback is
|
|
558
577
|
// never read as a model decision. Best-effort through the same run-log appender.
|
|
559
578
|
appendRunLogLine(
|
|
@@ -574,23 +593,34 @@ async function runDecideMode(input: {
|
|
|
574
593
|
}
|
|
575
594
|
|
|
576
595
|
/**
|
|
577
|
-
* `--persist-out --from <worktree-path>` (task 0975 R1
|
|
578
|
-
* chain and copy the worktree's inline-run provenance into THIS
|
|
579
|
-
* lives here — the app's `persistWorktreeRuns` (→ domain
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
596
|
+
* `--persist-out --from <worktree-path>` (task 0975 R1; `--task-file` per 0984 R2): resolve
|
|
597
|
+
* the app through the shared chain and copy the worktree's inline-run provenance into THIS
|
|
598
|
+
* tree. No persistence policy lives here — the app's `persistWorktreeRuns` (→ domain
|
|
599
|
+
* `transferRunTables`) owns the DB transfer, the citation selection/copy rules, the
|
|
600
|
+
* idempotence and the conflict skips; this delegate only forwards the merged task file
|
|
601
|
+
* paths. Success prints `{ok:true,persisted:<n>,skipped:[…]}` and exits 0; any failure
|
|
602
|
+
* prints `{ok:false,error}` and exits 1 (the driver routes to WT-5 and retains the
|
|
603
|
+
* worktree).
|
|
583
604
|
*/
|
|
584
|
-
async function runPersistOutMode(input: {
|
|
605
|
+
async function runPersistOutMode(input: {
|
|
606
|
+
from: string;
|
|
607
|
+
taskFiles: readonly string[];
|
|
608
|
+
spurBin: string;
|
|
609
|
+
}): Promise<number> {
|
|
585
610
|
try {
|
|
586
611
|
const { entry } = resolveAppEntry(input.spurBin);
|
|
587
612
|
const app = (await import(entry)) as {
|
|
588
613
|
persistWorktreeRuns: (i: {
|
|
589
614
|
fromWorkdir: string;
|
|
590
615
|
toWorkdir: string;
|
|
616
|
+
taskFiles?: readonly string[];
|
|
591
617
|
}) => Promise<{ ok: true; persisted: number; skipped: ReadonlyArray<{ id: string; reason: string }> }>;
|
|
592
618
|
};
|
|
593
|
-
const result = await app.persistWorktreeRuns({
|
|
619
|
+
const result = await app.persistWorktreeRuns({
|
|
620
|
+
fromWorkdir: input.from,
|
|
621
|
+
toWorkdir: process.cwd(),
|
|
622
|
+
...(input.taskFiles.length > 0 ? { taskFiles: input.taskFiles } : {}),
|
|
623
|
+
});
|
|
594
624
|
process.stdout.write(`${JSON.stringify({ ok: true, persisted: result.persisted, skipped: result.skipped })}\n`);
|
|
595
625
|
return 0;
|
|
596
626
|
} catch (error) {
|
|
@@ -612,7 +642,7 @@ async function main(): Promise<void> {
|
|
|
612
642
|
let runId = '';
|
|
613
643
|
let file = '';
|
|
614
644
|
let fingerprint = false;
|
|
615
|
-
|
|
645
|
+
const taskFiles: string[] = [];
|
|
616
646
|
let featureFile = '';
|
|
617
647
|
let action = false;
|
|
618
648
|
let close = false;
|
|
@@ -632,7 +662,7 @@ async function main(): Promise<void> {
|
|
|
632
662
|
if (argv[i] === '--run-id') runId = argv[++i] ?? '';
|
|
633
663
|
else if (argv[i] === '--file') file = argv[++i] ?? '';
|
|
634
664
|
else if (argv[i] === '--fingerprint') fingerprint = true;
|
|
635
|
-
else if (argv[i] === '--task-file')
|
|
665
|
+
else if (argv[i] === '--task-file') taskFiles.push(argv[++i] ?? '');
|
|
636
666
|
else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
|
|
637
667
|
else if (argv[i] === '--action') action = true;
|
|
638
668
|
else if (argv[i] === '--close') close = true;
|
|
@@ -651,9 +681,10 @@ async function main(): Promise<void> {
|
|
|
651
681
|
|
|
652
682
|
// Two mutually exclusive modes share this entry point: create/attach a run (run-id + file), or
|
|
653
683
|
// print the proof digest for the inline driver (task 0862 R5). Mixing them is a usage error.
|
|
684
|
+
// `--task-file` belongs to the fingerprint (exactly one) and persist-out (zero or more) modes.
|
|
654
685
|
if (fingerprint) {
|
|
655
|
-
if (runId !== '' || file !== '' ||
|
|
656
|
-
process.exit(await printFingerprint(
|
|
686
|
+
if (runId !== '' || file !== '' || taskFiles.length !== 1 || (taskFiles[0] ?? '').trim() === '') usage();
|
|
687
|
+
process.exit(await printFingerprint(taskFiles[0] ?? '', featureFile, spurBin));
|
|
657
688
|
}
|
|
658
689
|
|
|
659
690
|
// ADR-117 emission modes (task 0868): the inline driver reports one completed action
|
|
@@ -663,18 +694,20 @@ async function main(): Promise<void> {
|
|
|
663
694
|
// fingerprint, and trace modes; it requires the run id (filename-guarded), the state id
|
|
664
695
|
// for the trace row, and the options JSON file.
|
|
665
696
|
if (decide) {
|
|
666
|
-
if (action || close || fingerprint || file !== '' ||
|
|
697
|
+
if (action || close || fingerprint || file !== '' || taskFiles.length > 0) usage();
|
|
667
698
|
if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
|
|
668
699
|
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
669
700
|
process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
|
|
670
701
|
}
|
|
671
702
|
|
|
672
703
|
// Worktree provenance persist-out (task 0975 R1): mutually exclusive with every other
|
|
673
|
-
// mode; requires the source worktree path
|
|
704
|
+
// mode; requires the source worktree path and accepts zero or more merged task files
|
|
705
|
+
// (0984 R2) whose cited `.spur/run/` evidence the app service copies/verifies. Runs
|
|
706
|
+
// with cwd = the invoking tree.
|
|
674
707
|
if (persistOut) {
|
|
675
|
-
if (fingerprint || decide || action || close || runId !== '' || file !== ''
|
|
676
|
-
if (from.trim() === '') usage();
|
|
677
|
-
process.exit(await runPersistOutMode({ from, spurBin }));
|
|
708
|
+
if (fingerprint || decide || action || close || runId !== '' || file !== '') usage();
|
|
709
|
+
if (from.trim() === '' || taskFiles.some((taskFile) => taskFile.trim() === '')) usage();
|
|
710
|
+
process.exit(await runPersistOutMode({ from, taskFiles, spurBin }));
|
|
678
711
|
}
|
|
679
712
|
|
|
680
713
|
if (action || close) {
|
|
@@ -682,7 +715,7 @@ async function main(): Promise<void> {
|
|
|
682
715
|
if (runId.trim() === '' || status.trim() === '') usage();
|
|
683
716
|
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
684
717
|
if (close) {
|
|
685
|
-
if (!
|
|
718
|
+
if (!isCloseStatus(status)) usage();
|
|
686
719
|
// 0937 R2: a failed close requires a declared reason, and any declared reason
|
|
687
720
|
// must be a closed-enum value — both fail loudly BEFORE any write happens.
|
|
688
721
|
if (reason.trim() === '') {
|
|
@@ -705,7 +738,7 @@ async function main(): Promise<void> {
|
|
|
705
738
|
);
|
|
706
739
|
}
|
|
707
740
|
if (node.trim() === '' || kind.trim() === '') usage();
|
|
708
|
-
if (!
|
|
741
|
+
if (!isActionStatus(status)) usage();
|
|
709
742
|
// `--ok` and `--duration-ms` are required and exact for the action mode
|
|
710
743
|
// (review finding #2): a miscased `--ok True` or an omitted `--duration-ms`
|
|
711
744
|
// must be a loud usage error, never a silently-defaulted `ok=0` /
|
|
@@ -169,6 +169,7 @@ export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
169
169
|
const booleans = new Set<string>();
|
|
170
170
|
for (let i = 0; i < rest.length; i++) {
|
|
171
171
|
const tok = rest[i];
|
|
172
|
+
if (tok === undefined) continue;
|
|
172
173
|
if (BOOL_FLAGS.has(tok)) {
|
|
173
174
|
booleans.add(tok);
|
|
174
175
|
} else if (VALUE_FLAGS.has(tok)) {
|
|
@@ -410,17 +410,19 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
410
410
|
return { status, attempts: gateAttempt, logFile, findingsFile, statusFile, attemptFile, receiptFile };
|
|
411
411
|
}
|
|
412
412
|
var QUALITY_GATE_USAGE = "usage: quality-gate.ts <run|recheck|light|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)";
|
|
413
|
-
function main(argv,
|
|
413
|
+
function main(argv, rawEnv = getEnvVars(), options = {}) {
|
|
414
414
|
const mode = argv[0];
|
|
415
415
|
if (mode !== "run" && mode !== "recheck" && mode !== "light" && mode !== "status") {
|
|
416
416
|
process.stderr.write(`${QUALITY_GATE_USAGE}
|
|
417
417
|
`);
|
|
418
418
|
return 2;
|
|
419
419
|
}
|
|
420
|
-
|
|
420
|
+
const wbs = rawEnv.wbs ?? "";
|
|
421
|
+
if (wbs.length === 0) {
|
|
421
422
|
process.stderr.write("quality-gate: env `wbs` is required\n");
|
|
422
423
|
return 2;
|
|
423
424
|
}
|
|
425
|
+
const env = { ...rawEnv, wbs };
|
|
424
426
|
if (mode === "light") {
|
|
425
427
|
runLightGate(env);
|
|
426
428
|
} else if (mode === "status") {
|