@gobing-ai/spur 0.3.90 → 0.3.92

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 (130) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +9 -2
  3. package/config/plugin-scripts.json +17 -1
  4. package/config/workflows/decision-routing-example.yaml +134 -0
  5. package/config/workflows/feature-lifecycle.yaml +14 -5
  6. package/config/workflows/feature-verification.yaml +35 -27
  7. package/config/workflows/history-anatomy.yaml +14 -25
  8. package/config/workflows/idea-pipeline.yaml +17 -5
  9. package/config/workflows/task-pipeline.yaml +187 -4
  10. package/config/workflows/wayfinder-resolution.yaml +2 -0
  11. package/config/workflows/wrapup-pipeline.yaml +49 -5
  12. package/package.json +9 -9
  13. package/plugins/sp/README.md +1 -0
  14. package/plugins/sp/agents/expert-spur.md +2 -2
  15. package/plugins/sp/agents/super-planner.md +14 -5
  16. package/plugins/sp/commands/dev-dogfood.md +4 -4
  17. package/plugins/sp/commands/dev-fixall.md +8 -5
  18. package/plugins/sp/commands/dev-run.md +6 -0
  19. package/plugins/sp/commands/dev-runall.md +12 -6
  20. package/plugins/sp/commands/dev-verify.md +9 -0
  21. package/plugins/sp/commands/dev-verifyall.md +5 -0
  22. package/plugins/sp/lib/idea-handoff.generated.mjs +301 -300
  23. package/plugins/sp/lib/inline-run.generated.d.mts +17 -0
  24. package/plugins/sp/lib/inline-run.generated.mjs +1460 -0
  25. package/plugins/sp/plugin.json +1 -1
  26. package/plugins/sp/references/environment-lens.md +1 -1
  27. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
  28. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
  29. package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
  30. package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
  31. package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
  32. package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
  33. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +2 -0
  34. package/plugins/sp/scripts/inline-run-setup.mjs +349 -0
  35. package/plugins/sp/scripts/inline-run-setup.ts +192 -75
  36. package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
  37. package/plugins/sp/scripts/record-feature-sync.ts +84 -0
  38. package/plugins/sp/scripts/residual-scan.mjs +476 -0
  39. package/plugins/sp/scripts/residual-scan.ts +614 -0
  40. package/plugins/sp/scripts/surface-drift-inventory.ts +71 -6
  41. package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
  42. package/plugins/sp/scripts/task-size-precheck.ts +8 -3
  43. package/plugins/sp/scripts/validate-flag-contracts.ts +3 -3
  44. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  45. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
  46. package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
  47. package/plugins/sp/skills/code-verification/SKILL.md +21 -0
  48. package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
  49. package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
  50. package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
  51. package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
  52. package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
  53. package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
  54. package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
  55. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  56. package/plugins/sp/skills/session-review/SKILL.md +12 -2
  57. package/plugins/sp/skills/spur-cli/references/agent.md +11 -2
  58. package/plugins/sp/skills/spur-cli/references/features.md +1 -1
  59. package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
  60. package/plugins/sp/skills/spur-cli/references/self.md +5 -1
  61. package/plugins/sp/skills/spur-cli/references/workflows.md +42 -20
  62. package/plugins/sp/skills/spur-dev/SKILL.md +11 -4
  63. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +25 -3
  64. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
  65. package/plugins/sp/skills/spur-dev/references/execution-batch.md +285 -57
  66. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -5
  67. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +19 -4
  68. package/plugins/sp/skills/spur-dev/references/glossary.md +9 -1
  69. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +51 -16
  70. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +5 -0
  71. package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
  72. package/spur.js +23422 -19364
  73. package/web/_astro/BoardApp.CerSBgis.js +192 -0
  74. package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
  75. package/web/_astro/{TaskDetail.DwTmbQp5.js → TaskDetail.DCqiC-OZ.js} +1 -1
  76. package/web/_astro/arc.CPwg6Rw0.js +1 -0
  77. package/web/_astro/{architectureDiagram-3BPJPVTR.jvdDahWM.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
  78. package/web/_astro/{blockDiagram-GPEHLZMM.zSg4AmFD.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
  79. package/web/_astro/{c4Diagram-AAUBKEIU.BkUIUQWH.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
  80. package/web/_astro/channel.MYZLKNwy.js +1 -0
  81. package/web/_astro/{chunk-2J33WTMH.DvfQ_f50.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
  82. package/web/_astro/{chunk-4BX2VUAB.DuI4gQqX.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
  83. package/web/_astro/{chunk-55IACEB6.D3BWBOpF.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
  84. package/web/_astro/{chunk-727SXJPM.3QSi0a9M.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
  85. package/web/_astro/{chunk-AQP2D5EJ.xazCQrAF.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
  86. package/web/_astro/{chunk-FMBD7UC4.B2g6u4rA.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
  87. package/web/_astro/{chunk-ND2GUHAM.wWwWs99t.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
  88. package/web/_astro/{chunk-QZHKN3VN.BD5g3qa9.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
  89. package/web/_astro/{classDiagram-4FO5ZUOK.C7CzCdsX.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
  90. package/web/_astro/{classDiagram-v2-Q7XG4LA2.C7CzCdsX.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
  91. package/web/_astro/{cose-bilkent-S5V4N54A.Xyiau0gw.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
  92. package/web/_astro/{cynefin-OW5HDTMX.BeC5MWas.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
  93. package/web/_astro/{dagre-BM42HDAG.yZbMN9vc.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
  94. package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
  95. package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
  96. package/web/_astro/{diagram-KO2AKTUF.CR6k3Y3G.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
  97. package/web/_astro/{diagram-LMA3HP47.x7mwu8jz.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
  98. package/web/_astro/{diagram-OG6HWLK6.D8aTTvUr.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
  99. package/web/_astro/{erDiagram-TEJ5UH35.BoBqcKXQ.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
  100. package/web/_astro/{flowDiagram-I6XJVG4X.D3mTQdrU.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
  101. package/web/_astro/{ganttDiagram-6RSMTGT7.H-cqgIh-.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
  102. package/web/_astro/{gitGraphDiagram-PVQCEYII.B6s9zbfC.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
  103. package/web/_astro/index.Hjbr15fG.css +1 -0
  104. package/web/_astro/{infoDiagram-5YYISTIA.BzgCoV6P.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
  105. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BZzVhy1-.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
  106. package/web/_astro/{journeyDiagram-JHISSGLW.BV3195Py.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
  107. package/web/_astro/{kanban-definition-UN3LZRKU.BjRd2DWz.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
  108. package/web/_astro/{linear.BILTgS5N.js → linear.C2_IkbZT.js} +1 -1
  109. package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
  110. package/web/_astro/{mindmap-definition-RKZ34NQL.BiEjaI4-.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
  111. package/web/_astro/{pieDiagram-4H26LBE5.i_8V5pIn.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
  112. package/web/_astro/{quadrantDiagram-W4KKPZXB.BWaW3MHn.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
  113. package/web/_astro/{requirementDiagram-4Y6WPE33.CzddBbtg.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
  114. package/web/_astro/{sankeyDiagram-5OEKKPKP.X2ww0e-D.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
  115. package/web/_astro/{sequenceDiagram-3UESZ5HK.DSA4kTcc.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
  116. package/web/_astro/{stateDiagram-AJRCARHV.D0DtFSpR.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
  117. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BfQq0zQv.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
  118. package/web/_astro/{timeline-definition-PNZ67QCA.Dmlrgi1m.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
  119. package/web/_astro/{vennDiagram-CIIHVFJN.D5mpl00Z.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
  120. package/web/_astro/{wardleyDiagram-YWT4CUSO.Df4BdzO4.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
  121. package/web/_astro/{xychartDiagram-2RQKCTM6.DiTRreKN.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
  122. package/web/index.html +2 -2
  123. package/web/_astro/BoardApp.CDUcHlTJ.js +0 -188
  124. package/web/_astro/BoardApp.CaCGU_uX.js +0 -1
  125. package/web/_astro/arc.BzF71EFI.js +0 -1
  126. package/web/_astro/channel.SSVY0JPQ.js +0 -1
  127. package/web/_astro/diagram-2AECGRRQ.Cmo2zQM-.js +0 -43
  128. package/web/_astro/diagram-5GNKFQAL.D033eSVi.js +0 -10
  129. package/web/_astro/index.CcU5weKX.css +0 -1
  130. package/web/_astro/mermaid.core.DBy_WKeW.js +0 -301
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // plugins/sp/scripts/residual-scan.ts
5
+ import { spawnSync } from "child_process";
6
+ import { createHash } from "crypto";
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
8
+ import { tmpdir } from "os";
9
+ import { join } from "path";
10
+
11
+ // plugins/sp/lib/env.ts
12
+ function getEnvVars() {
13
+ return process.env;
14
+ }
15
+
16
+ // plugins/sp/scripts/residual-scan.ts
17
+ var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
18
+ var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
19
+ var PRIORITY_PATTERN = /^P[1-4]/;
20
+ var NONE_FINDING = /^(none|\u2014)$/i;
21
+ var ANCHOR_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
22
+ var RANGE_ANCHOR = /([A-Za-z0-9_./-]+\.[A-Za-z]+):([0-9]+)-[0-9]+/g;
23
+ var EXCLUDED_PATHS = ["docs/tasks", "docs/features/", ".spur/"];
24
+ var ALLOW_PRAGMA = "residual-scan:allow";
25
+ function spurCommand(spurBin) {
26
+ const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
27
+ return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
28
+ }
29
+ function run(cmd, args, cwd) {
30
+ const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
31
+ if (result.error !== undefined)
32
+ return { status: result.status ?? 1, stdout: "" };
33
+ return { status: result.status ?? 1, stdout: result.stdout ?? "" };
34
+ }
35
+ function makeItemId(category, location, text) {
36
+ const normalized = text.trim().replace(/\s+/g, " ");
37
+ const hex = createHash("sha256").update(`${location}${normalized}`).digest("hex");
38
+ return `${category}:${hex.slice(0, 8)}`;
39
+ }
40
+ function normalizeAnchor(location) {
41
+ return location.replace(RANGE_ANCHOR, "$1:$2");
42
+ }
43
+ function locationOf(locationCell, finding) {
44
+ const cell = locationCell.trim().replace(/`/g, "");
45
+ if (cell.length > 0 && cell !== "\u2014")
46
+ return normalizeAnchor(cell);
47
+ const backtick = finding.match(/`([^`]+)`/);
48
+ return backtick === null ? "" : normalizeAnchor(backtick[1]);
49
+ }
50
+ function parseReviewFindings(taskContent) {
51
+ const section = taskContent.split(/^### Review\b/m)[1];
52
+ if (section === undefined)
53
+ return [];
54
+ const body = section.split(/^### /m)[0];
55
+ const out = [];
56
+ const lines = body.split(`
57
+ `);
58
+ for (let i = 0;i < lines.length; i++) {
59
+ const line = lines[i];
60
+ if (line === undefined || !line.trimStart().startsWith("|"))
61
+ continue;
62
+ const header = splitRow(line);
63
+ const priorityCol = header.findIndex((h) => h.trim() === "Priority");
64
+ if (priorityCol === -1) {
65
+ while (i + 1 < lines.length && lines[i + 1]?.trimStart().startsWith("|"))
66
+ i++;
67
+ continue;
68
+ }
69
+ const findingCol = header.findIndex((h) => h.trim() === "Finding");
70
+ const locationCol = header.findIndex((h) => h.trim() === "Location");
71
+ i++;
72
+ const sep = lines[i];
73
+ if (sep !== undefined && /^\s*\|[\s:|-]+\|\s*$/.test(sep))
74
+ i++;
75
+ while (i < lines.length) {
76
+ const row = lines[i];
77
+ if (row === undefined || !row.trimStart().startsWith("|"))
78
+ break;
79
+ const cells = splitRow(row);
80
+ const priority = (cells[priorityCol] ?? "").trim();
81
+ const finding = (cells[findingCol] ?? "").trim();
82
+ if (PRIORITY_PATTERN.test(priority) && !NONE_FINDING.test(finding) && finding.length > 0) {
83
+ out.push({ priority, location: locationOf(cells[locationCol] ?? "", finding), text: finding });
84
+ }
85
+ i++;
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ function splitRow(line) {
91
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
92
+ }
93
+ function collectAddedLines(root, base) {
94
+ const out = [];
95
+ const diff = run("git", ["diff", "--unified=0", base], root);
96
+ let file = "";
97
+ let newLine = 0;
98
+ for (const line of diff.stdout.split(`
99
+ `)) {
100
+ if (line.startsWith("+++ b/"))
101
+ file = line.slice(6);
102
+ else if (line.startsWith("@@")) {
103
+ const m = line.match(/\+[0-9]+/);
104
+ newLine = m === null ? newLine : Number.parseInt(m[0].slice(1), 10);
105
+ } else if (line.startsWith("+") && !line.startsWith("+++")) {
106
+ out.push({ file, line: newLine, text: line.slice(1) });
107
+ newLine++;
108
+ }
109
+ }
110
+ const untracked = run("git", ["ls-files", "--others", "--exclude-standard"], root);
111
+ for (const f of untracked.stdout.split(`
112
+ `)) {
113
+ if (f.length === 0)
114
+ continue;
115
+ try {
116
+ if (!statSync(join(root, f)).isFile())
117
+ continue;
118
+ } catch {
119
+ continue;
120
+ }
121
+ const content = readFileSync(join(root, f), "utf8").split(`
122
+ `);
123
+ content.forEach((text, idx) => {
124
+ out.push({ file: f, line: idx + 1, text });
125
+ });
126
+ }
127
+ return out;
128
+ }
129
+ function parseDiffMarkers(addedLines) {
130
+ return addedLines.filter((l) => !EXCLUDED_PATHS.some((p) => l.file.startsWith(p))).filter((l) => !l.text.includes(ALLOW_PRAGMA)).filter((l) => MARKER_PATTERN.test(l.text)).map((l) => ({ location: `${l.file}:${l.line}`, text: l.text.trim() }));
131
+ }
132
+ function findUncheckedBoxes(taskContent) {
133
+ const path = "task-file";
134
+ return taskContent.split(`
135
+ `).map((text, idx) => ({ text: text.trim(), line: idx + 1 })).filter((l) => l.text.startsWith("- [ ]")).map((l) => ({ location: `${path}:${l.line}`, text: l.text }));
136
+ }
137
+ function listStagingResidue(tmpDir, wbs) {
138
+ let names;
139
+ try {
140
+ names = readdirSync(tmpDir);
141
+ } catch {
142
+ return [];
143
+ }
144
+ return names.filter((n) => n.startsWith(`${wbs}-`)).filter((n) => {
145
+ try {
146
+ return statSync(join(tmpDir, n)).isFile();
147
+ } catch {
148
+ return false;
149
+ }
150
+ }).map((n) => join(tmpDir, n));
151
+ }
152
+ function readDeferrals(runDir, wbs) {
153
+ const path = join(runDir, `${wbs}-residual-deferrals.json`);
154
+ if (!existsSync(path))
155
+ return [];
156
+ try {
157
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
158
+ if (!Array.isArray(parsed))
159
+ return [];
160
+ return parsed.filter((e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0);
161
+ } catch {
162
+ return [];
163
+ }
164
+ }
165
+ function classify(items, deferrals) {
166
+ const deferred = new Map(deferrals.map((d) => [d.id, d.reason]));
167
+ return items.map((item) => {
168
+ const id = makeItemId(item.category, item.location, item.text);
169
+ let klass;
170
+ if (item.category === "review-finding")
171
+ klass = item.priority?.startsWith("P4") ? "advisory" : "blocking";
172
+ else if (item.category === "staging-residue")
173
+ klass = "housekeeping";
174
+ else
175
+ klass = "blocking";
176
+ if (klass === "blocking" && item.category !== "unchecked-box") {
177
+ const p3Like = item.category === "diff-marker" || item.category === "review-finding" && (item.priority ?? "").startsWith("P3");
178
+ const reason = deferred.get(id);
179
+ if (p3Like && reason !== undefined && reason.trim().length > 0)
180
+ klass = "deferrable";
181
+ }
182
+ return {
183
+ id,
184
+ category: item.category,
185
+ class: klass,
186
+ priority: item.priority,
187
+ location: item.location,
188
+ text: item.text
189
+ };
190
+ });
191
+ }
192
+ function scanResiduals(root, wbs, tmpDir, taskContent, _env) {
193
+ const runDir = join(root, ".spur", "run");
194
+ const basePath = join(runDir, `${wbs}-base.sha`);
195
+ const base = existsSync(basePath) ? readFileSync(basePath, "utf8").trim() : null;
196
+ const review = parseReviewFindings(taskContent).map((r) => ({
197
+ category: "review-finding",
198
+ priority: r.priority,
199
+ location: r.location,
200
+ text: r.text
201
+ }));
202
+ const markers = base === null ? [] : parseDiffMarkers(collectAddedLines(root, base)).map((m) => ({
203
+ category: "diff-marker",
204
+ location: m.location,
205
+ text: m.text
206
+ }));
207
+ const boxes = findUncheckedBoxes(taskContent).map((b) => ({
208
+ category: "unchecked-box",
209
+ location: b.location,
210
+ text: b.text
211
+ }));
212
+ const residue = listStagingResidue(tmpDir, wbs).map((p) => ({
213
+ category: "staging-residue",
214
+ location: p,
215
+ text: p
216
+ }));
217
+ const items = classify([...review, ...markers, ...boxes, ...residue], readDeferrals(runDir, wbs));
218
+ const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
219
+ for (const item of items)
220
+ counts[item.class]++;
221
+ return {
222
+ wbs,
223
+ base,
224
+ scanned: {
225
+ "review-finding": true,
226
+ "diff-marker": base !== null,
227
+ "unchecked-box": true,
228
+ "staging-residue": true
229
+ },
230
+ items,
231
+ counts
232
+ };
233
+ }
234
+ function blockingAnchors(items) {
235
+ const anchors = new Set;
236
+ for (const item of items) {
237
+ if (item.class !== "blocking")
238
+ continue;
239
+ for (const m of normalizeAnchor(item.location).matchAll(ANCHOR_PATTERN))
240
+ anchors.add(m[0]);
241
+ }
242
+ return [...anchors];
243
+ }
244
+ function foldVerdict(verdict, scan, existingFindings, maxFindings = 20) {
245
+ const blocking = scan.items.filter((i) => i.class === "blocking");
246
+ const deferrable = scan.items.filter((i) => i.class === "deferrable");
247
+ const evidence = `blocking=${blocking.length} deferrable=${deferrable.length} advisory=${scan.counts.advisory} housekeeping=${scan.counts.housekeeping}` + (blocking.length > 0 ? `; blocking ids: ${blocking.map((i) => i.id).join(", ")}` : "") + (deferrable.length > 0 ? `; deferrable ids: ${deferrable.map((i) => i.id).join(", ")}` : "");
248
+ const checks = verdict.checks.filter((c) => c.name !== "residual-sweep");
249
+ checks.push({ name: "residual-sweep", status: blocking.length > 0 ? "fail" : "pass", evidence });
250
+ const merged = new Set([
251
+ ...existingFindings.split(/\s+/).filter((a) => a.length > 0),
252
+ ...blockingAnchors(scan.items)
253
+ ]);
254
+ const findings = [...merged].sort().slice(0, maxFindings).map((a) => `${a} `).join("");
255
+ let verdictStatus = verdict.verdict === "PASS" ? "PASS" : "FAIL";
256
+ if (verdict.verdict === "PARTIAL")
257
+ verdictStatus = "PARTIAL";
258
+ else if (verdict.verdict === "FAIL")
259
+ verdictStatus = "FAIL";
260
+ else if (blocking.length > 0)
261
+ verdictStatus = "PARTIAL";
262
+ return { verdict: verdictStatus, checks, findings };
263
+ }
264
+ function renderReport(wbs, items, attemptCount) {
265
+ const lines = [
266
+ `# Residual report \u2014 ${wbs}`,
267
+ "",
268
+ `Attempt: ${attemptCount}`,
269
+ "",
270
+ "| Category | Class | Location | Text |",
271
+ "| --- | --- | --- | --- |"
272
+ ];
273
+ for (const item of items) {
274
+ lines.push(`| ${item.category} | ${item.class} | ${item.location} | ${item.text.replace(/\|/g, "\\|")} |`);
275
+ }
276
+ return `${lines.join(`
277
+ `)}
278
+ `;
279
+ }
280
+ function parseArgs(argv) {
281
+ let mode = "";
282
+ let wbs = "";
283
+ let spurBin;
284
+ let root = process.cwd();
285
+ let tmpDir = tmpdir();
286
+ for (let i = 0;i < argv.length; i++) {
287
+ const a = argv[i];
288
+ if (a === undefined)
289
+ break;
290
+ if (a === "--spur-bin")
291
+ spurBin = argv[++i];
292
+ else if (a === "--root")
293
+ root = argv[++i] ?? root;
294
+ else if (a === "--tmp-dir")
295
+ tmpDir = argv[++i] ?? tmpDir;
296
+ else if (a === "--help" || a === "-h")
297
+ return null;
298
+ else if (mode === "")
299
+ mode = a;
300
+ else if (wbs === "")
301
+ wbs = a;
302
+ }
303
+ if (mode === "" || wbs === "")
304
+ return null;
305
+ return { mode, wbs, spurBin, root, tmpDir };
306
+ }
307
+ function spur(env, spurBinFlag, args, cwd) {
308
+ return run(spurCommand(spurBinFlag ?? env.spurBin).cmd, [...spurCommand(spurBinFlag ?? env.spurBin).prefix, ...args], cwd);
309
+ }
310
+ function loadTask(env, spurBinFlag, wbs, root) {
311
+ const res = spur(env, spurBinFlag, ["task", "show", wbs, "--json"], root);
312
+ if (res.status !== 0)
313
+ throw new Error(`task show ${wbs} failed`);
314
+ const parsed = JSON.parse(res.stdout);
315
+ const content = typeof parsed.content === "string" ? parsed.content : "";
316
+ const fm = parsed.frontmatter;
317
+ const featureId = typeof parsed.feature_id === "string" ? parsed.feature_id : fm !== null && typeof fm === "object" && typeof fm.feature_id === "string" ? fm.feature_id : "";
318
+ return { content, featureId };
319
+ }
320
+ function loadVerdict(runDir, wbs) {
321
+ return JSON.parse(readFileSync(join(runDir, `${wbs}-verdict.json`), "utf8"));
322
+ }
323
+ function scanMode(opts, env, io) {
324
+ const task = loadTask(env, opts.spurBin, opts.wbs, opts.root);
325
+ const runDir = join(opts.root, ".spur", "run");
326
+ mkdirSync(runDir, { recursive: true });
327
+ const artifact = scanResiduals(opts.root, opts.wbs, opts.tmpDir, task.content, env);
328
+ writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(artifact, null, 2)}
329
+ `);
330
+ io.out(`residual-scan: ${opts.wbs} blocking=${artifact.counts.blocking} deferrable=${artifact.counts.deferrable} advisory=${artifact.counts.advisory} housekeeping=${artifact.counts.housekeeping}
331
+ `);
332
+ return 0;
333
+ }
334
+ function foldMode(opts, _env, io) {
335
+ const runDir = join(opts.root, ".spur", "run");
336
+ const scan = JSON.parse(readFileSync(join(runDir, `${opts.wbs}-residuals.json`), "utf8"));
337
+ const verdictPath = join(runDir, `${opts.wbs}-verdict.json`);
338
+ const verdict = loadVerdict(runDir, opts.wbs);
339
+ const findingsPath = join(runDir, `${opts.wbs}-test-gate.findings`);
340
+ const existing = existsSync(findingsPath) ? readFileSync(findingsPath, "utf8") : "";
341
+ const folded = foldVerdict(verdict, scan, existing);
342
+ writeFileSync(verdictPath, `${JSON.stringify({ ...verdict, verdict: folded.verdict, checks: folded.checks }, null, 2)}
343
+ `);
344
+ writeFileSync(findingsPath, folded.findings);
345
+ io.out(`residual-fold: ${opts.wbs} verdict=${folded.verdict} residual-sweep=${folded.checks.find((c) => c.name === "residual-sweep")?.status}
346
+ `);
347
+ return 0;
348
+ }
349
+ function settleMode(opts, env, io) {
350
+ const task = loadTask(env, opts.spurBin, opts.wbs, opts.root);
351
+ const runDir = join(opts.root, ".spur", "run");
352
+ const scan = scanResiduals(opts.root, opts.wbs, opts.tmpDir, task.content, env);
353
+ const residualsPath = join(runDir, `${opts.wbs}-residuals.json`);
354
+ const deferred = scan.items.filter((i) => i.class === "deferrable");
355
+ const prior = existsSync(residualsPath) ? JSON.parse(readFileSync(residualsPath, "utf8")) : {};
356
+ if (deferred.length > 0 && prior.followUp === undefined) {
357
+ if (task.featureId === "") {
358
+ io.err(`residual-settle: ${opts.wbs} deferrals pending but feature_id unknown; re-run: residual-scan settle ${opts.wbs}
359
+ `);
360
+ return 0;
361
+ }
362
+ const created = spur(env, opts.spurBin, ["task", "create", `Residuals from ${opts.wbs}`, "--feature", task.featureId, "--skip-ready", "--json"], opts.root);
363
+ if (created.status !== 0) {
364
+ io.err(`residual-settle: task create failed; re-run: residual-scan settle ${opts.wbs}
365
+ `);
366
+ return 0;
367
+ }
368
+ let wbsNew = "";
369
+ try {
370
+ const parsed = JSON.parse(created.stdout);
371
+ const pick = (o) => typeof o.wbs === "string" ? o.wbs : "";
372
+ wbsNew = pick(parsed) || (parsed.data !== null && typeof parsed.data === "object" ? pick(parsed.data) : "");
373
+ } catch {
374
+ wbsNew = "";
375
+ }
376
+ if (wbsNew === "") {
377
+ io.err(`residual-settle: could not read created task wbs; re-run: residual-scan settle ${opts.wbs}
378
+ `);
379
+ return 0;
380
+ }
381
+ const bg = [
382
+ `Source task: ${opts.wbs} (feature ${task.featureId}) \u2014 deferred residuals filed by residual-scan settle.`,
383
+ "",
384
+ ...deferred.map((i) => `- ${i.id} \u2014 ${i.location}: ${i.text}`)
385
+ ].join(`
386
+ `);
387
+ const bgFile = join(runDir, `${opts.wbs}-residual-background.md`);
388
+ writeFileSync(bgFile, `${bg}
389
+ `);
390
+ const upd = spur(env, opts.spurBin, ["task", "update", wbsNew, "--section", "Background", "--from-file", bgFile], opts.root);
391
+ if (upd.status !== 0) {
392
+ io.err(`residual-settle: background write failed; re-run: residual-scan settle ${opts.wbs}
393
+ `);
394
+ return 0;
395
+ }
396
+ prior.followUp = wbsNew;
397
+ io.out(`residual-settle: filed follow-up ${wbsNew} for ${deferred.length} deferred item(s)
398
+ `);
399
+ }
400
+ for (const path of listStagingResidue(opts.tmpDir, opts.wbs)) {
401
+ try {
402
+ rmSync(path, { force: true });
403
+ } catch {
404
+ io.err(`residual-settle: could not remove ${path}; re-run: residual-scan settle ${opts.wbs}
405
+ `);
406
+ return 0;
407
+ }
408
+ }
409
+ writeFileSync(residualsPath, `${JSON.stringify({ ...scan, ...prior }, null, 2)}
410
+ `);
411
+ return 0;
412
+ }
413
+ function reportMode(opts, env, io) {
414
+ const runDir = join(opts.root, ".spur", "run");
415
+ const verdict = loadVerdict(runDir, opts.wbs);
416
+ const sweep = verdict.checks.find((c) => c.name === "residual-sweep");
417
+ if (sweep === undefined || sweep.status !== "fail")
418
+ return 0;
419
+ const task = loadTask(env, opts.spurBin, opts.wbs, opts.root);
420
+ const scan = scanResiduals(opts.root, opts.wbs, opts.tmpDir, task.content, env);
421
+ const blocking = scan.items.filter((i) => i.class === "blocking");
422
+ const attemptFile = join(runDir, `${opts.wbs}-test-fix-attempt`);
423
+ const attempts = existsSync(attemptFile) ? Number.parseInt(readFileSync(attemptFile, "utf8").trim() || "0", 10) : 0;
424
+ const reportPath = join(runDir, `${opts.wbs}-residual-report.md`);
425
+ writeFileSync(reportPath, renderReport(opts.wbs, blocking, Number.isNaN(attempts) ? 0 : attempts));
426
+ io.out(`Recovery: fix the items in .spur/run/${opts.wbs}-residual-report.md, then /sp:dev-run ${opts.wbs}
427
+ `);
428
+ return 0;
429
+ }
430
+ function main(argv, env = getEnvVars(), options = {}) {
431
+ const io = options.io ?? {
432
+ out: (line) => process.stdout.write(line),
433
+ err: (line) => process.stderr.write(line)
434
+ };
435
+ const opts = parseArgs(argv);
436
+ if (opts === null) {
437
+ io.err(`${RESIDUAL_SCAN_USAGE}
438
+ `);
439
+ return 2;
440
+ }
441
+ const cwd = options.cwd ?? process.cwd();
442
+ const resolved = { ...opts, root: opts.root.startsWith("/") ? opts.root : join(cwd, opts.root) };
443
+ if (resolved.mode === "scan")
444
+ return scanMode(resolved, env, io);
445
+ if (resolved.mode === "fold")
446
+ return foldMode(resolved, env, io);
447
+ if (resolved.mode === "settle")
448
+ return settleMode(resolved, env, io);
449
+ if (resolved.mode === "report")
450
+ return reportMode(resolved, env, io);
451
+ io.err(`${RESIDUAL_SCAN_USAGE}
452
+ `);
453
+ return 2;
454
+ }
455
+ {
456
+ process.exit(main(process.argv.slice(2)));
457
+ }
458
+ export {
459
+ spurCommand,
460
+ scanResiduals,
461
+ renderReport,
462
+ parseReviewFindings,
463
+ parseDiffMarkers,
464
+ normalizeAnchor,
465
+ makeItemId,
466
+ main,
467
+ locationOf,
468
+ listStagingResidue,
469
+ foldVerdict,
470
+ findUncheckedBoxes,
471
+ collectAddedLines,
472
+ classify,
473
+ blockingAnchors,
474
+ RESIDUAL_SCAN_USAGE,
475
+ ALLOW_PRAGMA
476
+ };