@gobing-ai/spur 0.3.94 → 0.3.96

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 (198) 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 +3 -52
  5. package/config/rules/boundary/sp-script-placement.yaml +19 -0
  6. package/config/rules/strict/runtime-boundaries.yaml +5 -0
  7. package/config/rules/structure/test-location.yaml +2 -0
  8. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
  9. package/config/rules/typescript/output-boundaries.yaml +15 -2
  10. package/config/script-placement-baseline.json +51 -0
  11. package/config/workflows/feature-verification.yaml +7 -4
  12. package/config/workflows/history-anatomy.yaml +23 -11
  13. package/config/workflows/idea-pipeline.yaml +87 -86
  14. package/config/workflows/pr-review.yaml +18 -9
  15. package/config/workflows/task-pipeline.yaml +84 -109
  16. package/config/workflows/wrapup-pipeline.yaml +173 -46
  17. package/package.json +15 -11
  18. package/plugins/sp/README.md +8 -10
  19. package/plugins/sp/agents/super-reviewer.md +43 -18
  20. package/plugins/sp/commands/dev-fixgha.md +83 -0
  21. package/plugins/sp/commands/dev-gitmsg.md +8 -6
  22. package/plugins/sp/commands/dev-gtd.md +2 -2
  23. package/plugins/sp/commands/dev-idea.md +22 -12
  24. package/plugins/sp/commands/dev-plan.md +8 -9
  25. package/plugins/sp/commands/dev-review.md +22 -13
  26. package/plugins/sp/commands/dev-verify.md +7 -4
  27. package/plugins/sp/commands/dev-verifyall.md +5 -4
  28. package/plugins/sp/commands/dev-wrap.md +1 -1
  29. package/plugins/sp/commands/spur-init.md +2 -2
  30. package/plugins/sp/hooks/context-post-tool.ts +2 -2
  31. package/plugins/sp/hooks/context-session-start.ts +2 -1
  32. package/plugins/sp/hooks/context-session-stop.ts +22 -14
  33. package/plugins/sp/hooks/pi/guard-extension.ts +109 -150
  34. package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
  35. package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
  36. package/plugins/sp/lib/idea-handoff.generated.mjs +6 -5
  37. package/plugins/sp/lib/inline-run.generated.d.mts +13 -2
  38. package/plugins/sp/lib/inline-run.generated.mjs +33 -16
  39. package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
  40. package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
  41. package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
  42. package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
  43. package/plugins/sp/lib/spur-bin.ts +36 -0
  44. package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
  45. package/plugins/sp/lib/step-profile.generated.mjs +174 -0
  46. package/plugins/sp/plugin.json +1 -1
  47. package/plugins/sp/references/roles.md +1 -1
  48. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
  49. package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
  50. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
  51. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
  52. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
  53. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
  54. package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
  55. package/plugins/sp/scripts/history-anatomy-cache.mjs +23 -22
  56. package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
  57. package/plugins/sp/scripts/inline-run-setup.mjs +90 -315
  58. package/plugins/sp/scripts/inline-run-setup.ts +109 -639
  59. package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
  60. package/plugins/sp/scripts/pr-reviewing.ts +1 -0
  61. package/plugins/sp/scripts/quality-gate.mjs +38 -21
  62. package/plugins/sp/scripts/quality-gate.ts +20 -660
  63. package/plugins/sp/scripts/residual-scan.mjs +138 -159
  64. package/plugins/sp/scripts/residual-scan.ts +102 -499
  65. package/plugins/sp/scripts/script-root.mjs +129 -0
  66. package/plugins/sp/scripts/script-root.ts +199 -0
  67. package/plugins/sp/scripts/workflow-step-profile.mjs +27 -16
  68. package/plugins/sp/scripts/workflow-step-profile.ts +22 -314
  69. package/plugins/sp/scripts/wrapup-drift-probe.mjs +12 -3
  70. package/plugins/sp/scripts/wrapup-drift-probe.ts +6 -3
  71. package/plugins/sp/scripts/wrapup-steps.mjs +42 -40
  72. package/plugins/sp/scripts/wrapup-steps.ts +64 -46
  73. package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
  74. package/plugins/sp/skills/code-verification/SKILL.md +46 -16
  75. package/plugins/sp/skills/code-verification/references/verdict-schema.md +13 -2
  76. package/plugins/sp/skills/functional-review/SKILL.md +8 -5
  77. package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
  78. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
  79. package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
  80. package/plugins/sp/skills/next-router/references/routing-table.md +2 -2
  81. package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
  82. package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
  83. package/plugins/sp/skills/spur-cli/references/features.md +17 -6
  84. package/plugins/sp/skills/spur-cli/references/init.md +17 -16
  85. package/plugins/sp/skills/spur-cli/references/self.md +3 -2
  86. package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
  87. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +28 -5
  88. package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
  89. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
  90. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
  91. package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
  92. package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
  93. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
  94. package/plugins/sp/skills/spur-dev/references/execution-batch.md +107 -44
  95. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
  96. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
  97. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
  98. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
  99. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
  100. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +35 -6
  101. package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
  102. package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
  103. package/plugins/sp/tsconfig.json +8 -0
  104. package/spur.js +6638 -4775
  105. package/web/_astro/BoardApp.BIjMatT1.js +1 -0
  106. package/web/_astro/{BoardApp.FTEs3-N8.js → BoardApp.GvjIe9Z6.js} +150 -153
  107. package/web/_astro/TaskDetail.BZM3EAFx.js +1 -0
  108. package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
  109. package/web/_astro/{arc.uuAf51IT.js → arc.HkRiZnoI.js} +1 -1
  110. package/web/_astro/architectureDiagram-3BPJPVTR.BDdK-tgZ.js +36 -0
  111. package/web/_astro/{blockDiagram-GPEHLZMM.D1mGCq3p.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
  112. package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
  113. package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
  114. package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
  115. package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
  116. package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
  117. package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
  118. package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
  119. package/web/_astro/{c4Diagram-AAUBKEIU.CMsolcde.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
  120. package/web/_astro/channel.DlhX2MEt.js +1 -0
  121. package/web/_astro/{chunk-2J33WTMH.CrGA3fik.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
  122. package/web/_astro/{chunk-4BX2VUAB.DsLVVla0.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
  123. package/web/_astro/{chunk-55IACEB6.Dxc59Tfi.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
  124. package/web/_astro/{chunk-727SXJPM.CaEVE2Wy.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
  125. package/web/_astro/{chunk-AQP2D5EJ.BiJ4HXeI.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
  126. package/web/_astro/{chunk-FMBD7UC4.Mxf1fru5.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
  127. package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
  128. package/web/_astro/{chunk-ND2GUHAM.pyOWQixH.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
  129. package/web/_astro/{chunk-QZHKN3VN.BmEsg4vr.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
  130. package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
  131. package/web/_astro/{classDiagram-4FO5ZUOK.BHhhMFTO.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
  132. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BHhhMFTO.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
  133. package/web/_astro/client.BhluLQqe.js +1 -0
  134. package/web/_astro/client.DBpSRLnx.js +9 -0
  135. package/web/_astro/cose-bilkent-S5V4N54A.BraxQ2Nt.js +1 -0
  136. package/web/_astro/{cynefin-OW5HDTMX.C2j1_lKL.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
  137. package/web/_astro/{dagre-BM42HDAG.hZ2NCTdT.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
  138. package/web/_astro/{diagram-2AECGRRQ.Bfw5_EzK.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
  139. package/web/_astro/{diagram-5GNKFQAL.Bt1V_Tmk.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
  140. package/web/_astro/{diagram-KO2AKTUF.DEk-YFwp.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
  141. package/web/_astro/{diagram-LMA3HP47.CDjndtQm.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
  142. package/web/_astro/{diagram-OG6HWLK6.cFnUHScG.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
  143. package/web/_astro/{erDiagram-TEJ5UH35.DlhYp7NV.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
  144. package/web/_astro/{flowDiagram-I6XJVG4X.DNTpsxfx.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
  145. package/web/_astro/ganttDiagram-6RSMTGT7.Dmdxgd-M.js +292 -0
  146. package/web/_astro/{gitGraphDiagram-PVQCEYII.DkEqNI0P.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
  147. package/web/_astro/index.5dn-Zm-2.js +1 -0
  148. package/web/_astro/index.C-IqPpW2.js +1 -0
  149. package/web/_astro/index.Cyc_GA4f.js +1 -0
  150. package/web/_astro/index.EoZlzLK-.css +1 -0
  151. package/web/_astro/index.tZfqwSut.js +1 -0
  152. package/web/_astro/{infoDiagram-5YYISTIA.CrjioCTG.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
  153. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BcK0CN8n.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
  154. package/web/_astro/{journeyDiagram-JHISSGLW.p0CbBDX1.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
  155. package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
  156. package/web/_astro/{kanban-definition-UN3LZRKU.puHIFt6J.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
  157. package/web/_astro/{linear.Bb-3a1d7.js → linear.BnzMBgo_.js} +1 -1
  158. package/web/_astro/{mermaid.core.CJDgXOJs.js → mermaid.core.DSqeFu2Y.js} +4 -4
  159. package/web/_astro/{mindmap-definition-RKZ34NQL.BoAZEI9t.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
  160. package/web/_astro/{pieDiagram-4H26LBE5.CJNSdqY7.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
  161. package/web/_astro/{quadrantDiagram-W4KKPZXB.DtTy7_0Z.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
  162. package/web/_astro/{requirementDiagram-4Y6WPE33.BNYV98Xg.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
  163. package/web/_astro/{sankeyDiagram-5OEKKPKP.DRq9DGHp.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
  164. package/web/_astro/{sequenceDiagram-3UESZ5HK.B7KdBFCm.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
  165. package/web/_astro/{stateDiagram-AJRCARHV.CD62ZJ2H.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
  166. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CeVk1TAj.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
  167. package/web/_astro/{timeline-definition-PNZ67QCA.C_ZPA5tT.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
  168. package/web/_astro/{vennDiagram-CIIHVFJN.CrThO_FQ.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
  169. package/web/_astro/{wardleyDiagram-YWT4CUSO.DSZCA5nl.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
  170. package/web/_astro/{xychartDiagram-2RQKCTM6.KI7baTuk.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
  171. package/web/board-runtime.json +69 -0
  172. package/web/index.html +2 -2
  173. package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
  174. package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
  175. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -298
  176. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -479
  177. package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
  178. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
  179. package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
  180. package/plugins/sp/scripts/record-feature-sync.ts +0 -84
  181. package/plugins/sp/scripts/script-contract-check.ts +0 -377
  182. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  183. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
  184. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -187
  185. package/plugins/sp/scripts/task-size-precheck.ts +0 -210
  186. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  187. package/plugins/sp/scripts/validate-commands.ts +0 -689
  188. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
  189. package/plugins/sp/scripts/verify-answer-lint.ts +0 -547
  190. package/web/_astro/BoardApp.C02hAHPO.js +0 -1
  191. package/web/_astro/TaskDetail.C-GdsS-t.js +0 -1
  192. package/web/_astro/architectureDiagram-3BPJPVTR.CGe629A1.js +0 -36
  193. package/web/_astro/channel.fsgl7o5j.js +0 -1
  194. package/web/_astro/client.yhYJvxCU.js +0 -9
  195. package/web/_astro/cose-bilkent-S5V4N54A.2fH4YOlp.js +0 -1
  196. package/web/_astro/ganttDiagram-6RSMTGT7.DC_p36PI.js +0 -292
  197. package/web/_astro/index.De90oHcH.js +0 -1
  198. package/web/_astro/index.Hjbr15fG.css +0 -1
@@ -0,0 +1,210 @@
1
+ // packages/app/src/services/residual-scan.ts
2
+ import { createHash } from "node:crypto";
3
+ var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
4
+ var PRIORITY_PATTERN = /^P[1-4]/;
5
+ var NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|—)\s*(\(.*\))?\.?$/i;
6
+ var DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
7
+ var RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
8
+ var DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
9
+ var ANCHOR_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
10
+ var RANGE_ANCHOR = /([A-Za-z0-9_./-]+\.[A-Za-z]+):([0-9]+)-[0-9]+/g;
11
+ var EXCLUDED_PATHS = ["docs/tasks", "docs/features/", ".spur/"];
12
+ var ALLOW_PRAGMA = "residual-scan:allow";
13
+ function makeItemId(category, location, text) {
14
+ const normalized = text.trim().replace(/\s+/g, " ");
15
+ const hex = createHash("sha256").update(`${location}${normalized}`).digest("hex");
16
+ return `${category}:${hex.slice(0, 8)}`;
17
+ }
18
+ function normalizeAnchor(location) {
19
+ return location.replace(RANGE_ANCHOR, "$1:$2");
20
+ }
21
+ function locationOf(locationCell, finding) {
22
+ const cell = locationCell.trim().replace(/`/g, "");
23
+ if (cell.length > 0 && cell !== "—")
24
+ return normalizeAnchor(cell);
25
+ const backtick = finding.match(/`([^`]+)`/)?.[1];
26
+ return backtick === undefined ? "" : normalizeAnchor(backtick);
27
+ }
28
+ function parseReviewFindings(taskContent) {
29
+ const section = taskContent.split(/^### Review\b/m)[1];
30
+ if (section === undefined)
31
+ return [];
32
+ const body = section.split(/^### /m)[0] ?? "";
33
+ const out = [];
34
+ const lines = body.split(`
35
+ `);
36
+ for (let i = 0;i < lines.length; i++) {
37
+ const line = lines[i];
38
+ if (line === undefined || !line.trimStart().startsWith("|"))
39
+ continue;
40
+ const header = splitRow(line);
41
+ const priorityCol = header.findIndex((h) => h.trim() === "Priority");
42
+ if (priorityCol === -1) {
43
+ while (i + 1 < lines.length && lines[i + 1]?.trimStart().startsWith("|"))
44
+ i++;
45
+ continue;
46
+ }
47
+ const findingCol = header.findIndex((h) => h.trim() === "Finding");
48
+ const locationCol = header.findIndex((h) => h.trim() === "Location");
49
+ const dispositionCol = header.findIndex((h) => DISPOSITION_HEADER.test(h.trim()));
50
+ i++;
51
+ const sep = lines[i];
52
+ if (sep !== undefined && /^\s*\|[\s:|-]+\|\s*$/.test(sep))
53
+ i++;
54
+ while (i < lines.length) {
55
+ const row = lines[i];
56
+ if (row === undefined || !row.trimStart().startsWith("|"))
57
+ break;
58
+ const cells = splitRow(row);
59
+ const priority = (cells[priorityCol] ?? "").trim();
60
+ const finding = (cells[findingCol] ?? "").trim();
61
+ const disposition = dispositionCol === -1 ? "" : (cells[dispositionCol] ?? "").trim();
62
+ if (PRIORITY_PATTERN.test(priority) && !NONE_FINDING.test(finding) && finding.length > 0 && !RESOLVED_DISPOSITION.test(disposition)) {
63
+ const location = locationOf(cells[locationCol] ?? "", finding);
64
+ out.push(DEFERRED_DISPOSITION.test(disposition) ? { priority, location, text: finding, deferral: disposition } : { priority, location, text: finding });
65
+ }
66
+ i++;
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ function splitRow(line) {
72
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
73
+ }
74
+ function parseDiffMarkers(addedLines) {
75
+ return addedLines.filter((l) => !EXCLUDED_PATHS.some((p) => l.file.startsWith(p))).filter((l) => !l.text.includes(ALLOW_PRAGMA)).filter((l) => MARKER_PATTERN.test(l.text)).map((l) => ({ location: `${l.file}:${l.line}`, text: l.text.trim() }));
76
+ }
77
+ function findUncheckedBoxes(taskContent) {
78
+ const path = "task-file";
79
+ return taskContent.split(`
80
+ `).map((text, idx) => ({ text: text.trim(), line: idx + 1 })).filter((l) => l.text.startsWith("- [ ]")).map((l) => ({ location: `${path}:${l.line}`, text: l.text }));
81
+ }
82
+ function classify(items, deferrals) {
83
+ const deferred = new Map(deferrals.map((d) => [d.id, d.reason]));
84
+ return items.map((item) => {
85
+ const id = makeItemId(item.category, item.location, item.text);
86
+ let klass;
87
+ if (item.category === "review-finding")
88
+ klass = item.priority?.startsWith("P4") ? "advisory" : "blocking";
89
+ else if (item.category === "staging-residue")
90
+ klass = "housekeeping";
91
+ else
92
+ klass = "blocking";
93
+ if (klass === "blocking" && item.category !== "unchecked-box") {
94
+ const p3Like = item.category === "diff-marker" || item.category === "review-finding" && (item.priority ?? "").startsWith("P3");
95
+ const reason = deferred.get(id);
96
+ if (p3Like && reason !== undefined && reason.trim().length > 0)
97
+ klass = "deferrable";
98
+ }
99
+ return {
100
+ id,
101
+ category: item.category,
102
+ class: klass,
103
+ priority: item.priority,
104
+ location: item.location,
105
+ text: item.text
106
+ };
107
+ });
108
+ }
109
+ function scanResiduals(inputs) {
110
+ const { wbs, base, taskContent, addedLines, stagingResidue, deferrals } = inputs;
111
+ const reviewRows = parseReviewFindings(taskContent);
112
+ const tableDeferrals = reviewRows.flatMap((r) => r.deferral === undefined ? [] : [{ id: makeItemId("review-finding", r.location, r.text), reason: r.deferral }]);
113
+ const review = reviewRows.map((r) => ({
114
+ category: "review-finding",
115
+ priority: r.priority,
116
+ location: r.location,
117
+ text: r.text
118
+ }));
119
+ const markers = base === null ? [] : parseDiffMarkers(addedLines).map((m) => ({
120
+ category: "diff-marker",
121
+ location: m.location,
122
+ text: m.text
123
+ }));
124
+ const boxes = findUncheckedBoxes(taskContent).map((b) => ({
125
+ category: "unchecked-box",
126
+ location: b.location,
127
+ text: b.text
128
+ }));
129
+ const residue = stagingResidue.map((p) => ({
130
+ category: "staging-residue",
131
+ location: p,
132
+ text: p
133
+ }));
134
+ const items = classify([...review, ...markers, ...boxes, ...residue], [...tableDeferrals, ...deferrals]);
135
+ const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
136
+ for (const item of items)
137
+ counts[item.class]++;
138
+ return {
139
+ wbs,
140
+ base,
141
+ scanned: {
142
+ "review-finding": true,
143
+ "diff-marker": base !== null,
144
+ "unchecked-box": true,
145
+ "staging-residue": true
146
+ },
147
+ items,
148
+ counts
149
+ };
150
+ }
151
+ function blockingAnchors(items) {
152
+ const anchors = new Set;
153
+ for (const item of items) {
154
+ if (item.class !== "blocking")
155
+ continue;
156
+ for (const m of normalizeAnchor(item.location).matchAll(ANCHOR_PATTERN))
157
+ anchors.add(m[0]);
158
+ }
159
+ return [...anchors];
160
+ }
161
+ function foldVerdict(verdict, scan, existingFindings, maxFindings = 20) {
162
+ const blocking = scan.items.filter((i) => i.class === "blocking");
163
+ const deferrable = scan.items.filter((i) => i.class === "deferrable");
164
+ const evidence = `blocking=${blocking.length} deferrable=${deferrable.length} advisory=${scan.counts.advisory} housekeeping=${scan.counts.housekeeping}` + (blocking.length > 0 ? `; blocking ids: ${blocking.map((i) => i.id).join(", ")}` : "") + (deferrable.length > 0 ? `; deferrable ids: ${deferrable.map((i) => i.id).join(", ")}` : "");
165
+ const checks = verdict.checks.filter((c) => c.name !== "residual-sweep");
166
+ checks.push({ name: "residual-sweep", status: blocking.length > 0 ? "fail" : "pass", evidence });
167
+ const merged = new Set([
168
+ ...existingFindings.split(/\s+/).filter((a) => a.length > 0),
169
+ ...blockingAnchors(scan.items)
170
+ ]);
171
+ const findings = [...merged].sort().slice(0, maxFindings).map((a) => `${a} `).join("");
172
+ let verdictStatus = verdict.verdict === "PASS" ? "PASS" : "FAIL";
173
+ if (verdict.verdict === "PARTIAL")
174
+ verdictStatus = "PARTIAL";
175
+ else if (verdict.verdict === "FAIL")
176
+ verdictStatus = "FAIL";
177
+ else if (blocking.length > 0)
178
+ verdictStatus = "PARTIAL";
179
+ return { verdict: verdictStatus, checks, findings };
180
+ }
181
+ function renderReport(wbs, items, attemptCount) {
182
+ const lines = [
183
+ `# Residual report — ${wbs}`,
184
+ "",
185
+ `Attempt: ${attemptCount}`,
186
+ "",
187
+ "| Category | Class | Location | Text |",
188
+ "| --- | --- | --- | --- |"
189
+ ];
190
+ for (const item of items) {
191
+ lines.push(`| ${item.category} | ${item.class} | ${item.location} | ${item.text.replace(/\|/g, "\\|")} |`);
192
+ }
193
+ return `${lines.join(`
194
+ `)}
195
+ `;
196
+ }
197
+ export {
198
+ scanResiduals,
199
+ renderReport,
200
+ parseReviewFindings,
201
+ parseDiffMarkers,
202
+ normalizeAnchor,
203
+ makeItemId,
204
+ locationOf,
205
+ foldVerdict,
206
+ findUncheckedBoxes,
207
+ classify,
208
+ blockingAnchors,
209
+ ALLOW_PRAGMA
210
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared spur-invocation helper for the sp plugin scripts (task 1007 R9, RF-architect-001).
3
+ *
4
+ * `spurCommand` and `defaultSpurBin` moved verbatim from `wrapup-steps.ts` and
5
+ * `workflow-step-profile.ts` so every surviving plugin script imports one copy (imported by
6
+ * relative path like `lib/env.ts`, so the standalone contract holds; not an app bundle
7
+ * because it carries no Spur-domain logic). Node/bun builtins and the vendored `./env`
8
+ * gateway only — bundled standalone by `superskill install`, so no `@gobing-ai/*` value
9
+ * imports (task 0669).
10
+ */
11
+
12
+ import { existsSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { getEnvVar } from './env';
15
+
16
+ /** `spurBin` splits on whitespace into a command plus prefix args (so `bun x.ts` works). */
17
+ export function spurCommand(spurBin: string | undefined): { cmd: string; prefix: string[] } {
18
+ const parts = (spurBin ?? 'spur')
19
+ .trim()
20
+ .split(/\s+/)
21
+ .filter((p) => p.length > 0);
22
+ return { cmd: parts[0] ?? 'spur', prefix: parts.slice(1) };
23
+ }
24
+
25
+ /**
26
+ * Resolve the spur CLI monorepo-safely (twin-safe resolution:
27
+ * self-contained and no plugin script imports another):
28
+ * --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
29
+ */
30
+ export function defaultSpurBin(): string {
31
+ const fromEnv = getEnvVar('SPUR_BIN');
32
+ if (fromEnv) return fromEnv;
33
+ const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
34
+ if (existsSync(local)) return `bun ${local}`;
35
+ return 'spur';
36
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * GENERATED by scripts/commands/bundle-plugin-lib.ts — do not edit.
3
+ * Source of truth: packages/app/src/workflow/step-profile.ts (task 1005 R1).
4
+ */
5
+ export declare interface ActionCostLike {
6
+ cacheHit: number | null;
7
+ }
8
+ export declare interface ActionCostAttributionLike {
9
+ exact: ActionCostLike | null;
10
+ estimated: ActionCostLike | null;
11
+ }
12
+ export declare interface TraceActionEvent {
13
+ kind: string;
14
+ node?: string;
15
+ actionKind?: string;
16
+ durationMs?: number | null;
17
+ startedAt?: string | null;
18
+ completedAt?: string | null;
19
+ invocation?: Record<string, string | number | boolean> | null;
20
+ cost?: ActionCostAttributionLike | null;
21
+ }
22
+ export declare interface StepProfileRun {
23
+ runId: string;
24
+ events: readonly TraceActionEvent[];
25
+ }
26
+ export declare interface TraceRunEntry {
27
+ runId: string;
28
+ isDryRun?: boolean;
29
+ status?: string;
30
+ workflowName?: string;
31
+ }
32
+ export declare type SessionMode = 'fresh' | 'resumed' | 'mixed';
33
+ export declare interface StepProfileExecution {
34
+ runId: string;
35
+ node: string;
36
+ actionKind: string;
37
+ durationMs: number | null;
38
+ idleGapMs: number | null;
39
+ session: 'fresh' | 'resumed' | null;
40
+ cacheHit: number | null;
41
+ }
42
+ export declare interface StepProfileRow {
43
+ node: string;
44
+ actionKind: string;
45
+ runs: number;
46
+ executions: number;
47
+ durationMs: { p50: number | null; max: number | null };
48
+ idleGapMs: { p50: number | null };
49
+ session: SessionMode | null;
50
+ cacheHit: { p50: number | null; known: number; of: number };
51
+ flags: string[];
52
+ }
53
+ export declare interface StepProfile {
54
+ workflow: string;
55
+ windowSec: number;
56
+ sampledRuns: number;
57
+ rows: StepProfileRow[];
58
+ }
59
+ export declare const DEFAULT_LAST: number;
60
+ export declare const DEFAULT_WINDOW_SEC: number;
61
+ export declare function nearestRankP50(values: readonly number[]): number | null;
62
+ export declare function extractExecutions(runId: string, events: readonly TraceActionEvent[]): StepProfileExecution[];
63
+ export declare function rowFlags(row: StepProfileRow, windowSec: number): string[];
64
+ export declare function buildRows(executions: readonly StepProfileExecution[], windowSec: number): StepProfileRow[];
65
+ export declare function buildStepProfile(input: {
66
+ workflow: string;
67
+ windowSec: number;
68
+ runs: readonly StepProfileRun[];
69
+ }): StepProfile;
70
+ export declare function nonDryRuns(entries: readonly TraceRunEntry[]): TraceRunEntry[];
71
+ export declare function formatStepProfileHuman(profile: StepProfile): string;
@@ -0,0 +1,174 @@
1
+ // packages/app/src/workflow/step-profile.ts
2
+ var DEFAULT_LAST = 20;
3
+ var DEFAULT_WINDOW_SEC = 300;
4
+ function nearestRankP50(values) {
5
+ if (values.length === 0)
6
+ return null;
7
+ const sorted = [...values].sort((a, b) => a - b);
8
+ return sorted[Math.ceil(sorted.length / 2) - 1] ?? null;
9
+ }
10
+ function numeric(value) {
11
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
12
+ }
13
+ function msBetween(from, to) {
14
+ if (typeof from !== "string" || typeof to !== "string")
15
+ return null;
16
+ const start = Date.parse(from);
17
+ const end = Date.parse(to);
18
+ if (!Number.isFinite(start) || !Number.isFinite(end))
19
+ return null;
20
+ return end - start;
21
+ }
22
+ function sessionOf(event) {
23
+ const value = event.invocation?.continue;
24
+ if (value === true)
25
+ return "resumed";
26
+ if (value === false)
27
+ return "fresh";
28
+ return null;
29
+ }
30
+ function cacheHitOf(event) {
31
+ return numeric(event.cost?.exact?.cacheHit);
32
+ }
33
+ function startKey(event) {
34
+ const parsed = typeof event.startedAt === "string" ? Date.parse(event.startedAt) : Number.NaN;
35
+ return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
36
+ }
37
+ function extractExecutions(runId, events) {
38
+ const actions = events.filter((e) => e.kind === "action").sort((a, b) => {
39
+ const left = startKey(a);
40
+ const right = startKey(b);
41
+ return left === right ? 0 : left - right;
42
+ });
43
+ const executions = [];
44
+ for (const [index, event] of actions.entries()) {
45
+ const previous = index === 0 ? undefined : actions[index - 1];
46
+ executions.push({
47
+ runId,
48
+ node: event.node ?? "",
49
+ actionKind: event.actionKind ?? "",
50
+ durationMs: numeric(event.durationMs),
51
+ idleGapMs: msBetween(previous?.completedAt, event.startedAt),
52
+ session: sessionOf(event),
53
+ cacheHit: cacheHitOf(event)
54
+ });
55
+ }
56
+ return executions;
57
+ }
58
+ function foldSession(executions) {
59
+ const known = new Set(executions.map((e) => e.session).filter((s) => s !== null));
60
+ if (known.size === 0)
61
+ return null;
62
+ if (known.size === 1)
63
+ return [...known][0] ?? null;
64
+ return "mixed";
65
+ }
66
+ function rowFlags(row, windowSec) {
67
+ const wMs = windowSec * 1000;
68
+ const flags = [];
69
+ const agentRun = row.actionKind === "agent.run";
70
+ const resumed = row.session === "resumed" || row.session === "mixed";
71
+ const p50 = row.durationMs.p50;
72
+ if (!agentRun && p50 !== null && p50 > wMs)
73
+ flags.push("step-over-window");
74
+ const idle = row.idleGapMs.p50;
75
+ if (agentRun && resumed && idle !== null && idle > wMs)
76
+ flags.push("resume-after-idle");
77
+ const hit = row.cacheHit.p50;
78
+ if (agentRun && resumed && row.cacheHit.known > 0 && hit !== null && hit < 0.5)
79
+ flags.push("resume-cold-cache");
80
+ if (agentRun && p50 !== null && p50 > 2 * wMs)
81
+ flags.push("agent-run-over-2w");
82
+ return flags;
83
+ }
84
+ function buildRows(executions, windowSec) {
85
+ const grouped = new Map;
86
+ for (const execution of executions) {
87
+ const key = `${execution.node}\x00${execution.actionKind}`;
88
+ const bucket = grouped.get(key);
89
+ if (bucket === undefined)
90
+ grouped.set(key, [execution]);
91
+ else
92
+ bucket.push(execution);
93
+ }
94
+ const rows = [];
95
+ for (const bucket of grouped.values()) {
96
+ const first = bucket[0];
97
+ if (first === undefined)
98
+ continue;
99
+ const durations = bucket.map((e) => e.durationMs).filter((d) => d !== null);
100
+ const gaps = bucket.map((e) => e.idleGapMs).filter((g) => g !== null);
101
+ const hits = bucket.map((e) => e.cacheHit).filter((h) => h !== null);
102
+ const row = {
103
+ node: first.node,
104
+ actionKind: first.actionKind,
105
+ runs: new Set(bucket.map((e) => e.runId)).size,
106
+ executions: bucket.length,
107
+ durationMs: {
108
+ p50: nearestRankP50(durations),
109
+ max: durations.length === 0 ? null : Math.max(...durations)
110
+ },
111
+ idleGapMs: { p50: nearestRankP50(gaps) },
112
+ session: first.actionKind === "agent.run" ? foldSession(bucket) : null,
113
+ cacheHit: {
114
+ p50: nearestRankP50(hits),
115
+ known: hits.length,
116
+ of: bucket.length
117
+ },
118
+ flags: []
119
+ };
120
+ row.flags = rowFlags(row, windowSec);
121
+ rows.push(row);
122
+ }
123
+ return rows.sort((a, b) => a.node === b.node ? a.actionKind.localeCompare(b.actionKind) : a.node.localeCompare(b.node));
124
+ }
125
+ function buildStepProfile(input) {
126
+ const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
127
+ return {
128
+ workflow: input.workflow,
129
+ windowSec: input.windowSec,
130
+ sampledRuns: input.runs.length,
131
+ rows: buildRows(executions, input.windowSec)
132
+ };
133
+ }
134
+ function nonDryRuns(entries) {
135
+ return entries.filter((e) => e.isDryRun !== true && typeof e.runId === "string" && e.runId.length > 0);
136
+ }
137
+ function seconds(value) {
138
+ return value === null ? "?" : `${(value / 1000).toFixed(1)}s`;
139
+ }
140
+ function ratio(value) {
141
+ return value === null ? "?" : value.toFixed(2);
142
+ }
143
+ function formatStepProfileHuman(profile) {
144
+ const lines = [
145
+ `step profile — ${profile.workflow} (window ${profile.windowSec}s, ${profile.sampledRuns} sampled runs)`
146
+ ];
147
+ for (const row of profile.rows) {
148
+ lines.push([
149
+ row.node.padEnd(20),
150
+ row.actionKind.padEnd(14),
151
+ `${row.runs}/${row.executions}`.padEnd(8),
152
+ `p50=${seconds(row.durationMs.p50)}`.padEnd(12),
153
+ `max=${seconds(row.durationMs.max)}`.padEnd(12),
154
+ `idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
155
+ `session=${row.session ?? "?"}`.padEnd(15),
156
+ `cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
157
+ `flags=${row.flags.length === 0 ? "-" : row.flags.join(",")}`
158
+ ].join(" "));
159
+ }
160
+ return `${lines.join(`
161
+ `)}
162
+ `;
163
+ }
164
+ export {
165
+ rowFlags,
166
+ nonDryRuns,
167
+ nearestRankP50,
168
+ formatStepProfileHuman,
169
+ extractExecutions,
170
+ buildStepProfile,
171
+ buildRows,
172
+ DEFAULT_WINDOW_SEC,
173
+ DEFAULT_LAST
174
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.94",
3
+ "version": "0.3.96",
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"]
@@ -54,7 +54,7 @@ roles:
54
54
  stages: [changelog]
55
55
  - id: coder
56
56
  tier: standard
57
- commands: [dev-run, dev-unit, dev-debug, dev-simplify, dev-fixall, dev-reverse, dev-wrap, dev-wrapall, dev-gtd]
57
+ commands: [dev-run, dev-unit, dev-debug, dev-simplify, dev-fixall, dev-fixgha, dev-reverse, dev-wrap, dev-wrapall, dev-gtd]
58
58
  stages: [implement, test, wrap]
59
59
  - id: reviewer
60
60
  tier: capable-1
@@ -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;