@gobing-ai/spur 0.3.99 → 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 (172) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/gates.yaml +36 -0
  4. package/config/decisions/history-anatomy.yaml +35 -0
  5. package/config/decisions/idea-pipeline.yaml +38 -0
  6. package/config/decisions/task-pipeline.yaml +55 -0
  7. package/config/plugin-scripts.json +25 -0
  8. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  9. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  10. package/config/rules/quality/coverage-gate.yaml +5 -3
  11. package/config/rules/recommended-pre-check.yaml +8 -3
  12. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  13. package/config/rules/structure/protected-files.yaml +3 -0
  14. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  15. package/config/script-placement-baseline.json +2 -2
  16. package/config/workflows/feature-lifecycle.yaml +9 -2
  17. package/config/workflows/history-anatomy.yaml +22 -0
  18. package/config/workflows/idea-pipeline.yaml +75 -0
  19. package/config/workflows/task-pipeline.yaml +368 -49
  20. package/config/workflows/wrapup-pipeline.yaml +106 -13
  21. package/package.json +9 -9
  22. package/plugins/sp/README.md +25 -2
  23. package/plugins/sp/agents/super-planner.md +11 -0
  24. package/plugins/sp/agents/super-reviewer.md +39 -11
  25. package/plugins/sp/bunfig.toml +8 -0
  26. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  27. package/plugins/sp/commands/dev-changelog.md +5 -4
  28. package/plugins/sp/commands/dev-find-way.md +80 -0
  29. package/plugins/sp/commands/dev-idea.md +6 -0
  30. package/plugins/sp/commands/dev-parallel.md +8 -2
  31. package/plugins/sp/commands/dev-plan.md +6 -0
  32. package/plugins/sp/commands/dev-review-session.md +14 -7
  33. package/plugins/sp/commands/dev-run.md +17 -4
  34. package/plugins/sp/commands/dev-runall.md +21 -10
  35. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  36. package/plugins/sp/hooks/context-session-start.ts +1 -1
  37. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  38. package/plugins/sp/hooks/hooks.json +45 -1
  39. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
  42. package/plugins/sp/lib/inline-run-app.ts +28 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
  44. package/plugins/sp/lib/inline-run.generated.mjs +364 -46
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
  48. package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  50. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  51. package/plugins/sp/lib/run-summary-core.ts +561 -0
  52. package/plugins/sp/lib/transcript.ts +275 -0
  53. package/plugins/sp/plugin.json +1 -1
  54. package/plugins/sp/references/roles.md +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +335 -0
  56. package/plugins/sp/scripts/batch-plan.ts +100 -0
  57. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  58. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  59. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  60. package/plugins/sp/scripts/commit-guard.ts +249 -0
  61. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  62. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  63. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  64. package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
  65. package/plugins/sp/scripts/inline-run-setup.ts +65 -97
  66. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  67. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  68. package/plugins/sp/scripts/quality-gate.mjs +30407 -73
  69. package/plugins/sp/scripts/quality-gate.ts +21 -5
  70. package/plugins/sp/scripts/residual-scan.mjs +88 -38
  71. package/plugins/sp/scripts/residual-scan.ts +35 -38
  72. package/plugins/sp/scripts/run-summary.mjs +693 -0
  73. package/plugins/sp/scripts/run-summary.ts +105 -0
  74. package/plugins/sp/scripts/session-timeline.mjs +331 -0
  75. package/plugins/sp/scripts/session-timeline.ts +204 -0
  76. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  77. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  78. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  79. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  80. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  81. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  82. package/plugins/sp/skills/code-verification/SKILL.md +8 -4
  83. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  84. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  85. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  86. package/plugins/sp/skills/session-review/SKILL.md +77 -24
  87. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  88. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  89. package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
  90. package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
  91. package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
  92. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  93. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
  94. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  95. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  96. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  97. package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
  98. package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
  99. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  103. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
  104. package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
  105. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
  106. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
  107. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  108. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  109. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
  110. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  111. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
  112. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  113. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  114. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  115. package/schemas/decision-catalog.schema.json +226 -0
  116. package/schemas/spur-config.schema.json +25 -0
  117. package/schemas/state-machine-workflow.schema.json +23 -0
  118. package/spur.js +28165 -19051
  119. package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
  120. package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
  121. package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
  122. package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
  123. package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
  124. package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
  125. package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
  126. package/web/_astro/channel.DZVfto-r.js +1 -0
  127. package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
  128. package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
  129. package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
  130. package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
  131. package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
  132. package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
  133. package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
  134. package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
  135. package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
  136. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
  137. package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
  138. package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
  139. package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
  140. package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
  141. package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
  142. package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
  143. package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
  144. package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
  145. package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
  146. package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
  147. package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
  148. package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
  149. package/web/_astro/index.COh_kBxN.css +1 -0
  150. package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
  151. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
  152. package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
  153. package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
  154. package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
  155. package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
  156. package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
  157. package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
  158. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
  159. package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
  160. package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
  161. package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
  162. package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
  163. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
  164. package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
  165. package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
  166. package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
  167. package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
  168. package/web/board-runtime.json +1 -1
  169. package/web/index.html +2 -2
  170. package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
  171. package/web/_astro/channel.6IFIKBhu.js +0 -1
  172. package/web/_astro/index.CbNz17Sx.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"));
