@gobing-ai/spur 0.3.92 → 0.3.94

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 (119) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +0 -7
  3. package/config/plugin-scripts.json +10 -0
  4. package/config/rules/boundary/test-subpath-boundary.yaml +48 -0
  5. package/config/rules/strict/runtime-boundaries.yaml +2 -1
  6. package/config/rules/structure/protected-files.yaml +4 -0
  7. package/config/templates/AGENTS.md +4 -0
  8. package/config/templates/docs/02_ROADMAP.md +2 -0
  9. package/config/templates/docs/03_ARCHITECTURE.md +3 -1
  10. package/config/templates/docs/04_DESIGN.md +2 -0
  11. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +10 -2
  12. package/config/workflow-candidates.json +72 -1
  13. package/config/workflows/feature-verification.yaml +1 -0
  14. package/config/workflows/history-anatomy.yaml +2 -0
  15. package/config/workflows/idea-pipeline.yaml +67 -18
  16. package/config/workflows/pr-review.yaml +8 -0
  17. package/config/workflows/task-pipeline.yaml +171 -39
  18. package/config/workflows/wayfinder-resolution.yaml +5 -0
  19. package/config/workflows/wrapup-pipeline.yaml +55 -14
  20. package/package.json +9 -9
  21. package/plugins/sp/README.md +8 -3
  22. package/plugins/sp/commands/dev-review-session.md +4 -4
  23. package/plugins/sp/commands/dev-review.md +16 -5
  24. package/plugins/sp/commands/dev-run.md +2 -2
  25. package/plugins/sp/commands/dev-runall.md +2 -2
  26. package/plugins/sp/hooks/pi/guard-extension.ts +17 -36
  27. package/plugins/sp/lib/idea-handoff.generated.mjs +8 -4
  28. package/plugins/sp/lib/inline-run.generated.d.mts +3 -0
  29. package/plugins/sp/lib/inline-run.generated.mjs +48 -19
  30. package/plugins/sp/plugin.json +1 -1
  31. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +1 -1
  32. package/plugins/sp/scripts/inline-run-setup.mjs +117 -7
  33. package/plugins/sp/scripts/inline-run-setup.ts +211 -7
  34. package/plugins/sp/scripts/quality-gate.mjs +255 -6
  35. package/plugins/sp/scripts/quality-gate.ts +425 -9
  36. package/plugins/sp/scripts/residual-scan.mjs +13 -5
  37. package/plugins/sp/scripts/residual-scan.ts +34 -7
  38. package/plugins/sp/scripts/task-diffstat.mjs +156 -0
  39. package/plugins/sp/scripts/task-diffstat.ts +229 -0
  40. package/plugins/sp/scripts/wrapup-drift-probe.mjs +181 -0
  41. package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
  42. package/plugins/sp/scripts/wrapup-steps.mjs +60 -1
  43. package/plugins/sp/scripts/wrapup-steps.ts +89 -4
  44. package/plugins/sp/skills/brainstorm/SKILL.md +2 -0
  45. package/plugins/sp/skills/brainstorm/references/workflows.md +17 -2
  46. package/plugins/sp/skills/code-verification/SKILL.md +2 -2
  47. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -2
  48. package/plugins/sp/skills/session-review/SKILL.md +11 -9
  49. package/plugins/sp/skills/spur-check/SKILL.md +112 -0
  50. package/plugins/sp/skills/spur-dev/SKILL.md +2 -1
  51. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +14 -2
  52. package/plugins/sp/skills/spur-dev/references/dev-operations.md +8 -2
  53. package/plugins/sp/skills/spur-dev/references/document-authoring.md +85 -0
  54. package/plugins/sp/skills/spur-dev/references/execution-batch.md +46 -18
  55. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +24 -3
  56. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
  57. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +34 -2
  58. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +5 -5
  59. package/plugins/sp/skills/spur-dev/templates/design.md +31 -0
  60. package/plugins/sp/skills/spur-dev/templates/plan.md +32 -0
  61. package/plugins/sp/skills/spur-doctor/SKILL.md +60 -14
  62. package/schemas/state-machine-workflow.schema.json +4 -0
  63. package/spur.js +19577 -19822
  64. package/web/_astro/BoardApp.C02hAHPO.js +1 -0
  65. package/web/_astro/{BoardApp.CerSBgis.js → BoardApp.FTEs3-N8.js} +107 -105
  66. package/web/_astro/{TaskDetail.DCqiC-OZ.js → TaskDetail.C-GdsS-t.js} +1 -1
  67. package/web/_astro/arc.uuAf51IT.js +1 -0
  68. package/web/_astro/{architectureDiagram-3BPJPVTR.DM_vp_hO.js → architectureDiagram-3BPJPVTR.CGe629A1.js} +1 -1
  69. package/web/_astro/{blockDiagram-GPEHLZMM.DXVIiv0p.js → blockDiagram-GPEHLZMM.D1mGCq3p.js} +1 -1
  70. package/web/_astro/{c4Diagram-AAUBKEIU.BbF_zCxW.js → c4Diagram-AAUBKEIU.CMsolcde.js} +1 -1
  71. package/web/_astro/channel.fsgl7o5j.js +1 -0
  72. package/web/_astro/{chunk-2J33WTMH.CAgQHpPC.js → chunk-2J33WTMH.CrGA3fik.js} +1 -1
  73. package/web/_astro/{chunk-4BX2VUAB.BN-5tpw4.js → chunk-4BX2VUAB.DsLVVla0.js} +1 -1
  74. package/web/_astro/{chunk-55IACEB6.CnPkEEr0.js → chunk-55IACEB6.Dxc59Tfi.js} +1 -1
  75. package/web/_astro/{chunk-727SXJPM.BQzQeMVm.js → chunk-727SXJPM.CaEVE2Wy.js} +4 -4
  76. package/web/_astro/{chunk-AQP2D5EJ.B6xNyDnL.js → chunk-AQP2D5EJ.BiJ4HXeI.js} +1 -1
  77. package/web/_astro/{chunk-FMBD7UC4.C7f9Ih78.js → chunk-FMBD7UC4.Mxf1fru5.js} +1 -1
  78. package/web/_astro/{chunk-ND2GUHAM.CNV1dFXT.js → chunk-ND2GUHAM.pyOWQixH.js} +1 -1
  79. package/web/_astro/{chunk-QZHKN3VN.Cudn2TkJ.js → chunk-QZHKN3VN.BmEsg4vr.js} +1 -1
  80. package/web/_astro/{classDiagram-4FO5ZUOK.D1NwP50q.js → classDiagram-4FO5ZUOK.BHhhMFTO.js} +1 -1
  81. package/web/_astro/{classDiagram-v2-Q7XG4LA2.D1NwP50q.js → classDiagram-v2-Q7XG4LA2.BHhhMFTO.js} +1 -1
  82. package/web/_astro/{cose-bilkent-S5V4N54A.B1wSL-Xb.js → cose-bilkent-S5V4N54A.2fH4YOlp.js} +1 -1
  83. package/web/_astro/{cynefin-OW5HDTMX.BmK52w8G.js → cynefin-OW5HDTMX.C2j1_lKL.js} +1 -1
  84. package/web/_astro/{dagre-BM42HDAG.Bfy5CTDT.js → dagre-BM42HDAG.hZ2NCTdT.js} +2 -2
  85. package/web/_astro/diagram-2AECGRRQ.Bfw5_EzK.js +43 -0
  86. package/web/_astro/diagram-5GNKFQAL.Bt1V_Tmk.js +10 -0
  87. package/web/_astro/{diagram-KO2AKTUF.CW_vMJ4z.js → diagram-KO2AKTUF.DEk-YFwp.js} +3 -3
  88. package/web/_astro/{diagram-LMA3HP47.B_8ZGF67.js → diagram-LMA3HP47.CDjndtQm.js} +1 -1
  89. package/web/_astro/{diagram-OG6HWLK6.BppnHsdS.js → diagram-OG6HWLK6.cFnUHScG.js} +1 -1
  90. package/web/_astro/{erDiagram-TEJ5UH35.BEuHXcjJ.js → erDiagram-TEJ5UH35.DlhYp7NV.js} +5 -5
  91. package/web/_astro/{flowDiagram-I6XJVG4X.CH-UlnGr.js → flowDiagram-I6XJVG4X.DNTpsxfx.js} +4 -4
  92. package/web/_astro/{ganttDiagram-6RSMTGT7.BO81S85v.js → ganttDiagram-6RSMTGT7.DC_p36PI.js} +1 -1
  93. package/web/_astro/{gitGraphDiagram-PVQCEYII.XnPxPPZN.js → gitGraphDiagram-PVQCEYII.DkEqNI0P.js} +1 -1
  94. package/web/_astro/{infoDiagram-5YYISTIA.JyjYRu_T.js → infoDiagram-5YYISTIA.CrjioCTG.js} +1 -1
  95. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js → ishikawaDiagram-YF4QCWOH.BcK0CN8n.js} +5 -5
  96. package/web/_astro/{journeyDiagram-JHISSGLW.C_iymSyp.js → journeyDiagram-JHISSGLW.p0CbBDX1.js} +1 -1
  97. package/web/_astro/{kanban-definition-UN3LZRKU.DdfW-Oqt.js → kanban-definition-UN3LZRKU.puHIFt6J.js} +7 -7
  98. package/web/_astro/{linear.C2_IkbZT.js → linear.Bb-3a1d7.js} +1 -1
  99. package/web/_astro/mermaid.core.CJDgXOJs.js +301 -0
  100. package/web/_astro/{mindmap-definition-RKZ34NQL.DAZIxQSK.js → mindmap-definition-RKZ34NQL.BoAZEI9t.js} +2 -2
  101. package/web/_astro/{pieDiagram-4H26LBE5.CN8sIhKM.js → pieDiagram-4H26LBE5.CJNSdqY7.js} +3 -3
  102. package/web/_astro/{quadrantDiagram-W4KKPZXB.3dGcX5GP.js → quadrantDiagram-W4KKPZXB.DtTy7_0Z.js} +1 -1
  103. package/web/_astro/{requirementDiagram-4Y6WPE33.BV2y4dd6.js → requirementDiagram-4Y6WPE33.BNYV98Xg.js} +3 -3
  104. package/web/_astro/{sankeyDiagram-5OEKKPKP.Cqo15Tvo.js → sankeyDiagram-5OEKKPKP.DRq9DGHp.js} +4 -4
  105. package/web/_astro/{sequenceDiagram-3UESZ5HK.CROCPMJB.js → sequenceDiagram-3UESZ5HK.B7KdBFCm.js} +1 -1
  106. package/web/_astro/{stateDiagram-AJRCARHV.RfXZrkFE.js → stateDiagram-AJRCARHV.CD62ZJ2H.js} +1 -1
  107. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CPXmbBs9.js → stateDiagram-v2-BHNVJYJU.CeVk1TAj.js} +1 -1
  108. package/web/_astro/{timeline-definition-PNZ67QCA.DdgKTiO8.js → timeline-definition-PNZ67QCA.C_ZPA5tT.js} +3 -3
  109. package/web/_astro/{vennDiagram-CIIHVFJN.CPNVSHF1.js → vennDiagram-CIIHVFJN.CrThO_FQ.js} +5 -5
  110. package/web/_astro/{wardleyDiagram-YWT4CUSO.CQhA0Jyr.js → wardleyDiagram-YWT4CUSO.DSZCA5nl.js} +3 -3
  111. package/web/_astro/{xychartDiagram-2RQKCTM6.n61BWyy4.js → xychartDiagram-2RQKCTM6.KI7baTuk.js} +1 -1
  112. package/web/index.html +1 -1
  113. package/config/workflows/decision-routing-example.yaml +0 -134
  114. package/web/_astro/BoardApp.eoTz0pZs.js +0 -1
  115. package/web/_astro/arc.CPwg6Rw0.js +0 -1
  116. package/web/_astro/channel.MYZLKNwy.js +0 -1
  117. package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +0 -43
  118. package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +0 -10
  119. package/web/_astro/mermaid.core.GAOYeSR0.js +0 -303
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * wrapup-drift-probe — deterministic doc-ownership drift probe behind the wrapup-pipeline
4
+ * task-resolve onEnter (task 0944, feature D64, ADR-115 composition budgets).
5
+ *
6
+ * Reads the validated wrapup capture `.spur/run/<runId>-wrapup-tasks.json` (never raw
7
+ * vars.tasks — 0783 contract), runs `spur task show <wbs> --json` per member, collects the
8
+ * changed paths from each task's `### Solution` file:line map, and writes:
9
+ * - `<runId>-drift-probe.json` `{ clean: boolean, reasons: string[], paths: string[] }`
10
+ * - `<runId>-mode.txt` projected wrapup mode: `fast` when clean, empty otherwise
11
+ *
12
+ * The probe runs ONLY when the caller left `mode` empty; a caller-set mode is projected
13
+ * verbatim by the workflow wrapper and this script is never invoked (0944 R3).
14
+ *
15
+ * Dirty (clean=false, mode stays empty → the safety route) whenever ANY changed path
16
+ * matches a doc-owned surface (AGENTS.md doc map + docs/99_PROJECT_CONSTITUTION.md
17
+ * ownership table), or a task's Solution section is empty or unparseable (fail safe —
18
+ * a wrapup without a readable change map must still reach doc-sync). Paths under the
19
+ * task/feature corpus are never treated as drift.
20
+ *
21
+ * Node-builtin imports only; the spur lookup is a spawnable `SpurRunner` so tests can
22
+ * fake it; `main(argv, env, options)` is the same injection point the workflow wrapper
23
+ * and its node twin exercise. Always exits 0 after probing; only an empty `__runId`
24
+ * (mis-invocation) exits 1.
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 DriftProbeEnv {
33
+ __runId?: string;
34
+ spurBin?: string;
35
+ [key: string]: string | undefined;
36
+ }
37
+
38
+ export interface DriftProbeOptions {
39
+ /** Base directory for `.spur/run`; defaults to the process cwd. */
40
+ cwd?: string;
41
+ }
42
+
43
+ /** Minimal spawnable spur surface — tests fake this to return canned `task show` output. */
44
+ export interface SpurShowResult {
45
+ status: number;
46
+ stdout: string;
47
+ }
48
+ export type SpurRunner = (args: string[]) => SpurShowResult;
49
+
50
+ export interface DriftProbeResult {
51
+ clean: boolean;
52
+ reasons: string[];
53
+ paths: string[];
54
+ probeFile: string;
55
+ modeFile: string;
56
+ /** Exit code the workflow wrapper observes (only an empty __runId is a hard failure). */
57
+ exitCode: number;
58
+ }
59
+
60
+ /**
61
+ * Doc-owned surfaces (0944 R2): a changed path under any of these means doc-sync must
62
+ * still run. Mirrors the AGENTS.md documentation map and the
63
+ * docs/99_PROJECT_CONSTITUTION.md ownership table: contracts / cli commands / config /
64
+ * migrations / workflow YAML / plugin commands, skills and hooks / root manifest / the
65
+ * authoritative docs (00_ADR, 03_ARCHITECTURE, 04_DESIGN, docs/design). docs/tasks* and
66
+ * docs/features/* (the corpus) are intentionally absent — they are never drift.
67
+ *
68
+ * WHY join('config', 'workflows'): rule sp-runtime-path forbids the literal
69
+ * config/<dir> in runtime source; this constant is a drift TARGET (Solution
70
+ * change-map paths against the build-time SSOT), not a runtime config read.
71
+ */
72
+ const WORKFLOWS_GLOB = `${join('config', 'workflows')}/**`;
73
+ export const DOC_OWNED_SURFACES = [
74
+ 'packages/contracts/**',
75
+ 'apps/cli/src/commands/**',
76
+ 'packages/config/src/**',
77
+ 'drizzle/*.sql',
78
+ WORKFLOWS_GLOB,
79
+ 'plugins/sp/commands/**',
80
+ 'plugins/sp/skills/**',
81
+ 'plugins/sp/hooks/**',
82
+ 'package.json',
83
+ 'docs/00_ADR.md',
84
+ 'docs/03_ARCHITECTURE.md',
85
+ 'docs/04_DESIGN.md',
86
+ 'docs/design/**',
87
+ ] as const;
88
+
89
+ /**
90
+ * Top-level workspace entries from the AGENTS.md stack layout. A changed path whose first
91
+ * segment is none of these is a new top-level workspace directory — doc-owned by default.
92
+ * Single-segment paths (root files) are not directories and are exempt.
93
+ */
94
+ const KNOWN_TOP_LEVEL = new Set([
95
+ 'apps',
96
+ 'packages',
97
+ 'plugins',
98
+ 'config',
99
+ 'docs',
100
+ 'drizzle',
101
+ 'scripts',
102
+ 'vendors',
103
+ 'package.json',
104
+ 'bun.lock',
105
+ 'bunfig.toml',
106
+ ]);
107
+
108
+ /** Corpus paths (task/feature records) are never drift — excluded before surface matching. */
109
+ const CORPUS_PREFIXES = ['docs/tasks', 'docs/features/'];
110
+
111
+ /** `### Solution` section of a task record (tolerates `##`–`####` heading depth). */
112
+ export function solutionSectionOf(content: string): string | null {
113
+ const heading = /^#{2,4}\s+Solution\s*$/m.exec(content);
114
+ if (!heading) return null;
115
+ const rest = content.slice(heading.index + heading[0].length);
116
+ const next = /^#{2,4}\s+\S/m.exec(rest);
117
+ return next ? rest.slice(0, next.index) : rest;
118
+ }
119
+
120
+ const CHANGE_ENTRY = /`([^`\r\n]+):(\d+)(?:-\d+)?`/g;
121
+
122
+ /** Backticked `path:line` (or `path:line-range`) tokens from a Solution section. */
123
+ export function changedPathsOf(section: string | null): string[] {
124
+ if (section === null) return [];
125
+ const paths: string[] = [];
126
+ for (const match of section.matchAll(CHANGE_ENTRY)) {
127
+ const path = match[1];
128
+ // Path-like: no whitespace and at least one separator or dotted suffix, so prose
129
+ // backticks never masquerade as change-map entries.
130
+ if (/\s/.test(path)) continue;
131
+ if (!path.includes('/') && !path.includes('.')) continue;
132
+ paths.push(path);
133
+ }
134
+ return paths;
135
+ }
136
+
137
+ function surfaceRegex(glob: string): RegExp {
138
+ const escaped = glob
139
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
140
+ .replaceAll('**', '\u0000')
141
+ .replaceAll('*', '[^/]*')
142
+ .replaceAll('\u0000', '.*');
143
+ return new RegExp(`^${escaped}$`);
144
+ }
145
+
146
+ const SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfaceRegex(glob) }));
147
+
148
+ /** 0944 R2 surface match: doc-owned glob, corpus exclusion, new top-level directory. */
149
+ export function driftReasonForPath(path: string): string | null {
150
+ if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) return null;
151
+ if (path.includes('/') && !KNOWN_TOP_LEVEL.has(path.split('/')[0])) {
152
+ return 'new top-level workspace directory';
153
+ }
154
+ const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
155
+ return match ? `matches doc-owned surface ${match.glob}` : null;
156
+ }
157
+
158
+ function defaultSpurRunner(env: DriftProbeEnv, cwd?: string): SpurRunner {
159
+ const parts = (env.spurBin ?? 'spur').split(/\s+/).filter((part) => part.length > 0);
160
+ return (args: string[]): SpurShowResult => {
161
+ const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
162
+ cwd,
163
+ encoding: 'utf8',
164
+ env: getEnvVars(),
165
+ });
166
+ return { status: run.status ?? 1, stdout: run.stdout ?? '' };
167
+ };
168
+ }
169
+
170
+ /**
171
+ * `probe` — classify the wrapup as clean (pure application-code change, no doc-owned
172
+ * surface touched) or dirty, and project the wrapup mode. Fail safe: every lookup or
173
+ * parse problem is a dirty probe, never a silent clean.
174
+ */
175
+ export function runDriftProbe(
176
+ env: DriftProbeEnv,
177
+ options: DriftProbeOptions = {},
178
+ spur = defaultSpurRunner(env, options.cwd),
179
+ ): DriftProbeResult {
180
+ const cwd = options.cwd;
181
+ const runId = env.__runId ?? '';
182
+ if (runId.length === 0) {
183
+ process.stderr.write('wrapup-drift-probe: __runId is empty — refusing the legacy fixed-path fallback\n');
184
+ return { clean: false, reasons: [], paths: [], probeFile: '', modeFile: '', exitCode: 1 };
185
+ }
186
+ const relProbeFile = join('.spur', 'run', `${runId}-drift-probe.json`);
187
+ const relModeFile = join('.spur', 'run', `${runId}-mode.txt`);
188
+ const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
189
+ const abs = (p: string): string => (cwd ? join(cwd, p) : p);
190
+ mkdirSync(abs(join('.spur', 'run')), { recursive: true });
191
+ // Write the dirty projection first so a crash mid-probe fails safe (mode stays empty).
192
+ writeFileSync(abs(relModeFile), '\n');
193
+
194
+ const finish = (clean: boolean, reasons: string[], paths: string[]): DriftProbeResult => {
195
+ writeFileSync(abs(relProbeFile), `${JSON.stringify({ clean, reasons, paths })}\n`);
196
+ writeFileSync(abs(relModeFile), clean ? 'fast\n' : '\n');
197
+ return { clean, reasons, paths, probeFile: relProbeFile, modeFile: relModeFile, exitCode: 0 };
198
+ };
199
+
200
+ let tasks: unknown;
201
+ try {
202
+ tasks = JSON.parse(readFileSync(abs(relTasksFile), 'utf8'));
203
+ } catch {
204
+ tasks = undefined;
205
+ }
206
+ if (!Array.isArray(tasks) || !tasks.every((wbs) => typeof wbs === 'string')) {
207
+ process.stderr.write(
208
+ 'wrapup-drift-probe: normalized task capture is missing or corrupted — failing safe (dirty)\n',
209
+ );
210
+ return finish(false, ['normalized task capture missing or corrupted'], []);
211
+ }
212
+
213
+ const reasons: string[] = [];
214
+ const allPaths = new Set<string>();
215
+ for (const wbs of tasks as string[]) {
216
+ const shown = spur(['task', 'show', wbs, '--json']);
217
+ if (shown.status !== 0) {
218
+ reasons.push(`${wbs}: task show failed (status=${shown.status})`);
219
+ continue;
220
+ }
221
+ let content: unknown;
222
+ try {
223
+ content = JSON.parse(shown.stdout).content;
224
+ } catch {
225
+ content = undefined;
226
+ }
227
+ if (typeof content !== 'string') {
228
+ reasons.push(`${wbs}: task show output unparseable`);
229
+ continue;
230
+ }
231
+ const changedPaths = changedPathsOf(solutionSectionOf(content));
232
+ if (changedPaths.length === 0) {
233
+ reasons.push(`${wbs}: Solution empty or unparseable`);
234
+ continue;
235
+ }
236
+ for (const path of changedPaths) {
237
+ if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;
238
+ allPaths.add(path);
239
+ const drift = driftReasonForPath(path);
240
+ if (drift) reasons.push(`${wbs}: ${path} ${drift}`);
241
+ }
242
+ }
243
+ return finish(reasons.length === 0, reasons, [...allPaths].sort());
244
+ }
245
+
246
+ export const WRAPUP_DRIFT_PROBE_USAGE = 'usage: wrapup-drift-probe (env: __runId, spurBin)';
247
+
248
+ export function main(argv: string[], env: DriftProbeEnv = getEnvVars(), options: DriftProbeOptions = {}): number {
249
+ if (argv.length > 0) {
250
+ process.stderr.write(`${WRAPUP_DRIFT_PROBE_USAGE}\n`);
251
+ return 2;
252
+ }
253
+ return runDriftProbe(env, options).exitCode;
254
+ }
255
+
256
+ if (import.meta.main) {
257
+ process.exit(main(process.argv.slice(2)));
258
+ }
@@ -116,6 +116,62 @@ function resolveTasks(env, options = {}) {
116
116
  `);
