@gobing-ai/spur 0.4.8 → 0.4.10

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 (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -2
  3. package/config/rules/README.md +1 -1
  4. package/config/rules/boundary/config-loading-ownership.yaml +1 -0
  5. package/config/rules/boundary/env-var-hygiene.yaml +0 -1
  6. package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
  7. package/config/rules/boundary/sp-script-placement.yaml +3 -3
  8. package/config/rules/docs/sourced-api-claims.yaml +29 -64
  9. package/config/rules/strict/runtime-boundaries.yaml +40 -1
  10. package/config/rules/structure/protected-files.yaml +1 -1
  11. package/config/rules/surface/check-cli-surface.yaml +33 -0
  12. package/config/rules/typescript/no-debugger.yaml +3 -0
  13. package/config/workflow-candidates.json +2 -2
  14. package/config/workflows/feature-verification.yaml +6 -19
  15. package/config/workflows/history-anatomy.yaml +14 -28
  16. package/config/workflows/idea-pipeline.yaml +6 -19
  17. package/config/workflows/pr-review.yaml +9 -16
  18. package/config/workflows/task-pipeline.yaml +18 -25
  19. package/config/workflows/wrapup-pipeline.yaml +21 -63
  20. package/package.json +3 -3
  21. package/plugins/sp/README.md +70 -37
  22. package/plugins/sp/agents/expert-spur.md +0 -1
  23. package/plugins/sp/agents/super-planner.md +5 -5
  24. package/plugins/sp/agents/super-reviewer.md +2 -2
  25. package/plugins/sp/commands/dev-changelog.md +1 -1
  26. package/plugins/sp/commands/dev-daily.md +1 -1
  27. package/plugins/sp/commands/dev-pr-review.md +1 -1
  28. package/plugins/sp/commands/dev-review-session.md +1 -1
  29. package/plugins/sp/commands/dev-runall.md +4 -4
  30. package/plugins/sp/commands/dev-verify.md +4 -6
  31. package/plugins/sp/commands/dev-verifyall.md +4 -5
  32. package/plugins/sp/hooks/hooks.json +11 -1
  33. package/plugins/sp/lib/context-post-tool.ts +1 -1
  34. package/plugins/sp/lib/context-session-start.ts +1 -1
  35. package/plugins/sp/lib/context-session-stop.ts +1 -1
  36. package/plugins/sp/lib/destructive-policy.ts +65 -1
  37. package/plugins/sp/plugin.json +2 -5
  38. package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
  39. package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
  40. package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
  41. package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
  42. package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
  43. package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
  44. package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
  45. package/plugins/sp/skills/brainstorm/SKILL.md +12 -11
  46. package/plugins/sp/skills/brainstorm/agents/openai.yaml +1 -1
  47. package/plugins/sp/skills/brainstorm/examples/ideation-example.md +0 -4
  48. package/plugins/sp/skills/brainstorm/metadata.openclaw +1 -1
  49. package/plugins/sp/skills/brainstorm/references/workflows.md +4 -14
  50. package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
  51. package/plugins/sp/skills/code-verification/SKILL.md +8 -8
  52. package/plugins/sp/skills/code-verification/references/secu-review.md +4 -4
  53. package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
  54. package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
  55. package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
  56. package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
  57. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
  58. package/plugins/sp/skills/history-anatomy/references/operations.md +3 -4
  59. package/plugins/sp/skills/history-anatomy/references/report-contract.md +2 -3
  60. package/plugins/sp/skills/issue-finding/SKILL.md +1 -1
  61. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  62. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  63. package/plugins/sp/skills/parallel-execution/references/result-synthesis.md +9 -10
  64. package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
  65. package/plugins/sp/skills/reverse-engineering/SKILL.md +4 -6
  66. package/plugins/sp/skills/session-review/SKILL.md +13 -29
  67. package/plugins/sp/skills/source-driven-development/SKILL.md +26 -27
  68. package/plugins/sp/skills/spur-check/SKILL.md +14 -13
  69. package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
  70. package/plugins/sp/skills/spur-cli/references/agent.md +17 -2
  71. package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
  72. package/plugins/sp/skills/spur-cli/references/features.md +6 -1
  73. package/plugins/sp/skills/spur-cli/references/history.md +3 -0
  74. package/plugins/sp/skills/spur-cli/references/self.md +5 -3
  75. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
  76. package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
  77. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +1 -1
  78. package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
  79. package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
  80. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
  81. package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -13
  82. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
  83. package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
  84. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
  85. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +9 -12
  86. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
  87. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
  88. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
  89. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
  90. package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
  91. package/spur.js +12726 -1456
  92. package/web/_astro/{BoardApp.CaNvQBD_.js → BoardApp.B5rTVNd_.js} +1 -1
  93. package/web/_astro/{BoardApp.C5jnkkKP.js → BoardApp.BMprmPRc.js} +141 -141
  94. package/web/_astro/{TaskDetail.DGKndhI8.js → TaskDetail.Ddsur8qd.js} +1 -1
  95. package/web/_astro/{arc.DHrwdg3G.js → arc.DLqVVBxP.js} +1 -1
  96. package/web/_astro/{architectureDiagram-3BPJPVTR.pvKpK1s5.js → architectureDiagram-3BPJPVTR.C-Pk3uAi.js} +1 -1
  97. package/web/_astro/{blockDiagram-GPEHLZMM.BQmRcZsS.js → blockDiagram-GPEHLZMM.BRO1i_Nm.js} +1 -1
  98. package/web/_astro/{c4Diagram-AAUBKEIU.CCLjHeH1.js → c4Diagram-AAUBKEIU.BU_cWu6e.js} +1 -1
  99. package/web/_astro/channel.Bs3Illhy.js +1 -0
  100. package/web/_astro/{chunk-2J33WTMH.DL6Yuoma.js → chunk-2J33WTMH.D2VXiQ2t.js} +1 -1
  101. package/web/_astro/{chunk-4BX2VUAB.CzlyEI7l.js → chunk-4BX2VUAB.BcHT772J.js} +1 -1
  102. package/web/_astro/{chunk-55IACEB6.B60EWxp1.js → chunk-55IACEB6.DhDtICmo.js} +1 -1
  103. package/web/_astro/{chunk-727SXJPM.c7v_fRLo.js → chunk-727SXJPM.BoKHp7gV.js} +1 -1
  104. package/web/_astro/{chunk-AQP2D5EJ.8BxLWdDc.js → chunk-AQP2D5EJ.Chu4JRpE.js} +1 -1
  105. package/web/_astro/{chunk-FMBD7UC4.DmKuxGFT.js → chunk-FMBD7UC4.wHdwKPlq.js} +1 -1
  106. package/web/_astro/{chunk-ND2GUHAM.1Zwd4mqV.js → chunk-ND2GUHAM.ffN-M1JB.js} +1 -1
  107. package/web/_astro/{chunk-QZHKN3VN.BvYLu-A1.js → chunk-QZHKN3VN.FRubwg4S.js} +1 -1
  108. package/web/_astro/{classDiagram-4FO5ZUOK.CEqRt0WU.js → classDiagram-4FO5ZUOK.c22tQsc_.js} +1 -1
  109. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CEqRt0WU.js → classDiagram-v2-Q7XG4LA2.c22tQsc_.js} +1 -1
  110. package/web/_astro/{cose-bilkent-S5V4N54A.C7JiUdj2.js → cose-bilkent-S5V4N54A.B9c6fiSj.js} +1 -1
  111. package/web/_astro/{cynefin-OW5HDTMX.B6mZ-1be.js → cynefin-OW5HDTMX.DrZsrUHl.js} +1 -1
  112. package/web/_astro/{dagre-BM42HDAG.BuryfDy6.js → dagre-BM42HDAG.DG1U4b3y.js} +1 -1
  113. package/web/_astro/{diagram-2AECGRRQ.dao39YQM.js → diagram-2AECGRRQ.CH15b-K3.js} +1 -1
  114. package/web/_astro/{diagram-5GNKFQAL.lnGZe4At.js → diagram-5GNKFQAL.BH1j7twR.js} +1 -1
  115. package/web/_astro/{diagram-KO2AKTUF.uWfhzEfT.js → diagram-KO2AKTUF.q2zCkbAj.js} +1 -1
  116. package/web/_astro/{diagram-LMA3HP47.ppq8-198.js → diagram-LMA3HP47.BLaaZnHV.js} +1 -1
  117. package/web/_astro/{diagram-OG6HWLK6.CKSCqDt5.js → diagram-OG6HWLK6.BcpRdQwI.js} +1 -1
  118. package/web/_astro/{erDiagram-TEJ5UH35.sFSf2yBN.js → erDiagram-TEJ5UH35.D7KBlqfM.js} +1 -1
  119. package/web/_astro/{flowDiagram-I6XJVG4X.BJQjKLbz.js → flowDiagram-I6XJVG4X.D_gmuRqg.js} +1 -1
  120. package/web/_astro/{ganttDiagram-6RSMTGT7.DAHKNj2N.js → ganttDiagram-6RSMTGT7.DzgWYniD.js} +1 -1
  121. package/web/_astro/{gitGraphDiagram-PVQCEYII.BlmzcW_O.js → gitGraphDiagram-PVQCEYII.576i3hm3.js} +1 -1
  122. package/web/_astro/index.DiC1a6lh.css +1 -0
  123. package/web/_astro/{infoDiagram-5YYISTIA.DGhjaqsi.js → infoDiagram-5YYISTIA.DMCclBiz.js} +1 -1
  124. package/web/_astro/{ishikawaDiagram-YF4QCWOH.gKIpgt6F.js → ishikawaDiagram-YF4QCWOH.9Kyca4SH.js} +1 -1
  125. package/web/_astro/{journeyDiagram-JHISSGLW.DG3tmP0R.js → journeyDiagram-JHISSGLW.chbdG-8q.js} +1 -1
  126. package/web/_astro/{kanban-definition-UN3LZRKU.CgHDSxkR.js → kanban-definition-UN3LZRKU.Bds54-e0.js} +1 -1
  127. package/web/_astro/{linear.BuGEpcuM.js → linear.D5ND9GN_.js} +1 -1
  128. package/web/_astro/{mermaid.core.DBVqkWBp.js → mermaid.core.D9N_PGbj.js} +4 -4
  129. package/web/_astro/{mindmap-definition-RKZ34NQL.D-kOcji9.js → mindmap-definition-RKZ34NQL.t0xop5vp.js} +1 -1
  130. package/web/_astro/{pieDiagram-4H26LBE5.BvGPwtZt.js → pieDiagram-4H26LBE5.DqgKh6hM.js} +1 -1
  131. package/web/_astro/{quadrantDiagram-W4KKPZXB.BKb_9FV6.js → quadrantDiagram-W4KKPZXB.DeuHVi2t.js} +1 -1
  132. package/web/_astro/{requirementDiagram-4Y6WPE33.Cx0fvD8v.js → requirementDiagram-4Y6WPE33.BdYFSG4W.js} +1 -1
  133. package/web/_astro/{sankeyDiagram-5OEKKPKP.fatULi0W.js → sankeyDiagram-5OEKKPKP.k1LPPETJ.js} +1 -1
  134. package/web/_astro/{sequenceDiagram-3UESZ5HK.ChHyKqco.js → sequenceDiagram-3UESZ5HK.DdyfpzoG.js} +1 -1
  135. package/web/_astro/{stateDiagram-AJRCARHV.BdUpKWyj.js → stateDiagram-AJRCARHV.DSEI8il0.js} +1 -1
  136. package/web/_astro/{stateDiagram-v2-BHNVJYJU.fmJxqIm5.js → stateDiagram-v2-BHNVJYJU.CoMnQRKo.js} +1 -1
  137. package/web/_astro/{timeline-definition-PNZ67QCA.BayDn5-V.js → timeline-definition-PNZ67QCA.DM1AHNm-.js} +1 -1
  138. package/web/_astro/{vennDiagram-CIIHVFJN.Bm2Aggwa.js → vennDiagram-CIIHVFJN.C-cJmVtl.js} +1 -1
  139. package/web/_astro/{wardleyDiagram-YWT4CUSO.DI75O-uG.js → wardleyDiagram-YWT4CUSO.D0xMzKPb.js} +1 -1
  140. package/web/_astro/{xychartDiagram-2RQKCTM6.DL90E7Q7.js → xychartDiagram-2RQKCTM6.BH5xJcEq.js} +1 -1
  141. package/web/index.html +2 -2
  142. package/config/plugin-scripts.json +0 -116
  143. package/config/script-placement-baseline.json +0 -33
  144. package/plugins/sp/hooks/careful-guard.ts +0 -74
  145. package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
  146. package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
  147. package/plugins/sp/lib/dogfood-report.ts +0 -278
  148. package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
  149. package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
  150. package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
  151. package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
  152. package/plugins/sp/lib/inline-run-app.ts +0 -68
  153. package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
  154. package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
  155. package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
  156. package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
  157. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
  158. package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
  159. package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
  160. package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
  161. package/plugins/sp/lib/run-summary-core.ts +0 -225
  162. package/plugins/sp/lib/spur-bin.ts +0 -36
  163. package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
  164. package/plugins/sp/lib/step-profile.generated.mjs +0 -174
  165. package/plugins/sp/lib/transcript.ts +0 -112
  166. package/plugins/sp/scripts/batch-plan.mjs +0 -1096
  167. package/plugins/sp/scripts/batch-plan.ts +0 -145
  168. package/plugins/sp/scripts/batch-preflight.mjs +0 -423
  169. package/plugins/sp/scripts/batch-preflight.ts +0 -270
  170. package/plugins/sp/scripts/commit-guard.mjs +0 -226
  171. package/plugins/sp/scripts/commit-guard.ts +0 -248
  172. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
  173. package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
  174. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
  175. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
  176. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
  177. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
  178. package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
  179. package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
  180. package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
  181. package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
  182. package/plugins/sp/scripts/idea-handoff.mjs +0 -32
  183. package/plugins/sp/scripts/idea-handoff.ts +0 -47
  184. package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
  185. package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
  186. package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
  187. package/plugins/sp/scripts/inline-run-setup.ts +0 -198
  188. package/plugins/sp/scripts/persist-out-check.mjs +0 -217
  189. package/plugins/sp/scripts/persist-out-check.ts +0 -249
  190. package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
  191. package/plugins/sp/scripts/pr-reviewing.ts +0 -952
  192. package/plugins/sp/scripts/quality-gate.mjs +0 -30491
  193. package/plugins/sp/scripts/quality-gate.ts +0 -80
  194. package/plugins/sp/scripts/residual-scan.mjs +0 -634
  195. package/plugins/sp/scripts/residual-scan.ts +0 -248
  196. package/plugins/sp/scripts/run-summary.mjs +0 -758
  197. package/plugins/sp/scripts/run-summary.ts +0 -107
  198. package/plugins/sp/scripts/script-root.mjs +0 -129
  199. package/plugins/sp/scripts/script-root.ts +0 -199
  200. package/plugins/sp/scripts/session-timeline.mjs +0 -359
  201. package/plugins/sp/scripts/session-timeline.ts +0 -190
  202. package/plugins/sp/scripts/task-diffstat.mjs +0 -181
  203. package/plugins/sp/scripts/task-diffstat.ts +0 -219
  204. package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
  205. package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
  206. package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
  207. package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
  208. package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
  209. package/plugins/sp/scripts/wrapup-steps.ts +0 -891
  210. package/web/_astro/channel.C7wG9RxA.js +0 -1
  211. package/web/_astro/index.BCY1JJHd.css +0 -1