@@ -427,12 +472,7 @@ function settleMode(opts, env, io) {
427
472
  prior = JSON.parse(fs.readFileSync(residualsPath, "utf8"));
428
473
  const deferred = scan.items.filter((i) => i.class === "deferrable");
429
474
  if (deferred.length > 0 && prior.followUp === undefined) {
430
- if (task.featureId === "") {
431
- io.err(`residual-settle: ${wbs} deferrals pending but feature_id unknown; re-run: residual-scan settle ${wbs}
432
- `);
433
- return 0;
434
- }
435
- const args = ["task", "create", `Residuals from ${wbs}`, "--feature", task.featureId, "--skip-ready", "--json"];
475
+ const args = ["task", "create", `Residuals from ${wbs}`, "--skip-ready", "--json"];
436
476
  const created = spur(env, opts.spurBin, args, opts.root);
437
477
  let wbsNew = "";
438
478
  try {
@@ -448,7 +488,7 @@ function settleMode(opts, env, io) {
448
488
  return 0;
449
489
  }
450
490
  const rows = deferred.map((i) => `- ${i.id} \u2014 ${i.location}: ${i.text}`);
451
- const head = `Source task: ${wbs} (feature ${task.featureId}) \u2014 deferred residuals filed by residual-scan settle.`;
491
+ const head = `Source task: ${wbs}${task.featureId === "" ? "" : ` (feature ${task.featureId})`} \u2014 deferred residuals filed by residual-scan settle (unlinked: a deferral must not hold the completing feature open).`;
452
492
  const bgFile = join2(runDir, `${wbs}-residual-background.md`);
453
493
  fs.writeFileSync(bgFile, `${[head, "", ...rows].join(`
454
494
  `)}
@@ -497,8 +537,14 @@ function main(argv, env = getEnvVars(), options = {}) {
497
537
  err: (line) => process.stderr.write(line)
498
538
  };
499
539
  const cwd = options.cwd ?? process.cwd();
500
- const opts = parseArgs(argv);
501
- 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
+ };
502
548
  const modeFn = opts === null ? undefined : modes[opts.mode];
503
549
  if (opts === null || modeFn === undefined) {
504
550
  io.err(`${RESIDUAL_SCAN_USAGE}
@@ -514,8 +560,10 @@ export {
514
560
  scanResiduals2 as scanResiduals,
515
561
  renderReport,
516
562
  recordedVerdictPath,
563
+ parseScanArgs,
517
564
  parseReviewFindings,
518
565
  parseDiffMarkers,
566
+ parseDeferralEntries,
519
567
  normalizeAnchor,
520
568
  makeItemId,
521
569
  main,
@@ -525,6 +573,8 @@ export {
525
573
  findUncheckedBoxes,
526
574
  collectAddedLines,
527
575
  classify,
576
+ buildReviewGateArtifact,
577
+ blockingReviewFindings,
528
578
  blockingAnchors,
529
579
  RESIDUAL_SCAN_USAGE,
530
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(
@@ -170,13 +162,12 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
170
162
  if (fs.existsSync(residualsPath)) prior = JSON.parse(fs.readFileSync(residualsPath, 'utf8'));
171
163
  const deferred = scan.items.filter((i) => i.class === 'deferrable');
172
164
  if (deferred.length > 0 && prior.followUp === undefined) {
173
- if (task.featureId === '') {
174
- io.err(
175
- `residual-settle: ${wbs} deferrals pending but feature_id unknown; re-run: residual-scan settle ${wbs}\n`,
176
- );
177
- return 0;
178
- }
179
- const args = ['task', 'create', `Residuals from ${wbs}`, '--feature', task.featureId, '--skip-ready', '--json'];
165
+ // The follow-up is filed UNLINKED: carrying a feature edge to the feature whose run produced
166
+ // it made that feature's done-gate fail with `L4.verifying-incomplete-tasks` the moment a
167
+ // residual was deferred (2026-10-04 session, E72/1086→1087), so a deferral must not be able
168
+ // to block the completing feature. The source task and its feature stay named in the
169
+ // Background, which keeps the traceability without the lifecycle coupling.
170
+ const args = ['task', 'create', `Residuals from ${wbs}`, '--skip-ready', '--json'];
180
171
  const created = spur(env, opts.spurBin, args, opts.root);
181
172
  let wbsNew = '';
182
173
  try {
@@ -191,7 +182,7 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
191
182
  return 0;
192
183
  }
193
184
  const rows = deferred.map((i) => `- ${i.id} — ${i.location}: ${i.text}`);
194
- const head = `Source task: ${wbs} (feature ${task.featureId}) — deferred residuals filed by residual-scan settle.`;
185
+ 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).`;
195
186
  const bgFile = join(runDir, `${wbs}-residual-background.md`);
196
187
  fs.writeFileSync(bgFile, `${[head, '', ...rows].join('\n')}\n`);
197
188
  const updArgs = ['task', 'update', wbsNew, '--section', 'Background', '--from-file', bgFile];
@@ -237,8 +228,14 @@ export function main(argv: string[], env: ScanEnv = getEnvVars(), options: ScanO
237
228
  err: (line) => process.stderr.write(line),
238
229
  };
239
230
  const cwd = options.cwd ?? process.cwd();
240
- const opts = parseArgs(argv);
241
- 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
+ };
242
239
  const modeFn = opts === null ? undefined : modes[opts.mode];
243
240
  if (opts === null || modeFn === undefined) {
244
241
  io.err(`${RESIDUAL_SCAN_USAGE}\n`);