@gobing-ai/spur 0.4.3 → 0.4.4

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 (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/rules/docs/verdict-confidence.yaml +68 -0
  3. package/config/rules/quality/coverage-gate.yaml +13 -0
  4. package/config/rules/recommended-pre-check.yaml +6 -2
  5. package/config/rules/strict/runtime-boundaries.yaml +3 -0
  6. package/config/rules/surface/check-cli-surface.yaml +1 -3
  7. package/config/templates/AGENTS.md +7 -0
  8. package/config/workflows/feature-verification.yaml +9 -1
  9. package/config/workflows/task-lifecycle.yaml +50 -0
  10. package/config/workflows/task-pipeline.yaml +15 -9
  11. package/package.json +10 -10
  12. package/plugins/sp/agents/super-reviewer.md +5 -2
  13. package/plugins/sp/commands/dev-changelog.md +1 -1
  14. package/plugins/sp/commands/dev-gitmsg.md +6 -2
  15. package/plugins/sp/commands/dev-handover.md +1 -0
  16. package/plugins/sp/commands/dev-job-dump.md +2 -1
  17. package/plugins/sp/commands/dev-refine.md +14 -2
  18. package/plugins/sp/commands/dev-refineall.md +7 -1
  19. package/plugins/sp/commands/dev-review.md +1 -0
  20. package/plugins/sp/lib/history-anatomy.generated.mjs +18 -18
  21. package/plugins/sp/lib/idea-handoff.generated.mjs +91 -91
  22. package/plugins/sp/lib/inline-run.generated.mjs +233 -277
  23. package/plugins/sp/lib/plan-projection.generated.mjs +62 -38
  24. package/plugins/sp/lib/quality-gate.generated.d.mts +1 -0
  25. package/plugins/sp/lib/quality-gate.generated.mjs +13793 -13966
  26. package/plugins/sp/lib/residual-scan.generated.d.mts +13 -0
  27. package/plugins/sp/lib/residual-scan.generated.mjs +51 -21
  28. package/plugins/sp/lib/step-profile.generated.mjs +8 -8
  29. package/plugins/sp/plugin.json +1 -1
  30. package/plugins/sp/scripts/batch-plan.mjs +62 -40
  31. package/plugins/sp/scripts/batch-preflight.mjs +34 -12
  32. package/plugins/sp/scripts/commit-guard.mjs +6 -6
  33. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +26 -28
  34. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +11 -11
  35. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +4 -4
  36. package/plugins/sp/scripts/feature-verification-steps.mjs +15 -4
  37. package/plugins/sp/scripts/feature-verification-steps.ts +29 -1
  38. package/plugins/sp/scripts/history-anatomy-cache.mjs +19 -19
  39. package/plugins/sp/scripts/idea-handoff.mjs +2 -2
  40. package/plugins/sp/scripts/inline-run-dispatch.mjs +2 -2
  41. package/plugins/sp/scripts/inline-run-setup.mjs +20 -20
  42. package/plugins/sp/scripts/persist-out-check.mjs +5 -5
  43. package/plugins/sp/scripts/pr-reviewing.mjs +13 -13
  44. package/plugins/sp/scripts/quality-gate.mjs +13715 -13884
  45. package/plugins/sp/scripts/residual-scan.mjs +58 -27
  46. package/plugins/sp/scripts/residual-scan.ts +8 -8
  47. package/plugins/sp/scripts/run-summary.mjs +15 -15
  48. package/plugins/sp/scripts/script-root.mjs +6 -6
  49. package/plugins/sp/scripts/session-timeline.mjs +16 -16
  50. package/plugins/sp/scripts/task-diffstat.mjs +8 -8
  51. package/plugins/sp/scripts/workflow-step-profile.mjs +12 -12
  52. package/plugins/sp/scripts/wrapup-drift-probe.mjs +9 -9
  53. package/plugins/sp/scripts/wrapup-steps.mjs +25 -25
  54. package/plugins/sp/skills/next-router/SKILL.md +3 -1
  55. package/plugins/sp/skills/next-router/references/routing-table.md +9 -3
  56. package/plugins/sp/skills/spur-cli/references/tasks.md +24 -4
  57. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +8 -1
  58. package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -11
  59. package/plugins/sp/skills/spur-dev/references/execution-batch.md +6 -3
  60. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +4 -2
  61. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +22 -2
  62. package/schemas/section-matrix.schema.json +3 -2
  63. package/spur.js +24403 -32459
  64. package/web/_astro/{BoardApp.5IIl2O-z.js → BoardApp.Niubpe6z.js} +1 -1
  65. package/web/_astro/{BoardApp.Drnx-KeM.js → BoardApp.hF0RAIrg.js} +112 -112
  66. package/web/_astro/TaskDetail.DJpapt38.js +1 -0
  67. package/web/_astro/{arc.j2EVkYVd.js → arc.BgKcK0i8.js} +1 -1
  68. package/web/_astro/{architectureDiagram-3BPJPVTR.DZWSFqEG.js → architectureDiagram-3BPJPVTR.ChP-Ig0V.js} +1 -1
  69. package/web/_astro/{blockDiagram-GPEHLZMM.DE6GpXCN.js → blockDiagram-GPEHLZMM.BVaCBoeN.js} +1 -1
  70. package/web/_astro/{c4Diagram-AAUBKEIU.DoVR1G6B.js → c4Diagram-AAUBKEIU.CHSDpEO5.js} +1 -1
  71. package/web/_astro/channel.DkpvN2tD.js +1 -0
  72. package/web/_astro/{chunk-2J33WTMH.Do6-Gbyf.js → chunk-2J33WTMH.MYRcYVRa.js} +1 -1
  73. package/web/_astro/{chunk-4BX2VUAB.BYV1HGHx.js → chunk-4BX2VUAB.S5j22Zqa.js} +1 -1
  74. package/web/_astro/{chunk-55IACEB6.CvI1zkrc.js → chunk-55IACEB6.gciRPvmZ.js} +1 -1
  75. package/web/_astro/{chunk-727SXJPM.DNGSCRlX.js → chunk-727SXJPM.CdqrSQkX.js} +1 -1
  76. package/web/_astro/{chunk-AQP2D5EJ.DXhhgT5z.js → chunk-AQP2D5EJ.CyokdHT3.js} +1 -1
  77. package/web/_astro/{chunk-FMBD7UC4.dzkRKP2u.js → chunk-FMBD7UC4.Bg9KJhjW.js} +1 -1
  78. package/web/_astro/{chunk-ND2GUHAM.DujGLeEf.js → chunk-ND2GUHAM.1gxxI-vD.js} +1 -1
  79. package/web/_astro/{chunk-QZHKN3VN.BWtIT3ZU.js → chunk-QZHKN3VN.DDEYtCKU.js} +1 -1
  80. package/web/_astro/{classDiagram-4FO5ZUOK.DyApg0Nn.js → classDiagram-4FO5ZUOK.Ch3zi37T.js} +1 -1
  81. package/web/_astro/{classDiagram-v2-Q7XG4LA2.DyApg0Nn.js → classDiagram-v2-Q7XG4LA2.Ch3zi37T.js} +1 -1
  82. package/web/_astro/{cose-bilkent-S5V4N54A.DJQB-u_l.js → cose-bilkent-S5V4N54A.D2q5AINI.js} +1 -1
  83. package/web/_astro/{cynefin-OW5HDTMX.bi3D_siV.js → cynefin-OW5HDTMX.BA9kC2YT.js} +1 -1
  84. package/web/_astro/{dagre-BM42HDAG.C-f7TrRg.js → dagre-BM42HDAG.CHO08nRp.js} +1 -1
  85. package/web/_astro/{diagram-2AECGRRQ.DrPfjHQB.js → diagram-2AECGRRQ.BfFQt0LW.js} +1 -1
  86. package/web/_astro/{diagram-5GNKFQAL.C9S4WUHO.js → diagram-5GNKFQAL.CHMwMTe4.js} +1 -1
  87. package/web/_astro/{diagram-KO2AKTUF.Dzzye8QH.js → diagram-KO2AKTUF.BJ-7gK-w.js} +1 -1
  88. package/web/_astro/{diagram-LMA3HP47.DX-LRiof.js → diagram-LMA3HP47.C5xqcc0t.js} +1 -1
  89. package/web/_astro/{diagram-OG6HWLK6.Z-kAKwIz.js → diagram-OG6HWLK6.C2pQjfrC.js} +1 -1
  90. package/web/_astro/{erDiagram-TEJ5UH35.D6dyClSv.js → erDiagram-TEJ5UH35._FoNyxgC.js} +1 -1
  91. package/web/_astro/{flowDiagram-I6XJVG4X.Cx3q2cxD.js → flowDiagram-I6XJVG4X.B_ENkT2I.js} +1 -1
  92. package/web/_astro/{ganttDiagram-6RSMTGT7.DU3gH6CT.js → ganttDiagram-6RSMTGT7.B3q8h77C.js} +1 -1
  93. package/web/_astro/{gitGraphDiagram-PVQCEYII.ISmCnJ87.js → gitGraphDiagram-PVQCEYII.QWZFDboZ.js} +1 -1
  94. package/web/_astro/index.BFyW13JJ.css +1 -0
  95. package/web/_astro/{infoDiagram-5YYISTIA.DR6_iNOK.js → infoDiagram-5YYISTIA.Cs6eEc92.js} +1 -1
  96. package/web/_astro/{ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js → ishikawaDiagram-YF4QCWOH.BU9DJMHA.js} +1 -1
  97. package/web/_astro/{journeyDiagram-JHISSGLW.DkP9-WK8.js → journeyDiagram-JHISSGLW.D35k6Jpe.js} +1 -1
  98. package/web/_astro/{kanban-definition-UN3LZRKU.wtgY98M2.js → kanban-definition-UN3LZRKU.DBqcAT4m.js} +1 -1
  99. package/web/_astro/{linear.CI6djNi7.js → linear.D-LSNK8W.js} +1 -1
  100. package/web/_astro/{mermaid.core.CuQPxS8b.js → mermaid.core.CSruWVOa.js} +4 -4
  101. package/web/_astro/{mindmap-definition-RKZ34NQL.5HNIEG7R.js → mindmap-definition-RKZ34NQL.BiFaUsfj.js} +1 -1
  102. package/web/_astro/{pieDiagram-4H26LBE5.CchhDFjE.js → pieDiagram-4H26LBE5.D1KTKqb9.js} +1 -1
  103. package/web/_astro/{quadrantDiagram-W4KKPZXB.PYXXJPSJ.js → quadrantDiagram-W4KKPZXB.DBHflsMG.js} +1 -1
  104. package/web/_astro/{requirementDiagram-4Y6WPE33.cUQuNAKU.js → requirementDiagram-4Y6WPE33.hwX9CNdk.js} +1 -1
  105. package/web/_astro/{sankeyDiagram-5OEKKPKP.BO7MnRIA.js → sankeyDiagram-5OEKKPKP.DkI8mJiE.js} +1 -1
  106. package/web/_astro/{sequenceDiagram-3UESZ5HK.D9bElyD6.js → sequenceDiagram-3UESZ5HK.DMz7jBDx.js} +1 -1
  107. package/web/_astro/{stateDiagram-AJRCARHV.DhGdRz7I.js → stateDiagram-AJRCARHV.B6Ef2xJ1.js} +1 -1
  108. package/web/_astro/{stateDiagram-v2-BHNVJYJU.5GvLeAoL.js → stateDiagram-v2-BHNVJYJU.CgoaufB9.js} +1 -1
  109. package/web/_astro/{timeline-definition-PNZ67QCA.LtAT9haM.js → timeline-definition-PNZ67QCA.B3c8h1t-.js} +1 -1
  110. package/web/_astro/{vennDiagram-CIIHVFJN.DRth1yLA.js → vennDiagram-CIIHVFJN.BdHPAxAb.js} +1 -1
  111. package/web/_astro/{wardleyDiagram-YWT4CUSO.h9MlZfcr.js → wardleyDiagram-YWT4CUSO.BrJXPPAj.js} +1 -1
  112. package/web/_astro/{xychartDiagram-2RQKCTM6.DHqy9DYA.js → xychartDiagram-2RQKCTM6.CMH5yFsV.js} +1 -1
  113. package/web/index.html +2 -2
  114. package/web/spur_logo.svg +1 -1
  115. package/web/_astro/TaskDetail.Ci4LNn9S.js +0 -1
  116. package/web/_astro/channel.DmdDxGsq.js +0 -1
  117. package/web/_astro/index.Do2lLdgo.css +0 -1
@@ -229,10 +229,35 @@ function reviewContractViolations(taskContent) {
229
229
  }
230
230
  return violations;
231
231
  }
