@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,275 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * feature-verification-steps — the single writer of feature verification
4
+ * receipts behind the `feature-verification` workflow (task 0915, remediated
5
+ * to the frozen v1 contract; no public CLI verb exists by design).
6
+ *
7
+ * Usage (workflow onEnter, vars: featureId, verificationCmd, spurBin, __runId):
8
+ * bun plugins/sp/scripts/feature-verification-steps.ts verify \
9
+ * --feature-id D63 --run-id <runId> --cmd "bun run spur-check-feature"
10
+ *
11
+ * One pass, in order:
12
+ * 1. resolve the selected `feature-verification` definition (project override
13
+ * wins) for the verifier identity + configured command,
14
+ * 2. capture the before proof-input digest, record the RUNNING receipt (both
15
+ * copies) and register the run-scoped receipt as a run artifact,
16
+ * 3. run the effective command via the safe launch splitter (never `sh -c`
17
+ * interpolation), output to the run-scoped log,
18
+ * 4. capture the after digest — a mismatch records FAIL,
19
+ * 5. complete the receipt PASS/FAIL (both copies) and the coarse status file.
20
+ *
21
+ * Always exits 0 after verify; the transition guard reads the status file and
22
+ * the completion boundary validates the receipt (fail-closed). Unsafe ids are
23
+ * refused outright. Node-builtin imports only (ADR-065); the application seams come
24
+ * from the generated inline bundle (`plugins/sp/lib/inline-run.generated.mjs`) — never
25
+ * from `packages/app/src/index.ts`, which is not a plugin-script contract surface (0948 R3).
26
+ */
27
+
28
+ import { spawnSync } from 'node:child_process';
29
+ import {
30
+ closeSync,
31
+ existsSync,
32
+ mkdirSync,
33
+ openSync,
34
+ readdirSync,
35
+ readFileSync,
36
+ renameSync,
37
+ writeFileSync,
38
+ } from 'node:fs';
39
+ import { join } from 'node:path';
40
+ import { fileURLToPath } from 'node:url';
41
+ import { getEnvVar } from '../lib/env';
42
+
43
+ /** Single safe filename component — same class as the inline-run-setup run-id guard. */
44
+ const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
45
+
46
+ /** Shape of the generated inline bundle / repo app source this script consumes. */
47
+ interface VerificationModule {
48
+ startFeatureVerificationReceipt: typeof import('../../packages/app/src/workflow/feature-verification-receipt')['startFeatureVerificationReceipt'];
49
+ completeFeatureVerificationReceipt: typeof import('../../packages/app/src/workflow/feature-verification-receipt')['completeFeatureVerificationReceipt'];
50
+ captureFeatureReceiptDigest: typeof import('../../packages/app/src/workflow/feature-verification-receipt')['captureFeatureReceiptDigest'];
51
+ resolveWorkflowDefinition: typeof import('../../packages/app/src/workflow/workflow-resolver')['resolveWorkflowDefinition'];
52
+ splitLaunchCommand: typeof import('../../packages/app/src/workflow/split-launch-command')['splitLaunchCommand'];
53
+ openInlineRunProjectDb: typeof import('../../packages/app/src/services/inline-run-setup')['openInlineRunProjectDb'];
54
+ ArtifactDao: new (
55
+ adapter: never,
56
+ ) => { record: (input: { runId: string; path: string; kind: string }) => Promise<unknown> };
57
+ }
58
+
59
+ /** Explicit module surface. Source is not the contract; the generated bundle is (0948 R3). */
60
+ type ModuleMode = 'source' | 'bundle';
61
+
62
+ function bundleEntry(): string {
63
+ return fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
64
+ }
65
+
66
+ /**
67
+ * Source mode names the mode and the fix. The app source entry does not export
68
+ * the verification seams, and "rebuild/install the sp plugin" cannot add them.
69
+ */
70
+ function sourceModeError(spurBin: string): Error {
71
+ return new Error(
72
+ `source mode: refusing to load packages/app/src/index.ts (spurBin=${JSON.stringify(spurBin)}). ` +
73
+ 'That entry is not the feature-verification contract surface — it does not export ' +
74
+ 'splitLaunchCommand / ArtifactDao. Fix: run in bundle mode so ' +
75
+ 'plugins/sp/lib/inline-run.generated.mjs is loaded (omit --module-mode, or pass --module-mode bundle) ' +
76
+ 'and regenerate that bundle if it is missing. Reinstalling the sp plugin does not add these seams to the app source entry.',
77
+ );
78
+ }
79
+
80
+ function resolveModuleMode(raw: string): ModuleMode {
81
+ if (raw === '' || raw === 'bundle') return 'bundle';
82
+ if (raw === 'source') return 'source';
83
+ throw new Error(`unknown --module-mode ${JSON.stringify(raw)}; expected "source" or "bundle"`);
84
+ }
85
+
86
+ /**
87
+ * Bundle mode loads the generated inline bundle. Source mode fails closed with a named fix.
88
+ *
89
+ * `spurBin` no longer selects the module (it named the *caller's* CLI entry, not this
90
+ * script's runtime surface); it is kept only to make the failure message actionable.
91
+ */
92
+ async function loadModule(spurBin: string, mode: ModuleMode): Promise<VerificationModule> {
93
+ if (mode === 'source') throw sourceModeError(spurBin);
94
+ const bundle = bundleEntry();
95
+ if (!existsSync(bundle)) {
96
+ throw new Error(
97
+ `feature-verification-steps: mode=bundle — the generated inline bundle is missing at ${bundle}` +
98
+ `${spurBin === '' ? '' : ` (spurBin=${spurBin})`}. ` +
99
+ 'Fix: in a source checkout run `bun run build:bundle`; for an installed plugin reinstall it.',
100
+ );
101
+ }
102
+ return requireModule(bundle, mode);
103
+ }
104
+
105
+ /** Dynamic-import the entry and refuse modules missing the verification seams. */
106
+ async function requireModule(entry: string, mode: ModuleMode): Promise<VerificationModule> {
107
+ const mod = (await import(entry)) as Partial<VerificationModule> & Record<string, unknown>;
108
+ const missing = (
109
+ [
110
+ 'startFeatureVerificationReceipt',
111
+ 'completeFeatureVerificationReceipt',
112
+ 'captureFeatureReceiptDigest',
113
+ 'resolveWorkflowDefinition',
114
+ 'splitLaunchCommand',
115
+ 'openInlineRunProjectDb',
116
+ 'ArtifactDao',
117
+ ] as const
118
+ ).filter((k) => typeof mod[k] !== 'function');
119
+ if (missing.length > 0) {
120
+ throw new Error(
121
+ `feature-verification-steps: mode=${mode} — application entry ${entry} is missing feature-verification seams (${missing.join(', ')}). ` +
122
+ 'Fix: the generated inline bundle is stale or incomplete — run `bun run build:bundle` in a source checkout, or reinstall the sp plugin.',
123
+ );
124
+ }
125
+ return mod as VerificationModule;
126
+ }
127
+
128
+ /** Refuse ids that could escape the run directory through receipt filenames. */
129
+ function assertSafeId(kind: string, id: string): void {
130
+ if (!SAFE_ID_RE.test(id) || id.includes('..')) {
131
+ throw new Error(`refusing unsafe ${kind}: ${id}`);
132
+ }
133
+ }
134
+
135
+ /** Locate `<id>_<slug>.md` under the features directory. */
136
+ function findFeatureFile(featureDir: string, featureId: string): string | undefined {
137
+ if (!existsSync(featureDir)) return undefined;
138
+ const name = readdirSync(featureDir).find((n) => n.startsWith(`${featureId}_`) && n.endsWith('.md'));
139
+ return name === undefined ? undefined : join(featureDir, name);
140
+ }
141
+
142
+ // ponytail: the receipt service is filesystem-pure over the ts-runtime
143
+ // FileSystem port; only ensureDir/writeFile/rename are exercised on the
144
+ // start/complete paths. Swap to createNodeFileSystem if more surface is needed.
145
+ const nodeFsShim = {
146
+ ensureDir: (dir: string) => {
147
+ mkdirSync(dir, { recursive: true });
148
+ },
149
+ writeFile: (path: string, body: string) => {
150
+ writeFileSync(path, body);
151
+ },
152
+ rename: (src: string, dest: string) => {
153
+ renameSync(src, dest);
154
+ },
155
+ readFile: (path: string) => {
156
+ return readFileSync(path, 'utf8');
157
+ },
158
+ } as never;
159
+
160
+ /** One full verification pass for one feature. Always exits 0. */
161
+ async function verify(
162
+ featureId: string,
163
+ runId: string,
164
+ cmdOverride: string,
165
+ spurBin: string,
166
+ mode: ModuleMode,
167
+ ): Promise<void> {
168
+ assertSafeId('feature id', featureId);
169
+ assertSafeId('run id', runId);
170
+ const cwd = process.cwd();
171
+ const mod = await loadModule(spurBin, mode);
172
+ const runDir = join(cwd, '.spur', 'run');
173
+ mkdirSync(runDir, { recursive: true });
174
+
175
+ // 1. Selected verifier definition (project override wins); its configured
176
+ // command is the completion contract — an override records but can't complete.
177
+ const selected = await mod.resolveWorkflowDefinition(cwd, 'feature-verification');
178
+ const vars = selected.workflow.vars as { verificationCmd?: unknown } | undefined;
179
+ const configuredCmd =
180
+ typeof vars?.verificationCmd === 'string' && vars.verificationCmd.length > 0
181
+ ? vars.verificationCmd
182
+ : 'bun run spur-check-feature';
183
+ const effectiveCmd = cmdOverride !== '' ? cmdOverride : configuredCmd;
184
+
185
+ const featureFile = findFeatureFile(join(cwd, 'docs', 'features'), featureId);
186
+ if (featureFile === undefined) {
187
+ throw new Error(`feature ${featureId} not found under ${join(cwd, 'docs', 'features')}`);
188
+ }
189
+ const featureContent = readFileSync(featureFile, 'utf8');
190
+ const learningsPath = join(cwd, '.spur', 'context', 'learnings.md');
191
+ const learningsContent = existsSync(learningsPath) ? readFileSync(learningsPath, 'utf8') : undefined;
192
+
193
+ // 2. Before digest + RUNNING receipt (both copies) + artifact registration.
194
+ const beforeDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
195
+ const receipt = await mod.startFeatureVerificationReceipt(nodeFsShim, runDir, {
196
+ featureId,
197
+ runId,
198
+ workdir: cwd,
199
+ verifier: {
200
+ name: 'feature-verification',
201
+ sourcePath: selected.path,
202
+ layer: selected.layer,
203
+ definitionDigest: selected.digest,
204
+ },
205
+ verificationCmd: effectiveCmd,
206
+ inputDigest: beforeDigest,
207
+ });
208
+ const db = await mod.openInlineRunProjectDb(cwd);
209
+ try {
210
+ await new mod.ArtifactDao(db.adapter as never).record({
211
+ runId,
212
+ path: join(runDir, `${runId}-feature-verification.json`),
213
+ kind: 'feature-verification',
214
+ });
215
+ } finally {
216
+ db.close();
217
+ }
218
+
219
+ // 3. Run the effective command via the safe splitter (no `sh -c`).
220
+ const logPath = join(runDir, `${runId}-feature-verification.log`);
221
+ const launch = mod.splitLaunchCommand(effectiveCmd, 'feature-verification verificationCmd');
222
+ if ('error' in launch) {
223
+ await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, {
224
+ status: 'FAIL',
225
+ inputDigest: beforeDigest,
226
+ });
227
+ console.log(`feature-verification-steps: ${featureId} verification FAIL (${launch.error})`);
228
+ return;
229
+ }
230
+ const logFd = openSync(logPath, 'a');
231
+ try {
232
+ const result = spawnSync(launch.command, launch.leadingArgs, {
233
+ cwd,
234
+ stdio: ['ignore', logFd, logFd],
235
+ timeout: 4 * 60 * 60 * 1000,
236
+ });
237
+ const exit = result.status ?? 1;
238
+
239
+ // 4/5. After digest — a mismatch records FAIL (the pass did not verify
240
+ // the tree it left behind); otherwise the exit code decides.
241
+ const afterDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
242
+ const status: 'PASS' | 'FAIL' = exit === 0 && afterDigest === beforeDigest ? 'PASS' : 'FAIL';
243
+ await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, { status, inputDigest: afterDigest });
244
+ console.log(`feature-verification-steps: ${featureId} verification ${status} (run ${runId}, log ${logPath})`);
245
+ } finally {
246
+ closeSync(logFd);
247
+ }
248
+ }
249
+
250
+ function main(): void {
251
+ const args = process.argv.slice(2);
252
+ const command = args[0];
253
+ const flag = (name: string): string => {
254
+ const i = args.indexOf(name);
255
+ return i >= 0 && i + 1 < args.length ? (args[i + 1] ?? '') : '';
256
+ };
257
+ if (command !== 'verify') {
258
+ console.error('Usage: feature-verification-steps.ts verify --feature-id <id> --run-id <id> [--cmd <command>]');
259
+ process.exit(2);
260
+ }
261
+ verify(
262
+ flag('--feature-id') || getEnvVar('featureId') || '',
263
+ flag('--run-id') || getEnvVar('__runId') || '',
264
+ flag('--cmd') || getEnvVar('verificationCmd') || '',
265
+ flag('--spur-bin') || getEnvVar('spurBin') || '',
266
+ resolveModuleMode(flag('--module-mode')),
267
+ )
268
+ .then(() => process.exit(0))
269
+ .catch((err: unknown) => {
270
+ console.error(`feature-verification-steps: ${String(err)}`);
271
+ process.exit(1);
272
+ });
273
+ }
274
+
275
+ main();
@@ -9,6 +9,7 @@ import {
9
9
  closeSync,
10
10
  existsSync,
11
11
  fsyncSync,
12
+ mkdirSync,
12
13
  openSync,
13
14
  readdirSync,
14
15
  readFileSync,
@@ -434,6 +435,75 @@ HA_BASELINE_UNTIL=${baseline.until}
434
435
  `;
435
436
  return env;
436
437
  }
438
+ function isRealDate(ymd) {
439
+ const m = ymd.match(/^(\d{4})-(\d{2})-(\d{2})$/);
440
+ if (!m)
441
+ return false;
442
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
443
+ const dt = new Date(Date.UTC(y, mo - 1, d));
444
+ return dt.getUTCFullYear() === y && dt.getUTCMonth() === mo - 1 && dt.getUTCDate() === d;
445
+ }
446
+ function validateSelector(opts) {
447
+ const errors = [];
448
+ const has = (v) => v !== undefined && v !== "";
449
+ const val = (v) => has(v) ? v : null;
450
+ const mode = has(opts.mode) ? opts.mode : "daily";
451
+ if (mode !== "daily" && mode !== "ad-hoc") {
452
+ return { ok: false, errors: [`--mode must be "daily" or "ad-hoc", got "${opts.mode}"`] };
453
+ }
454
+ const recompute = opts.recompute ?? "";
455
+ if (recompute !== "" && recompute !== "true" && recompute !== "false") {
456
+ errors.push(`--recompute must be "true" or "false", got "${recompute}"`);
457
+ }
458
+ if (mode === "daily") {
459
+ for (const [flag, v] of [
460
+ ["--focus", opts.focus],
461
+ ["--since", opts.since],
462
+ ["--until", opts.until],
463
+ ["--output", opts.output]
464
+ ]) {
465
+ if (has(v))
466
+ errors.push(`daily mode rejects ${flag}`);
467
+ }
468
+ if (has(opts.date) && !isRealDate(opts.date)) {
469
+ errors.push(`--date must be a real YYYY-MM-DD calendar day, got "${opts.date}"`);
470
+ }
471
+ } else {
472
+ if (has(opts.date))
473
+ errors.push("ad-hoc mode rejects --date");
474
+ if (recompute === "true")
475
+ errors.push("ad-hoc mode rejects --recompute");
476
+ if (!has(opts.focus))
477
+ errors.push("ad-hoc mode requires a non-empty --focus");
478
+ const sinceOk = has(opts.since);
479
+ const untilOk = has(opts.until);
480
+ if (!sinceOk)
481
+ errors.push("ad-hoc mode requires --since (inclusive ISO instant)");
482
+ if (!untilOk)
483
+ errors.push("ad-hoc mode requires --until (inclusive ISO instant)");
484
+ if (sinceOk && untilOk) {
485
+ const since = new Date(opts.since ?? "");
486
+ const until = new Date(opts.until ?? "");
487
+ if (Number.isNaN(since.getTime()))
488
+ errors.push(`--since must be a parseable ISO instant, got "${opts.since}"`);
489
+ if (Number.isNaN(until.getTime()))
490
+ errors.push(`--until must be a parseable ISO instant, got "${opts.until}"`);
491
+ if (!Number.isNaN(since.getTime()) && !Number.isNaN(until.getTime()) && since.getTime() > until.getTime()) {
492
+ errors.push(`--since must not be after --until ("${opts.since}" > "${opts.until}")`);
493
+ }
494
+ }
495
+ }
496
+ if (errors.length)
497
+ return { ok: false, errors };
498
+ return {
499
+ ok: true,
500
+ mode,
501
+ date: val(opts.date),
502
+ focus: val(opts.focus),
503
+ since: val(opts.since),
504
+ until: val(opts.until)
505
+ };
506
+ }
437
507
  function buildProvenance(opts) {
438
508
  let raw;
439
509
  try {
@@ -699,19 +769,48 @@ ${result.problems.map((p) => `- ${p}
699
769
  return {
700
770
  exitCode: 1,
701
771
  stdout: "",
702
- stderr: `usage: <script> paths --helper <p> --out <env> [--report-dir <d>] [--date <d>] [--output <p>] [--mode <m>] [--since <s>] [--until <u>]
772
+ stderr: `usage: <script> paths --helper <p> --out <env> [--report-dir <d>] [--date <d>] [--output <p>] [--mode <m>] [--since <s>] [--until <u>] [--focus <text>] [--recompute true|false] [--run-id <id>]
703
773
  `
704
774
  };
705
775
  }
706
- writeFileSync(f.out, resolvePaths({
776
+ const v = validateSelector({
777
+ mode: f.mode,
778
+ date: f.date,
779
+ since: f.since,
780
+ until: f.until,
781
+ focus: f.focus,
782
+ recompute: f.recompute,
783
+ output: f.output
784
+ });
785
+ if (!v.ok)
786
+ return { exitCode: 1, stdout: "", stderr: `${v.errors.join(`
787
+ `)}
788
+ ` };
789
+ const env = resolvePaths({
707
790
  helper: f.helper,
708
791
  reportDir: f["report-dir"] ?? "docs/report",
709
792
  date: f.date,
710
793
  output: f.output,
711
- mode: f.mode,
794
+ mode: v.mode,
712
795
  since: f.since,
713
796
  until: f.until
714
- }));
797
+ });
798
+ writeFileSync(f.out, env);
799
+ if (f["run-id"] !== undefined && f["run-id"] !== "") {
800
+ mkdirSync(".spur/run", { recursive: true });
801
+ const envVars = Object.fromEntries(env.trim().split(`
802
+ `).map((l) => [l.slice(0, l.indexOf("=")), l.slice(l.indexOf("=") + 1)]));
803
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
804
+ writeFileSync(`.spur/run/${f["run-id"]}-selector.json`, `${JSON.stringify({
805
+ mode: v.mode,
806
+ date: envVars.HA_DATE ?? null,
807
+ focus: v.focus,
808
+ since: envVars.HA_SINCE ?? null,
809
+ until: envVars.HA_UNTIL ?? null,
810
+ timezone: tz
811
+ }, null, 2)}
812
+ `);
813
+ }
715
814
  return { exitCode: 0, stdout: "", stderr: "" };
716
815
  }
717
816
  case "probe": {
@@ -805,6 +904,7 @@ ${reasons}`, stderr: "" };
805
904
  process.exitCode = exitCode;
806
905
  }