117
117
  return { status: "PASS", statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
118
118
  }
119
+ var ROUTE_REASON_TABLE = {
120
+ fast: "fast:evidence complete+consistent",
121
+ "": "safety:missing evidence (mode empty)",
122
+ unknown: "safety:unknown evidence quality",
123
+ conflict: "safety:conflicting evidence",
124
+ safety: "safety:operator-forced doc-sync"
125
+ };
126
+ function writeRouteReason(env, options = {}) {
127
+ const cwd = options.cwd;
128
+ const runId = env.__runId ?? "";
129
+ if (runId.length === 0) {
130
+ process.stderr.write(`task-resolve: __runId is empty \u2014 refusing to write a route reason
131
+ `);
132
+ return { reason: "", reasonFile: "", exitCode: 1 };
133
+ }
134
+ mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
135
+ mkdirSync(cwd ? join(cwd, ".spur", "memory") : join(".spur", "memory"), { recursive: true });
136
+ const relReasonFile = join(".spur", "run", `${runId}-route-reason.txt`);
137
+ const abs = (p) => cwd ? join(cwd, p) : p;
138
+ const statusFile = join(".spur", "run", `${runId}-wrapup-resolve.status`);
139
+ if (readFileSyncSafe(abs(statusFile))?.trim() === "FAIL") {
140
+ return { reason: "", reasonFile: relReasonFile, exitCode: 0 };
141
+ }
142
+ let taskCount = -1;
143
+ try {
144
+ const parsed = JSON.parse(readFileSync(abs(join(".spur", "run", `${runId}-wrapup-tasks.json`)), "utf8"));
145
+ if (Array.isArray(parsed))
146
+ taskCount = parsed.length;
147
+ } catch {}
148
+ let probeClean = false;
149
+ try {
150
+ const probe = JSON.parse(readFileSync(abs(join(".spur", "run", `${runId}-drift-probe.json`)), "utf8"));
151
+ probeClean = probe?.clean === true;
152
+ } catch {}
153
+ const mode = env.mode ?? "";
154
+ let reason;
155
+ if (taskCount === 0) {
156
+ reason = "skipped:empty task list";
157
+ } else if (mode === "fast" && probeClean) {
158
+ reason = "fast:drift-probe-clean";
159
+ } else {
160
+ reason = ROUTE_REASON_TABLE[mode] ?? `safety:unrecognized evidence (mode=${mode})`;
161
+ }
162
+ writeFileSync(abs(relReasonFile), `${reason}
163
+ `);
164
+ appendFileSync(abs(join(".spur", "memory", "wrapup-routes.log")), `${runId} ${reason}
165
+ `);
166
+ return { reason, reasonFile: relReasonFile, exitCode: 0 };
167
+ }
168
+ function readFileSyncSafe(path) {
169
+ try {
170
+ return readFileSync(path, "utf8");
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
119
175
  function runMetrics(env, options = {}) {
120
176
  const cwd = options.cwd;
121
177
  const runId = env.__runId ?? "";
@@ -325,11 +381,13 @@ function runFeatureTransition(env, options = {}) {
325
381
  `);
326
382
  return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
327
383
  }
328
- var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|metrics|feature-transition> (env: __runId, tasks, feature, featureGateCmd, spurBin)";
384
+ var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)";
329
385
  function main(argv, env = getEnvVars(), options = {}) {
330
386
  const sub = argv[0];
331
387
  if (sub === "resolve")
332
388
  return resolveTasks(env, options).exitCode;
389
+ if (sub === "route-reason")
390
+ return writeRouteReason(env, options).exitCode;
333
391
  if (sub === "metrics") {
334
392
  runMetrics(env, options);
335
393
  return 0;
@@ -344,6 +402,7 @@ function main(argv, env = getEnvVars(), options = {}) {
344
402
  process.exit(main(process.argv.slice(2)));
345
403
  }
346
404
  export {
405
+ writeRouteReason,
347
406
  taskStatusOf,
348
407
  spurCommand,
349
408
  runMetrics,
@@ -3,12 +3,13 @@
3
3
  * wrapup-steps — deterministic wrap-up capture, metrics and feature sync behind the
4
4
  * wrapup-pipeline wrappers (task 0824, feature I21, governance §1.2 composition budgets).
5
5
  *
6
- * Reproduces the former wrapup-pipeline `task-resolve:onEnter:0`, `metrics-record:onEnter:0`
7
- * and `feature-transition:onEnter:0` shell programs one-for-one so the workflow stays inside
6
+ * Reproduces the former wrapup-pipeline `task-resolve:onEnter:0`, `task-resolve:onEnter:1`
7
+ * (route-reason writer, moved here in 0944), `metrics-record:onEnter:0` and
8
+ * `feature-transition:onEnter:0` shell programs one-for-one so the workflow stays inside
8
9
  * the shell-program caps while writing the same `.spur/run` artifacts:
9
10
  * - `<runId>-wrapup-tasks.json` normalized, deduplicated WBS capture (resolve)
10
11
  * - `<runId>-wrapup-resolve.status` `PASS`/`FAIL` (resolve)
11
- * - `<runId>-route-reason.txt` route reason (written by the workflow route writer)
12
+ * - `<runId>-route-reason.txt` route reason (route-reason subcommand)
12
13
  * - `.spur/memory/wrapup-metrics.jsonl` one row per task (metrics)
13
14
  * - `<runId>-wrapup-metrics.status` `PASS`/`FAIL` (metrics)
14
15
  * - `<runId>-wrapup-sync.status` `PASS`/`FAIL` (feature-transition)
@@ -185,6 +186,89 @@ export function resolveTasks(env: WrapupStepsEnv, options: WrapupStepsOptions =
185
186
  return { status: 'PASS', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
186
187
  }
187
188
 
189
+ /**
190
+ * 0944 R3 route-reason map — mirrors the former inline jq object one-for-one, plus the
191
+ * `safety` entry (operator-forced doc-sync). `fast:drift-probe-clean` is NOT in the map:
192
+ * it is only claimable when the drift probe itself classified the wrapup clean.
193
+ */
194
+ const ROUTE_REASON_TABLE: Record<string, string> = {
195
+ fast: 'fast:evidence complete+consistent',
196
+ '': 'safety:missing evidence (mode empty)',
197
+ unknown: 'safety:unknown evidence quality',
198
+ conflict: 'safety:conflicting evidence',
199
+ safety: 'safety:operator-forced doc-sync',
200
+ };
201
+
202
+ export interface RouteReasonResult {
203
+ reason: string;
204
+ reasonFile: string;
205
+ exitCode: number;
206
+ }
207
+
208
+ /**
209
+ * `route-reason` — derive the task-resolve route reason from the validated capture, the
210
+ * projected mode and the drift probe verdict (0944). A resolve FAIL keeps its own reason
211
+ * (nothing is written, exit 0). A missing or corrupted capture never yields a `skipped`
212
+ * or `fast:drift-probe-clean` claim. The log line is run-attributed (0770).
213
+ */
214
+ export function writeRouteReason(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): RouteReasonResult {
215
+ const cwd = options.cwd;
216
+ const runId = env.__runId ?? '';
217
+ if (runId.length === 0) {
218
+ process.stderr.write('task-resolve: __runId is empty — refusing to write a route reason\n');
219
+ return { reason: '', reasonFile: '', exitCode: 1 };
220
+ }
221
+ mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
222
+ mkdirSync(cwd ? join(cwd, '.spur', 'memory') : join('.spur', 'memory'), { recursive: true });
223
+ const relReasonFile = join('.spur', 'run', `${runId}-route-reason.txt`);
224
+ const abs = (p: string): string => (cwd ? join(cwd, p) : p);
225
+
226
+ const statusFile = join('.spur', 'run', `${runId}-wrapup-resolve.status`);
227
+ if (readFileSyncSafe(abs(statusFile))?.trim() === 'FAIL') {
228
+ // The failed reason stands; the failed edge already owns the run.
229
+ return { reason: '', reasonFile: relReasonFile, exitCode: 0 };
230
+ }
231
+
232
+ // A missing/corrupted capture stays at -1: never 0, so no skipped claim can be invented.
233
+ let taskCount = -1;
234
+ try {
235
+ const parsed: unknown = JSON.parse(
236
+ readFileSync(abs(join('.spur', 'run', `${runId}-wrapup-tasks.json`)), 'utf8'),
237
+ );
238
+ if (Array.isArray(parsed)) taskCount = parsed.length;
239
+ } catch {
240
+ // treated as uncountable below
241
+ }
242
+ let probeClean = false;
243
+ try {
244
+ const probe: unknown = JSON.parse(readFileSync(abs(join('.spur', 'run', `${runId}-drift-probe.json`)), 'utf8'));
245
+ probeClean = (probe as { clean?: unknown } | null)?.clean === true;
246
+ } catch {
247
+ // no probe verdict — the map decides
248
+ }
249
+
250
+ const mode = env.mode ?? '';
251
+ let reason: string;
252
+ if (taskCount === 0) {
253
+ reason = 'skipped:empty task list';
254
+ } else if (mode === 'fast' && probeClean) {
255
+ reason = 'fast:drift-probe-clean';
256
+ } else {
257
+ reason = ROUTE_REASON_TABLE[mode] ?? `safety:unrecognized evidence (mode=${mode})`;
258
+ }
259
+ writeFileSync(abs(relReasonFile), `${reason}\n`);
260
+ appendFileSync(abs(join('.spur', 'memory', 'wrapup-routes.log')), `${runId} ${reason}\n`);
261
+ return { reason, reasonFile: relReasonFile, exitCode: 0 };
262
+ }
263
+
264
+ function readFileSyncSafe(path: string): string | null {
265
+ try {
266
+ return readFileSync(path, 'utf8');
267
+ } catch {
268
+ return null;
269
+ }
270
+ }
271
+
188
272
  export interface MetricsResult {
189
273
  status: 'PASS' | 'FAIL';
190
274
  statusFile: string;
@@ -448,11 +532,12 @@ export function runFeatureTransition(env: WrapupStepsEnv, options: WrapupStepsOp
448
532
  }
449
533
 
450
534
  export const WRAPUP_STEPS_USAGE =
451
- 'usage: wrapup-steps.ts <resolve|metrics|feature-transition> (env: __runId, tasks, feature, featureGateCmd, spurBin)';
535
+ 'usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)';
452
536
 
453
537
  export function main(argv: string[], env: WrapupStepsEnv = getEnvVars(), options: WrapupStepsOptions = {}): number {
454
538
  const sub = argv[0];
455
539
  if (sub === 'resolve') return resolveTasks(env, options).exitCode;
540
+ if (sub === 'route-reason') return writeRouteReason(env, options).exitCode;
456
541
  if (sub === 'metrics') {
457
542
  runMetrics(env, options);
458
543
  return 0;
@@ -137,6 +137,8 @@ template, source-citation format) is needed only inside each phase, not at the p
137
137
  2. IDEATE → Generate 2-3 approaches with trade-offs (delegate research inline; escalate via spur agent run on a trigger)
138
138
  3. OUTPUT → Structured markdown (Overview → Approaches → Recommendations → Next Steps),
139
139
  delivered incrementally; saved to docs/plans/YYYY-MM-DD-<topic>-brainstorm.md
140
+ with the spur-dev plan frontmatter (kind: plan, tags: [brainstorm, …]) plus
141
+ needs_design/run_id; keeps its own sections and the required ## Design Summary
140
142
  ```
141
143
 
142
144
  ## Design Approval Gate
@@ -158,10 +158,25 @@ DO NOT implement research directly. Delegate to specialized skills:
158
158
 
159
159
  ### Output Template
160
160
 
161
+ Frontmatter follows the spur-dev
162
+ [document-authoring vocabulary](../../spur-dev/references/document-authoring.md#frontmatter-vocabulary);
163
+ `needs_design` and `run_id` are brainstorm keys kept beside it. The body keeps this brainstorm shape,
164
+ not the plan template's sections.
165
+
161
166
  ```markdown
162
- # Brainstorm: [Topic]
167
+ ---
168
+ kind: plan
169
+ title: "Brainstorm: [Topic]"
170
+ status: proposed
171
+ created_at: YYYY-MM-DD
172
+ updated_at: YYYY-MM-DD
173
+ related: [] # owning feature/task ids when known
174
+ tags: [brainstorm] # then feature ids, then ≤2 area tags
175
+ needs_design: true
176
+ run_id: <run id, omit when none>
177
+ ---
163
178
 
164
- **Date:** YYYY-MM-DD
179
+ # Brainstorm: [Topic]
165
180
 
166
181
  ## Overview
167
182
 
@@ -261,8 +261,8 @@ the deterministic Testing writer `spur task record` (section authorship never ha
261
261
  # write .spur/run/<wbs>-verdict.json (shape in references/verdict-schema.md), then:
262
262
  # F96 residual sweep (observe-only): scan + fold BEFORE record, under every --fix mode.
263
263
  RESIDUAL=$(superskill script path sp residual-scan.mjs)
264
- node "$RESIDUAL" scan --wbs <wbs> --base .spur/run/<wbs>-base.sha
265
- node "$RESIDUAL" fold --wbs <wbs> --verdict .spur/run/<wbs>-verdict.json
264
+ node "$RESIDUAL" scan <wbs>
265
+ node "$RESIDUAL" fold <wbs>
266
266
  spur task record <wbs> --verdict-file .spur/run/<wbs>-verdict.json # renders ## Testing
267
267
  ```
268
268
 
@@ -82,8 +82,9 @@ verdict artifact so quality failures are not lost behind a requirements-only PAS
82
82
 
83
83
  Before declaring a task `done`, run this lightweight checklist. It catches the most common oversights that survive the formal pipeline gates:
84
84
 
85
- - [ ] All tests pass (`bun run test` exits 0).
86
- - [ ] Lint clean (`bun run lint` exits 0).
85
+ - [ ] Check receipt `.spur/run/<wbs>-check-receipt.json` reports `reuse: true` for the current
86
+ digest via `quality-gate.ts status`; run `bun run spur-check` only when it reports stale or
87
+ missing (0940).
87
88
  - [ ] No `TODO` or `FIXME` without a linked task WBS.
88
89
  - [ ] `git status` shows only intentional changes (no debug artifacts, no temp files).
89
90
  - [ ] No `console.log` / `console.error` in production code (use the project logger).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: session-review
3
- description: "Review the active coding-agent session: separate resolved from open issues with evidence, propose bounded improvements. With --triage, apply pure-doc / 1–2-line fixes inline and file the rest as one task. Triggers: review this session, wrap-up, triage findings."
3
+ description: "Review the active coding-agent session: separate resolved from open issues with evidence, propose bounded improvements. With --triage, apply pure-doc / 1–2-line fixes inline and file the rest as one or more tasks. Triggers: review this session, wrap-up, triage findings."
4
4
  license: Apache-2.0
5
5
  version: 1.1.0
6
6
  metadata:
@@ -31,7 +31,7 @@ cross-agent windows, recurrence, trends, or quantitative performance forensics.
31
31
  | Argument | Description | Default |
32
32
  | --- | --- | --- |
33
33
  | `[focus]` | Question or operation to emphasize. It changes ordering, not evidence collection. | full active session |
34
- | `--triage` | Opt into bounded remediation after the report: apply direct fixes (pure docs / one-to-two-line fixes) inline, then file all remaining actionable findings as exactly one new task via the CLI-gated corpus surface. | off (report-only) |
34
+ | `--triage` | Opt into bounded remediation after the report: apply direct fixes (pure docs / one-to-two-line fixes) inline, then file all remaining actionable findings as one or more implement-ready tasks via the CLI-gated corpus surface. | off (report-only) |
35
35
 
36
36
  ## Evidence boundary
37
37
 
@@ -64,13 +64,15 @@ three buckets — never skip triage and start fixing from the raw findings list.
64
64
  - **Note** — pre-existing, environmental, or ownerless observations; report only.
65
65
  2. **Apply direct fixes inline** — smallest surgical diff, project style, and re-verify each
66
66
  with the targeted check (lint / test / the exact command that exhibited the issue).
67
- 3. **Create exactly one task** for the Task bucket through the CLI-gated corpus surface
68
- (`spur task create`, then `spur task update <wbs> --section <s> --from-file` per section).
69
- One task, not one per finding: each finding keeps its evidence, a suggested fix direction,
70
- and an AC where verifiable. Exclude what direct fixes already resolved — say so in the task
71
- Background instead.
67
+ 3. **File the Task bucket as one or more tasks** by the shared filing rule in
68
+ [dev-operations.md § 2. review](../spur-dev/references/dev-operations.md#2-review) (Triage
69
+ step 3): one task per cohesive unit one agent can implement and verify in one run, under the
70
+ existing feature that owns the surface (never a new root feature), implement-ready, written
71
+ only through `spur task create --skip-ready` + `spur task update <wbs> --section <s> --from-file`.
72
+ Not one task per finding: each finding keeps its evidence, fix direction, and an AC where
73
+ verifiable. Exclude what direct fixes already resolved — say so in the task Background.
72
74
  4. **Report** — add a Triage section: applied fixes (path + one-line what + verification) and
73
- the created task WBS. The Resolved/Open tables keep their evidence rules unchanged.
75
+ each created task WBS. The Resolved/Open tables keep their evidence rules unchanged.
74
76
 
75
77
  ## Protocol
76
78
 
@@ -156,7 +158,7 @@ improvement. Use `None` when the session is complete and no follow-up is justifi
156
158
  to imported-history analysis.
157
159
  - Report-only by default: do not create or update corpus items or edit files. The single exception
158
160
  is `--triage` mode, which permits exactly two mutation classes — direct fixes from the triage
159
- bucket, and the one triage task. Anything beyond that stays a proposal.
161
+ bucket, and the triage tasks. Anything beyond that stays a proposal.
160
162
  - Do not turn a single low-impact observation into a new policy. Report it as a candidate until it
161
163
  recurs or demonstrates a high-impact contract violation.
162
164