@gobing-ai/spur 0.4.0 → 0.4.1

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 (154) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/decisions/gates.yaml +36 -0
  3. package/config/decisions/history-anatomy.yaml +35 -0
  4. package/config/decisions/idea-pipeline.yaml +38 -0
  5. package/config/plugin-scripts.json +15 -0
  6. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  7. package/config/rules/recommended-pre-check.yaml +8 -3
  8. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  9. package/config/script-placement-baseline.json +2 -2
  10. package/config/workflows/feature-lifecycle.yaml +9 -2
  11. package/config/workflows/history-anatomy.yaml +22 -0
  12. package/config/workflows/idea-pipeline.yaml +75 -0
  13. package/config/workflows/task-pipeline.yaml +216 -57
  14. package/config/workflows/wrapup-pipeline.yaml +106 -13
  15. package/package.json +9 -9
  16. package/plugins/sp/README.md +11 -1
  17. package/plugins/sp/agents/super-planner.md +11 -0
  18. package/plugins/sp/agents/super-reviewer.md +14 -0
  19. package/plugins/sp/bunfig.toml +8 -0
  20. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  21. package/plugins/sp/commands/dev-find-way.md +80 -0
  22. package/plugins/sp/commands/dev-idea.md +6 -0
  23. package/plugins/sp/commands/dev-parallel.md +8 -2
  24. package/plugins/sp/commands/dev-plan.md +6 -0
  25. package/plugins/sp/commands/dev-review-session.md +3 -1
  26. package/plugins/sp/commands/dev-run.md +12 -2
  27. package/plugins/sp/commands/dev-runall.md +18 -10
  28. package/plugins/sp/hooks/context-session-start.ts +1 -1
  29. package/plugins/sp/lib/idea-handoff.generated.mjs +41 -5
  30. package/plugins/sp/lib/inline-run-app.ts +28 -0
  31. package/plugins/sp/lib/inline-run.generated.d.mts +5 -0
  32. package/plugins/sp/lib/inline-run.generated.mjs +333 -45
  33. package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
  34. package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
  35. package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
  36. package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
  37. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  38. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  39. package/plugins/sp/lib/run-summary-core.ts +561 -0
  40. package/plugins/sp/lib/transcript.ts +121 -6
  41. package/plugins/sp/plugin.json +1 -1
  42. package/plugins/sp/references/roles.md +1 -1
  43. package/plugins/sp/scripts/batch-plan.mjs +335 -0
  44. package/plugins/sp/scripts/batch-plan.ts +100 -0
  45. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  46. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  47. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  48. package/plugins/sp/scripts/commit-guard.ts +249 -0
  49. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  50. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  51. package/plugins/sp/scripts/inline-run-setup.mjs +551 -80
  52. package/plugins/sp/scripts/inline-run-setup.ts +52 -78
  53. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  54. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  55. package/plugins/sp/scripts/quality-gate.mjs +30407 -73
  56. package/plugins/sp/scripts/quality-gate.ts +21 -5
  57. package/plugins/sp/scripts/residual-scan.mjs +86 -31
  58. package/plugins/sp/scripts/residual-scan.ts +28 -30
  59. package/plugins/sp/scripts/run-summary.mjs +421 -14
  60. package/plugins/sp/scripts/run-summary.ts +41 -177
  61. package/plugins/sp/scripts/session-timeline.mjs +109 -12
  62. package/plugins/sp/scripts/session-timeline.ts +71 -19
  63. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  64. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  65. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  66. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  67. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  68. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  69. package/plugins/sp/skills/code-verification/SKILL.md +3 -4
  70. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  71. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  72. package/plugins/sp/skills/fleet-join/SKILL.md +1 -1
  73. package/plugins/sp/skills/session-review/SKILL.md +38 -7
  74. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  75. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  76. package/plugins/sp/skills/spur-cli/SKILL.md +5 -2
  77. package/plugins/sp/skills/spur-cli/references/agent.md +20 -15
  78. package/plugins/sp/skills/spur-cli/references/decision.md +7 -6
  79. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  80. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +19 -3
  81. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  82. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  83. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  84. package/plugins/sp/skills/spur-composer/SKILL.md +1 -1
  85. package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
  86. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
  87. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  88. package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -16
  89. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  90. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
  91. package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -1020
  92. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
  93. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +6 -0
  94. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  95. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  96. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +35 -5
  97. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  98. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +343 -163
  99. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  100. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  101. package/schemas/state-machine-workflow.schema.json +23 -0
  102. package/spur.js +20728 -16630
  103. package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.D66k_s_T.js} +1 -1
  104. package/web/_astro/{BoardApp.Bzr4OXrW.js → BoardApp.D6sjAJr_.js} +173 -173
  105. package/web/_astro/{TaskDetail.LAyR_Pvn.js → TaskDetail.BynLnMNc.js} +1 -1
  106. package/web/_astro/{arc.f4e0OYLg.js → arc._BUadrdH.js} +1 -1
  107. package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
  108. package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
  109. package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
  110. package/web/_astro/channel.DZVfto-r.js +1 -0
  111. package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
  112. package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
  113. package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
  114. package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
  115. package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
  116. package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
  117. package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
  118. package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
  119. package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
  120. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
  121. package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
  122. package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
  123. package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
  124. package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
  125. package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
  126. package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
  127. package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
  128. package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
  129. package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
  130. package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
  131. package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
  132. package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
  133. package/web/_astro/index.COh_kBxN.css +1 -0
  134. package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
  135. package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
  136. package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
  137. package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
  138. package/web/_astro/{linear.DguUUe6V.js → linear.MzmRrNER.js} +1 -1
  139. package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.fK-WNswS.js} +4 -4
  140. package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
  141. package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
  142. package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
  143. package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
  144. package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
  145. package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
  146. package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
  147. package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
  148. package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
  149. package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
  150. package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
  151. package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
  152. package/web/index.html +2 -2
  153. package/web/_astro/channel.CUFdtWhm.js +0 -1
  154. package/web/_astro/index.Dg_3bOaJ.css +0 -1