232
- function buildReviewGateArtifact(wbs, taskContent, deferrals) {
232
+ function reviewConfidenceViolation(reviewAnswer, ackLowConfidence) {
233
+ if (ackLowConfidence === "true")
234
+ return null;
235
+ const level = extractAnswerConfidence(reviewAnswer.replace(/\*\*/g, ""));
236
+ if (level === "HIGH" || level === "MEDIUM")
237
+ return null;
238
+ return {
239
+ location: "review-answer:Confidence",
240
+ text: level === undefined ? "The review answer carries no valid `**Confidence:** HIGH | MEDIUM | LOW` line — a " + "PASS must be graded, or the operator must set ackLowConfidence" : `The review answer graded its PASS ${level} — HIGH/MEDIUM cross unadorned, ` + "LOW/absent needs the operator’s ackLowConfidence"
241
+ };
242
+ }
243
+ function reviewGradingInput(root, reviewAnswer, ackLowConfidence, fs) {
244
+ if (reviewAnswer === undefined)
245
+ return;
246
+ const path = join(root, reviewAnswer);
247
+ let isFile = false;
248
+ try {
249
+ isFile = fs.statSync(path).isFile();
250
+ } catch {
251
+ isFile = false;
252
+ }
253
+ return { answer: isFile ? fs.readFileSync(path, "utf8") : "", ackLowConfidence };
254
+ }
255
+ function buildReviewGateArtifact(wbs, taskContent, deferrals, review) {
256
+ const confidence = review === undefined ? null : reviewConfidenceViolation(review.answer, review.ackLowConfidence);
257
+ const contractViolations = [...reviewContractViolations(taskContent), ...confidence === null ? [] : [confidence]];
233
258
  const items = [
234
259
  ...blockingReviewFindings(taskContent, deferrals),
235
- ...reviewContractViolations(taskContent).map((v) => ({
260
+ ...contractViolations.map((v) => ({
236
261
  id: makeItemId("review-contract", v.location, v.text),
237
262
  category: "review-contract",
238
263
  class: "blocking",
@@ -365,6 +390,7 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
365
390
  let spurBin;
366
391
  let root = cwd;
367
392
  let tmpDir = defaultTmpDir;
393
+ let reviewAnswer;
368
394
  for (let i = 0;i < argv.length; i++) {
369
395
  const a = argv[i];
370
396
  if (a === undefined)
@@ -375,6 +401,8 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
375
401
  root = argv[++i] ?? root;
376
402
  else if (a === "--tmp-dir")
377
403
  tmpDir = argv[++i] ?? tmpDir;
404
+ else if (a === "--review-answer")
405
+ reviewAnswer = argv[++i];
378
406
  else if (a === "--help" || a === "-h")
379
407
  return null;
380
408
  else if (mode === "")
@@ -382,7 +410,7 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
382
410
  else if (wbs === "")
383
411
  wbs = a;
384
412
  }
385
- return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
413
+ return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir, reviewAnswer };
386
414
  }
387
415
 
388
416
  // plugins/sp/lib/spur-bin.ts
@@ -392,7 +420,7 @@ function spurCommand(spurBin) {
392
420
  }
393
421
 
394
422
  // plugins/sp/scripts/residual-scan.ts
395
- var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
423
+ var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>] [--review-answer <path>]";
396
424
  function run(cmd, args, cwd) {
397
425
  const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
398
426
  if (result.error !== undefined)
@@ -487,7 +515,8 @@ function reviewGateMode(opts, env, io) {
487
515
  const runDir = join2(opts.root, ".spur", "run");
488
516
  fs.mkdirSync(runDir, { recursive: true });
489
517
  const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
490
- const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
518
+ const grading = reviewGradingInput(opts.root, opts.reviewAnswer, env.ackLowConfidence, fs);
519
+ const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs), grading);
491
520
  fs.writeFileSync(join2(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}
492
521
  `);
493
522
  io.out(gate.note);
@@ -622,27 +651,29 @@ function main(argv, env = getEnvVars(), options = {}) {
622
651
  }
623
652
  process.exit(main(process.argv.slice(2)));
624
653
  export {
625
- verdictDisagreementNote,
626
- scanResiduals2 as scanResiduals,
627
- reviewContractViolations,
628
- renderReport,
629
- recordedVerdictPath,
630
- parseScanArgs,
631
- parseReviewFindings,
632
- parseDiffMarkers,
633
- parseDeferralEntries,
634
- normalizeAnchor,
635
- makeItemId,
636
- main,
637
- locationOf,
638
- listStagingResidue,
639
- foldVerdict,
640
- findUncheckedBoxes,
641
- collectAddedLines,
642
- classify,
643
- buildReviewGateArtifact,
644
- blockingReviewFindings,
645
- blockingAnchors,
654
+ ALLOW_PRAGMA,
646
655
  RESIDUAL_SCAN_USAGE,
647
- ALLOW_PRAGMA
656
+ blockingAnchors,
657
+ blockingReviewFindings,
658
+ buildReviewGateArtifact,
659
+ classify,
660
+ collectAddedLines,
661
+ findUncheckedBoxes,
662
+ foldVerdict,
663
+ listStagingResidue,
664
+ locationOf,
665
+ main,
666
+ makeItemId,
667
+ normalizeAnchor,
668
+ parseDeferralEntries,
669
+ parseDiffMarkers,
670
+ parseReviewFindings,
671
+ parseScanArgs,
672
+ recordedVerdictPath,
673
+ renderReport,
674
+ reviewConfidenceViolation,
675
+ reviewContractViolations,
676
+ reviewGradingInput,
677
+ scanResiduals2 as scanResiduals,
678
+ verdictDisagreementNote
648
679
  };
@@ -10,7 +10,7 @@ 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|review-gate> <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>] [--review-answer <path>]';
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 };
@@ -104,18 +104,18 @@ function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
104
104
  return 0;
105
105
  }
106
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.
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, or on a review answer graded LOW or left
110
+ * ungraded without `env.ackLowConfidence` (task 1204, graded via `core.reviewGradingInput` — it
111
+ * cannot sit inline in the edge because ADR-115 caps a guard at 5 logical commands).
113
112
  */
114
113
  function reviewGateMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
115
114
  const runDir = join(opts.root, '.spur', 'run');
116
115
  fs.mkdirSync(runDir, { recursive: true });
117
116
  const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
118
- const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
117
+ const grading = core.reviewGradingInput(opts.root, opts.reviewAnswer, env.ackLowConfidence, fs);
118
+ const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs), grading);
119
119
  fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}\n`);
120
120
  io.out(gate.note);
121
121
  return gate.artifact.counts.blocking > 0 ? 1 : 0;
@@ -114,21 +114,21 @@ function accumulate(acc, row, ts) {
114
114
  if (row.type === "message") {
115
115
  const m = row.message;
116
116
  if (m?.role === "assistant") {
117
- const blocks2 = Array.isArray(m.content) ? m.content : [];
118
- for (const b of blocks2) {
117
+ const blocks = Array.isArray(m.content) ? m.content : [];
118
+ for (const b of blocks) {
119
119
  if (b.type === "toolCall" && b.id) {
120
120
  acc.toolIds.add(b.id);
121
121
  if (b.name && OPERATOR_TOOLS.has(b.name))
122
122
  acc.asks.set(b.id, ts);
123
123
  }
124
124
  }
125
- const u2 = m.usage;
126
- if (u2 && row.id)
125
+ const u = m.usage;
126
+ if (u && row.id)
127
127
  acc.messages.set(row.id, {
128
- input: u2.input ?? 0,
129
- cacheCreate: u2.cacheWrite ?? 0,
130
- cacheRead: u2.cacheRead ?? 0,
131
- output: u2.output ?? 0
128
+ input: u.input ?? 0,
129
+ cacheCreate: u.cacheWrite ?? 0,
130
+ cacheRead: u.cacheRead ?? 0,
131
+ output: u.output ?? 0
132
132
  });
133
133
  } else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
134
134
  acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
@@ -747,12 +747,12 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
747
747
  }
748
748
  process.exit(main(process.argv.slice(2)));
749
749
  export {
750
- writeCloseSummary,
751
- renderSummaryMarkdown,
752
- renderRollupMarkdown,
753
- main,
754
- gateFromReceipt,
755
- buildRunSummary,
750
+ RUN_SUMMARY_USAGE,
756
751
  actorSplit,
757
- RUN_SUMMARY_USAGE
752
+ buildRunSummary,
753
+ gateFromReceipt,
754
+ main,
755
+ renderRollupMarkdown,
756
+ renderSummaryMarkdown,
757
+ writeCloseSummary
758
758
  };
@@ -119,11 +119,11 @@ function main(argv, env = getEnvVars(), options = {}) {
119
119
  process.exit(main(process.argv.slice(2)));
120
120
  }
121
121
  export {
122
- runScriptRoot,
123
- main,
124
- digestScriptSet,
125
- SOURCE_REPO_MARKER,
126
- SCRIPT_ROOT_USAGE,
122
+ PROJECT_SCRIPTS_DIR,
127
123
  SCRIPT_ROOT_TWIN_REL,
128
- PROJECT_SCRIPTS_DIR
124
+ SCRIPT_ROOT_USAGE,
125
+ SOURCE_REPO_MARKER,
126
+ digestScriptSet,
127
+ main,
128
+ runScriptRoot
129
129
  };
@@ -107,21 +107,21 @@ function accumulate(acc, row, ts) {
107
107
  if (row.type === "message") {
108
108
  const m = row.message;
109
109
  if (m?.role === "assistant") {
110
- const blocks2 = Array.isArray(m.content) ? m.content : [];
111
- for (const b of blocks2) {
110
+ const blocks = Array.isArray(m.content) ? m.content : [];
111
+ for (const b of blocks) {
112
112
  if (b.type === "toolCall" && b.id) {
113
113
  acc.toolIds.add(b.id);
114
114
  if (b.name && OPERATOR_TOOLS.has(b.name))
115
115
  acc.asks.set(b.id, ts);
116
116
  }
117
117
  }
118
- const u2 = m.usage;
119
- if (u2 && row.id)
118
+ const u = m.usage;
119
+ if (u && row.id)
120
120
  acc.messages.set(row.id, {
121
- input: u2.input ?? 0,
122
- cacheCreate: u2.cacheWrite ?? 0,
123
- cacheRead: u2.cacheRead ?? 0,
124
- output: u2.output ?? 0
121
+ input: u.input ?? 0,
122
+ cacheCreate: u.cacheWrite ?? 0,
123
+ cacheRead: u.cacheRead ?? 0,
124
+ output: u.output ?? 0
125
125
  });
126
126
  } else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
127
127
  acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
@@ -347,13 +347,13 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
347
347
  }
348
348
  process.exit(main(process.argv.slice(2)));
349
349
  export {
350
- zeroSegmentReason,
351
- resolveTranscript,
352
- parseGroups,
353
- main,
354
- formatTokens,
355
- formatTokenSplit,
356
- formatDuration,
350
+ SESSION_TIMELINE_USAGE,
357
351
  buildTimeline,
358
- SESSION_TIMELINE_USAGE
352
+ formatDuration,
353
+ formatTokenSplit,
354
+ formatTokens,
355
+ main,
356
+ parseGroups,
357
+ resolveTranscript,
358
+ zeroSegmentReason
359
359
  };
@@ -84,9 +84,9 @@ function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
84
84
  const failSafe = (reason) => {
85
85
  process.stderr.write(`task-diffstat: ${reason} \u2014 failing safe (sensitive)
86
86
  `);
87
- const row2 = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
88
- writeArtifact(abs(relResult), row2);
89
- return { ...row2, resultFile: relResult, exitCode: 0 };
87
+ const row = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
88
+ writeArtifact(abs(relResult), row);
89
+ return { ...row, resultFile: relResult, exitCode: 0 };
90
90
  };
91
91
  let stdoutIsArtifact = false;
92
92
  try {
@@ -172,10 +172,10 @@ function main(argv, env = getEnvVars(), options = {}) {
172
172
  process.exit(main(process.argv.slice(2)));
173
173
  }
174
174
  export {
175
- sensitiveReasonForPath,
176
- runDiffstat,
177
- main,
178
- TASK_DIFFSTAT_USAGE,
175
+ SENSITIVE_GLOBS,
179
176
  SENSITIVE_PREFIXES,
180
- SENSITIVE_GLOBS
177
+ TASK_DIFFSTAT_USAGE,
178
+ main,
179
+ runDiffstat,
180
+ sensitiveReasonForPath
181
181
  };
@@ -322,17 +322,17 @@ function main(argv) {
322
322
  process.exit(main(process.argv.slice(2)));
323
323
  }
324
324
  export {
325
- runStepProfileCli,
326
- rowFlags,
327
- parseStepProfileCliArgs,
328
- nonDryRuns,
329
- nearestRankP50,
330
- main,
331
- formatStepProfileHuman,
332
- extractExecutions,
333
- buildStepProfile,
334
- buildRows,
335
- STEP_PROFILE_USAGE,
325
+ DEFAULT_LAST,
336
326
  DEFAULT_WINDOW_SEC,
337
- DEFAULT_LAST
327
+ STEP_PROFILE_USAGE,
328
+ buildRows,
329
+ buildStepProfile,
330
+ extractExecutions,
331
+ formatStepProfileHuman,
332
+ main,
333
+ nearestRankP50,
334
+ nonDryRuns,
335
+ parseStepProfileCliArgs,
336
+ rowFlags,
337
+ runStepProfileCli
338
338
  };
@@ -114,13 +114,13 @@ function runDriftProbe(env, options = {}, spur = defaultSpurRunner(env, options.
114
114
  mkdirSync(abs(join(".spur", "run")), { recursive: true });
115
115
  writeFileSync(abs(relModeFile), `
116
116
  `);
117
- const finish = (clean, reasons2, paths) => {
118
- writeFileSync(abs(relProbeFile), `${JSON.stringify({ clean, reasons: reasons2, paths })}
117
+ const finish = (clean, reasons, paths) => {
118
+ writeFileSync(abs(relProbeFile), `${JSON.stringify({ clean, reasons, paths })}
119
119
  `);
120
120
  writeFileSync(abs(relModeFile), clean ? `fast
121
121
  ` : `
122
122
  `);
123
- return { clean, reasons: reasons2, paths, probeFile: relProbeFile, modeFile: relModeFile, exitCode: 0 };
123
+ return { clean, reasons, paths, probeFile: relProbeFile, modeFile: relModeFile, exitCode: 0 };
124
124
  };
125
125
  let tasks;
126
126
  try {
@@ -180,11 +180,11 @@ function main(argv, env = getEnvVars(), options = {}) {
180
180
  process.exit(main(process.argv.slice(2)));
181
181
  }
182
182
  export {
183
- solutionSectionOf,
184
- runDriftProbe,
185
- main,
186
- driftReasonForPath,
187
- changedPathsOf,
183
+ DOC_OWNED_SURFACES,
188
184
  WRAPUP_DRIFT_PROBE_USAGE,
189
- DOC_OWNED_SURFACES
185
+ changedPathsOf,
186
+ driftReasonForPath,
187
+ main,
188
+ runDriftProbe,
189
+ solutionSectionOf
190
190
  };
@@ -296,7 +296,7 @@ function runMetrics(env, options = {}) {
296
296
  const parsed = JSON.parse(shown.stdout);
297
297
  const frontmatter = parsed.frontmatter !== null && typeof parsed.frontmatter === "object" ? parsed.frontmatter : {};
298
298
  const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ""));
299
- const status2 = String(jqPick(frontmatter.status, parsed.status, "unknown"));
299
+ const status = String(jqPick(frontmatter.status, parsed.status, "unknown"));
300
300
  const durable = join(".spur", "memory", "evidence", `${wbs}-verdict.json`);
301
301
  const hasDurable = existsSync(abs(durable));
302
302
  const verdictPath = hasDurable ? durable : join(".spur", "run", `${wbs}-verdict.json`);
@@ -308,7 +308,7 @@ function runMetrics(env, options = {}) {
308
308
  `);
309
309
  }
310
310
  const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
311
- const row = { wbs, feature_id: featureId, status: status2, verdict, timestamp };
311
+ const row = { wbs, feature_id: featureId, status, verdict, timestamp };
312
312
  try {
313
313
  appendFileSync(abs(relMetricsFile), `${JSON.stringify(row)}
314
314
  `);
@@ -600,28 +600,28 @@ function main(argv, env = getEnvVars(), options = {}) {
600
600
  process.exit(main(process.argv.slice(2)));
601
601
  }
602
602
  export {
603
- writeRouteReason,
604
- verdictFromTestingSection,
605
- taskStatusOf,
606
- runMetrics,
607
- runFeatureTransition,
608
- runDocSyncSnapshot,
609
- runDocSyncScope,
610
- runDocSupersessionCheck,
611
- resolveTasks,
612
- preflightFeature,
613
- porcelainPath,
614
- main,
615
- jqPick,
616
- isDocSyncAllowedPath,
617
- docSyncScopeViolations,
618
- classifySync,
619
- WRAPUP_STEPS_USAGE,
620
- WBS_PATTERN,
621
- SUPERSESSION_PIN,
622
- SNAPSHOT_MISSING,
623
- GIT_UNAVAILABLE_MARKER,
624
- DOC_SYNC_SUPERSESSION_STATUS_SUFFIX,
603
+ DOC_SYNC_SCOPE_STATUS_SUFFIX,
625
604
  DOC_SYNC_SNAPSHOT_SUFFIX,
626
- DOC_SYNC_SCOPE_STATUS_SUFFIX
605
+ DOC_SYNC_SUPERSESSION_STATUS_SUFFIX,
606
+ GIT_UNAVAILABLE_MARKER,
607
+ SNAPSHOT_MISSING,
608
+ SUPERSESSION_PIN,
609
+ WBS_PATTERN,
610
+ WRAPUP_STEPS_USAGE,
611
+ classifySync,
612
+ docSyncScopeViolations,
613
+ isDocSyncAllowedPath,
614
+ jqPick,
615
+ main,
616
+ porcelainPath,
617
+ preflightFeature,
618
+ resolveTasks,
619
+ runDocSupersessionCheck,
620
+ runDocSyncScope,
621
+ runDocSyncSnapshot,
622
+ runFeatureTransition,
623
+ runMetrics,
624
+ taskStatusOf,
625
+ verdictFromTestingSection,
626
+ writeRouteReason
627
627
  };
@@ -142,7 +142,8 @@ but redundant. See the glossary entry for the disambiguation in full.
142
142
 
143
143
  Never: reimplement the pipeline inside the router; default `todo` to `--mode full`; infer targets
144
144
  from git/chat; auto-`--merge` on wrap; auto-author rules; use `dev-runall` as the feature default;
145
- bypass lifecycle guards (`--no-lifecycle`) to force progress.
145
+ bypass lifecycle guards (`--no-lifecycle`) to force progress; select a `deprecated` task for any
146
+ lifecycle hop (TABLE A A10 — frozen work has no forward step; un-deprecate or cancel it first).
146
147
 
147
148
  ## Common Rationalizations
148
149
 
@@ -158,6 +159,7 @@ bypass lifecycle guards (`--no-lifecycle`) to force progress.
158
159
  - Dispatching more than one primary command in a single invocation.
159
160
  - `--auto` used to choose among HITL candidates.
160
161
  - A route that is not in TABLE A/B/C (invented dispatch targets).
162
+ - A `deprecated` WBS routed into any lifecycle command instead of stopping (A10/ADR-137).
161
163
  - `--once` honored by merely skipping the re-invoke while leaving `--next` in the child argv.
162
164
  - Omitting the `dev-next:` prefix on stop/plan messages.
163
165
 
@@ -54,7 +54,7 @@ it or when chaining (chain links already use `--auto` per existing refine→run
54
54
 
55
55
  ## 1. TABLE A — Task primary routes (corpus status)
56
56
 
57
- Statuses from `TASK_STATUSES`: backlog | todo | wip | testing | blocked | done | cancelled.
57
+ Statuses from `TASK_STATUSES`: backlog | todo | wip | testing | blocked | done | cancelled | deprecated.
58
58
 
59
59
  | # | Precondition | Primary signal | Dispatch | probe | Chain on success? | Stop / notes |
60
60
  | --- | --- | --- | --- | --- | --- | --- |
@@ -67,8 +67,9 @@ Statuses from `TASK_STATUSES`: backlog | todo | wip | testing | blocked | done |
67
67
  | A7 | `status == blocked` | `task.show` + Notes/History for blocker text | `/sp:dev-handover "<blocker summary from Notes or status>"` | no | no | **STOP** after handover doc; human unblocks |
68
68
  | A8 | `status == done` | `task.show.status` | `/sp:dev-wrap <wbs>` (`--agent <value>` preserved when the originating command supplied it; omission remains omission) | no | **no** (wrap's `--merge` is irreversible HITL; never auto) | Operator may re-invoke with `--merge` explicitly later |
69
69
  | A9 | `status == cancelled` | `task.show.status` | *(none)* | no | no | **STOP** — no-op; print "cancelled — nothing to advance" |
70
+ | A10 | `status == deprecated` | `task.show.status` | *(none)* | no | no | **STOP** — frozen and retained (ADR-137); nothing to advance. Print "deprecated — frozen; un-deprecate with `spur task update <wbs> todo`, or cancel it once removed". Never falls through to another row |
70
71
 
71
- **Priority when multiple TABLE A rows could match:** lowest row number wins (A1–A9 are mutually
72
+ **Priority when multiple TABLE A rows could match:** lowest row number wins (A1–A10 are mutually
72
73
  exclusive by status except A2/A3 which split `todo`).
73
74
 
74
75
  ## 2. TABLE B — Feature-ID path (rollup)
@@ -84,7 +85,7 @@ When input is a feature id:
84
85
  | B4 | No frontier tasks AND `feature.status == backlog` AND AC placeholder/invalid | `feature check` / AC body | `/sp:dev-plan` continuation is not WBS-shaped — **STOP** with: run `/sp:dev-plan --feature <id>` or fix AC then `spur feature check <id>` | no | no | Planning-half entry needs a description; do not invent idea text |
85
86
  | B5 | No frontier tasks AND feature has valid AC but zero tasks | feature + empty task roster | **STOP** with: `/sp:dev-plan --feature <id>` (decompose/batch-create) | no | no | Avoid auto-running plan without operator description confirmation in v1 |
86
87
  | B6 | No frontier tasks AND all child tasks `done` AND feature in `active`\|`verifying` | task list all done | `/sp:dev-wrapall --feature <id>` (`--agent <value>` preserved when the originating command supplied it; omission remains omission) | no | no | Advances feature via wrapall guards; `--merge` never auto |
87
- | B7 | No frontier tasks AND mixed cancelled/done only | statuses | **STOP** — print summary; suggest feature status update manually | no | no | |
88
+ | B7 | No frontier tasks AND mixed terminal only (any combination drawn from `cancelled`, `done`, `deprecated`) | statuses | **STOP** — print summary; suggest feature status update manually | no | no | |
88
89
  | B8 | `feature.status == blocked` | feature status | **STOP** — print feature blocked; do not pick tasks | no | no | |
89
90
 
90
91
  **Frontier selection algorithm (B3) — precise:**
@@ -101,6 +102,11 @@ if pick is null → TABLE B rows B4–B7
101
102
  else → TABLE A(pick)
102
103
  ```
103
104
 
105
+ The candidate set is exactly the open statuses: `done`, `cancelled` **and `deprecated`** are never
106
+ frontier candidates (ADR-137 — a frozen task is retained, not resumable), and a `deprecated`
107
+ dependency is not a satisfied one, so it keeps its dependents out of the frontier exactly like any
108
+ unfinished dep.
109
+
104
110
  ## 3. TABLE C — Light-gate short-circuit (probe=yes rows only)
105
111
 
106
112
  **Order is sequential short-circuit** (first match wins). Never parallel in v1 (simpler, lower
@@ -126,12 +126,32 @@ mutually exclusive.
126
126
  spur task update 0040 wip
127
127
  ```
128
128
 
129
- Valid statuses: `backlog · todo · wip · testing · blocked · done · cancelled` (the lifecycle engine
130
- enforces legal transitions). Two transitions are **guarded by `check`**: `wip→testing` runs
131
- `spur task check <wbs> --as testing`, and `testing→done` runs `spur task check <wbs> --as done`
132
- (F92 R3 — target-aware guards) — a failing
129
+ Valid statuses are the canonical `TaskStatus` vocabulary — **`TASK_STATUSES` in
130
+ `packages/domain/src/planning/schema.ts` is the only list**, and the lifecycle engine enforces legal
131
+ transitions against the shipped `task-lifecycle` graph (the engine's bundled workflow, resolved per
132
+ project).
133
+ Operator-facing semantics (the freeze, its guards, the exempt write set) live in the design
134
+ satellite [`task-deprecated-status.md`](../../../../../docs/design/task-deprecated-status.md) — read it
135
+ there rather than restating the vocabulary. Two transitions are **guarded by `check`**:
136
+ `wip→testing` runs `spur task check <wbs> --as testing`, and `testing→done` runs
137
+ `spur task check <wbs> --as done` (F92 R3 — target-aware guards) — a failing
133
138
  gate blocks the transition (§7.5).
134
139
 
140
+ **`deprecated` — frozen but retained (ADR-137).** Non-terminal: the record stays readable and
141
+ countable, but nothing new attaches to it and it never advances. Legal edges (from the shipped
142
+ graph): `backlog`, `todo`, `wip`, `testing`, `blocked` and `done` may move to `deprecated`, and it leaves only to `todo`
143
+ (un-deprecate, mandatory `History` entry) or `cancelled` (removed). Any other status write is
144
+ refused by the graph, and the refusal names the un-deprecate command.
145
+
146
+ Two guards carry the freeze past the graph: a **new** dependency edge onto a `deprecated` task is
147
+ refused (ready-set resolver and pipeline preflight), and a live dependent is reported by the
148
+ `L4.task-deprecated` warning. The freeze is prospective — an edge recorded before the deprecation is
149
+ reported, never rewritten.
150
+
151
+ Writes that stay **permitted** on a `deprecated` task (each edits the record rather than declaring
152
+ the work resumable): `spur task record`, `migrate-anchors` and verdict writes, and
153
+ `spur task update <wbs> --section <any>` — the section write lands and returns a freeze warning.
154
+
135
155
  **`--no-lifecycle`** suppresses lifecycle workflow *run record* creation (use during pipeline-driven
136
156
  transitions so nested lifecycle runs are not orphaned). **It is not a guard bypass** — the
137
157
  `wip→testing` / `testing→done` `check` gates still run, evaluated inline when the FSM guard does not.
@@ -572,7 +572,14 @@ does not earn another full-gate run:
572
572
  4. Give the test an explicit per-test timeout and a WHY comment (see
573
573
  `plugins/sp/tests/inline-run-close-reason.test.ts:100-102`) as soon as EITHER (a) the same test
574
574
  load-flakes in two separate runs, OR (b) it times out once **outside the change scope** and then
575
- passes green alone on three consecutive isolated reruns. Waiting for a second gate run to license
575
+ passes green alone on three consecutive isolated reruns. **Prefer making the test cheap or
576
+ hermetic first** — a test that hashes the live checkout transitively, or writes a probe file into
577
+ it, should build a `mkdtemp` fixture repo instead of buying a bigger budget
578
+ (`packages/app/tests/workflow/actions/proof-fingerprint.test.ts` is the pattern, 2026-10-13).
579
+ Only then hand the remaining repeat flakers an explicit budget: the suite-wide `--timeout` is
580
+ not the lever, and raising it was measured NOT to help (a 60s suite default still failed 7
581
+ spawn-heavy tests under a host shared with two sibling agent suites, while every one passed
582
+ alone in 0.15-0.3s — 2026-10-13). Waiting for a second gate run to license
576
583
  the fix cost 21 minutes on 2026-10-08 (task 1128, the 1058 two-tree canary: 5145 ms then 5967 ms
577
584
  against bun's 5000 ms default, green 3/3 alone). Never raise the global timeout, skip the test,
578
585
  or loop full-gate reruns.