@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/pipeline-budgets.json +2 -9
- package/config/plugin-scripts.json +27 -1
- package/config/templates/AGENTS.md +4 -0
- package/config/templates/docs/02_ROADMAP.md +2 -0
- package/config/templates/docs/03_ARCHITECTURE.md +3 -1
- package/config/templates/docs/04_DESIGN.md +2 -0
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +10 -2
- package/config/workflow-candidates.json +72 -1
- package/config/workflows/feature-lifecycle.yaml +14 -5
- package/config/workflows/feature-verification.yaml +36 -27
- package/config/workflows/history-anatomy.yaml +16 -25
- package/config/workflows/idea-pipeline.yaml +76 -23
- package/config/workflows/pr-review.yaml +8 -0
- package/config/workflows/task-pipeline.yaml +362 -40
- package/config/workflows/wayfinder-resolution.yaml +5 -0
- package/config/workflows/wrapup-pipeline.yaml +102 -19
- package/package.json +9 -9
- package/plugins/sp/README.md +7 -2
- package/plugins/sp/agents/super-planner.md +14 -5
- package/plugins/sp/commands/dev-dogfood.md +4 -4
- package/plugins/sp/commands/dev-fixall.md +8 -5
- package/plugins/sp/commands/dev-run.md +8 -2
- package/plugins/sp/commands/dev-runall.md +14 -8
- package/plugins/sp/commands/dev-verify.md +9 -0
- package/plugins/sp/commands/dev-verifyall.md +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +306 -301
- package/plugins/sp/lib/inline-run.generated.d.mts +18 -0
- package/plugins/sp/lib/inline-run.generated.mjs +1468 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/environment-lens.md +1 -1
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
- package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
- package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
- package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +2 -1
- package/plugins/sp/scripts/inline-run-setup.mjs +421 -0
- package/plugins/sp/scripts/inline-run-setup.ts +325 -78
- package/plugins/sp/scripts/quality-gate.mjs +248 -6
- package/plugins/sp/scripts/quality-gate.ts +410 -9
- package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
- package/plugins/sp/scripts/record-feature-sync.ts +84 -0
- package/plugins/sp/scripts/residual-scan.mjs +484 -0
- package/plugins/sp/scripts/residual-scan.ts +640 -0
- package/plugins/sp/scripts/task-diffstat.mjs +156 -0
- package/plugins/sp/scripts/task-diffstat.ts +229 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
- package/plugins/sp/scripts/task-size-precheck.ts +8 -3
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +181 -0
- package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +60 -1
- package/plugins/sp/scripts/wrapup-steps.ts +89 -4
- package/plugins/sp/skills/brainstorm/SKILL.md +2 -0
- package/plugins/sp/skills/brainstorm/references/workflows.md +17 -2
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
- package/plugins/sp/skills/code-verification/SKILL.md +21 -0
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -2
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
- package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
- package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
- package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
- package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/session-review/SKILL.md +12 -2
- package/plugins/sp/skills/spur-check/SKILL.md +112 -0
- package/plugins/sp/skills/spur-cli/references/features.md +1 -1
- package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
- package/plugins/sp/skills/spur-cli/references/workflows.md +39 -19
- package/plugins/sp/skills/spur-dev/SKILL.md +13 -5
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +18 -3
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
- package/plugins/sp/skills/spur-dev/references/document-authoring.md +85 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +286 -58
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +1 -1
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +27 -4
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +61 -16
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +10 -5
- package/plugins/sp/skills/spur-dev/templates/design.md +31 -0
- package/plugins/sp/skills/spur-dev/templates/plan.md +32 -0
- package/plugins/sp/skills/spur-doctor/SKILL.md +60 -14
- package/schemas/state-machine-workflow.schema.json +4 -0
- package/spur.js +22501 -19644
- package/web/_astro/BoardApp.CerSBgis.js +192 -0
- package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
- package/web/_astro/{TaskDetail.CXGltuT_.js → TaskDetail.DCqiC-OZ.js} +1 -1
- package/web/_astro/arc.CPwg6Rw0.js +1 -0
- package/web/_astro/{architectureDiagram-3BPJPVTR.DJ8DHkWE.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.D8DHK3Jl.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.BugQbX9u.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
- package/web/_astro/channel.MYZLKNwy.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Mv26KlVn.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CD51JoT_.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.D_PFaIEe.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DemMW1ao.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
- package/web/_astro/{chunk-AQP2D5EJ.Iu2V5-ex.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.MsNgSP-E.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.DfbRaAlm.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BbJAQ4h-.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CPurtiC2.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CPurtiC2.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CKKdx1bM.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.CoKMTg-R.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.D4h4_k56.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
- package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
- package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
- package/web/_astro/{diagram-KO2AKTUF.BFoCkiCr.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
- package/web/_astro/{diagram-LMA3HP47.exHn9OVx.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.CeqO34nN.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.D_v7HqxR.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
- package/web/_astro/{flowDiagram-I6XJVG4X.EUmrpbwh.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
- package/web/_astro/{ganttDiagram-6RSMTGT7.BOCF5lII.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Di7otYZD.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
- package/web/_astro/index.Hjbr15fG.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.TcBkCAJk.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.D-2y4M0c.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
- package/web/_astro/{journeyDiagram-JHISSGLW.DPbJI_n2.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.EFxhQ9Fj.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
- package/web/_astro/{linear.DSAsQLzs.js → linear.C2_IkbZT.js} +1 -1
- package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
- package/web/_astro/{mindmap-definition-RKZ34NQL.CJY1N_7V.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
- package/web/_astro/{pieDiagram-4H26LBE5.567ZNoL2.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
- package/web/_astro/{quadrantDiagram-W4KKPZXB.mqfz9-MY.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.Bv1Gv9In.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
- package/web/_astro/{sankeyDiagram-5OEKKPKP.B6Gs4X4r.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
- package/web/_astro/{sequenceDiagram-3UESZ5HK.BhYj4v-m.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BPbBnkpw.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.C4squMNK.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.C_SwIHgl.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
- package/web/_astro/{vennDiagram-CIIHVFJN.Bz4NZGpQ.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CozMVZ3i.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
- package/web/_astro/{xychartDiagram-2RQKCTM6.BwMGBwjB.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
- package/web/index.html +2 -2
- package/config/workflows/decision-routing-example.yaml +0 -134
- package/web/_astro/BoardApp.BEDWpzsr.js +0 -188
- package/web/_astro/BoardApp.DQG2xfEz.js +0 -1
- package/web/_astro/arc.C0rrflm_.js +0 -1
- package/web/_astro/channel.SRrg1P-w.js +0 -1
- package/web/_astro/diagram-2AECGRRQ.DJ0h9zgw.js +0 -43
- package/web/_astro/diagram-5GNKFQAL.DvPk1jYd.js +0 -10
- package/web/_astro/index.Bx6GY4RH.css +0 -1
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
935
|
-
f.
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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.
|
|
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;
|