807
906
  export {
907
+ validateSelector,
808
908
  stampReport,
809
909
  semanticArtifactDigest,
810
910
  runCacheCli,
@@ -34,6 +34,7 @@ import {
34
34
  closeSync,
35
35
  existsSync,
36
36
  fsyncSync,
37
+ mkdirSync,
37
38
  openSync,
38
39
  readdirSync,
39
40
  readFileSync,
@@ -594,6 +595,94 @@ export function resolvePaths(opts: {
594
595
  return env;
595
596
  }
596
597
 
598
+ /**
599
+ * Per-mode argument grammar for the `paths` command (0920): the deterministic owner of the
600
+ * mode/window validation the resolve-scope model hop used to perform. An empty string means
601
+ * "not provided" — the workflow passes every declared var, empty by default.
602
+ */
603
+ export type SelectorValidation =
604
+ | {
605
+ ok: true;
606
+ mode: 'daily' | 'ad-hoc';
607
+ date: string | null;
608
+ focus: string | null;
609
+ since: string | null;
610
+ until: string | null;
611
+ }
612
+ | { ok: false; errors: string[] };
613
+
614
+ /** Real calendar day (rejects e.g. 2026-02-30); tz-independent UTC round-trip. */
615
+ function isRealDate(ymd: string): boolean {
616
+ const m = ymd.match(/^(\d{4})-(\d{2})-(\d{2})$/);
617
+ if (!m) return false;
618
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
619
+ const dt = new Date(Date.UTC(y, mo - 1, d));
620
+ return dt.getUTCFullYear() === y && dt.getUTCMonth() === mo - 1 && dt.getUTCDate() === d;
621
+ }
622
+
623
+ export function validateSelector(opts: {
624
+ mode?: string;
625
+ date?: string;
626
+ since?: string;
627
+ until?: string;
628
+ focus?: string;
629
+ recompute?: string;
630
+ output?: string;
631
+ }): SelectorValidation {
632
+ const errors: string[] = [];
633
+ const has = (v?: string) => v !== undefined && v !== '';
634
+ const val = (v?: string) => (has(v) ? v : null);
635
+ const mode = has(opts.mode) ? opts.mode : 'daily';
636
+ if (mode !== 'daily' && mode !== 'ad-hoc') {
637
+ return { ok: false, errors: [`--mode must be "daily" or "ad-hoc", got "${opts.mode}"`] };
638
+ }
639
+ const recompute = opts.recompute ?? '';
640
+ if (recompute !== '' && recompute !== 'true' && recompute !== 'false') {
641
+ errors.push(`--recompute must be "true" or "false", got "${recompute}"`);
642
+ }
643
+ if (mode === 'daily') {
644
+ for (const [flag, v] of [
645
+ ['--focus', opts.focus],
646
+ ['--since', opts.since],
647
+ ['--until', opts.until],
648
+ ['--output', opts.output],
649
+ ] as const) {
650
+ if (has(v)) errors.push(`daily mode rejects ${flag}`);
651
+ }
652
+ if (has(opts.date) && !isRealDate(opts.date)) {
653
+ errors.push(`--date must be a real YYYY-MM-DD calendar day, got "${opts.date}"`);
654
+ }
655
+ } else {
656
+ if (has(opts.date)) errors.push('ad-hoc mode rejects --date');
657
+ if (recompute === 'true') errors.push('ad-hoc mode rejects --recompute');
658
+ if (!has(opts.focus)) errors.push('ad-hoc mode requires a non-empty --focus');
659
+ const sinceOk = has(opts.since);
660
+ const untilOk = has(opts.until);
661
+ if (!sinceOk) errors.push('ad-hoc mode requires --since (inclusive ISO instant)');
662
+ if (!untilOk) errors.push('ad-hoc mode requires --until (inclusive ISO instant)');
663
+ if (sinceOk && untilOk) {
664
+ const since = new Date(opts.since ?? '');
665
+ const until = new Date(opts.until ?? '');
666
+ if (Number.isNaN(since.getTime()))
667
+ errors.push(`--since must be a parseable ISO instant, got "${opts.since}"`);
668
+ if (Number.isNaN(until.getTime()))
669
+ errors.push(`--until must be a parseable ISO instant, got "${opts.until}"`);
670
+ if (!Number.isNaN(since.getTime()) && !Number.isNaN(until.getTime()) && since.getTime() > until.getTime()) {
671
+ errors.push(`--since must not be after --until ("${opts.since}" > "${opts.until}")`);
672
+ }
673
+ }
674
+ }
675
+ if (errors.length) return { ok: false, errors };
676
+ return {
677
+ ok: true,
678
+ mode,
679
+ date: val(opts.date),
680
+ focus: val(opts.focus),
681
+ since: val(opts.since),
682
+ until: val(opts.until),
683
+ };
684
+ }
685
+
597
686
  export interface ProbeOptions {
598
687
  artifact: string;
599
688
  target: string;
@@ -928,21 +1017,56 @@ export function runCacheCli(argv: string[]): CacheCliResult {
928
1017
  return {
929
1018
  exitCode: 1,
930
1019
  stdout: '',
931
- stderr: 'usage: <script> paths --helper <p> --out <env> [--report-dir <d>] [--date <d>] [--output <p>] [--mode <m>] [--since <s>] [--until <u>]\n',
1020
+ stderr: 'usage: <script> paths --helper <p> --out <env> [--report-dir <d>] [--date <d>] [--output <p>] [--mode <m>] [--since <s>] [--until <u>] [--focus <text>] [--recompute true|false] [--run-id <id>]\n',
932
1021
  };
933
1022
  }
934
- writeFileSync(
935
- f.out,
936
- resolvePaths({
937
- helper: f.helper,
938
- reportDir: f['report-dir'] ?? 'docs/report',
939
- date: f.date,
940
- output: f.output,
941
- mode: f.mode,
942
- since: f.since,
943
- until: f.until,
944
- }),
945
- );
1023
+ const v = validateSelector({
1024
+ mode: f.mode,
1025
+ date: f.date,
1026
+ since: f.since,
1027
+ until: f.until,
1028
+ focus: f.focus,
1029
+ recompute: f.recompute,
1030
+ output: f.output,
1031
+ });
1032
+ if (!v.ok) return { exitCode: 1, stdout: '', stderr: `${v.errors.join('\n')}\n` };
1033
+ const env = resolvePaths({
1034
+ helper: f.helper,
1035
+ reportDir: f['report-dir'] ?? 'docs/report',
1036
+ date: f.date,
1037
+ output: f.output,
1038
+ mode: v.mode,
1039
+ since: f.since,
1040
+ until: f.until,
1041
+ });
1042
+ writeFileSync(f.out, env);
1043
+ // 0920: run-scoped selector observation artifact — written only after successful
1044
+ // validation; historical selector files have incompatible shapes and are not consumed.
1045
+ if (f['run-id'] !== undefined && f['run-id'] !== '') {
1046
+ mkdirSync('.spur/run', { recursive: true });
1047
+ const envVars = Object.fromEntries(
1048
+ env
1049
+ .trim()
1050
+ .split('\n')
1051
+ .map((l) => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)]),
1052
+ );
1053
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC';
1054
+ writeFileSync(
1055
+ `.spur/run/${f['run-id']}-selector.json`,
1056
+ `${JSON.stringify(
1057
+ {
1058
+ mode: v.mode,
1059
+ date: envVars.HA_DATE ?? null,
1060
+ focus: v.focus,
1061
+ since: envVars.HA_SINCE ?? null,
1062
+ until: envVars.HA_UNTIL ?? null,
1063
+ timezone: tz,
1064
+ },
1065
+ null,
1066
+ 2,
1067
+ )}\n`,
1068
+ );
1069
+ }
946
1070
  return { exitCode: 0, stdout: '', stderr: '' };
947
1071
  }
948
1072
  case 'probe': {
@@ -44,11 +44,12 @@ const DOCUMENTED = {
44
44
  'doctor.probe',
45
45
  'file.read.into-var',
46
46
  'hitl.confirm',
47
- 'hitl.select',
47
+ 'hitl.input',
48
48
  'agent.run',
49
49
  'proof.fingerprint',
50
50
  'run.artifact',
51
51
  'command.gate',
52
+ 'decide',
52
53
  ]),
53
54
  guards: new Set(['always', 'shell', 'action-ok', 'contract-violation']),
54
55
  } as const;