@@ -1,248 +0,0 @@
1
- #!/usr/bin/env bun
2
- /** F96 residual IO glue. Pure logic: residual-scan.generated.mjs; contract: task-residual-sweep.md. */
3
- import { spawnSync } from 'node:child_process';
4
- import * as fs from 'node:fs';
5
- import { tmpdir } from 'node:os';
6
- import { join } from 'node:path';
7
- import { getEnvVars } from '../lib/env';
8
- import * as core from '../lib/residual-scan.generated.mjs';
9
- import { spurCommand } from '../lib/spur-bin';
10
-
11
- export * from '../lib/residual-scan.generated.mjs';
12
- export const RESIDUAL_SCAN_USAGE =
13
- 'usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]';
14
- export type ScanEnv = { spurBin?: string } & Record<string, string | undefined>;
15
- export type ScanIo = { out: (line: string) => void; err: (line: string) => void };
16
- export type ScanOptions = { cwd?: string; io?: ScanIo };
17
- type RunResult = { status: number; stdout: string };
18
- type ParsedArgs = core.ParsedScanArgs;
19
- type ModeFn = (o: ParsedArgs, e: ScanEnv, i: ScanIo) => number;
20
- type ScanTask = { content: string; featureId: string };
21
- type VerdictFile = { verdict: string; checks: Array<{ name: string; status: string; evidence: string }> };
22
- function run(cmd: string, args: string[], cwd: string): RunResult {
23
- const result = spawnSync(cmd, args, { cwd, encoding: 'utf8' });
24
- if (result.error !== undefined) return { status: result.status ?? 1, stdout: '' };
25
- return { status: result.status ?? 1, stdout: result.stdout ?? '' };
26
- }
27
- function spur(env: ScanEnv, spurBinFlag: string | undefined, args: string[], cwd: string): RunResult {
28
- const { cmd, prefix } = spurCommand(spurBinFlag ?? env.spurBin);
29
- return run(cmd, [...prefix, ...args], cwd);
30
- }
31
- function isRegularFile(path: string): boolean {
32
- try {
33
- return fs.statSync(path).isFile();
34
- } catch {
35
- return false;
36
- }
37
- }
38
- export function collectAddedLines(root: string, base: string): Array<{ file: string; line: number; text: string }> {
39
- const out: Array<{ file: string; line: number; text: string }> = [];
40
- const diff = run('git', ['diff', '--unified=0', base], root);
41
- let file = '';
42
- let newLine = 0;
43
- for (const line of diff.stdout.split('\n')) {
44
- if (line.startsWith('+++ b/')) file = line.slice(6);
45
- else if (line.startsWith('@@')) newLine = Number.parseInt((line.match(/\+[0-9]+/) ?? ['+0'])[0].slice(1), 10);
46
- else if (line.startsWith('+') && !line.startsWith('+++'))
47
- out.push({ file, line: newLine++, text: line.slice(1) });
48
- }
49
- const untracked = run('git', ['ls-files', '--others', '--exclude-standard'], root);
50
- for (const f of untracked.stdout.split('\n')) {
51
- if (f.length === 0 || !isRegularFile(join(root, f))) continue;
52
- for (const [idx, text] of fs.readFileSync(join(root, f), 'utf8').split('\n').entries())
53
- out.push({ file: f, line: idx + 1, text });
54
- }
55
- return out;
56
- }
57
- export function listStagingResidue(tmpDir: string, wbs: string): string[] {
58
- let names: string[];
59
- try {
60
- names = fs.readdirSync(tmpDir);
61
- } catch {
62
- return [];
63
- }
64
- return names.filter((n) => n.startsWith(`${wbs}-`) && isRegularFile(join(tmpDir, n))).map((n) => join(tmpDir, n));
65
- }
66
- function readDeferrals(runDir: string, wbs: string): core.Deferral[] {
67
- const path = join(runDir, `${wbs}-residual-deferrals.json`);
68
- if (!fs.existsSync(path)) return [];
69
- try {
70
- return core.parseDeferralEntries(JSON.parse(fs.readFileSync(path, 'utf8')));
71
- } catch {
72
- return [];
73
- }
74
- }
75
- export function scanResiduals(root: string, wbs: string, tmpDir: string, taskContent: string, _env: ScanEnv) {
76
- const runDir = join(root, '.spur', 'run');
77
- const basePath = join(runDir, `${wbs}-base.sha`);
78
- const base = fs.existsSync(basePath) ? fs.readFileSync(basePath, 'utf8').trim() : null;
79
- const stagingResidue = listStagingResidue(tmpDir, wbs);
80
- const deferrals = readDeferrals(runDir, wbs);
81
- const addedLines = base === null ? [] : collectAddedLines(root, base);
82
- return core.scanResiduals({ wbs, base, taskContent, addedLines, stagingResidue, deferrals });
83
- }
84
- function loadTask(env: ScanEnv, spurBinFlag: string | undefined, wbs: string, root: string): ScanTask {
85
- const res = spur(env, spurBinFlag, ['task', 'show', wbs, '--json'], root);
86
- if (res.status !== 0) throw new Error(`task show ${wbs} failed`);
87
- type TaskJson = { content?: unknown; feature_id?: unknown; frontmatter?: { feature_id?: unknown } | null };
88
- const parsed = JSON.parse(res.stdout) as TaskJson;
89
- const featureId =
90
- [parsed.feature_id, parsed.frontmatter?.feature_id].find((v): v is string => typeof v === 'string') ?? '';
91
- return { content: typeof parsed.content === 'string' ? parsed.content : '', featureId };
92
- }
93
- const loadVerdict = (runDir: string, wbs: string): VerdictFile =>
94
- JSON.parse(fs.readFileSync(core.recordedVerdictPath(runDir, wbs, fs), 'utf8'));
95
- function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
96
- const runDir = join(opts.root, '.spur', 'run');
97
- fs.mkdirSync(runDir, { recursive: true });
98
- const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
99
- const artifact = scanResiduals(opts.root, opts.wbs, opts.tmpDir, content, env);
100
- fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(artifact, null, 2)}\n`);
101
- io.out(
102
- `residual-scan: ${opts.wbs} blocking=${artifact.counts.blocking} deferrable=${artifact.counts.deferrable} advisory=${artifact.counts.advisory} housekeeping=${artifact.counts.housekeeping}\n`,
103
- );
104
- return 0;
105
- }
106
- /**
107
- * Task 1122 R2: the review-time gate — the record sweep's review-finding slice
108
- * (core.buildReviewGateArtifact) at the review PASS edges; writes the same `<wbs>-residuals.json`
109
- * the test-fix hop reads. Exit 1 on an open P1-P3 finding. The answer-grade rule (1204) was
110
- * retired with the asserted confidence machinery (F511); findings stay the gate.
111
- */
112
- function reviewGateMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
113
- const runDir = join(opts.root, '.spur', 'run');
114
- fs.mkdirSync(runDir, { recursive: true });
115
- const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
116
- const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
117
- fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}\n`);
118
- io.out(gate.note);
119
- return gate.artifact.counts.blocking > 0 ? 1 : 0;
120
- }
121
- function foldMode(opts: ParsedArgs, _env: ScanEnv, io: ScanIo): number {
122
- const runDir = join(opts.root, '.spur', 'run');
123
- const scan = JSON.parse(
124
- fs.readFileSync(join(runDir, `${opts.wbs}-residuals.json`), 'utf8'),
125
- ) as core.ResidualArtifact;
126
- const target = core.recordedVerdictPath(runDir, opts.wbs, fs);
127
- const note = core.verdictDisagreementNote(runDir, opts.wbs, fs);
128
- if (note !== null) io.out(`${note}\n`);
129
- const verdict = loadVerdict(runDir, opts.wbs);
130
- const findingsPath = join(runDir, `${opts.wbs}-test-gate.findings`);
131
- const fold = core.foldVerdict(
132
- verdict,
133
- scan,
134
- fs.existsSync(findingsPath) ? fs.readFileSync(findingsPath, 'utf8') : '',
135
- );
136
- const bytes = `${JSON.stringify({ ...verdict, verdict: fold.verdict, checks: fold.checks }, null, 2)}\n`;
137
- const temporary = `${target}.${process.pid}.tmp`;
138
- try {
139
- fs.writeFileSync(temporary, bytes, { flag: 'wx' });
140
- fs.renameSync(temporary, target);
141
- } finally {
142
- fs.rmSync(temporary, { force: true });
143
- }
144
- // The pipeline still consumes the attempt copy after folding; the recorded copy is authoritative.
145
- const scratch = join(runDir, `${opts.wbs}-verdict.json`);
146
- if (target !== scratch) fs.writeFileSync(scratch, bytes);
147
- fs.writeFileSync(findingsPath, fold.findings);
148
- io.out(
149
- `residual-fold: ${opts.wbs} verdict=${fold.verdict} residual-sweep=${fold.checks.find((c) => c.name === 'residual-sweep')?.status}\n`,
150
- );
151
- return 0;
152
- }
153
- function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
154
- const wbs = opts.wbs;
155
- const task = loadTask(env, opts.spurBin, wbs, opts.root);
156
- const runDir = join(opts.root, '.spur', 'run');
157
- const scan = scanResiduals(opts.root, wbs, opts.tmpDir, task.content, env);
158
- const residualsPath = join(runDir, `${wbs}-residuals.json`);
159
- let prior: Partial<core.ResidualArtifact & { followUp?: string }> = {};
160
- if (fs.existsSync(residualsPath)) prior = JSON.parse(fs.readFileSync(residualsPath, 'utf8'));
161
- const deferred = scan.items.filter((i) => i.class === 'deferrable');
162
- if (deferred.length > 0 && prior.followUp === undefined) {
163
- // Filed UNLINKED: a feature edge failed that feature's done-gate (`L4.verifying-incomplete-tasks`,
164
- // E72/1086→1087); the Background still names the source task and feature for traceability.
165
- const args = ['task', 'create', `Residuals from ${wbs}`, '--skip-ready', '--json'];
166
- const created = spur(env, opts.spurBin, args, opts.root);
167
- let wbsNew = '';
168
- try {
169
- const parsed = JSON.parse(created.stdout) as { wbs?: unknown; data?: { wbs?: unknown } | null };
170
- const data = typeof parsed.data === 'object' && parsed.data !== null ? parsed.data : {};
171
- wbsNew = [parsed.wbs, data.wbs].find((v): v is string => typeof v === 'string') ?? '';
172
- } catch {
173
- wbsNew = '';
174
- }
175
- if (created.status !== 0 || wbsNew === '') {
176
- io.err(`residual-settle: task create failed or unreadable wbs; re-run: residual-scan settle ${wbs}\n`);
177
- return 0;
178
- }
179
- const rows = deferred.map((i) => `- ${i.id} — ${i.location}: ${i.text}`);
180
- const head = `Source task: ${wbs}${task.featureId === '' ? '' : ` (feature ${task.featureId})`} — deferred residuals filed by residual-scan settle (unlinked: a deferral must not hold the completing feature open).`;
181
- const bgFile = join(runDir, `${wbs}-residual-background.md`);
182
- fs.writeFileSync(bgFile, `${[head, '', ...rows].join('\n')}\n`);
183
- const updArgs = ['task', 'update', wbsNew, '--section', 'Background', '--from-file', bgFile];
184
- if (spur(env, opts.spurBin, updArgs, opts.root).status !== 0) {
185
- io.err(`residual-settle: background write failed; re-run: residual-scan settle ${wbs}\n`);
186
- return 0;
187
- }
188
- prior.followUp = wbsNew;
189
- io.out(`residual-settle: filed follow-up ${wbsNew} for ${deferred.length} deferred item(s)\n`);
190
- }
191
- for (const path of listStagingResidue(opts.tmpDir, wbs)) {
192
- try {
193
- fs.rmSync(path, { force: true });
194
- } catch {
195
- io.err(`residual-settle: could not remove ${path}; re-run: residual-scan settle ${wbs}\n`);
196
- return 0;
197
- }
198
- }
199
- fs.writeFileSync(residualsPath, `${JSON.stringify({ ...scan, ...prior }, null, 2)}\n`);
200
- return 0;
201
- }
202
- function reportMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
203
- const runDir = join(opts.root, '.spur', 'run');
204
- // A fix-budget-exhausted run never reaches verify: no verdict is a no-op, not ENOENT (1178 run 1).
205
- const verdictPath = core.recordedVerdictPath(runDir, opts.wbs, fs);
206
- if (!fs.existsSync(verdictPath)) {
207
- io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}\n`);
208
- return 0;
209
- }
210
- const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === 'residual-sweep');
211
- if (sweep === undefined || sweep.status !== 'fail') return 0;
212
- const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
213
- const scan = scanResiduals(opts.root, opts.wbs, opts.tmpDir, content, env);
214
- const blocking = scan.items.filter((i) => i.class === 'blocking');
215
- const attemptFile = join(runDir, `${opts.wbs}-test-fix-attempt`);
216
- const attempts = fs.existsSync(attemptFile)
217
- ? Number.parseInt(fs.readFileSync(attemptFile, 'utf8').trim() || '0', 10)
218
- : 0;
219
- fs.writeFileSync(
220
- join(runDir, `${opts.wbs}-residual-report.md`),
221
- core.renderReport(opts.wbs, blocking, Number.isNaN(attempts) ? 0 : attempts),
222
- );
223
- io.out(`Recovery: fix the items in .spur/run/${opts.wbs}-residual-report.md, then /sp:dev-run ${opts.wbs}\n`);
224
- return 0;
225
- }
226
- export function main(argv: string[], env: ScanEnv = getEnvVars(), options: ScanOptions = {}): number {
227
- const io: ScanIo = options.io ?? {
228
- out: (line) => process.stdout.write(line),
229
- err: (line) => process.stderr.write(line),
230
- };
231
- const cwd = options.cwd ?? process.cwd();
232
- const opts = core.parseScanArgs(argv, cwd, tmpdir());
233
- const modes: Record<string, ModeFn> = {
234
- scan: scanMode,
235
- fold: foldMode,
236
- settle: settleMode,
237
- report: reportMode,
238
- 'review-gate': reviewGateMode,
239
- };
240
- const modeFn = opts === null ? undefined : modes[opts.mode];
241
- if (opts === null || modeFn === undefined) {
242
- io.err(`${RESIDUAL_SCAN_USAGE}\n`);
243
- return 2;
244
- }
245
- const resolved: ParsedArgs = { ...opts, root: opts.root.startsWith('/') ? opts.root : join(cwd, opts.root) };
246
- return modeFn(resolved, env, io);
247
- }
248
- if (import.meta.main) process.exit(main(process.argv.slice(2)));