@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.
Files changed (153) 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/rules/boundary/test-subpath-boundary.yaml +48 -0
  6. package/config/rules/strict/runtime-boundaries.yaml +2 -1
  7. package/config/rules/structure/protected-files.yaml +4 -0
  8. package/config/workflows/feature-verification.yaml +7 -4
  9. package/config/workflows/idea-pipeline.yaml +10 -4
  10. package/config/workflows/task-pipeline.yaml +71 -52
  11. package/config/workflows/wrapup-pipeline.yaml +92 -17
  12. package/package.json +15 -11
  13. package/plugins/sp/README.md +1 -1
  14. package/plugins/sp/commands/dev-review-session.md +4 -4
  15. package/plugins/sp/commands/dev-review.md +16 -5
  16. package/plugins/sp/commands/dev-verify.md +7 -4
  17. package/plugins/sp/commands/dev-verifyall.md +4 -3
  18. package/plugins/sp/commands/dev-wrap.md +1 -1
  19. package/plugins/sp/hooks/context-post-tool.ts +2 -2
  20. package/plugins/sp/hooks/context-session-start.ts +2 -1
  21. package/plugins/sp/hooks/context-session-stop.ts +22 -14
  22. package/plugins/sp/hooks/pi/guard-extension.ts +112 -172
  23. package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
  24. package/plugins/sp/lib/inline-run.generated.d.mts +4 -2
  25. package/plugins/sp/lib/inline-run.generated.mjs +35 -14
  26. package/plugins/sp/plugin.json +1 -1
  27. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
  28. package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
  29. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
  30. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
  31. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
  32. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
  33. package/plugins/sp/scripts/feature-sync-bounded.mjs +5 -2
  34. package/plugins/sp/scripts/feature-sync-bounded.ts +3 -1
  35. package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
  36. package/plugins/sp/scripts/history-anatomy-cache.mjs +3 -3
  37. package/plugins/sp/scripts/history-anatomy-cache.ts +4 -4
  38. package/plugins/sp/scripts/inline-run-setup.mjs +60 -12
  39. package/plugins/sp/scripts/inline-run-setup.ts +122 -15
  40. package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
  41. package/plugins/sp/scripts/pr-reviewing.ts +1 -0
  42. package/plugins/sp/scripts/quality-gate.mjs +15 -6
  43. package/plugins/sp/scripts/quality-gate.ts +30 -9
  44. package/plugins/sp/scripts/residual-scan.mjs +4 -4
  45. package/plugins/sp/scripts/residual-scan.ts +5 -4
  46. package/plugins/sp/scripts/script-contract-check.ts +175 -46
  47. package/plugins/sp/scripts/script-root.mjs +125 -0
  48. package/plugins/sp/scripts/script-root.ts +195 -0
  49. package/plugins/sp/scripts/stage-registry-adapter.ts +1 -1
  50. package/plugins/sp/scripts/task-evidence-precheck.ts +4 -2
  51. package/plugins/sp/scripts/task-size-precheck.ts +3 -1
  52. package/plugins/sp/scripts/validate-commands.ts +12 -12
  53. package/plugins/sp/scripts/validate-flag-contracts.ts +42 -30
  54. package/plugins/sp/scripts/verify-answer-lint.ts +3 -1
  55. package/plugins/sp/scripts/workflow-step-profile.mjs +5 -2
  56. package/plugins/sp/scripts/workflow-step-profile.ts +3 -1
  57. package/plugins/sp/scripts/wrapup-drift-probe.mjs +5 -2
  58. package/plugins/sp/scripts/wrapup-drift-probe.ts +4 -2
  59. package/plugins/sp/scripts/wrapup-steps.mjs +32 -8
  60. package/plugins/sp/scripts/wrapup-steps.ts +56 -10
  61. package/plugins/sp/skills/code-verification/SKILL.md +12 -7
  62. package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -0
  63. package/plugins/sp/skills/functional-review/SKILL.md +1 -1
  64. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  65. package/plugins/sp/skills/session-review/SKILL.md +11 -9
  66. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +9 -1
  67. package/plugins/sp/skills/spur-dev/references/dev-operations.md +8 -2
  68. package/plugins/sp/skills/spur-dev/references/execution-batch.md +65 -17
  69. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -3
  70. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +22 -5
  71. package/plugins/sp/tsconfig.json +8 -0
  72. package/spur.js +20934 -21087
  73. package/web/_astro/{BoardApp.CerSBgis.js → BoardApp.BxJuwD7I.js} +151 -152
  74. package/web/_astro/BoardApp.Cpxntzad.js +1 -0
  75. package/web/_astro/TaskDetail.C5bW4WGV.js +1 -0
  76. package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
  77. package/web/_astro/arc.CyjRvNMY.js +1 -0
  78. package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
  79. package/web/_astro/{blockDiagram-GPEHLZMM.DXVIiv0p.js → blockDiagram-GPEHLZMM.Be38USQ2.js} +1 -1
  80. package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
  81. package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
  82. package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
  83. package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
  84. package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
  85. package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
  86. package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
  87. package/web/_astro/{c4Diagram-AAUBKEIU.BbF_zCxW.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
  88. package/web/_astro/channel.CI6N_tCg.js +1 -0
  89. package/web/_astro/{chunk-2J33WTMH.CAgQHpPC.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
  90. package/web/_astro/{chunk-4BX2VUAB.BN-5tpw4.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
  91. package/web/_astro/{chunk-55IACEB6.CnPkEEr0.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
  92. package/web/_astro/{chunk-727SXJPM.BQzQeMVm.js → chunk-727SXJPM.Bi-TUb_V.js} +4 -4
  93. package/web/_astro/{chunk-AQP2D5EJ.B6xNyDnL.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
  94. package/web/_astro/{chunk-FMBD7UC4.C7f9Ih78.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
  95. package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
  96. package/web/_astro/{chunk-ND2GUHAM.CNV1dFXT.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
  97. package/web/_astro/{chunk-QZHKN3VN.Cudn2TkJ.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
  98. package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
  99. package/web/_astro/{classDiagram-4FO5ZUOK.D1NwP50q.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
  100. package/web/_astro/{classDiagram-v2-Q7XG4LA2.D1NwP50q.js → classDiagram-v2-Q7XG4LA2.D_apfHS7.js} +1 -1
  101. package/web/_astro/client.BhluLQqe.js +1 -0
  102. package/web/_astro/client.DBpSRLnx.js +9 -0
  103. package/web/_astro/cose-bilkent-S5V4N54A.DNLU_L-x.js +1 -0
  104. package/web/_astro/{cynefin-OW5HDTMX.BmK52w8G.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
  105. package/web/_astro/{dagre-BM42HDAG.Bfy5CTDT.js → dagre-BM42HDAG.Uc0lvjcV.js} +2 -2
  106. package/web/_astro/diagram-2AECGRRQ.DDIxtDBo.js +43 -0
  107. package/web/_astro/diagram-5GNKFQAL.DVoDnD3H.js +10 -0
  108. package/web/_astro/{diagram-KO2AKTUF.CW_vMJ4z.js → diagram-KO2AKTUF.CRubTJ-0.js} +3 -3
  109. package/web/_astro/{diagram-LMA3HP47.B_8ZGF67.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
  110. package/web/_astro/{diagram-OG6HWLK6.BppnHsdS.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
  111. package/web/_astro/{erDiagram-TEJ5UH35.BEuHXcjJ.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +5 -5
  112. package/web/_astro/{flowDiagram-I6XJVG4X.CH-UlnGr.js → flowDiagram-I6XJVG4X.DrZcys16.js} +4 -4
  113. package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
  114. package/web/_astro/{gitGraphDiagram-PVQCEYII.XnPxPPZN.js → gitGraphDiagram-PVQCEYII.BWc9oJ4l.js} +1 -1
  115. package/web/_astro/index.5dn-Zm-2.js +1 -0
  116. package/web/_astro/index.C-IqPpW2.js +1 -0
  117. package/web/_astro/index.CENnIEqT.css +1 -0
  118. package/web/_astro/index.Cyc_GA4f.js +1 -0
  119. package/web/_astro/index.tZfqwSut.js +1 -0
  120. package/web/_astro/{infoDiagram-5YYISTIA.JyjYRu_T.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
  121. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +5 -5
  122. package/web/_astro/{journeyDiagram-JHISSGLW.C_iymSyp.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
  123. package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
  124. package/web/_astro/{kanban-definition-UN3LZRKU.DdfW-Oqt.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +7 -7
  125. package/web/_astro/{linear.C2_IkbZT.js → linear.zRsuuDTE.js} +1 -1
  126. package/web/_astro/mermaid.core.jAJTcMKc.js +301 -0
  127. package/web/_astro/{mindmap-definition-RKZ34NQL.DAZIxQSK.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +2 -2
  128. package/web/_astro/{pieDiagram-4H26LBE5.CN8sIhKM.js → pieDiagram-4H26LBE5.CacwWaE3.js} +3 -3
  129. package/web/_astro/{quadrantDiagram-W4KKPZXB.3dGcX5GP.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
  130. package/web/_astro/{requirementDiagram-4Y6WPE33.BV2y4dd6.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +3 -3
  131. package/web/_astro/{sankeyDiagram-5OEKKPKP.Cqo15Tvo.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +4 -4
  132. package/web/_astro/{sequenceDiagram-3UESZ5HK.CROCPMJB.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
  133. package/web/_astro/{stateDiagram-AJRCARHV.RfXZrkFE.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
  134. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CPXmbBs9.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
  135. package/web/_astro/{timeline-definition-PNZ67QCA.DdgKTiO8.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +3 -3
  136. package/web/_astro/{vennDiagram-CIIHVFJN.CPNVSHF1.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +5 -5
  137. package/web/_astro/{wardleyDiagram-YWT4CUSO.CQhA0Jyr.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +3 -3
  138. package/web/_astro/{xychartDiagram-2RQKCTM6.n61BWyy4.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
  139. package/web/board-runtime.json +69 -0
  140. package/web/index.html +2 -2
  141. package/web/_astro/BoardApp.eoTz0pZs.js +0 -1
  142. package/web/_astro/TaskDetail.DCqiC-OZ.js +0 -1
  143. package/web/_astro/arc.CPwg6Rw0.js +0 -1
  144. package/web/_astro/architectureDiagram-3BPJPVTR.DM_vp_hO.js +0 -36
  145. package/web/_astro/channel.MYZLKNwy.js +0 -1
  146. package/web/_astro/client.yhYJvxCU.js +0 -9
  147. package/web/_astro/cose-bilkent-S5V4N54A.B1wSL-Xb.js +0 -1
  148. package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +0 -43
  149. package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +0 -10
  150. package/web/_astro/ganttDiagram-6RSMTGT7.BO81S85v.js +0 -292
  151. package/web/_astro/index.De90oHcH.js +0 -1
  152. package/web/_astro/index.Hjbr15fG.css +0 -1
  153. package/web/_astro/mermaid.core.GAOYeSR0.js +0 -303
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.93",
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,6 +30,8 @@ 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> [--task-file <path>]... [--spur-bin <path>]");
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");
33
35
  console.error(" bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]");
34
36
  process.exit(2);
35
37
  }
@@ -152,6 +154,9 @@ async function printFingerprint(taskFile, featureFile, spurBin) {
152
154
  return 0;
153
155
  }
154
156
  var CLOSE_STATUSES = new Set(["done", "failed", "paused"]);
157
+ function isCloseStatus(status) {
158
+ return CLOSE_STATUSES.has(status);
159
+ }
155
160
  var TERMINAL_REASONS = new Set([
156
161
  "done",
157
162
  "paused-operator",
@@ -164,6 +169,9 @@ var TERMINAL_REASONS = new Set([
164
169
  "retry-exhausted"
165
170
  ]);
166
171
  var ACTION_STATUSES = new Set(["done", "failed"]);
172
+ function isActionStatus(status) {
173
+ return ACTION_STATUSES.has(status);
174
+ }
167
175
  function runRecordLogPath(runDir, runId) {
168
176
  const markdownPath = join(runDir, `${runId}.md`);
169
177
  const legacyLogPath = join(runDir, `${runId}.log`);
@@ -171,7 +179,7 @@ function runRecordLogPath(runDir, runId) {
171
179
  return legacyLogPath;
172
180
  return markdownPath;
173
181
  }
174
- function appendTraceFailureLine(runId, detail) {
182
+ function appendRunLogLine(runId, detail) {
175
183
  try {
176
184
  const runDir = join(process.cwd(), ".spur", "run");
177
185
  if (!existsSync(runDir))
@@ -185,7 +193,7 @@ function appendTraceFailureLine(runId, detail) {
185
193
  async function runTraceMode(input) {
186
194
  const operation = input.close ? "run.close" : "action.finish";
187
195
  const fail = (error) => {
188
- appendTraceFailureLine(input.runId, `trace-emission-failed operation=${operation} run=${input.runId}${input.node === "" ? "" : ` node=${input.node}`}${input.kind === "" ? "" : ` kind=${input.kind}`}: ${error}`);
196
+ appendRunLogLine(input.runId, `trace-emission-failed operation=${operation} run=${input.runId}${input.node === "" ? "" : ` node=${input.node}`}${input.kind === "" ? "" : ` kind=${input.kind}`}: ${error}`);
189
197
  process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}
190
198
  `);
191
199
  return input.close ? 1 : 0;
@@ -197,7 +205,7 @@ async function runTraceMode(input) {
197
205
  projectDb = await app.openInlineRunProjectDb(process.cwd());
198
206
  const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure) => {
199
207
  const detail = failure;
200
- appendTraceFailureLine(input.runId, `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? "unknown error"}`);
208
+ appendRunLogLine(input.runId, `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? "unknown error"}`);
201
209
  });
202
210
  const result = input.close ? await writer.closeRun(input.runId, input.status, undefined, input.reason) : await writer.recordAction({
203
211
  runId: input.runId,
@@ -211,13 +219,20 @@ async function runTraceMode(input) {
211
219
  const failure = result.failure;
212
220
  return fail(failure.error ?? "unknown trace emission failure");
213
221
  }
222
+ if (input.close && input.status === "done" && result.actionRows === 0) {
223
+ const error = `run ${input.runId} closed done with zero action_runs rows`;
224
+ appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${error}`);
225
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error, code: "NO_ACTION_ROWS", actionRows: 0 })}
226
+ `);
227
+ return 1;
228
+ }
214
229
  process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}
215
230
  `);
216
231
  return 0;
217
232
  } catch (error) {
218
233
  if (input.close && error.name === "RunRowNotFoundError") {
219
234
  const message = error instanceof Error ? error.message : String(error);
220
- appendTraceFailureLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
235
+ appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
221
236
  process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: message, code: "RUN_NOT_FOUND" })}
222
237
  `);
223
238
  return 1;
@@ -249,8 +264,9 @@ async function runDecideMode(input) {
249
264
  }
250
265
  if (!outcome.ok)
251
266
  return decideFailed(outcome.error ?? "decide failed without an error message");
252
- 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 })}
253
268
  `);
269
+ appendRunLogLine(input.runId, `decide node=${input.node} value=${outcome.value ?? ""} source=${outcome.source ?? "default"} reason=${outcome.reason ?? ""}`);
254
270
  return await runTraceMode({
255
271
  runId: input.runId,
256
272
  close: false,
@@ -262,6 +278,25 @@ async function runDecideMode(input) {
262
278
  spurBin: input.spurBin
263
279
  });
264
280
  }
281
+ async function runPersistOutMode(input) {
282
+ try {
283
+ const { entry } = resolveAppEntry(input.spurBin);
284
+ const app = await import(entry);
285
+ const result = await app.persistWorktreeRuns({
286
+ fromWorkdir: input.from,
287
+ toWorkdir: process.cwd(),
288
+ ...input.taskFiles.length > 0 ? { taskFiles: input.taskFiles } : {}
289
+ });
290
+ process.stdout.write(`${JSON.stringify({ ok: true, persisted: result.persisted, skipped: result.skipped })}
291
+ `);
292
+ return 0;
293
+ } catch (error) {
294
+ const message = error instanceof Error ? error.message : String(error);
295
+ process.stdout.write(`${JSON.stringify({ ok: false, error: message })}
296
+ `);
297
+ return 1;
298
+ }
299
+ }
265
300
  async function main() {
266
301
  if (!process.versions.bun) {
267
302
  const child = spawnSync("bun", [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
@@ -274,11 +309,13 @@ async function main() {
274
309
  let runId = "";
275
310
  let file = "";
276
311
  let fingerprint = false;
277
- let taskFile = "";
312
+ const taskFiles = [];
278
313
  let featureFile = "";
279
314
  let action = false;
280
315
  let close = false;
281
316
  let decide = false;
317
+ let persistOut = false;
318
+ let from = "";
282
319
  let optionsJson = "";
283
320
  let node = "";
284
321
  let kind = "";
@@ -296,7 +333,7 @@ async function main() {
296
333
  else if (argv[i] === "--fingerprint")
297
334
  fingerprint = true;
298
335
  else if (argv[i] === "--task-file")
299
- taskFile = argv[++i] ?? "";
336
+ taskFiles.push(argv[++i] ?? "");
300
337
  else if (argv[i] === "--feature-file")
301
338
  featureFile = argv[++i] ?? "";
302
339
  else if (argv[i] === "--action")
@@ -305,6 +342,10 @@ async function main() {
305
342
  close = true;
306
343
  else if (argv[i] === "--decide")
307
344
  decide = true;
345
+ else if (argv[i] === "--persist-out")
346
+ persistOut = true;
347
+ else if (argv[i] === "--from")
348
+ from = argv[++i] ?? "";
308
349
  else if (argv[i] === "--options-json")
309
350
  optionsJson = argv[++i] ?? "";
310
351
  else if (argv[i] === "--node")
@@ -323,12 +364,12 @@ async function main() {
323
364
  spurBin = argv[++i] ?? spurBin;
324
365
  }
325
366
  if (fingerprint) {
326
- if (runId !== "" || file !== "" || taskFile.trim() === "")
367
+ if (runId !== "" || file !== "" || taskFiles.length !== 1 || (taskFiles[0] ?? "").trim() === "")
327
368
  usage();
328
- process.exit(await printFingerprint(taskFile, featureFile, spurBin));
369
+ process.exit(await printFingerprint(taskFiles[0] ?? "", featureFile, spurBin));
329
370
  }
330
371
  if (decide) {
331
- if (action || close || fingerprint || file !== "" || taskFile !== "")
372
+ if (action || close || fingerprint || file !== "" || taskFiles.length > 0)
332
373
  usage();
333
374
  if (runId.trim() === "" || node.trim() === "" || optionsJson.trim() === "")
334
375
  usage();
@@ -336,6 +377,13 @@ async function main() {
336
377
  refuseUnsafeRunId(runId);
337
378
  process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
338
379
  }
380
+ if (persistOut) {
381
+ if (fingerprint || decide || action || close || runId !== "" || file !== "")
382
+ usage();
383
+ if (from.trim() === "" || taskFiles.some((taskFile) => taskFile.trim() === ""))
384
+ usage();
385
+ process.exit(await runPersistOutMode({ from, taskFiles, spurBin }));
386
+ }
339
387
  if (action || close) {
340
388
  if (action && close)
341
389
  usage();
@@ -344,7 +392,7 @@ async function main() {
344
392
  if (!SAFE_RUN_ID_RE.test(runId))
345
393
  refuseUnsafeRunId(runId);
346
394
  if (close) {
347
- if (!CLOSE_STATUSES.has(status))
395
+ if (!isCloseStatus(status))
348
396
  usage();
349
397
  if (reason.trim() === "") {
350
398
  if (status === "failed")
@@ -366,7 +414,7 @@ async function main() {
366
414
  }
367
415
  if (node.trim() === "" || kind.trim() === "")
368
416
  usage();
369
- if (!ACTION_STATUSES.has(status))
417
+ if (!isActionStatus(status))
370
418
  usage();
371
419
  if (okRaw !== "true" && okRaw !== "false")
372
420
  usage();