@@ -17,12 +17,12 @@ import { appendFileSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
18
  import { getEnvVars } from '../lib/env';
19
19
  import type { QualityGateEnv, QualityGateOptions } from '../lib/quality-gate.generated.mjs';
20
- import { readReceiptStatus, runLightGate, runQualityGate } from '../lib/quality-gate.generated.mjs';
20
+ import { resolveReceiptReuse, runDeferredGate, runLightGate, runQualityGate } from '../lib/quality-gate.generated.mjs';
21
21
 
22
22
  export * from '../lib/quality-gate.generated.mjs';
23
23
 
24
24
  export const QUALITY_GATE_USAGE =
25
- 'usage: quality-gate.ts <run|recheck|light|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)';
25
+ 'usage: quality-gate.ts <run|recheck|light|deferred|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)';
26
26
 
27
27
  export function main(
28
28
  argv: string[],
@@ -30,7 +30,7 @@ export function main(
30
30
  options: QualityGateOptions = {},
31
31
  ): number {
32
32
  const mode = argv[0];
33
- if (mode !== 'run' && mode !== 'recheck' && mode !== 'light' && mode !== 'status') {
33
+ if (mode !== 'run' && mode !== 'recheck' && mode !== 'light' && mode !== 'deferred' && mode !== 'status') {
34
34
  process.stderr.write(`${QUALITY_GATE_USAGE}\n`);
35
35
  return 2;
36
36
  }
@@ -42,10 +42,26 @@ export function main(
42
42
  const env: QualityGateEnv = { ...rawEnv, wbs };
43
43
  if (mode === 'light') {
44
44
  runLightGate(env);
45
+ } else if (mode === 'deferred') {
46
+ // 1111 R1: light tier for per-task feedback, `DEFERRED`/`FAIL` for the status token —
47
+ // exit 0 on DEFERRED so the YAML's `; exit 0` wrapper and the action result agree.
48
+ return runDeferredGate(env, options).status === 'DEFERRED' ? 0 : 1;
45
49
  } else if (mode === 'status') {
46
50
  const runDir = join(options.cwd ?? '.', '.spur', 'run');
47
- const verdict = readReceiptStatus(join(runDir, `${env.wbs}-check-receipt.json`), env.proofDigest ?? '');
48
- if (verdict.reuse) {
51
+ // Task 1136 R3: the gate owns its reuse identity — recompute the proof-input fingerprint
52
+ // before consulting the receipt, so a caller-copied digest can never trigger reuse.
53
+ const decision = resolveReceiptReuse(
54
+ join(runDir, `${env.wbs}-check-receipt.json`),
55
+ env,
56
+ options.cwd ?? process.cwd(),
57
+ options.recomputeFingerprint,
58
+ );
59
+ const verdict = decision.receiptStatus;
60
+ if (decision.refusal !== undefined) {
61
+ const line = `check.reuse-refused — ${decision.refusal}\n`;
62
+ process.stdout.write(line); // tee: stdout and the log
63
+ appendFileSync(join(runDir, `${env.wbs}-test-gate.log`), line);
64
+ } else if (verdict.reuse) {
49
65
  // 0940 R3: reuse is observable in the gate log and on stdout (the action result
50
66
  // `data`); the `{reuse, reason}` JSON stays the last stdout line for machine readers.
51
67
  const line = `check.reused — full-tier receipt reused for input digest ${env.proofDigest ?? ''}\n`;
@@ -164,6 +164,43 @@ function scanResiduals(inputs) {
164
164
  counts
165
165
  };
166
166
  }
167
+ function parseDeferralEntries(raw) {
168
+ const isDeferral = (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0;
169
+ return (Array.isArray(raw) ? raw : []).filter(isDeferral);
170
+ }
171
+ function blockingReviewFindings(taskContent, deferrals) {
172
+ const rows = parseReviewFindings(taskContent);
173
+ const tableDeferrals = rows.flatMap((r) => r.deferral === undefined ? [] : [{ id: makeItemId("review-finding", r.location, r.text), reason: r.deferral }]);
174
+ const items = rows.map((r) => ({
175
+ category: "review-finding",
176
+ priority: r.priority,
177
+ location: r.location,
178
+ text: r.text
179
+ }));
180
+ return classify(items, [...tableDeferrals, ...deferrals]);
181
+ }
182
+ function buildReviewGateArtifact(wbs, taskContent, deferrals) {
183
+ const items = blockingReviewFindings(taskContent, deferrals);
184
+ const blocking = items.filter((i) => i.class === "blocking");
185
+ const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
186
+ for (const item of items)
187
+ counts[item.class]++;
188
+ const artifact = {
189
+ wbs,
190
+ base: null,
191
+ scanned: {
192
+ "review-finding": true,
193
+ "diff-marker": false,
194
+ "unchecked-box": false,
195
+ "staging-residue": false
196
+ },
197
+ items,
198
+ counts
199
+ };
200
+ const note = `residual-review-gate: ${wbs} blocking=${counts.blocking}` + (blocking.length > 0 ? ` ids=${blocking.map((i) => i.id).join(",")} anchors=${blockingAnchors(blocking).join(",")}` : "") + `
201
+ `;
202
+ return { artifact, note };
203
+ }
167
204
  function blockingAnchors(items) {
168
205
  const anchors = new Set;
169
206
  for (const item of items) {
@@ -262,6 +299,31 @@ function verdictDisagreementNote(runDir, wbs, fs) {
262
299
  const winner = mtimeOf(fs, durable) > mtimeOf(fs, run) ? "durable" : "run";
263
300
  return `residual-fold: ${wbs} verdict copies disagree — run=${run} (${value(run)}) durable=${durable} (${value(durable)})` + ` → chose ${winner} (newer mtime)`;
264
301
  }
302
+ function parseScanArgs(argv, cwd, defaultTmpDir) {
303
+ let mode = "";
304
+ let wbs = "";
305
+ let spurBin;
306
+ let root = cwd;
307
+ let tmpDir = defaultTmpDir;
308
+ for (let i = 0;i < argv.length; i++) {
309
+ const a = argv[i];
310
+ if (a === undefined)
311
+ break;
312
+ if (a === "--spur-bin")
313
+ spurBin = argv[++i];
314
+ else if (a === "--root")
315
+ root = argv[++i] ?? root;
316
+ else if (a === "--tmp-dir")
317
+ tmpDir = argv[++i] ?? tmpDir;
318
+ else if (a === "--help" || a === "-h")
319
+ return null;
320
+ else if (mode === "")
321
+ mode = a;
322
+ else if (wbs === "")
323
+ wbs = a;
324
+ }
325
+ return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
326
+ }
265
327
 
266
328
  // plugins/sp/lib/spur-bin.ts
267
329
  function spurCommand(spurBin) {
@@ -270,7 +332,7 @@ function spurCommand(spurBin) {
270
332
  }
271
333
 
272
334
  // plugins/sp/scripts/residual-scan.ts
273
- var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
335
+ var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
274
336
  function run(cmd, args, cwd) {
275
337
  const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
276
338
  if (result.error !== undefined)
@@ -326,10 +388,8 @@ function readDeferrals(runDir, wbs) {
326
388
  const path = join2(runDir, `${wbs}-residual-deferrals.json`);
327
389
  if (!fs.existsSync(path))
328
390
  return [];
329
- const isDeferral = (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0;
330
391
  try {
331
- const parsed = JSON.parse(fs.readFileSync(path, "utf8"));
332
- return (Array.isArray(parsed) ? parsed : []).filter(isDeferral);
392
+ return parseDeferralEntries(JSON.parse(fs.readFileSync(path, "utf8")));
333
393
  } catch {
334
394
  return [];
335
395
  }
@@ -343,31 +403,6 @@ function scanResiduals2(root, wbs, tmpDir, taskContent, _env) {
343
403
  const addedLines = base === null ? [] : collectAddedLines(root, base);
344
404
  return scanResiduals({ wbs, base, taskContent, addedLines, stagingResidue, deferrals });
345
405
  }
346
- function parseArgs(argv) {
347
- let mode = "";
348
- let wbs = "";
349
- let spurBin;
350
- let root = process.cwd();
351
- let tmpDir = tmpdir();
352
- for (let i = 0;i < argv.length; i++) {
353
- const a = argv[i];
354
- if (a === undefined)
355
- break;
356
- if (a === "--spur-bin")
357
- spurBin = argv[++i];
358
- else if (a === "--root")
359
- root = argv[++i] ?? root;
360
- else if (a === "--tmp-dir")
361
- tmpDir = argv[++i] ?? tmpDir;
362
- else if (a === "--help" || a === "-h")
363
- return null;
364
- else if (mode === "")
365
- mode = a;
366
- else if (wbs === "")
367
- wbs = a;
368
- }
369
- return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
370
- }
371
406
  function loadTask(env, spurBinFlag, wbs, root) {
372
407
  const res = spur(env, spurBinFlag, ["task", "show", wbs, "--json"], root);
373
408
  if (res.status !== 0)
@@ -388,6 +423,16 @@ function scanMode(opts, env, io) {
388
423
  `);
389
424
  return 0;
390
425
  }
426
+ function reviewGateMode(opts, env, io) {
427
+ const runDir = join2(opts.root, ".spur", "run");
428
+ fs.mkdirSync(runDir, { recursive: true });
429
+ const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
430
+ const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
431
+ fs.writeFileSync(join2(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}
432
+ `);
433
+ io.out(gate.note);
434
+ return gate.artifact.counts.blocking > 0 ? 1 : 0;
435
+ }
391
436
  function foldMode(opts, _env, io) {
392
437
  const runDir = join2(opts.root, ".spur", "run");
393
438
  const scan = JSON.parse(fs.readFileSync(join2(runDir, `${opts.wbs}-residuals.json`), "utf8"));
@@ -492,8 +537,14 @@ function main(argv, env = getEnvVars(), options = {}) {
492
537
  err: (line) => process.stderr.write(line)
493
538
  };
494
539
  const cwd = options.cwd ?? process.cwd();
495
- const opts = parseArgs(argv);
496
- const modes = { scan: scanMode, fold: foldMode, settle: settleMode, report: reportMode };
540
+ const opts = parseScanArgs(argv, cwd, tmpdir());
541
+ const modes = {
542
+ scan: scanMode,
543
+ fold: foldMode,
544
+ settle: settleMode,
545
+ report: reportMode,
546
+ "review-gate": reviewGateMode
547
+ };
497
548
  const modeFn = opts === null ? undefined : modes[opts.mode];
498
549
  if (opts === null || modeFn === undefined) {
499
550
  io.err(`${RESIDUAL_SCAN_USAGE}
@@ -509,8 +560,10 @@ export {
509
560
  scanResiduals2 as scanResiduals,
510
561
  renderReport,
511
562
  recordedVerdictPath,
563
+ parseScanArgs,
512
564
  parseReviewFindings,
513
565
  parseDiffMarkers,
566
+ parseDeferralEntries,
514
567
  normalizeAnchor,
515
568
  makeItemId,
516
569
  main,
@@ -520,6 +573,8 @@ export {
520
573
  findUncheckedBoxes,
521
574
  collectAddedLines,
522
575
  classify,
576
+ buildReviewGateArtifact,
577
+ blockingReviewFindings,
523
578
  blockingAnchors,
524
579
  RESIDUAL_SCAN_USAGE,
525
580
  ALLOW_PRAGMA
@@ -10,12 +10,12 @@ import { spurCommand } from '../lib/spur-bin';
10
10
 
11
11
  export * from '../lib/residual-scan.generated.mjs';
12
12
  export const RESIDUAL_SCAN_USAGE =
13
- 'usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]';
13
+ 'usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]';
14
14
  export type ScanEnv = { spurBin?: string } & Record<string, string | undefined>;
15
15
  export type ScanIo = { out: (line: string) => void; err: (line: string) => void };
16
16
  export type ScanOptions = { cwd?: string; io?: ScanIo };
17
17
  type RunResult = { status: number; stdout: string };
18
- type ParsedArgs = { mode: string; wbs: string; spurBin?: string; root: string; tmpDir: string };
18
+ type ParsedArgs = core.ParsedScanArgs;
19
19
  type ModeFn = (o: ParsedArgs, e: ScanEnv, i: ScanIo) => number;
20
20
  type ScanTask = { content: string; featureId: string };
21
21
  type VerdictFile = { verdict: string; checks: Array<{ name: string; status: string; evidence: string }> };
@@ -66,15 +66,8 @@ export function listStagingResidue(tmpDir: string, wbs: string): string[] {
66
66
  function readDeferrals(runDir: string, wbs: string): core.Deferral[] {
67
67
  const path = join(runDir, `${wbs}-residual-deferrals.json`);
68
68
  if (!fs.existsSync(path)) return [];
69
- const isDeferral = (e: unknown): e is core.Deferral =>
70
- typeof e === 'object' &&
71
- e !== null &&
72
- typeof (e as core.Deferral).id === 'string' &&
73
- typeof (e as core.Deferral).reason === 'string' &&
74
- (e as core.Deferral).reason.trim().length > 0;
75
69
  try {
76
- const parsed: unknown = JSON.parse(fs.readFileSync(path, 'utf8'));
77
- return (Array.isArray(parsed) ? parsed : []).filter(isDeferral);
70
+ return core.parseDeferralEntries(JSON.parse(fs.readFileSync(path, 'utf8')));
78
71
  } catch {
79
72
  return [];
80
73
  }
@@ -88,24 +81,6 @@ export function scanResiduals(root: string, wbs: string, tmpDir: string, taskCon
88
81
  const addedLines = base === null ? [] : collectAddedLines(root, base);
89
82
  return core.scanResiduals({ wbs, base, taskContent, addedLines, stagingResidue, deferrals });
90
83
  }
91
- function parseArgs(argv: string[]): ParsedArgs | null {
92
- let mode = '';
93
- let wbs = '';
94
- let spurBin: string | undefined;
95
- let root = process.cwd();
96
- let tmpDir = tmpdir();
97
- for (let i = 0; i < argv.length; i++) {
98
- const a = argv[i];
99
- if (a === undefined) break;
100
- if (a === '--spur-bin') spurBin = argv[++i];
101
- else if (a === '--root') root = argv[++i] ?? root;
102
- else if (a === '--tmp-dir') tmpDir = argv[++i] ?? tmpDir;
103
- else if (a === '--help' || a === '-h') return null;
104
- else if (mode === '') mode = a;
105
- else if (wbs === '') wbs = a;
106
- }
107
- return mode === '' || wbs === '' ? null : { mode, wbs, spurBin, root, tmpDir };
108
- }
109
84
  function loadTask(env: ScanEnv, spurBinFlag: string | undefined, wbs: string, root: string): ScanTask {
110
85
  const res = spur(env, spurBinFlag, ['task', 'show', wbs, '--json'], root);
111
86
  if (res.status !== 0) throw new Error(`task show ${wbs} failed`);
@@ -128,6 +103,23 @@ function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
128
103
  );
129
104
  return 0;
130
105
  }
106
+ /**
107
+ * Task 1122 R2: the review-time gate — the record sweep's review-finding slice with the same
108
+ * parse/classify/deferrals (core.buildReviewGateArtifact), applied at the review PASS edges
109
+ * before verify spends the cycle. Writes the same `<wbs>-residuals.json` artifact the test-fix
110
+ * hop already appends to the gate log (its remediation input); the record sweep stays the final
111
+ * authority and overwrites it. Exit 1 on an open P1-P3 finding fails the review PASS edge closed
112
+ * into review-fail-triage.
113
+ */
114
+ function reviewGateMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
115
+ const runDir = join(opts.root, '.spur', 'run');
116
+ fs.mkdirSync(runDir, { recursive: true });
117
+ const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
118
+ const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
119
+ fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}\n`);
120
+ io.out(gate.note);
121
+ return gate.artifact.counts.blocking > 0 ? 1 : 0;
122
+ }
131
123
  function foldMode(opts: ParsedArgs, _env: ScanEnv, io: ScanIo): number {
132
124
  const runDir = join(opts.root, '.spur', 'run');
133
125
  const scan = JSON.parse(
@@ -236,8 +228,14 @@ export function main(argv: string[], env: ScanEnv = getEnvVars(), options: ScanO
236
228
  err: (line) => process.stderr.write(line),
237
229
  };
238
230
  const cwd = options.cwd ?? process.cwd();
239
- const opts = parseArgs(argv);
240
- const modes: Record<string, ModeFn> = { scan: scanMode, fold: foldMode, settle: settleMode, report: reportMode };
231
+ const opts = core.parseScanArgs(argv, cwd, tmpdir());
232
+ const modes: Record<string, ModeFn> = {
233
+ scan: scanMode,
234
+ fold: foldMode,
235
+ settle: settleMode,
236
+ report: reportMode,
237
+ 'review-gate': reviewGateMode,
238
+ };
241
239
  const modeFn = opts === null ? undefined : modes[opts.mode];
242
240
  if (opts === null || modeFn === undefined) {
243
241
  io.err(`${RESIDUAL_SCAN_USAGE}\n`);