@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.
Files changed (140) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +42 -0
  3. package/board/index.d.ts +38 -0
  4. package/config/plugin-scripts.json +5 -0
  5. package/config/workflows/feature-verification.yaml +7 -4
  6. package/config/workflows/idea-pipeline.yaml +10 -4
  7. package/config/workflows/task-pipeline.yaml +54 -26
  8. package/config/workflows/wrapup-pipeline.yaml +92 -17
  9. package/package.json +15 -11
  10. package/plugins/sp/commands/dev-verify.md +7 -4
  11. package/plugins/sp/commands/dev-verifyall.md +4 -3
  12. package/plugins/sp/commands/dev-wrap.md +1 -1
  13. package/plugins/sp/hooks/context-post-tool.ts +2 -2
  14. package/plugins/sp/hooks/context-session-start.ts +2 -1
  15. package/plugins/sp/hooks/context-session-stop.ts +22 -14
  16. package/plugins/sp/hooks/pi/guard-extension.ts +109 -150
  17. package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
  18. package/plugins/sp/lib/inline-run.generated.d.mts +2 -2
  19. package/plugins/sp/lib/inline-run.generated.mjs +10 -10
  20. package/plugins/sp/plugin.json +1 -1
  21. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
  22. package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
  23. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
  24. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
  25. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
  26. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
  27. package/plugins/sp/scripts/feature-sync-bounded.mjs +5 -2
  28. package/plugins/sp/scripts/feature-sync-bounded.ts +3 -1
  29. package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
  30. package/plugins/sp/scripts/history-anatomy-cache.mjs +3 -3
  31. package/plugins/sp/scripts/history-anatomy-cache.ts +4 -4
  32. package/plugins/sp/scripts/inline-run-setup.mjs +23 -13
  33. package/plugins/sp/scripts/inline-run-setup.ts +60 -27
  34. package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
  35. package/plugins/sp/scripts/pr-reviewing.ts +1 -0
  36. package/plugins/sp/scripts/quality-gate.mjs +4 -2
  37. package/plugins/sp/scripts/quality-gate.ts +8 -2
  38. package/plugins/sp/scripts/residual-scan.mjs +3 -3
  39. package/plugins/sp/scripts/residual-scan.ts +3 -3
  40. package/plugins/sp/scripts/script-contract-check.ts +175 -46
  41. package/plugins/sp/scripts/script-root.mjs +125 -0
  42. package/plugins/sp/scripts/script-root.ts +195 -0
  43. package/plugins/sp/scripts/stage-registry-adapter.ts +1 -1
  44. package/plugins/sp/scripts/task-evidence-precheck.ts +4 -2
  45. package/plugins/sp/scripts/task-size-precheck.ts +3 -1
  46. package/plugins/sp/scripts/validate-commands.ts +12 -12
  47. package/plugins/sp/scripts/validate-flag-contracts.ts +42 -30
  48. package/plugins/sp/scripts/verify-answer-lint.ts +3 -1
  49. package/plugins/sp/scripts/workflow-step-profile.mjs +5 -2
  50. package/plugins/sp/scripts/workflow-step-profile.ts +3 -1
  51. package/plugins/sp/scripts/wrapup-drift-probe.mjs +5 -2
  52. package/plugins/sp/scripts/wrapup-drift-probe.ts +4 -2
  53. package/plugins/sp/scripts/wrapup-steps.mjs +32 -8
  54. package/plugins/sp/scripts/wrapup-steps.ts +56 -10
  55. package/plugins/sp/skills/code-verification/SKILL.md +12 -7
  56. package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -0
  57. package/plugins/sp/skills/functional-review/SKILL.md +1 -1
  58. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  59. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +9 -1
  60. package/plugins/sp/skills/spur-dev/references/execution-batch.md +30 -10
  61. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +6 -3
  62. package/plugins/sp/tsconfig.json +8 -0
  63. package/spur.js +5194 -4457
  64. package/web/_astro/{BoardApp.FTEs3-N8.js → BoardApp.BxJuwD7I.js} +150 -153
  65. package/web/_astro/BoardApp.Cpxntzad.js +1 -0
  66. package/web/_astro/TaskDetail.C5bW4WGV.js +1 -0
  67. package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
  68. package/web/_astro/{arc.uuAf51IT.js → arc.CyjRvNMY.js} +1 -1
  69. package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
  70. package/web/_astro/{blockDiagram-GPEHLZMM.D1mGCq3p.js → blockDiagram-GPEHLZMM.Be38USQ2.js} +1 -1
  71. package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
  72. package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
  73. package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
  74. package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
  75. package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
  76. package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
  77. package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
  78. package/web/_astro/{c4Diagram-AAUBKEIU.CMsolcde.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
  79. package/web/_astro/channel.CI6N_tCg.js +1 -0
  80. package/web/_astro/{chunk-2J33WTMH.CrGA3fik.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
  81. package/web/_astro/{chunk-4BX2VUAB.DsLVVla0.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
  82. package/web/_astro/{chunk-55IACEB6.Dxc59Tfi.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
  83. package/web/_astro/{chunk-727SXJPM.CaEVE2Wy.js → chunk-727SXJPM.Bi-TUb_V.js} +1 -1
  84. package/web/_astro/{chunk-AQP2D5EJ.BiJ4HXeI.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
  85. package/web/_astro/{chunk-FMBD7UC4.Mxf1fru5.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
  86. package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
  87. package/web/_astro/{chunk-ND2GUHAM.pyOWQixH.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
  88. package/web/_astro/{chunk-QZHKN3VN.BmEsg4vr.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
  89. package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
  90. package/web/_astro/{classDiagram-4FO5ZUOK.BHhhMFTO.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
  91. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BHhhMFTO.js → classDiagram-v2-Q7XG4LA2.D_apfHS7.js} +1 -1
  92. package/web/_astro/client.BhluLQqe.js +1 -0
  93. package/web/_astro/client.DBpSRLnx.js +9 -0
  94. package/web/_astro/cose-bilkent-S5V4N54A.DNLU_L-x.js +1 -0
  95. package/web/_astro/{cynefin-OW5HDTMX.C2j1_lKL.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
  96. package/web/_astro/{dagre-BM42HDAG.hZ2NCTdT.js → dagre-BM42HDAG.Uc0lvjcV.js} +1 -1
  97. package/web/_astro/{diagram-2AECGRRQ.Bfw5_EzK.js → diagram-2AECGRRQ.DDIxtDBo.js} +1 -1
  98. package/web/_astro/{diagram-5GNKFQAL.Bt1V_Tmk.js → diagram-5GNKFQAL.DVoDnD3H.js} +1 -1
  99. package/web/_astro/{diagram-KO2AKTUF.DEk-YFwp.js → diagram-KO2AKTUF.CRubTJ-0.js} +1 -1
  100. package/web/_astro/{diagram-LMA3HP47.CDjndtQm.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
  101. package/web/_astro/{diagram-OG6HWLK6.cFnUHScG.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
  102. package/web/_astro/{erDiagram-TEJ5UH35.DlhYp7NV.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +1 -1
  103. package/web/_astro/{flowDiagram-I6XJVG4X.DNTpsxfx.js → flowDiagram-I6XJVG4X.DrZcys16.js} +1 -1
  104. package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
  105. package/web/_astro/{gitGraphDiagram-PVQCEYII.DkEqNI0P.js → gitGraphDiagram-PVQCEYII.BWc9oJ4l.js} +1 -1
  106. package/web/_astro/index.5dn-Zm-2.js +1 -0
  107. package/web/_astro/index.C-IqPpW2.js +1 -0
  108. package/web/_astro/index.CENnIEqT.css +1 -0
  109. package/web/_astro/index.Cyc_GA4f.js +1 -0
  110. package/web/_astro/index.tZfqwSut.js +1 -0
  111. package/web/_astro/{infoDiagram-5YYISTIA.CrjioCTG.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
  112. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BcK0CN8n.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +1 -1
  113. package/web/_astro/{journeyDiagram-JHISSGLW.p0CbBDX1.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
  114. package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
  115. package/web/_astro/{kanban-definition-UN3LZRKU.puHIFt6J.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +1 -1
  116. package/web/_astro/{linear.Bb-3a1d7.js → linear.zRsuuDTE.js} +1 -1
  117. package/web/_astro/{mermaid.core.CJDgXOJs.js → mermaid.core.jAJTcMKc.js} +4 -4
  118. package/web/_astro/{mindmap-definition-RKZ34NQL.BoAZEI9t.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +1 -1
  119. package/web/_astro/{pieDiagram-4H26LBE5.CJNSdqY7.js → pieDiagram-4H26LBE5.CacwWaE3.js} +1 -1
  120. package/web/_astro/{quadrantDiagram-W4KKPZXB.DtTy7_0Z.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
  121. package/web/_astro/{requirementDiagram-4Y6WPE33.BNYV98Xg.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +1 -1
  122. package/web/_astro/{sankeyDiagram-5OEKKPKP.DRq9DGHp.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +1 -1
  123. package/web/_astro/{sequenceDiagram-3UESZ5HK.B7KdBFCm.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
  124. package/web/_astro/{stateDiagram-AJRCARHV.CD62ZJ2H.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
  125. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CeVk1TAj.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
  126. package/web/_astro/{timeline-definition-PNZ67QCA.C_ZPA5tT.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +1 -1
  127. package/web/_astro/{vennDiagram-CIIHVFJN.CrThO_FQ.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +1 -1
  128. package/web/_astro/{wardleyDiagram-YWT4CUSO.DSZCA5nl.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +1 -1
  129. package/web/_astro/{xychartDiagram-2RQKCTM6.KI7baTuk.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
  130. package/web/board-runtime.json +69 -0
  131. package/web/index.html +2 -2
  132. package/web/_astro/BoardApp.C02hAHPO.js +0 -1
  133. package/web/_astro/TaskDetail.C-GdsS-t.js +0 -1
  134. package/web/_astro/architectureDiagram-3BPJPVTR.CGe629A1.js +0 -36
  135. package/web/_astro/channel.fsgl7o5j.js +0 -1
  136. package/web/_astro/client.yhYJvxCU.js +0 -9
  137. package/web/_astro/cose-bilkent-S5V4N54A.2fH4YOlp.js +0 -1
  138. package/web/_astro/ganttDiagram-6RSMTGT7.DC_p36PI.js +0 -292
  139. package/web/_astro/index.De90oHcH.js +0 -1
  140. package/web/_astro/index.Hjbr15fG.css +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.94",
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;
@@ -133,6 +133,8 @@ function parseCliArgs(argv) {
133
133
  let help = false;
134
134
  for (let i = 0;i < argv.length; i++) {
135
135
  const a = argv[i];
136
+ if (a === undefined)
137
+ continue;
136
138
  if (a === "--help" || a === "-h") {
137
139
  help = true;
138
140
  } else if (a === "--json") {
@@ -296,6 +296,7 @@ export function parseCliArgs(argv: string[]): CliArgs {
296
296
 
297
297
  for (let i = 0; i < argv.length; i++) {
298
298
  const a = argv[i];
299
+ if (a === undefined) continue;
299
300
  if (a === '--help' || a === '-h') {
300
301
  help = true;
301
302
  } else if (a === '--json') {
@@ -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[1], 10) : null;
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[1], 10) : null;
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[1]) : null;
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[1], 10) : null;
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[1], 10) : null;
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[1]) : null;
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
- if (getEnvVar("SPUR_BIN"))
107
- return getEnvVar("SPUR_BIN");
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
- if (getEnvVar('SPUR_BIN')) return getEnvVar('SPUR_BIN');
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('../../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'];
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 segMatch = row.match(/^\|\s*([^:|]+):/);
294
- if (segMatch && !FINDING_CATEGORIES.includes(segMatch[1].trim())) {
295
- problems.push(`finding-invalid-key-category:${segMatch[1].trim()}`);
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 segMatch = row.match(/^\|\s*([^:|]+):/);
379
- if (segMatch && !(FINDING_CATEGORIES as readonly string[]).includes(segMatch[1].trim())) {
380
- problems.push(`finding-invalid-key-category:${segMatch[1].trim()}`);
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({ ok: true, runId: input.runId, node: input.node, ...outcome })}
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({ fromWorkdir: input.from, toWorkdir: process.cwd() });
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
- let taskFile = "";
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
- taskFile = argv[++i] ?? "";
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 !== "" || taskFile.trim() === "")
367
+ if (runId !== "" || file !== "" || taskFiles.length !== 1 || (taskFiles[0] ?? "").trim() === "")
358
368
  usage();
359
- process.exit(await printFingerprint(taskFile, featureFile, spurBin));
369
+ process.exit(await printFingerprint(taskFiles[0] ?? "", featureFile, spurBin));
360
370
  }
361
371
  if (decide) {
362
- if (action || close || fingerprint || file !== "" || taskFile !== "")
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 !== "" || taskFile !== "")
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 (!CLOSE_STATUSES.has(status))
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 (!ACTION_STATUSES.has(status))
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
- * Idempotent; collisions are reported as `skipped[{id,reason}]`, never overwrites. Exit 0 =
60
- * persisted (possibly with skips); exit 1 = fail closed — the driver routes to WT-5 and
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
- readonly status: string;
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({ ok: true, runId: input.runId, node: input.node, ...outcome })}\n`);
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): resolve the app through the shared
578
- * chain and copy the worktree's inline-run provenance into THIS tree. No persistence policy
579
- * lives here — the app's `persistWorktreeRuns` (→ domain `transferRunTables`) owns the DB
580
- * transfer, the idempotence and the conflict skips. Success prints
581
- * `{ok:true,persisted:<n>,skipped:[…]}` and exits 0; any failure prints `{ok:false,error}`
582
- * and exits 1 (the driver routes to WT-5 and retains the worktree).
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: { from: string; spurBin: string }): Promise<number> {
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({ fromWorkdir: input.from, toWorkdir: process.cwd() });
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
- let taskFile = '';
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') taskFile = argv[++i] ?? '';
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 !== '' || taskFile.trim() === '') usage();
656
- process.exit(await printFingerprint(taskFile, featureFile, spurBin));
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 !== '' || taskFile !== '') usage();
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. Runs with cwd = the invoking tree.
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 !== '' || taskFile !== '') usage();
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 (!CLOSE_STATUSES.has(status)) usage();
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 (!ACTION_STATUSES.has(status)) usage();
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` /
@@ -50,6 +50,8 @@ function parseArgs(argv) {
50
50
  const booleans = new Set;
51
51
  for (let i = 0;i < rest.length; i++) {
52
52
  const tok = rest[i];
53
+ if (tok === undefined)
54
+ continue;
53
55
  if (BOOL_FLAGS.has(tok)) {
54
56
  booleans.add(tok);
55
57
  } else if (VALUE_FLAGS.has(tok)) {
@@ -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, env = getEnvVars(), options = {}) {
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
- if ((env.wbs ?? "").length === 0) {
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") {