@gobing-ai/spur 0.3.91 → 0.3.93

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 (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -9
  3. package/config/plugin-scripts.json +27 -1
  4. package/config/templates/AGENTS.md +4 -0
  5. package/config/templates/docs/02_ROADMAP.md +2 -0
  6. package/config/templates/docs/03_ARCHITECTURE.md +3 -1
  7. package/config/templates/docs/04_DESIGN.md +2 -0
  8. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +10 -2
  9. package/config/workflow-candidates.json +72 -1
  10. package/config/workflows/feature-lifecycle.yaml +14 -5
  11. package/config/workflows/feature-verification.yaml +36 -27
  12. package/config/workflows/history-anatomy.yaml +16 -25
  13. package/config/workflows/idea-pipeline.yaml +76 -23
  14. package/config/workflows/pr-review.yaml +8 -0
  15. package/config/workflows/task-pipeline.yaml +362 -40
  16. package/config/workflows/wayfinder-resolution.yaml +5 -0
  17. package/config/workflows/wrapup-pipeline.yaml +102 -19
  18. package/package.json +9 -9
  19. package/plugins/sp/README.md +7 -2
  20. package/plugins/sp/agents/super-planner.md +14 -5
  21. package/plugins/sp/commands/dev-dogfood.md +4 -4
  22. package/plugins/sp/commands/dev-fixall.md +8 -5
  23. package/plugins/sp/commands/dev-run.md +8 -2
  24. package/plugins/sp/commands/dev-runall.md +14 -8
  25. package/plugins/sp/commands/dev-verify.md +9 -0
  26. package/plugins/sp/commands/dev-verifyall.md +5 -0
  27. package/plugins/sp/lib/idea-handoff.generated.mjs +306 -301
  28. package/plugins/sp/lib/inline-run.generated.d.mts +18 -0
  29. package/plugins/sp/lib/inline-run.generated.mjs +1468 -0
  30. package/plugins/sp/plugin.json +1 -1
  31. package/plugins/sp/references/environment-lens.md +1 -1
  32. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
  33. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
  34. package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
  35. package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
  36. package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
  37. package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
  38. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +2 -1
  39. package/plugins/sp/scripts/inline-run-setup.mjs +421 -0
  40. package/plugins/sp/scripts/inline-run-setup.ts +325 -78
  41. package/plugins/sp/scripts/quality-gate.mjs +248 -6
  42. package/plugins/sp/scripts/quality-gate.ts +410 -9
  43. package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
  44. package/plugins/sp/scripts/record-feature-sync.ts +84 -0
  45. package/plugins/sp/scripts/residual-scan.mjs +484 -0
  46. package/plugins/sp/scripts/residual-scan.ts +640 -0
  47. package/plugins/sp/scripts/task-diffstat.mjs +156 -0
  48. package/plugins/sp/scripts/task-diffstat.ts +229 -0
  49. package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
  50. package/plugins/sp/scripts/task-size-precheck.ts +8 -3
  51. package/plugins/sp/scripts/wrapup-drift-probe.mjs +181 -0
  52. package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
  53. package/plugins/sp/scripts/wrapup-steps.mjs +60 -1
  54. package/plugins/sp/scripts/wrapup-steps.ts +89 -4
  55. package/plugins/sp/skills/brainstorm/SKILL.md +2 -0
  56. package/plugins/sp/skills/brainstorm/references/workflows.md +17 -2
  57. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  58. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
  59. package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
  60. package/plugins/sp/skills/code-verification/SKILL.md +21 -0
  61. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -2
  62. package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
  63. package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
  64. package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
  65. package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
  66. package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
  67. package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
  68. package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
  69. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  70. package/plugins/sp/skills/session-review/SKILL.md +12 -2
  71. package/plugins/sp/skills/spur-check/SKILL.md +112 -0
  72. package/plugins/sp/skills/spur-cli/references/features.md +1 -1
  73. package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
  74. package/plugins/sp/skills/spur-cli/references/workflows.md +39 -19
  75. package/plugins/sp/skills/spur-dev/SKILL.md +13 -5
  76. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +18 -3
  77. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
  78. package/plugins/sp/skills/spur-dev/references/document-authoring.md +85 -0
  79. package/plugins/sp/skills/spur-dev/references/execution-batch.md +286 -58
  80. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +1 -1
  81. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +27 -4
  82. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
  83. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +61 -16
  84. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +10 -5
  85. package/plugins/sp/skills/spur-dev/templates/design.md +31 -0
  86. package/plugins/sp/skills/spur-dev/templates/plan.md +32 -0
  87. package/plugins/sp/skills/spur-doctor/SKILL.md +60 -14
  88. package/schemas/state-machine-workflow.schema.json +4 -0
  89. package/spur.js +22501 -19644
  90. package/web/_astro/BoardApp.CerSBgis.js +192 -0
  91. package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
  92. package/web/_astro/{TaskDetail.CXGltuT_.js → TaskDetail.DCqiC-OZ.js} +1 -1
  93. package/web/_astro/arc.CPwg6Rw0.js +1 -0
  94. package/web/_astro/{architectureDiagram-3BPJPVTR.DJ8DHkWE.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
  95. package/web/_astro/{blockDiagram-GPEHLZMM.D8DHK3Jl.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
  96. package/web/_astro/{c4Diagram-AAUBKEIU.BugQbX9u.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
  97. package/web/_astro/channel.MYZLKNwy.js +1 -0
  98. package/web/_astro/{chunk-2J33WTMH.Mv26KlVn.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
  99. package/web/_astro/{chunk-4BX2VUAB.CD51JoT_.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
  100. package/web/_astro/{chunk-55IACEB6.D_PFaIEe.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
  101. package/web/_astro/{chunk-727SXJPM.DemMW1ao.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
  102. package/web/_astro/{chunk-AQP2D5EJ.Iu2V5-ex.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
  103. package/web/_astro/{chunk-FMBD7UC4.MsNgSP-E.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
  104. package/web/_astro/{chunk-ND2GUHAM.DfbRaAlm.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
  105. package/web/_astro/{chunk-QZHKN3VN.BbJAQ4h-.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
  106. package/web/_astro/{classDiagram-4FO5ZUOK.CPurtiC2.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
  107. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CPurtiC2.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
  108. package/web/_astro/{cose-bilkent-S5V4N54A.CKKdx1bM.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
  109. package/web/_astro/{cynefin-OW5HDTMX.CoKMTg-R.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
  110. package/web/_astro/{dagre-BM42HDAG.D4h4_k56.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
  111. package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
  112. package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
  113. package/web/_astro/{diagram-KO2AKTUF.BFoCkiCr.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
  114. package/web/_astro/{diagram-LMA3HP47.exHn9OVx.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
  115. package/web/_astro/{diagram-OG6HWLK6.CeqO34nN.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
  116. package/web/_astro/{erDiagram-TEJ5UH35.D_v7HqxR.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
  117. package/web/_astro/{flowDiagram-I6XJVG4X.EUmrpbwh.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
  118. package/web/_astro/{ganttDiagram-6RSMTGT7.BOCF5lII.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
  119. package/web/_astro/{gitGraphDiagram-PVQCEYII.Di7otYZD.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
  120. package/web/_astro/index.Hjbr15fG.css +1 -0
  121. package/web/_astro/{infoDiagram-5YYISTIA.TcBkCAJk.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
  122. package/web/_astro/{ishikawaDiagram-YF4QCWOH.D-2y4M0c.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
  123. package/web/_astro/{journeyDiagram-JHISSGLW.DPbJI_n2.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
  124. package/web/_astro/{kanban-definition-UN3LZRKU.EFxhQ9Fj.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
  125. package/web/_astro/{linear.DSAsQLzs.js → linear.C2_IkbZT.js} +1 -1
  126. package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
  127. package/web/_astro/{mindmap-definition-RKZ34NQL.CJY1N_7V.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
  128. package/web/_astro/{pieDiagram-4H26LBE5.567ZNoL2.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
  129. package/web/_astro/{quadrantDiagram-W4KKPZXB.mqfz9-MY.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
  130. package/web/_astro/{requirementDiagram-4Y6WPE33.Bv1Gv9In.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
  131. package/web/_astro/{sankeyDiagram-5OEKKPKP.B6Gs4X4r.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
  132. package/web/_astro/{sequenceDiagram-3UESZ5HK.BhYj4v-m.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
  133. package/web/_astro/{stateDiagram-AJRCARHV.BPbBnkpw.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
  134. package/web/_astro/{stateDiagram-v2-BHNVJYJU.C4squMNK.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
  135. package/web/_astro/{timeline-definition-PNZ67QCA.C_SwIHgl.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
  136. package/web/_astro/{vennDiagram-CIIHVFJN.Bz4NZGpQ.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
  137. package/web/_astro/{wardleyDiagram-YWT4CUSO.CozMVZ3i.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
  138. package/web/_astro/{xychartDiagram-2RQKCTM6.BwMGBwjB.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
  139. package/web/index.html +2 -2
  140. package/config/workflows/decision-routing-example.yaml +0 -134
  141. package/web/_astro/BoardApp.BEDWpzsr.js +0 -188
  142. package/web/_astro/BoardApp.DQG2xfEz.js +0 -1
  143. package/web/_astro/arc.C0rrflm_.js +0 -1
  144. package/web/_astro/channel.SRrg1P-w.js +0 -1
  145. package/web/_astro/diagram-2AECGRRQ.DJ0h9zgw.js +0 -43
  146. package/web/_astro/diagram-5GNKFQAL.DvPk1jYd.js +0 -10
  147. package/web/_astro/index.Bx6GY4RH.css +0 -1
  148. package/web/_astro/mermaid.core.kAZjgJHG.js +0 -301
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // plugins/sp/scripts/task-diffstat.ts
5
+ import { spawnSync } from "child_process";
6
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { join } from "path";
8
+
9
+ // plugins/sp/lib/env.ts
10
+ function getEnvVars() {
11
+ return process.env;
12
+ }
13
+
14
+ // plugins/sp/scripts/task-diffstat.ts
15
+ var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
16
+ var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
17
+ function globToRegex(glob) {
18
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
19
+ return new RegExp(`^${escaped}$`);
20
+ }
21
+ var SENSITIVE_MATCHERS = [
22
+ ...SENSITIVE_PREFIXES.map((prefix) => ({
23
+ label: prefix,
24
+ test: (path) => path.startsWith(prefix)
25
+ })),
26
+ ...SENSITIVE_GLOBS.map((glob) => {
27
+ const regex = globToRegex(glob);
28
+ return { label: glob, test: (path) => regex.test(path) };
29
+ })
30
+ ];
31
+ function sensitiveReasonForPath(path) {
32
+ const match = SENSITIVE_MATCHERS.find((entry) => entry.test(path));
33
+ return match ? `matches sensitive pattern ${match.label}` : null;
34
+ }
35
+ function defaultGitRunner(cwd) {
36
+ return (args) => {
37
+ const run = spawnSync("git", args, { cwd, encoding: "utf8" });
38
+ return { status: run.status ?? 1, stdout: run.stdout ?? "" };
39
+ };
40
+ }
41
+ function countNewlines(path, cwd) {
42
+ let content;
43
+ try {
44
+ content = readFileSync(cwd ? join(cwd, path) : path, "utf8");
45
+ } catch {
46
+ return 0;
47
+ }
48
+ let lines = 0;
49
+ for (const ch of content)
50
+ if (ch === `
51
+ `)
52
+ lines += 1;
53
+ return lines;
54
+ }
55
+ function expandDiffPath(raw) {
56
+ const arrow = raw.indexOf(" => ");
57
+ if (arrow === -1)
58
+ return [raw];
59
+ const open = raw.indexOf("{");
60
+ const close = raw.lastIndexOf("}");
61
+ if (open !== -1 && close > open && open < arrow && arrow < close) {
62
+ const inner = raw.slice(open + 1, close);
63
+ const innerArrow = inner.indexOf(" => ");
64
+ if (innerArrow !== -1) {
65
+ const head = raw.slice(0, open);
66
+ const tail = raw.slice(close + 1);
67
+ return [head + inner.slice(0, innerArrow) + tail, head + inner.slice(innerArrow + 4) + tail];
68
+ }
69
+ }
70
+ return [raw.slice(0, arrow), raw.slice(arrow + 4)];
71
+ }
72
+ function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
73
+ const cwd = options.cwd;
74
+ const wbs = env.wbs ?? "";
75
+ if (wbs.length === 0) {
76
+ process.stderr.write(`task-diffstat: wbs is empty \u2014 refusing to guess the run artifact path
77
+ `);
78
+ return { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true, resultFile: "", exitCode: 1 };
79
+ }
80
+ const relRun = join(".spur", "run");
81
+ const relResult = join(relRun, `${wbs}-diffstat.json`);
82
+ const abs = (p) => cwd ? join(cwd, p) : p;
83
+ mkdirSync(abs(relRun), { recursive: true });
84
+ const failSafe = (reason) => {
85
+ process.stderr.write(`task-diffstat: ${reason} \u2014 failing safe (sensitive)
86
+ `);
87
+ const row2 = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
88
+ writeFileSync(abs(relResult), `${JSON.stringify(row2)}
89
+ `);
90
+ return { ...row2, resultFile: relResult, exitCode: 0 };
91
+ };
92
+ let base = "";
93
+ try {
94
+ base = readFileSync(abs(join(relRun, `${wbs}-base.sha`)), "utf8").trim();
95
+ } catch {}
96
+ if (!/^[0-9a-f]{7,40}$/i.test(base))
97
+ return failSafe("run base .spur/run/<wbs>-base.sha missing or malformed");
98
+ const numstat = git(["diff", "--numstat", base]);
99
+ if (numstat.status !== 0)
100
+ return failSafe(`git diff --numstat failed (status=${numstat.status})`);
101
+ const paths = new Set;
102
+ let insertions = 0;
103
+ let deletions = 0;
104
+ for (const line of numstat.stdout.split(`
105
+ `)) {
106
+ const row2 = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line);
107
+ if (!row2)
108
+ continue;
109
+ for (const p of expandDiffPath(row2[3] ?? ""))
110
+ paths.add(p);
111
+ if (row2[1] !== "-")
112
+ insertions += Number(row2[1]);
113
+ if (row2[2] !== "-")
114
+ deletions += Number(row2[2]);
115
+ }
116
+ const others = git(["ls-files", "--others", "--exclude-standard", "-z"]);
117
+ if (others.status !== 0)
118
+ return failSafe(`git ls-files failed (status=${others.status})`);
119
+ for (const path of others.stdout.split("\x00")) {
120
+ if (path.length === 0 || path === ".spur" || path.startsWith(".spur/"))
121
+ continue;
122
+ paths.add(path);
123
+ insertions += countNewlines(path, cwd);
124
+ }
125
+ const all = [...paths].filter((p) => p.length > 0).sort();
126
+ const sensitive = all.some((p) => sensitiveReasonForPath(p) !== null);
127
+ const row = { files: all.length, insertions, deletions, paths: all, sensitive };
128
+ writeFileSync(abs(relResult), `${JSON.stringify(row)}
129
+ `);
130
+ return { ...row, resultFile: relResult, exitCode: 0 };
131
+ }
132
+ var TASK_DIFFSTAT_USAGE = "usage: task-diffstat (env: wbs)";
133
+ function main(argv, env = getEnvVars(), options = {}) {
134
+ for (let i = 0;i < argv.length; i++) {
135
+ if (argv[i] === "--spur-bin") {
136
+ i++;
137
+ continue;
138
+ }
139
+ process.stderr.write(`${TASK_DIFFSTAT_USAGE}
140
+ `);
141
+ return 2;
142
+ }
143
+ return runDiffstat(env, options).exitCode;
144
+ }
145
+ {
146
+ process.exit(main(process.argv.slice(2)));
147
+ }
148
+ export {
149
+ sensitiveReasonForPath,
150
+ runDiffstat,
151
+ main,
152
+ expandDiffPath,
153
+ TASK_DIFFSTAT_USAGE,
154
+ SENSITIVE_PREFIXES,
155
+ SENSITIVE_GLOBS
156
+ };
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * task-diffstat — deterministic diff-evidence producer behind the task-pipeline `triage`
4
+ * onEnter (task 0943, feature D64, ADR-125 lane routing).
5
+ *
6
+ * Reads the run's anchored base commit `.spur/run/<wbs>-base.sha` (written once by
7
+ * `precheck`, F96 R1) and the current working tree, then writes:
8
+ * `.spur/run/<wbs>-diffstat.json` `{ files, insertions, deletions, paths[], sensitive }`
9
+ *
10
+ * Counts come from `git diff --numstat <base>` (tracked changes; binary rows count the
11
+ * path but no lines) plus untracked files (`git ls-files --others --exclude-standard`,
12
+ * newline-counted — # ponytail: untracked line counts are a newline scan, numstat-grade
13
+ * binary detection is unnecessary for a lane heuristic; switch to `--numstat --no-index`
14
+ * if a miscount ever flips a lane).
15
+ *
16
+ * `sensitive` is true when ANY changed path matches the exported SENSITIVE list (0943 R2c);
17
+ * the workflow's pre-decide guard maps sensitive (or >400 changed lines) to the safety lane
18
+ * without a model call. Fail safe: a missing/malformed base, a failing git call, or any
19
+ * other probe problem writes `sensitive: true` (and exits 0) so the lane degrades to
20
+ * safety, never to fast.
21
+ *
22
+ * Node-builtin imports only; the git lookup is a spawnable `GitRunner` so tests can fake
23
+ * it; `main(argv, env, options)` is the same injection point the workflow wrapper and its
24
+ * node twin exercise. Only an empty `wbs` (mis-invocation) exits nonzero.
25
+ */
26
+
27
+ import { spawnSync } from 'node:child_process';
28
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
29
+ import { join } from 'node:path';
30
+ import { getEnvVars } from '../lib/env';
31
+
32
+ export interface DiffstatEnv {
33
+ wbs?: string;
34
+ [key: string]: string | undefined;
35
+ }
36
+
37
+ export interface DiffstatOptions {
38
+ /** Base directory for `.spur/run` and the git probes; defaults to the process cwd. */
39
+ cwd?: string;
40
+ }
41
+
42
+ /** Minimal spawnable git surface — tests fake this to return canned probe output. */
43
+ export interface GitRunResult {
44
+ status: number;
45
+ stdout: string;
46
+ }
47
+ export type GitRunner = (args: string[]) => GitRunResult;
48
+
49
+ /** Diffstat row written to `.spur/run/<wbs>-diffstat.json` (0943 R2a, frozen shape). */
50
+ export interface Diffstat {
51
+ files: number;
52
+ insertions: number;
53
+ deletions: number;
54
+ paths: string[];
55
+ sensitive: boolean;
56
+ }
57
+
58
+ export interface DiffstatResult extends Diffstat {
59
+ resultFile: string;
60
+ /** Exit code the workflow wrapper observes (only an empty wbs is a hard failure). */
61
+ exitCode: number;
62
+ }
63
+
64
+ /**
65
+ * Sensitive paths (0943 R2c): any changed path under one of these forces the safety lane
66
+ * before the `task-triage` decide runs — sensitive work is never lane-negotiated with a
67
+ * model. Frozen list: `drizzle/`, `packages/config/`, auth-prefixed basenames anywhere
68
+ * under the server src tree, `*secret*` basenames, `.github/`, `plugins/sp/hooks/`, and
69
+ * any `*.sql` — spelled out in SENSITIVE_PREFIXES / SENSITIVE_GLOBS below (kept as data,
70
+ * not prose, so the `*` / `/` sequences survive the doc comment).
71
+ */
72
+ export const SENSITIVE_PREFIXES = ['drizzle/', 'packages/config/', '.github/', 'plugins/sp/hooks/'] as const;
73
+ export const SENSITIVE_GLOBS = ['apps/server/src/**/auth*', '**/*secret*', '**/*.sql'] as const;
74
+
75
+ // Glob → RegExp: `**` + `/` matches any-or-no directories, bare `**` any characters, `*` within one segment.
76
+ function globToRegex(glob: string): RegExp {
77
+ const escaped = glob
78
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
79
+ .replaceAll('**/', '\u0000')
80
+ .replaceAll('**', '\u0001')
81
+ .replaceAll('*', '[^/]*')
82
+ .replaceAll('\u0000', '(?:.*/)?')
83
+ .replaceAll('\u0001', '.*');
84
+ return new RegExp(`^${escaped}$`);
85
+ }
86
+
87
+ const SENSITIVE_MATCHERS: Array<{ label: string; test: (path: string) => boolean }> = [
88
+ ...SENSITIVE_PREFIXES.map((prefix) => ({
89
+ label: prefix,
90
+ test: (path: string): boolean => path.startsWith(prefix),
91
+ })),
92
+ ...SENSITIVE_GLOBS.map((glob) => {
93
+ const regex = globToRegex(glob);
94
+ return { label: glob, test: (path: string): boolean => regex.test(path) };
95
+ }),
96
+ ];
97
+
98
+ /** Sensitive-pattern match for one path, or null — mirrors wrapup-drift-probe's driftReasonForPath. */
99
+ export function sensitiveReasonForPath(path: string): string | null {
100
+ const match = SENSITIVE_MATCHERS.find((entry) => entry.test(path));
101
+ return match ? `matches sensitive pattern ${match.label}` : null;
102
+ }
103
+
104
+ function defaultGitRunner(cwd?: string): GitRunner {
105
+ return (args: string[]): GitRunResult => {
106
+ const run = spawnSync('git', args, { cwd, encoding: 'utf8' });
107
+ return { status: run.status ?? 1, stdout: run.stdout ?? '' };
108
+ };
109
+ }
110
+
111
+ /** Newline count (wc -l semantics) — untracked files have no numstat row. */
112
+ function countNewlines(path: string, cwd?: string): number {
113
+ let content: string;
114
+ try {
115
+ content = readFileSync(cwd ? join(cwd, path) : path, 'utf8');
116
+ } catch {
117
+ return 0;
118
+ }
119
+ let lines = 0;
120
+ for (const ch of content) if (ch === '\n') lines += 1;
121
+ return lines;
122
+ }
123
+
124
+ /**
125
+ * git renders renames in --numstat paths as `old => new` (full) or `head{old => new}tail`
126
+ * (partial). The sensitive matchers anchor on single paths, so both rename halves must be
127
+ * surfaced — otherwise a rename INTO a sensitive path (review 0943 P3#1) classified clean.
128
+ */
129
+ export function expandDiffPath(raw: string): string[] {
130
+ const arrow = raw.indexOf(' => ');
131
+ if (arrow === -1) return [raw];
132
+ const open = raw.indexOf('{');
133
+ const close = raw.lastIndexOf('}');
134
+ if (open !== -1 && close > open && open < arrow && arrow < close) {
135
+ const inner = raw.slice(open + 1, close);
136
+ const innerArrow = inner.indexOf(' => ');
137
+ if (innerArrow !== -1) {
138
+ const head = raw.slice(0, open);
139
+ const tail = raw.slice(close + 1);
140
+ return [head + inner.slice(0, innerArrow) + tail, head + inner.slice(innerArrow + 4) + tail];
141
+ }
142
+ }
143
+ return [raw.slice(0, arrow), raw.slice(arrow + 4)];
144
+ }
145
+
146
+ /**
147
+ * `probe` — write `.spur/run/<wbs>-diffstat.json`. Always exits 0 after probing; every
148
+ * probe problem fails safe (empty diff, sensitive true), never to a fast lane.
149
+ */
150
+ export function runDiffstat(
151
+ env: DiffstatEnv,
152
+ options: DiffstatOptions = {},
153
+ git: GitRunner = defaultGitRunner(options.cwd),
154
+ ): DiffstatResult {
155
+ const cwd = options.cwd;
156
+ const wbs = env.wbs ?? '';
157
+ if (wbs.length === 0) {
158
+ process.stderr.write('task-diffstat: wbs is empty — refusing to guess the run artifact path\n');
159
+ return { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true, resultFile: '', exitCode: 1 };
160
+ }
161
+ const relRun = join('.spur', 'run');
162
+ const relResult = join(relRun, `${wbs}-diffstat.json`);
163
+ const abs = (p: string): string => (cwd ? join(cwd, p) : p);
164
+ mkdirSync(abs(relRun), { recursive: true });
165
+
166
+ const failSafe = (reason: string): DiffstatResult => {
167
+ process.stderr.write(`task-diffstat: ${reason} — failing safe (sensitive)\n`);
168
+ const row: Diffstat = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
169
+ writeFileSync(abs(relResult), `${JSON.stringify(row)}\n`);
170
+ return { ...row, resultFile: relResult, exitCode: 0 };
171
+ };
172
+
173
+ let base = '';
174
+ try {
175
+ base = readFileSync(abs(join(relRun, `${wbs}-base.sha`)), 'utf8').trim();
176
+ } catch {
177
+ // fall through to the shape check below
178
+ }
179
+ if (!/^[0-9a-f]{7,40}$/i.test(base)) return failSafe('run base .spur/run/<wbs>-base.sha missing or malformed');
180
+
181
+ const numstat = git(['diff', '--numstat', base]);
182
+ if (numstat.status !== 0) return failSafe(`git diff --numstat failed (status=${numstat.status})`);
183
+ const paths = new Set<string>();
184
+ let insertions = 0;
185
+ let deletions = 0;
186
+ for (const line of numstat.stdout.split('\n')) {
187
+ // Binary rows report `-` for both counts; the path still counts as changed.
188
+ const row = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line);
189
+ if (!row) continue;
190
+ for (const p of expandDiffPath(row[3] ?? '')) paths.add(p);
191
+ if (row[1] !== '-') insertions += Number(row[1]);
192
+ if (row[2] !== '-') deletions += Number(row[2]);
193
+ }
194
+ const others = git(['ls-files', '--others', '--exclude-standard', '-z']);
195
+ if (others.status !== 0) return failSafe(`git ls-files failed (status=${others.status})`);
196
+ for (const path of others.stdout.split('\u0000')) {
197
+ // Pipeline bookkeeping (the run's own .spur state) is not product work — skip it.
198
+ if (path.length === 0 || path === '.spur' || path.startsWith('.spur/')) continue;
199
+ paths.add(path);
200
+ insertions += countNewlines(path, cwd);
201
+ }
202
+
203
+ const all = [...paths].filter((p) => p.length > 0).sort();
204
+ const sensitive = all.some((p) => sensitiveReasonForPath(p) !== null);
205
+ const row: Diffstat = { files: all.length, insertions, deletions, paths: all, sensitive };
206
+ writeFileSync(abs(relResult), `${JSON.stringify(row)}\n`);
207
+ return { ...row, resultFile: relResult, exitCode: 0 };
208
+ }
209
+
210
+ export const TASK_DIFFSTAT_USAGE = 'usage: task-diffstat (env: wbs)';
211
+
212
+ export function main(argv: string[], env: DiffstatEnv = getEnvVars(), options: DiffstatOptions = {}): number {
213
+ // 0482 R2: the workflow hands every plugin script `--spur-bin` for PATH-independence.
214
+ // task-diffstat shells only git (never spur), so the flag is accepted and unused;
215
+ // any other argv stays a usage error.
216
+ for (let i = 0; i < argv.length; i++) {
217
+ if (argv[i] === '--spur-bin') {
218
+ i++;
219
+ continue;
220
+ }
221
+ process.stderr.write(`${TASK_DIFFSTAT_USAGE}\n`);
222
+ return 2;
223
+ }
224
+ return runDiffstat(env, options).exitCode;
225
+ }
226
+
227
+ if (import.meta.main) {
228
+ process.exit(main(process.argv.slice(2)));
229
+ }
@@ -74,10 +74,15 @@ function parseArgs(argv: string[]): { wbs: string; spurBin: string } {
74
74
  if (arg === '--spur-bin') {
75
75
  spurBin = argv[i + 1] ?? defaultSpurBin();
76
76
  i += 2;
77
- } else if (!arg.startsWith('--')) {
78
- wbs = arg;
79
- i++;
77
+ } else if (arg.startsWith('-')) {
78
+ // 0948 R4: an unknown flag is a mis-invocation, not something to swallow.
79
+ // The old `else { i++; }` let `script 0926 --task-file x.md` run against x.md.
80
+ console.error(`task-evidence-precheck: unknown flag: ${arg}`);
81
+ usage();
80
82
  } else {
83
+ // 0948 R4: first positional wins — a later positional must never overwrite
84
+ // `wbs` (it used to write a garbage-named status file and mask the exit code).
85
+ if (wbs === '') wbs = arg;
81
86
  i++;
82
87
  }
83
88
  }
@@ -124,10 +124,15 @@ function parseArgs(argv: string[]): {
124
124
  } else if (arg === '--max-plan-items') {
125
125
  maxPlanItems = Number(argv[i + 1]) || 16;
126
126
  i += 2;
127
- } else if (!arg.startsWith('--')) {
128
- wbs = arg;
129
- i++;
127
+ } else if (arg.startsWith('-')) {
128
+ // 0948 R4: an unknown flag is a mis-invocation, not something to swallow.
129
+ // The old `else { i++; }` let `script 0926 --task-file x.md` run against x.md.
130
+ console.error(`task-size-precheck: unknown flag: ${arg}`);
131
+ usage();
130
132
  } else {
133
+ // 0948 R4: first positional wins — a later positional must never overwrite
134
+ // `wbs` (it used to write a garbage-named status file and mask the exit code).
135
+ if (wbs === '') wbs = arg;
131
136
  i++;
132
137
  }
133
138
  }
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // plugins/sp/scripts/wrapup-drift-probe.ts
5
+ import { spawnSync } from "child_process";
6
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { join } from "path";
8
+
9
+ // plugins/sp/lib/env.ts
10
+ function getEnvVars() {
11
+ return process.env;
12
+ }
13
+
14
+ // plugins/sp/scripts/wrapup-drift-probe.ts
15
+ var WORKFLOWS_GLOB = `${join("config", "workflows")}/**`;
16
+ var DOC_OWNED_SURFACES = [
17
+ "packages/contracts/**",
18
+ "apps/cli/src/commands/**",
19
+ "packages/config/src/**",
20
+ "drizzle/*.sql",
21
+ WORKFLOWS_GLOB,
22
+ "plugins/sp/commands/**",
23
+ "plugins/sp/skills/**",
24
+ "plugins/sp/hooks/**",
25
+ "package.json",
26
+ "docs/00_ADR.md",
27
+ "docs/03_ARCHITECTURE.md",
28
+ "docs/04_DESIGN.md",
29
+ "docs/design/**"
30
+ ];
31
+ var KNOWN_TOP_LEVEL = new Set([
32
+ "apps",
33
+ "packages",
34
+ "plugins",
35
+ "config",
36
+ "docs",
37
+ "drizzle",
38
+ "scripts",
39
+ "vendors",
40
+ "package.json",
41
+ "bun.lock",
42
+ "bunfig.toml"
43
+ ]);
44
+ var CORPUS_PREFIXES = ["docs/tasks", "docs/features/"];
45
+ function solutionSectionOf(content) {
46
+ const heading = /^#{2,4}\s+Solution\s*$/m.exec(content);
47
+ if (!heading)
48
+ return null;
49
+ const rest = content.slice(heading.index + heading[0].length);
50
+ const next = /^#{2,4}\s+\S/m.exec(rest);
51
+ return next ? rest.slice(0, next.index) : rest;
52
+ }
53
+ var CHANGE_ENTRY = /`([^`\r\n]+):(\d+)(?:-\d+)?`/g;
54
+ function changedPathsOf(section) {
55
+ if (section === null)
56
+ return [];
57
+ const paths = [];
58
+ for (const match of section.matchAll(CHANGE_ENTRY)) {
59
+ const path = match[1];
60
+ if (/\s/.test(path))
61
+ continue;
62
+ if (!path.includes("/") && !path.includes("."))
63
+ continue;
64
+ paths.push(path);
65
+ }
66
+ return paths;
67
+ }
68
+ function surfaceRegex(glob) {
69
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**", "\x00").replaceAll("*", "[^/]*").replaceAll("\x00", ".*");
70
+ return new RegExp(`^${escaped}$`);
71
+ }
72
+ var SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfaceRegex(glob) }));
73
+ function driftReasonForPath(path) {
74
+ if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix)))
75
+ return null;
76
+ if (path.includes("/") && !KNOWN_TOP_LEVEL.has(path.split("/")[0])) {
77
+ return "new top-level workspace directory";
78
+ }
79
+ const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
80
+ return match ? `matches doc-owned surface ${match.glob}` : null;
81
+ }
82
+ function defaultSpurRunner(env, cwd) {
83
+ const parts = (env.spurBin ?? "spur").split(/\s+/).filter((part) => part.length > 0);
84
+ return (args) => {
85
+ const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
86
+ cwd,
87
+ encoding: "utf8",
88
+ env: getEnvVars()
89
+ });
90
+ return { status: run.status ?? 1, stdout: run.stdout ?? "" };
91
+ };
92
+ }
93
+ function runDriftProbe(env, options = {}, spur = defaultSpurRunner(env, options.cwd)) {
94
+ const cwd = options.cwd;
95
+ const runId = env.__runId ?? "";
96
+ if (runId.length === 0) {
97
+ process.stderr.write(`wrapup-drift-probe: __runId is empty \u2014 refusing the legacy fixed-path fallback
98
+ `);
99
+ return { clean: false, reasons: [], paths: [], probeFile: "", modeFile: "", exitCode: 1 };
100
+ }
101
+ const relProbeFile = join(".spur", "run", `${runId}-drift-probe.json`);
102
+ const relModeFile = join(".spur", "run", `${runId}-mode.txt`);
103
+ const relTasksFile = join(".spur", "run", `${runId}-wrapup-tasks.json`);
104
+ const abs = (p) => cwd ? join(cwd, p) : p;
105
+ mkdirSync(abs(join(".spur", "run")), { recursive: true });
106
+ writeFileSync(abs(relModeFile), `
107
+ `);
108
+ const finish = (clean, reasons2, paths) => {
109
+ writeFileSync(abs(relProbeFile), `${JSON.stringify({ clean, reasons: reasons2, paths })}
110
+ `);
111
+ writeFileSync(abs(relModeFile), clean ? `fast
112
+ ` : `
113
+ `);
114
+ return { clean, reasons: reasons2, paths, probeFile: relProbeFile, modeFile: relModeFile, exitCode: 0 };
115
+ };
116
+ let tasks;
117
+ try {
118
+ tasks = JSON.parse(readFileSync(abs(relTasksFile), "utf8"));
119
+ } catch {
120
+ tasks = undefined;
121
+ }
122
+ if (!Array.isArray(tasks) || !tasks.every((wbs) => typeof wbs === "string")) {
123
+ process.stderr.write(`wrapup-drift-probe: normalized task capture is missing or corrupted \u2014 failing safe (dirty)
124
+ `);
125
+ return finish(false, ["normalized task capture missing or corrupted"], []);
126
+ }
127
+ const reasons = [];
128
+ const allPaths = new Set;
129
+ for (const wbs of tasks) {
130
+ const shown = spur(["task", "show", wbs, "--json"]);
131
+ if (shown.status !== 0) {
132
+ reasons.push(`${wbs}: task show failed (status=${shown.status})`);
133
+ continue;
134
+ }
135
+ let content;
136
+ try {
137
+ content = JSON.parse(shown.stdout).content;
138
+ } catch {
139
+ content = undefined;
140
+ }
141
+ if (typeof content !== "string") {
142
+ reasons.push(`${wbs}: task show output unparseable`);
143
+ continue;
144
+ }
145
+ const changedPaths = changedPathsOf(solutionSectionOf(content));
146
+ if (changedPaths.length === 0) {
147
+ reasons.push(`${wbs}: Solution empty or unparseable`);
148
+ continue;
149
+ }
150
+ for (const path of changedPaths) {
151
+ if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix)))
152
+ continue;
153
+ allPaths.add(path);
154
+ const drift = driftReasonForPath(path);
155
+ if (drift)
156
+ reasons.push(`${wbs}: ${path} ${drift}`);
157
+ }
158
+ }
159
+ return finish(reasons.length === 0, reasons, [...allPaths].sort());
160
+ }
161
+ var WRAPUP_DRIFT_PROBE_USAGE = "usage: wrapup-drift-probe (env: __runId, spurBin)";
162
+ function main(argv, env = getEnvVars(), options = {}) {
163
+ if (argv.length > 0) {
164
+ process.stderr.write(`${WRAPUP_DRIFT_PROBE_USAGE}
165
+ `);
166
+ return 2;
167
+ }
168
+ return runDriftProbe(env, options).exitCode;
169
+ }
170
+ {
171
+ process.exit(main(process.argv.slice(2)));
172
+ }
173
+ export {
174
+ solutionSectionOf,
175
+ runDriftProbe,
176
+ main,
177
+ driftReasonForPath,
178
+ changedPathsOf,
179
+ WRAPUP_DRIFT_PROBE_USAGE,
180
+ DOC_OWNED_SURFACES
181
+ };