@gobing-ai/spur 0.3.95 → 0.3.96

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 (155) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/plugin-scripts.json +0 -54
  3. package/config/rules/boundary/sp-script-placement.yaml +19 -0
  4. package/config/rules/strict/runtime-boundaries.yaml +5 -0
  5. package/config/rules/structure/test-location.yaml +2 -0
  6. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
  7. package/config/rules/typescript/output-boundaries.yaml +15 -2
  8. package/config/script-placement-baseline.json +51 -0
  9. package/config/workflows/feature-verification.yaml +1 -1
  10. package/config/workflows/history-anatomy.yaml +23 -11
  11. package/config/workflows/idea-pipeline.yaml +80 -85
  12. package/config/workflows/pr-review.yaml +18 -9
  13. package/config/workflows/task-pipeline.yaml +42 -95
  14. package/config/workflows/wrapup-pipeline.yaml +89 -37
  15. package/package.json +1 -1
  16. package/plugins/sp/README.md +8 -10
  17. package/plugins/sp/agents/super-reviewer.md +43 -18
  18. package/plugins/sp/commands/dev-fixgha.md +83 -0
  19. package/plugins/sp/commands/dev-gitmsg.md +8 -6
  20. package/plugins/sp/commands/dev-gtd.md +2 -2
  21. package/plugins/sp/commands/dev-idea.md +22 -12
  22. package/plugins/sp/commands/dev-plan.md +8 -9
  23. package/plugins/sp/commands/dev-review.md +22 -13
  24. package/plugins/sp/commands/dev-verifyall.md +1 -1
  25. package/plugins/sp/commands/spur-init.md +2 -2
  26. package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
  27. package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
  28. package/plugins/sp/lib/idea-handoff.generated.mjs +5 -4
  29. package/plugins/sp/lib/inline-run.generated.d.mts +11 -0
  30. package/plugins/sp/lib/inline-run.generated.mjs +25 -8
  31. package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
  32. package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
  33. package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
  34. package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
  35. package/plugins/sp/lib/spur-bin.ts +36 -0
  36. package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
  37. package/plugins/sp/lib/step-profile.generated.mjs +174 -0
  38. package/plugins/sp/plugin.json +1 -1
  39. package/plugins/sp/references/roles.md +1 -1
  40. package/plugins/sp/scripts/history-anatomy-cache.mjs +20 -19
  41. package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
  42. package/plugins/sp/scripts/inline-run-setup.mjs +85 -320
  43. package/plugins/sp/scripts/inline-run-setup.ts +104 -667
  44. package/plugins/sp/scripts/quality-gate.mjs +34 -19
  45. package/plugins/sp/scripts/quality-gate.ts +12 -658
  46. package/plugins/sp/scripts/residual-scan.mjs +135 -156
  47. package/plugins/sp/scripts/residual-scan.ts +102 -499
  48. package/plugins/sp/scripts/script-root.mjs +5 -1
  49. package/plugins/sp/scripts/script-root.ts +5 -1
  50. package/plugins/sp/scripts/workflow-step-profile.mjs +25 -17
  51. package/plugins/sp/scripts/workflow-step-profile.ts +21 -315
  52. package/plugins/sp/scripts/wrapup-drift-probe.mjs +8 -2
  53. package/plugins/sp/scripts/wrapup-drift-probe.ts +3 -2
  54. package/plugins/sp/scripts/wrapup-steps.mjs +10 -32
  55. package/plugins/sp/scripts/wrapup-steps.ts +8 -36
  56. package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
  57. package/plugins/sp/skills/code-verification/SKILL.md +34 -9
  58. package/plugins/sp/skills/code-verification/references/verdict-schema.md +3 -3
  59. package/plugins/sp/skills/functional-review/SKILL.md +7 -4
  60. package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
  61. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
  62. package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
  63. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  64. package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
  65. package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
  66. package/plugins/sp/skills/spur-cli/references/features.md +17 -6
  67. package/plugins/sp/skills/spur-cli/references/init.md +17 -16
  68. package/plugins/sp/skills/spur-cli/references/self.md +3 -2
  69. package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
  70. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +21 -6
  71. package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
  72. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
  73. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
  74. package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
  75. package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
  76. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
  77. package/plugins/sp/skills/spur-dev/references/execution-batch.md +77 -34
  78. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
  79. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
  80. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
  81. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
  82. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
  83. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +29 -3
  84. package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
  85. package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
  86. package/spur.js +1495 -369
  87. package/web/_astro/{BoardApp.Cpxntzad.js → BoardApp.BIjMatT1.js} +1 -1
  88. package/web/_astro/{BoardApp.BxJuwD7I.js → BoardApp.GvjIe9Z6.js} +42 -42
  89. package/web/_astro/{TaskDetail.C5bW4WGV.js → TaskDetail.BZM3EAFx.js} +1 -1
  90. package/web/_astro/{arc.CyjRvNMY.js → arc.HkRiZnoI.js} +1 -1
  91. package/web/_astro/{architectureDiagram-3BPJPVTR.B4lWRzJA.js → architectureDiagram-3BPJPVTR.BDdK-tgZ.js} +1 -1
  92. package/web/_astro/{blockDiagram-GPEHLZMM.Be38USQ2.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
  93. package/web/_astro/{c4Diagram-AAUBKEIU.DT9Fj5Qx.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
  94. package/web/_astro/channel.DlhX2MEt.js +1 -0
  95. package/web/_astro/{chunk-2J33WTMH.SeSBWLg5.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
  96. package/web/_astro/{chunk-4BX2VUAB.DoO4VE14.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
  97. package/web/_astro/{chunk-55IACEB6.DrmTFA53.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
  98. package/web/_astro/{chunk-727SXJPM.Bi-TUb_V.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
  99. package/web/_astro/{chunk-AQP2D5EJ.B-xp8Uhi.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
  100. package/web/_astro/{chunk-FMBD7UC4.DGWrDnUU.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
  101. package/web/_astro/{chunk-ND2GUHAM._BagvPDy.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
  102. package/web/_astro/{chunk-QZHKN3VN.VOboYWQ0.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
  103. package/web/_astro/{classDiagram-4FO5ZUOK.D_apfHS7.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
  104. package/web/_astro/{classDiagram-v2-Q7XG4LA2.D_apfHS7.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
  105. package/web/_astro/{cose-bilkent-S5V4N54A.DNLU_L-x.js → cose-bilkent-S5V4N54A.BraxQ2Nt.js} +1 -1
  106. package/web/_astro/{cynefin-OW5HDTMX.D8borhV-.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
  107. package/web/_astro/{dagre-BM42HDAG.Uc0lvjcV.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
  108. package/web/_astro/{diagram-2AECGRRQ.DDIxtDBo.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
  109. package/web/_astro/{diagram-5GNKFQAL.DVoDnD3H.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
  110. package/web/_astro/{diagram-KO2AKTUF.CRubTJ-0.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
  111. package/web/_astro/{diagram-LMA3HP47.m9SeYcXQ.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
  112. package/web/_astro/{diagram-OG6HWLK6.C6s2ZjV5.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
  113. package/web/_astro/{erDiagram-TEJ5UH35.nSzr5KTj.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
  114. package/web/_astro/{flowDiagram-I6XJVG4X.DrZcys16.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
  115. package/web/_astro/{ganttDiagram-6RSMTGT7.Do4M1b-K.js → ganttDiagram-6RSMTGT7.Dmdxgd-M.js} +1 -1
  116. package/web/_astro/{gitGraphDiagram-PVQCEYII.BWc9oJ4l.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
  117. package/web/_astro/index.EoZlzLK-.css +1 -0
  118. package/web/_astro/{infoDiagram-5YYISTIA.BIdOctFk.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
  119. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CV5SuG2t.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
  120. package/web/_astro/{journeyDiagram-JHISSGLW.CxE-rEJ-.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
  121. package/web/_astro/{kanban-definition-UN3LZRKU.BPRdKWs_.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
  122. package/web/_astro/{linear.zRsuuDTE.js → linear.BnzMBgo_.js} +1 -1
  123. package/web/_astro/{mermaid.core.jAJTcMKc.js → mermaid.core.DSqeFu2Y.js} +4 -4
  124. package/web/_astro/{mindmap-definition-RKZ34NQL.BffJxfCr.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
  125. package/web/_astro/{pieDiagram-4H26LBE5.CacwWaE3.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
  126. package/web/_astro/{quadrantDiagram-W4KKPZXB.hXELD06-.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
  127. package/web/_astro/{requirementDiagram-4Y6WPE33.DdEbPcqT.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
  128. package/web/_astro/{sankeyDiagram-5OEKKPKP.DgaDocKv.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
  129. package/web/_astro/{sequenceDiagram-3UESZ5HK.B0KScnAu.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
  130. package/web/_astro/{stateDiagram-AJRCARHV.CV9M_WNc.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
  131. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BOAo74Et.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
  132. package/web/_astro/{timeline-definition-PNZ67QCA.CqqepUMe.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
  133. package/web/_astro/{vennDiagram-CIIHVFJN.DDqe4dOG.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
  134. package/web/_astro/{wardleyDiagram-YWT4CUSO.B3R9OPdO.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
  135. package/web/_astro/{xychartDiagram-2RQKCTM6.B3SvgXpQ.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
  136. package/web/index.html +2 -2
  137. package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
  138. package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
  139. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -301
  140. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -481
  141. package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
  142. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
  143. package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
  144. package/plugins/sp/scripts/record-feature-sync.ts +0 -84
  145. package/plugins/sp/scripts/script-contract-check.ts +0 -506
  146. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  147. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
  148. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -189
  149. package/plugins/sp/scripts/task-size-precheck.ts +0 -212
  150. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  151. package/plugins/sp/scripts/validate-commands.ts +0 -689
  152. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -890
  153. package/plugins/sp/scripts/verify-answer-lint.ts +0 -549
  154. package/web/_astro/channel.CI6N_tCg.js +0 -1
  155. package/web/_astro/index.CENnIEqT.css +0 -1
@@ -0,0 +1,210 @@
1
+ // packages/app/src/services/residual-scan.ts
2
+ import { createHash } from "node:crypto";
3
+ var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
4
+ var PRIORITY_PATTERN = /^P[1-4]/;
5
+ var NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|—)\s*(\(.*\))?\.?$/i;
6
+ var DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
7
+ var RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
8
+ var DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
9
+ var ANCHOR_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
10
+ var RANGE_ANCHOR = /([A-Za-z0-9_./-]+\.[A-Za-z]+):([0-9]+)-[0-9]+/g;
11
+ var EXCLUDED_PATHS = ["docs/tasks", "docs/features/", ".spur/"];
12
+ var ALLOW_PRAGMA = "residual-scan:allow";
13
+ function makeItemId(category, location, text) {
14
+ const normalized = text.trim().replace(/\s+/g, " ");
15
+ const hex = createHash("sha256").update(`${location}${normalized}`).digest("hex");
16
+ return `${category}:${hex.slice(0, 8)}`;
17
+ }
18
+ function normalizeAnchor(location) {
19
+ return location.replace(RANGE_ANCHOR, "$1:$2");
20
+ }
21
+ function locationOf(locationCell, finding) {
22
+ const cell = locationCell.trim().replace(/`/g, "");
23
+ if (cell.length > 0 && cell !== "—")
24
+ return normalizeAnchor(cell);
25
+ const backtick = finding.match(/`([^`]+)`/)?.[1];
26
+ return backtick === undefined ? "" : normalizeAnchor(backtick);
27
+ }
28
+ function parseReviewFindings(taskContent) {
29
+ const section = taskContent.split(/^### Review\b/m)[1];
30
+ if (section === undefined)
31
+ return [];
32
+ const body = section.split(/^### /m)[0] ?? "";
33
+ const out = [];
34
+ const lines = body.split(`
35
+ `);
36
+ for (let i = 0;i < lines.length; i++) {
37
+ const line = lines[i];
38
+ if (line === undefined || !line.trimStart().startsWith("|"))
39
+ continue;
40
+ const header = splitRow(line);
41
+ const priorityCol = header.findIndex((h) => h.trim() === "Priority");
42
+ if (priorityCol === -1) {
43
+ while (i + 1 < lines.length && lines[i + 1]?.trimStart().startsWith("|"))
44
+ i++;
45
+ continue;
46
+ }
47
+ const findingCol = header.findIndex((h) => h.trim() === "Finding");
48
+ const locationCol = header.findIndex((h) => h.trim() === "Location");
49
+ const dispositionCol = header.findIndex((h) => DISPOSITION_HEADER.test(h.trim()));
50
+ i++;
51
+ const sep = lines[i];
52
+ if (sep !== undefined && /^\s*\|[\s:|-]+\|\s*$/.test(sep))
53
+ i++;
54
+ while (i < lines.length) {
55
+ const row = lines[i];
56
+ if (row === undefined || !row.trimStart().startsWith("|"))
57
+ break;
58
+ const cells = splitRow(row);
59
+ const priority = (cells[priorityCol] ?? "").trim();
60
+ const finding = (cells[findingCol] ?? "").trim();
61
+ const disposition = dispositionCol === -1 ? "" : (cells[dispositionCol] ?? "").trim();
62
+ if (PRIORITY_PATTERN.test(priority) && !NONE_FINDING.test(finding) && finding.length > 0 && !RESOLVED_DISPOSITION.test(disposition)) {
63
+ const location = locationOf(cells[locationCol] ?? "", finding);
64
+ out.push(DEFERRED_DISPOSITION.test(disposition) ? { priority, location, text: finding, deferral: disposition } : { priority, location, text: finding });
65
+ }
66
+ i++;
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ function splitRow(line) {
72
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
73
+ }
74
+ function parseDiffMarkers(addedLines) {
75
+ 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() }));
76
+ }
77
+ function findUncheckedBoxes(taskContent) {
78
+ const path = "task-file";
79
+ return taskContent.split(`
80
+ `).map((text, idx) => ({ text: text.trim(), line: idx + 1 })).filter((l) => l.text.startsWith("- [ ]")).map((l) => ({ location: `${path}:${l.line}`, text: l.text }));
81
+ }
82
+ function classify(items, deferrals) {
83
+ const deferred = new Map(deferrals.map((d) => [d.id, d.reason]));
84
+ return items.map((item) => {
85
+ const id = makeItemId(item.category, item.location, item.text);
86
+ let klass;
87
+ if (item.category === "review-finding")
88
+ klass = item.priority?.startsWith("P4") ? "advisory" : "blocking";
89
+ else if (item.category === "staging-residue")
90
+ klass = "housekeeping";
91
+ else
92
+ klass = "blocking";
93
+ if (klass === "blocking" && item.category !== "unchecked-box") {
94
+ const p3Like = item.category === "diff-marker" || item.category === "review-finding" && (item.priority ?? "").startsWith("P3");
95
+ const reason = deferred.get(id);
96
+ if (p3Like && reason !== undefined && reason.trim().length > 0)
97
+ klass = "deferrable";
98
+ }
99
+ return {
100
+ id,
101
+ category: item.category,
102
+ class: klass,
103
+ priority: item.priority,
104
+ location: item.location,
105
+ text: item.text
106
+ };
107
+ });
108
+ }
109
+ function scanResiduals(inputs) {
110
+ const { wbs, base, taskContent, addedLines, stagingResidue, deferrals } = inputs;
111
+ const reviewRows = parseReviewFindings(taskContent);
112
+ const tableDeferrals = reviewRows.flatMap((r) => r.deferral === undefined ? [] : [{ id: makeItemId("review-finding", r.location, r.text), reason: r.deferral }]);
113
+ const review = reviewRows.map((r) => ({
114
+ category: "review-finding",
115
+ priority: r.priority,
116
+ location: r.location,
117
+ text: r.text
118
+ }));
119
+ const markers = base === null ? [] : parseDiffMarkers(addedLines).map((m) => ({
120
+ category: "diff-marker",
121
+ location: m.location,
122
+ text: m.text
123
+ }));
124
+ const boxes = findUncheckedBoxes(taskContent).map((b) => ({
125
+ category: "unchecked-box",
126
+ location: b.location,
127
+ text: b.text
128
+ }));
129
+ const residue = stagingResidue.map((p) => ({
130
+ category: "staging-residue",
131
+ location: p,
132
+ text: p
133
+ }));
134
+ const items = classify([...review, ...markers, ...boxes, ...residue], [...tableDeferrals, ...deferrals]);
135
+ const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
136
+ for (const item of items)
137
+ counts[item.class]++;
138
+ return {
139
+ wbs,
140
+ base,
141
+ scanned: {
142
+ "review-finding": true,
143
+ "diff-marker": base !== null,
144
+ "unchecked-box": true,
145
+ "staging-residue": true
146
+ },
147
+ items,
148
+ counts
149
+ };
150
+ }
151
+ function blockingAnchors(items) {
152
+ const anchors = new Set;
153
+ for (const item of items) {
154
+ if (item.class !== "blocking")
155
+ continue;
156
+ for (const m of normalizeAnchor(item.location).matchAll(ANCHOR_PATTERN))
157
+ anchors.add(m[0]);
158
+ }
159
+ return [...anchors];
160
+ }
161
+ function foldVerdict(verdict, scan, existingFindings, maxFindings = 20) {
162
+ const blocking = scan.items.filter((i) => i.class === "blocking");
163
+ const deferrable = scan.items.filter((i) => i.class === "deferrable");
164
+ 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(", ")}` : "");
165
+ const checks = verdict.checks.filter((c) => c.name !== "residual-sweep");
166
+ checks.push({ name: "residual-sweep", status: blocking.length > 0 ? "fail" : "pass", evidence });
167
+ const merged = new Set([
168
+ ...existingFindings.split(/\s+/).filter((a) => a.length > 0),
169
+ ...blockingAnchors(scan.items)
170
+ ]);
171
+ const findings = [...merged].sort().slice(0, maxFindings).map((a) => `${a} `).join("");
172
+ let verdictStatus = verdict.verdict === "PASS" ? "PASS" : "FAIL";
173
+ if (verdict.verdict === "PARTIAL")
174
+ verdictStatus = "PARTIAL";
175
+ else if (verdict.verdict === "FAIL")
176
+ verdictStatus = "FAIL";
177
+ else if (blocking.length > 0)
178
+ verdictStatus = "PARTIAL";
179
+ return { verdict: verdictStatus, checks, findings };
180
+ }
181
+ function renderReport(wbs, items, attemptCount) {
182
+ const lines = [
183
+ `# Residual report — ${wbs}`,
184
+ "",
185
+ `Attempt: ${attemptCount}`,
186
+ "",
187
+ "| Category | Class | Location | Text |",
188
+ "| --- | --- | --- | --- |"
189
+ ];
190
+ for (const item of items) {
191
+ lines.push(`| ${item.category} | ${item.class} | ${item.location} | ${item.text.replace(/\|/g, "\\|")} |`);
192
+ }
193
+ return `${lines.join(`
194
+ `)}
195
+ `;
196
+ }
197
+ export {
198
+ scanResiduals,
199
+ renderReport,
200
+ parseReviewFindings,
201
+ parseDiffMarkers,
202
+ normalizeAnchor,
203
+ makeItemId,
204
+ locationOf,
205
+ foldVerdict,
206
+ findUncheckedBoxes,
207
+ classify,
208
+ blockingAnchors,
209
+ ALLOW_PRAGMA
210
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared spur-invocation helper for the sp plugin scripts (task 1007 R9, RF-architect-001).
3
+ *
4
+ * `spurCommand` and `defaultSpurBin` moved verbatim from `wrapup-steps.ts` and
5
+ * `workflow-step-profile.ts` so every surviving plugin script imports one copy (imported by
6
+ * relative path like `lib/env.ts`, so the standalone contract holds; not an app bundle
7
+ * because it carries no Spur-domain logic). Node/bun builtins and the vendored `./env`
8
+ * gateway only — bundled standalone by `superskill install`, so no `@gobing-ai/*` value
9
+ * imports (task 0669).
10
+ */
11
+
12
+ import { existsSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { getEnvVar } from './env';
15
+
16
+ /** `spurBin` splits on whitespace into a command plus prefix args (so `bun x.ts` works). */
17
+ export function spurCommand(spurBin: string | undefined): { cmd: string; prefix: string[] } {
18
+ const parts = (spurBin ?? 'spur')
19
+ .trim()
20
+ .split(/\s+/)
21
+ .filter((p) => p.length > 0);
22
+ return { cmd: parts[0] ?? 'spur', prefix: parts.slice(1) };
23
+ }
24
+
25
+ /**
26
+ * Resolve the spur CLI monorepo-safely (twin-safe resolution:
27
+ * self-contained and no plugin script imports another):
28
+ * --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
29
+ */
30
+ export function defaultSpurBin(): string {
31
+ const fromEnv = getEnvVar('SPUR_BIN');
32
+ if (fromEnv) return fromEnv;
33
+ const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
34
+ if (existsSync(local)) return `bun ${local}`;
35
+ return 'spur';
36
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * GENERATED by scripts/commands/bundle-plugin-lib.ts — do not edit.
3
+ * Source of truth: packages/app/src/workflow/step-profile.ts (task 1005 R1).
4
+ */
5
+ export declare interface ActionCostLike {
6
+ cacheHit: number | null;
7
+ }
8
+ export declare interface ActionCostAttributionLike {
9
+ exact: ActionCostLike | null;
10
+ estimated: ActionCostLike | null;
11
+ }
12
+ export declare interface TraceActionEvent {
13
+ kind: string;
14
+ node?: string;
15
+ actionKind?: string;
16
+ durationMs?: number | null;
17
+ startedAt?: string | null;
18
+ completedAt?: string | null;
19
+ invocation?: Record<string, string | number | boolean> | null;
20
+ cost?: ActionCostAttributionLike | null;
21
+ }
22
+ export declare interface StepProfileRun {
23
+ runId: string;
24
+ events: readonly TraceActionEvent[];
25
+ }
26
+ export declare interface TraceRunEntry {
27
+ runId: string;
28
+ isDryRun?: boolean;
29
+ status?: string;
30
+ workflowName?: string;
31
+ }
32
+ export declare type SessionMode = 'fresh' | 'resumed' | 'mixed';
33
+ export declare interface StepProfileExecution {
34
+ runId: string;
35
+ node: string;
36
+ actionKind: string;
37
+ durationMs: number | null;
38
+ idleGapMs: number | null;
39
+ session: 'fresh' | 'resumed' | null;
40
+ cacheHit: number | null;
41
+ }
42
+ export declare interface StepProfileRow {
43
+ node: string;
44
+ actionKind: string;
45
+ runs: number;
46
+ executions: number;
47
+ durationMs: { p50: number | null; max: number | null };
48
+ idleGapMs: { p50: number | null };
49
+ session: SessionMode | null;
50
+ cacheHit: { p50: number | null; known: number; of: number };
51
+ flags: string[];
52
+ }
53
+ export declare interface StepProfile {
54
+ workflow: string;
55
+ windowSec: number;
56
+ sampledRuns: number;
57
+ rows: StepProfileRow[];
58
+ }
59
+ export declare const DEFAULT_LAST: number;
60
+ export declare const DEFAULT_WINDOW_SEC: number;
61
+ export declare function nearestRankP50(values: readonly number[]): number | null;
62
+ export declare function extractExecutions(runId: string, events: readonly TraceActionEvent[]): StepProfileExecution[];
63
+ export declare function rowFlags(row: StepProfileRow, windowSec: number): string[];
64
+ export declare function buildRows(executions: readonly StepProfileExecution[], windowSec: number): StepProfileRow[];
65
+ export declare function buildStepProfile(input: {
66
+ workflow: string;
67
+ windowSec: number;
68
+ runs: readonly StepProfileRun[];
69
+ }): StepProfile;
70
+ export declare function nonDryRuns(entries: readonly TraceRunEntry[]): TraceRunEntry[];
71
+ export declare function formatStepProfileHuman(profile: StepProfile): string;
@@ -0,0 +1,174 @@
1
+ // packages/app/src/workflow/step-profile.ts
2
+ var DEFAULT_LAST = 20;
3
+ var DEFAULT_WINDOW_SEC = 300;
4
+ function nearestRankP50(values) {
5
+ if (values.length === 0)
6
+ return null;
7
+ const sorted = [...values].sort((a, b) => a - b);
8
+ return sorted[Math.ceil(sorted.length / 2) - 1] ?? null;
9
+ }
10
+ function numeric(value) {
11
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
12
+ }
13
+ function msBetween(from, to) {
14
+ if (typeof from !== "string" || typeof to !== "string")
15
+ return null;
16
+ const start = Date.parse(from);
17
+ const end = Date.parse(to);
18
+ if (!Number.isFinite(start) || !Number.isFinite(end))
19
+ return null;
20
+ return end - start;
21
+ }
22
+ function sessionOf(event) {
23
+ const value = event.invocation?.continue;
24
+ if (value === true)
25
+ return "resumed";
26
+ if (value === false)
27
+ return "fresh";
28
+ return null;
29
+ }
30
+ function cacheHitOf(event) {
31
+ return numeric(event.cost?.exact?.cacheHit);
32
+ }
33
+ function startKey(event) {
34
+ const parsed = typeof event.startedAt === "string" ? Date.parse(event.startedAt) : Number.NaN;
35
+ return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
36
+ }
37
+ function extractExecutions(runId, events) {
38
+ const actions = events.filter((e) => e.kind === "action").sort((a, b) => {
39
+ const left = startKey(a);
40
+ const right = startKey(b);
41
+ return left === right ? 0 : left - right;
42
+ });
43
+ const executions = [];
44
+ for (const [index, event] of actions.entries()) {
45
+ const previous = index === 0 ? undefined : actions[index - 1];
46
+ executions.push({
47
+ runId,
48
+ node: event.node ?? "",
49
+ actionKind: event.actionKind ?? "",
50
+ durationMs: numeric(event.durationMs),
51
+ idleGapMs: msBetween(previous?.completedAt, event.startedAt),
52
+ session: sessionOf(event),
53
+ cacheHit: cacheHitOf(event)
54
+ });
55
+ }
56
+ return executions;
57
+ }
58
+ function foldSession(executions) {
59
+ const known = new Set(executions.map((e) => e.session).filter((s) => s !== null));
60
+ if (known.size === 0)
61
+ return null;
62
+ if (known.size === 1)
63
+ return [...known][0] ?? null;
64
+ return "mixed";
65
+ }
66
+ function rowFlags(row, windowSec) {
67
+ const wMs = windowSec * 1000;
68
+ const flags = [];
69
+ const agentRun = row.actionKind === "agent.run";
70
+ const resumed = row.session === "resumed" || row.session === "mixed";
71
+ const p50 = row.durationMs.p50;
72
+ if (!agentRun && p50 !== null && p50 > wMs)
73
+ flags.push("step-over-window");
74
+ const idle = row.idleGapMs.p50;
75
+ if (agentRun && resumed && idle !== null && idle > wMs)
76
+ flags.push("resume-after-idle");
77
+ const hit = row.cacheHit.p50;
78
+ if (agentRun && resumed && row.cacheHit.known > 0 && hit !== null && hit < 0.5)
79
+ flags.push("resume-cold-cache");
80
+ if (agentRun && p50 !== null && p50 > 2 * wMs)
81
+ flags.push("agent-run-over-2w");
82
+ return flags;
83
+ }
84
+ function buildRows(executions, windowSec) {
85
+ const grouped = new Map;
86
+ for (const execution of executions) {
87
+ const key = `${execution.node}\x00${execution.actionKind}`;
88
+ const bucket = grouped.get(key);
89
+ if (bucket === undefined)
90
+ grouped.set(key, [execution]);
91
+ else
92
+ bucket.push(execution);
93
+ }
94
+ const rows = [];
95
+ for (const bucket of grouped.values()) {
96
+ const first = bucket[0];
97
+ if (first === undefined)
98
+ continue;
99
+ const durations = bucket.map((e) => e.durationMs).filter((d) => d !== null);
100
+ const gaps = bucket.map((e) => e.idleGapMs).filter((g) => g !== null);
101
+ const hits = bucket.map((e) => e.cacheHit).filter((h) => h !== null);
102
+ const row = {
103
+ node: first.node,
104
+ actionKind: first.actionKind,
105
+ runs: new Set(bucket.map((e) => e.runId)).size,
106
+ executions: bucket.length,
107
+ durationMs: {
108
+ p50: nearestRankP50(durations),
109
+ max: durations.length === 0 ? null : Math.max(...durations)
110
+ },
111
+ idleGapMs: { p50: nearestRankP50(gaps) },
112
+ session: first.actionKind === "agent.run" ? foldSession(bucket) : null,
113
+ cacheHit: {
114
+ p50: nearestRankP50(hits),
115
+ known: hits.length,
116
+ of: bucket.length
117
+ },
118
+ flags: []
119
+ };
120
+ row.flags = rowFlags(row, windowSec);
121
+ rows.push(row);
122
+ }
123
+ return rows.sort((a, b) => a.node === b.node ? a.actionKind.localeCompare(b.actionKind) : a.node.localeCompare(b.node));
124
+ }
125
+ function buildStepProfile(input) {
126
+ const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
127
+ return {
128
+ workflow: input.workflow,
129
+ windowSec: input.windowSec,
130
+ sampledRuns: input.runs.length,
131
+ rows: buildRows(executions, input.windowSec)
132
+ };
133
+ }
134
+ function nonDryRuns(entries) {
135
+ return entries.filter((e) => e.isDryRun !== true && typeof e.runId === "string" && e.runId.length > 0);
136
+ }
137
+ function seconds(value) {
138
+ return value === null ? "?" : `${(value / 1000).toFixed(1)}s`;
139
+ }
140
+ function ratio(value) {
141
+ return value === null ? "?" : value.toFixed(2);
142
+ }
143
+ function formatStepProfileHuman(profile) {
144
+ const lines = [
145
+ `step profile — ${profile.workflow} (window ${profile.windowSec}s, ${profile.sampledRuns} sampled runs)`
146
+ ];
147
+ for (const row of profile.rows) {
148
+ lines.push([
149
+ row.node.padEnd(20),
150
+ row.actionKind.padEnd(14),
151
+ `${row.runs}/${row.executions}`.padEnd(8),
152
+ `p50=${seconds(row.durationMs.p50)}`.padEnd(12),
153
+ `max=${seconds(row.durationMs.max)}`.padEnd(12),
154
+ `idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
155
+ `session=${row.session ?? "?"}`.padEnd(15),
156
+ `cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
157
+ `flags=${row.flags.length === 0 ? "-" : row.flags.join(",")}`
158
+ ].join(" "));
159
+ }
160
+ return `${lines.join(`
161
+ `)}
162
+ `;
163
+ }
164
+ export {
165
+ rowFlags,
166
+ nonDryRuns,
167
+ nearestRankP50,
168
+ formatStepProfileHuman,
169
+ extractExecutions,
170
+ buildStepProfile,
171
+ buildRows,
172
+ DEFAULT_WINDOW_SEC,
173
+ DEFAULT_LAST
174
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.95",
3
+ "version": "0.3.96",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -54,7 +54,7 @@ roles:
54
54
  stages: [changelog]
55
55
  - id: coder
56
56
  tier: standard
57
- commands: [dev-run, dev-unit, dev-debug, dev-simplify, dev-fixall, dev-reverse, dev-wrap, dev-wrapall, dev-gtd]
57
+ commands: [dev-run, dev-unit, dev-debug, dev-simplify, dev-fixall, dev-fixgha, dev-reverse, dev-wrap, dev-wrapall, dev-gtd]
58
58
  stages: [implement, test, wrap]
59
59
  - id: reviewer
60
60
  tier: capable-1
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
+ // @bun
4
3
 
5
4
  // plugins/sp/scripts/history-anatomy-cache.ts
6
- import { spawnSync } from "node:child_process";
5
+ import { spawnSync } from "child_process";
6
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
7
+
8
+ // plugins/sp/lib/history-anatomy.generated.mjs
7
9
  import { createHash as createHash2 } from "node:crypto";
8
10
  import {
9
11
  closeSync,
10
12
  existsSync,
11
13
  fsyncSync,
12
- mkdirSync,
13
14
  openSync,
14
15
  readdirSync,
15
16
  readFileSync,
@@ -19,8 +20,6 @@ import {
19
20
  writeFileSync
20
21
  } from "node:fs";
21
22
  import { join } from "node:path";
22
-
23
- // plugins/sp/lib/artifact-digest.generated.mjs
24
23
  import { createHash } from "node:crypto";
25
24
  var ARTIFACT_ARRAY_CLASSIFICATION = {
26
25
  byTool: "ranked",
@@ -63,8 +62,6 @@ function semanticArtifactDigest(artifactJson) {
63
62
  const material = JSON.stringify(canonicalize(artifactJson, "root"));
64
63
  return createHash("sha256").update(material).digest("hex");
65
64
  }
66
-
67
- // plugins/sp/scripts/history-anatomy-cache.ts
68
65
  var REPORT_SECTIONS = [
69
66
  "Scope and provenance",
70
67
  "Executive summary",
@@ -669,6 +666,8 @@ function diffPorcelain(before, now, expects) {
669
666
  const beforePaths = porcelainPaths(before);
670
667
  return [...porcelainPaths(now)].filter((p) => !beforePaths.has(p) && !expects.has(p)).sort();
671
668
  }
669
+
670
+ // plugins/sp/scripts/history-anatomy-cache.ts
672
671
  var VALID_COMMANDS = "digest, check, paths, assert-clean, probe, stamp, refresh, publish";
673
672
  var PROBE_USAGE = "<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>] [--helper <f>]";
674
673
  function parseFlags(args) {
@@ -698,7 +697,7 @@ function runCacheCli(argv) {
698
697
  }
699
698
  let artifact;
700
699
  try {
701
- artifact = JSON.parse(readFileSync(a, "utf8"));
700
+ artifact = JSON.parse(readFileSync2(a, "utf8"));
702
701
  } catch {
703
702
  return { exitCode: 1, stdout: "", stderr: `could not parse artifact at ${a}
704
703
  ` };
@@ -712,7 +711,7 @@ function runCacheCli(argv) {
712
711
  return { exitCode: 1, stdout: "", stderr: `usage: <script> check <report.md>
713
712
  ` };
714
713
  }
715
- const result = checkReportStructure(readFileSync(a, "utf8"));
714
+ const result = checkReportStructure(readFileSync2(a, "utf8"));
716
715
  const stdout = `${result.ok ? "PASS" : "FAIL"}
717
716
  ${result.problems.map((p) => `- ${p}
718
717
  `).join("")}`;
@@ -751,7 +750,7 @@ ${result.problems.map((p) => `- ${p}
751
750
  return { exitCode: 0, stdout: "", stderr: `assert-clean: git unavailable; skipped
752
751
  ` };
753
752
  }
754
- const undeclared = diffPorcelain(readFileSync(f.baseline, "utf8"), now, expects);
753
+ const undeclared = diffPorcelain(readFileSync2(f.baseline, "utf8"), now, expects);
755
754
  if (undeclared.length > 0) {
756
755
  return {
757
756
  exitCode: 1,
@@ -795,13 +794,13 @@ ${result.problems.map((p) => `- ${p}
795
794
  since: f.since,
796
795
  until: f.until
797
796
  });
798
- writeFileSync(f.out, env);
797
+ writeFileSync2(f.out, env);
799
798
  if (f["run-id"] !== undefined && f["run-id"] !== "") {
800
799
  mkdirSync(".spur/run", { recursive: true });
801
800
  const envVars = Object.fromEntries(env.trim().split(`
802
801
  `).map((l) => [l.slice(0, l.indexOf("=")), l.slice(l.indexOf("=") + 1)]));
803
802
  const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
804
- writeFileSync(`.spur/run/${f["run-id"]}-selector.json`, `${JSON.stringify({
803
+ writeFileSync2(`.spur/run/${f["run-id"]}-selector.json`, `${JSON.stringify({
805
804
  mode: v.mode,
806
805
  date: envVars.HA_DATE ?? null,
807
806
  focus: v.focus,
@@ -843,7 +842,7 @@ ${result.problems.map((p) => `- ${p}
843
842
  ` };
844
843
  }
845
844
  if (f.out !== undefined)
846
- writeFileSync(f.out, `${JSON.stringify(result.current, null, 2)}
845
+ writeFileSync2(f.out, `${JSON.stringify(result.current, null, 2)}
847
846
  `);
848
847
  const reasons = result.decision.reasons.map((r) => `- ${r}
849
848
  `).join("");
@@ -861,8 +860,8 @@ ${reasons}`, stderr: "" };
861
860
  };
862
861
  }
863
862
  try {
864
- const p = JSON.parse(readFileSync(f.provenance, "utf8"));
865
- writeFileSync(f.out, `${stampReport(readFileSync(f.candidate, "utf8"), p)}
863
+ const p = JSON.parse(readFileSync2(f.provenance, "utf8"));
864
+ writeFileSync2(f.out, `${stampReport(readFileSync2(f.candidate, "utf8"), p)}
866
865
  `);
867
866
  } catch {
868
867
  return { exitCode: 1, stdout: "", stderr: `stamp: could not read candidate or provenance
@@ -882,8 +881,8 @@ ${reasons}`, stderr: "" };
882
881
  }
883
882
  try {
884
883
  const disposition = f.disposition ?? "hit";
885
- const refreshed = refreshReport(readFileSync(f.report, "utf8"), f["validated-at"] ?? new Date().toISOString(), disposition);
886
- writeFileSync(f.out, refreshed.endsWith(`
884
+ const refreshed = refreshReport(readFileSync2(f.report, "utf8"), f["validated-at"] ?? new Date().toISOString(), disposition);
885
+ writeFileSync2(f.out, refreshed.endsWith(`
887
886
  `) ? refreshed : `${refreshed}
888
887
  `);
889
888
  } catch {
@@ -921,5 +920,7 @@ export {
921
920
  decideCache,
922
921
  checkReportStructure,
923
922
  buildProvenance,
924
- bannerLine
923
+ bannerLine,
924
+ REPORT_SECTIONS,
925
+ FINDING_FIELDS
925
926
  };