@gobing-ai/spur 0.4.8 → 0.4.10
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 -2
- package/config/rules/README.md +1 -1
- package/config/rules/boundary/config-loading-ownership.yaml +1 -0
- package/config/rules/boundary/env-var-hygiene.yaml +0 -1
- package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
- package/config/rules/boundary/sp-script-placement.yaml +3 -3
- package/config/rules/docs/sourced-api-claims.yaml +29 -64
- package/config/rules/strict/runtime-boundaries.yaml +40 -1
- package/config/rules/structure/protected-files.yaml +1 -1
- package/config/rules/surface/check-cli-surface.yaml +33 -0
- package/config/rules/typescript/no-debugger.yaml +3 -0
- package/config/workflow-candidates.json +2 -2
- package/config/workflows/feature-verification.yaml +6 -19
- package/config/workflows/history-anatomy.yaml +14 -28
- package/config/workflows/idea-pipeline.yaml +6 -19
- package/config/workflows/pr-review.yaml +9 -16
- package/config/workflows/task-pipeline.yaml +18 -25
- package/config/workflows/wrapup-pipeline.yaml +21 -63
- package/package.json +3 -3
- package/plugins/sp/README.md +70 -37
- package/plugins/sp/agents/expert-spur.md +0 -1
- package/plugins/sp/agents/super-planner.md +5 -5
- package/plugins/sp/agents/super-reviewer.md +2 -2
- package/plugins/sp/commands/dev-changelog.md +1 -1
- package/plugins/sp/commands/dev-daily.md +1 -1
- package/plugins/sp/commands/dev-pr-review.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +1 -1
- package/plugins/sp/commands/dev-runall.md +4 -4
- package/plugins/sp/commands/dev-verify.md +4 -6
- package/plugins/sp/commands/dev-verifyall.md +4 -5
- package/plugins/sp/hooks/hooks.json +11 -1
- package/plugins/sp/lib/context-post-tool.ts +1 -1
- package/plugins/sp/lib/context-session-start.ts +1 -1
- package/plugins/sp/lib/context-session-stop.ts +1 -1
- package/plugins/sp/lib/destructive-policy.ts +65 -1
- package/plugins/sp/plugin.json +2 -5
- package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
- package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
- package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
- package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
- package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
- package/plugins/sp/skills/brainstorm/SKILL.md +12 -11
- package/plugins/sp/skills/brainstorm/agents/openai.yaml +1 -1
- package/plugins/sp/skills/brainstorm/examples/ideation-example.md +0 -4
- package/plugins/sp/skills/brainstorm/metadata.openclaw +1 -1
- package/plugins/sp/skills/brainstorm/references/workflows.md +4 -14
- package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -8
- package/plugins/sp/skills/code-verification/references/secu-review.md +4 -4
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
- package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
- package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
- package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
- package/plugins/sp/skills/history-anatomy/references/operations.md +3 -4
- package/plugins/sp/skills/history-anatomy/references/report-contract.md +2 -3
- package/plugins/sp/skills/issue-finding/SKILL.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
- package/plugins/sp/skills/parallel-execution/references/result-synthesis.md +9 -10
- package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
- package/plugins/sp/skills/reverse-engineering/SKILL.md +4 -6
- package/plugins/sp/skills/session-review/SKILL.md +13 -29
- package/plugins/sp/skills/source-driven-development/SKILL.md +26 -27
- package/plugins/sp/skills/spur-check/SKILL.md +14 -13
- package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
- package/plugins/sp/skills/spur-cli/references/agent.md +17 -2
- package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
- package/plugins/sp/skills/spur-cli/references/features.md +6 -1
- package/plugins/sp/skills/spur-cli/references/history.md +3 -0
- package/plugins/sp/skills/spur-cli/references/self.md +5 -3
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
- package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +1 -1
- package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
- package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -13
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +9 -12
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
- package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
- package/spur.js +12726 -1456
- package/web/_astro/{BoardApp.CaNvQBD_.js → BoardApp.B5rTVNd_.js} +1 -1
- package/web/_astro/{BoardApp.C5jnkkKP.js → BoardApp.BMprmPRc.js} +141 -141
- package/web/_astro/{TaskDetail.DGKndhI8.js → TaskDetail.Ddsur8qd.js} +1 -1
- package/web/_astro/{arc.DHrwdg3G.js → arc.DLqVVBxP.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.pvKpK1s5.js → architectureDiagram-3BPJPVTR.C-Pk3uAi.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.BQmRcZsS.js → blockDiagram-GPEHLZMM.BRO1i_Nm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.CCLjHeH1.js → c4Diagram-AAUBKEIU.BU_cWu6e.js} +1 -1
- package/web/_astro/channel.Bs3Illhy.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.DL6Yuoma.js → chunk-2J33WTMH.D2VXiQ2t.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CzlyEI7l.js → chunk-4BX2VUAB.BcHT772J.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.B60EWxp1.js → chunk-55IACEB6.DhDtICmo.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.c7v_fRLo.js → chunk-727SXJPM.BoKHp7gV.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.8BxLWdDc.js → chunk-AQP2D5EJ.Chu4JRpE.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.DmKuxGFT.js → chunk-FMBD7UC4.wHdwKPlq.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.1Zwd4mqV.js → chunk-ND2GUHAM.ffN-M1JB.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BvYLu-A1.js → chunk-QZHKN3VN.FRubwg4S.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CEqRt0WU.js → classDiagram-4FO5ZUOK.c22tQsc_.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CEqRt0WU.js → classDiagram-v2-Q7XG4LA2.c22tQsc_.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.C7JiUdj2.js → cose-bilkent-S5V4N54A.B9c6fiSj.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.B6mZ-1be.js → cynefin-OW5HDTMX.DrZsrUHl.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.BuryfDy6.js → dagre-BM42HDAG.DG1U4b3y.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.dao39YQM.js → diagram-2AECGRRQ.CH15b-K3.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.lnGZe4At.js → diagram-5GNKFQAL.BH1j7twR.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.uWfhzEfT.js → diagram-KO2AKTUF.q2zCkbAj.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.ppq8-198.js → diagram-LMA3HP47.BLaaZnHV.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.CKSCqDt5.js → diagram-OG6HWLK6.BcpRdQwI.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.sFSf2yBN.js → erDiagram-TEJ5UH35.D7KBlqfM.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.BJQjKLbz.js → flowDiagram-I6XJVG4X.D_gmuRqg.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DAHKNj2N.js → ganttDiagram-6RSMTGT7.DzgWYniD.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.BlmzcW_O.js → gitGraphDiagram-PVQCEYII.576i3hm3.js} +1 -1
- package/web/_astro/index.DiC1a6lh.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DGhjaqsi.js → infoDiagram-5YYISTIA.DMCclBiz.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.gKIpgt6F.js → ishikawaDiagram-YF4QCWOH.9Kyca4SH.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.DG3tmP0R.js → journeyDiagram-JHISSGLW.chbdG-8q.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.CgHDSxkR.js → kanban-definition-UN3LZRKU.Bds54-e0.js} +1 -1
- package/web/_astro/{linear.BuGEpcuM.js → linear.D5ND9GN_.js} +1 -1
- package/web/_astro/{mermaid.core.DBVqkWBp.js → mermaid.core.D9N_PGbj.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.D-kOcji9.js → mindmap-definition-RKZ34NQL.t0xop5vp.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.BvGPwtZt.js → pieDiagram-4H26LBE5.DqgKh6hM.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.BKb_9FV6.js → quadrantDiagram-W4KKPZXB.DeuHVi2t.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.Cx0fvD8v.js → requirementDiagram-4Y6WPE33.BdYFSG4W.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.fatULi0W.js → sankeyDiagram-5OEKKPKP.k1LPPETJ.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.ChHyKqco.js → sequenceDiagram-3UESZ5HK.DdyfpzoG.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BdUpKWyj.js → stateDiagram-AJRCARHV.DSEI8il0.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.fmJxqIm5.js → stateDiagram-v2-BHNVJYJU.CoMnQRKo.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BayDn5-V.js → timeline-definition-PNZ67QCA.DM1AHNm-.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.Bm2Aggwa.js → vennDiagram-CIIHVFJN.C-cJmVtl.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DI75O-uG.js → wardleyDiagram-YWT4CUSO.D0xMzKPb.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DL90E7Q7.js → xychartDiagram-2RQKCTM6.BH5xJcEq.js} +1 -1
- package/web/index.html +2 -2
- package/config/plugin-scripts.json +0 -116
- package/config/script-placement-baseline.json +0 -33
- package/plugins/sp/hooks/careful-guard.ts +0 -74
- package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
- package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
- package/plugins/sp/lib/dogfood-report.ts +0 -278
- package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
- package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
- package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
- package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
- package/plugins/sp/lib/inline-run-app.ts +0 -68
- package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
- package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
- package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
- package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
- package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
- package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
- package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
- package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
- package/plugins/sp/lib/run-summary-core.ts +0 -225
- package/plugins/sp/lib/spur-bin.ts +0 -36
- package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
- package/plugins/sp/lib/step-profile.generated.mjs +0 -174
- package/plugins/sp/lib/transcript.ts +0 -112
- package/plugins/sp/scripts/batch-plan.mjs +0 -1096
- package/plugins/sp/scripts/batch-plan.ts +0 -145
- package/plugins/sp/scripts/batch-preflight.mjs +0 -423
- package/plugins/sp/scripts/batch-preflight.ts +0 -270
- package/plugins/sp/scripts/commit-guard.mjs +0 -226
- package/plugins/sp/scripts/commit-guard.ts +0 -248
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
- package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
- package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
- package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
- package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
- package/plugins/sp/scripts/idea-handoff.mjs +0 -32
- package/plugins/sp/scripts/idea-handoff.ts +0 -47
- package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
- package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
- package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
- package/plugins/sp/scripts/inline-run-setup.ts +0 -198
- package/plugins/sp/scripts/persist-out-check.mjs +0 -217
- package/plugins/sp/scripts/persist-out-check.ts +0 -249
- package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
- package/plugins/sp/scripts/pr-reviewing.ts +0 -952
- package/plugins/sp/scripts/quality-gate.mjs +0 -30491
- package/plugins/sp/scripts/quality-gate.ts +0 -80
- package/plugins/sp/scripts/residual-scan.mjs +0 -634
- package/plugins/sp/scripts/residual-scan.ts +0 -248
- package/plugins/sp/scripts/run-summary.mjs +0 -758
- package/plugins/sp/scripts/run-summary.ts +0 -107
- package/plugins/sp/scripts/script-root.mjs +0 -129
- package/plugins/sp/scripts/script-root.ts +0 -199
- package/plugins/sp/scripts/session-timeline.mjs +0 -359
- package/plugins/sp/scripts/session-timeline.ts +0 -190
- package/plugins/sp/scripts/task-diffstat.mjs +0 -181
- package/plugins/sp/scripts/task-diffstat.ts +0 -219
- package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
- package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
- package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
- package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
- package/plugins/sp/scripts/wrapup-steps.ts +0 -891
- package/web/_astro/channel.C7wG9RxA.js +0 -1
- package/web/_astro/index.BCY1JJHd.css +0 -1
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/** Inline run setup driver (ADR-117/1006 R3): thin glue — argv/env, app-entry resolution, the installed-CLI
|
|
3
|
-
* handshake, mode dispatch. Mode bodies live in the bundled app service (`runInlineRun*` exports); the `.mjs`
|
|
4
|
-
* twin re-enters Bun in `main()` (SQLite) and imports the bundle dynamically, so bare `node` never loads app code. */
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
// Type-only namespace import (erased before bundling; the smoke scanner exempts `import type`).
|
|
8
|
-
import type * as spurApp from '@gobing-ai/spur-app';
|
|
9
|
-
import { getEnvVar } from '../lib/env';
|
|
10
|
-
import { loadInlineApp, resolveAppEntry } from '../lib/inline-run-app';
|
|
11
|
-
import { writeCloseSummary } from './run-summary';
|
|
12
|
-
|
|
13
|
-
// EMBEDDED_SPUR_SCHEMAS: declared by the bundle twin, never exported by the barrel — optional
|
|
14
|
-
// intersection member, exactly what the previous structural cast expressed.
|
|
15
|
-
type InlineApp = typeof spurApp & { EMBEDDED_SPUR_SCHEMAS?: ReadonlyMap<string, string> };
|
|
16
|
-
|
|
17
|
-
function usage(): never {
|
|
18
|
-
const text = [
|
|
19
|
-
'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
|
|
20
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]',
|
|
21
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> --status <done|failed> --ok <true|false> [--duration-ms <n>] [--estimated] [--project-root <path>] [--spur-bin <path>]',
|
|
22
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --node-enter --run-id <id> --node <state> [--project-root <path>] [--spur-bin <path>] (1136 R4: stamps the node enter time)',
|
|
23
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --actions-file <json-file> --run-id <id> [--project-root <path>] [--spur-bin <path>] (1007 R5 batch trace emission)',
|
|
24
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--no-summary] [--project-root <path>] [--spur-bin <path>]',
|
|
25
|
-
' --project-root (1136 R1) names the tree that OWNS the run row; without it the run row is read from the cwd tree, and a miss exits 1 with RUN_NOT_FOUND',
|
|
26
|
-
' --duration-ms is optional on --action (1136 R4): omitted, the duration is measured from the calling --node-enter; supplied, it records provenance host-reported',
|
|
27
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]',
|
|
28
|
-
' terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, failed-timeout, failed-guard, cancelled, interrupted, retry-exhausted',
|
|
29
|
-
' close defaults (1051 AC1): --status done → reason done, --status paused → reason paused-operator; --status failed requires an explicit --reason',
|
|
30
|
-
' bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]',
|
|
31
|
-
].join('\n');
|
|
32
|
-
console.error(text);
|
|
33
|
-
process.exit(2);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Run ids become filenames under `.spur/run/` — single safe component only (0804 R8).
|
|
37
|
-
const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
38
|
-
|
|
39
|
-
function refuseUnsafeRunId(runId: string): never {
|
|
40
|
-
console.error(`inline-run-setup: refusing unsafe run id: ${runId}`);
|
|
41
|
-
console.error(
|
|
42
|
-
' The run id must be a single safe filename component (alphanumeric/._-, no leading dot, no path separators, interpolation or traversal; same class as the task-pipeline route-reason guard, task 0804 R8). Allocate a fresh run id (uuid or timestamp slug) and retry.',
|
|
43
|
-
);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Resolve the app entry: --spur-bin/SPUR_BIN main module if it proves a repo checkout, else the adjacent bundle. */
|
|
48
|
-
|
|
49
|
-
const SETUP_EXPORTS = ['readInstalledInventory', 'runInlineRunSetup', 'writeInlineRunOutcome'] as const;
|
|
50
|
-
|
|
51
|
-
async function main(): Promise<void> {
|
|
52
|
-
// The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
|
|
53
|
-
if (!process.versions.bun) {
|
|
54
|
-
const child = spawnSync('bun', [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
55
|
-
stdio: 'inherit',
|
|
56
|
-
});
|
|
57
|
-
if (child.error) console.error(`inline-run-setup requires Bun on PATH: ${child.error.message}`);
|
|
58
|
-
process.exit(child.status ?? 1);
|
|
59
|
-
}
|
|
60
|
-
const flags = new Map<string, string>();
|
|
61
|
-
let fingerprint = false,
|
|
62
|
-
action = false,
|
|
63
|
-
close = false,
|
|
64
|
-
decide = false,
|
|
65
|
-
persistOut = false,
|
|
66
|
-
nodeEnter = false,
|
|
67
|
-
estimated = false,
|
|
68
|
-
noSummary = false;
|
|
69
|
-
const taskFiles: string[] = [];
|
|
70
|
-
let spurBin = getEnvVar('SPUR_BIN') ?? '';
|
|
71
|
-
const argv = process.argv.slice(2);
|
|
72
|
-
for (let i = 0; i < argv.length; i++) {
|
|
73
|
-
const flag = argv[i];
|
|
74
|
-
if (flag === '--fingerprint') fingerprint = true;
|
|
75
|
-
else if (flag === '--action') action = true;
|
|
76
|
-
else if (flag === '--close') close = true;
|
|
77
|
-
else if (flag === '--decide') decide = true;
|
|
78
|
-
else if (flag === '--node-enter') nodeEnter = true;
|
|
79
|
-
else if (flag === '--persist-out') persistOut = true;
|
|
80
|
-
else if (flag === '--estimated') estimated = true;
|
|
81
|
-
else if (flag === '--no-summary') noSummary = true;
|
|
82
|
-
else if (flag === '--task-file') taskFiles.push(argv[++i] ?? '');
|
|
83
|
-
else if (flag === '--spur-bin') spurBin = argv[++i] ?? spurBin;
|
|
84
|
-
else if (flag !== undefined) flags.set(flag, argv[++i] ?? '');
|
|
85
|
-
}
|
|
86
|
-
// Repeated value flags: last occurrence wins (Map.set), matching the previous assignment chain.
|
|
87
|
-
const runId = flags.get('--run-id') ?? '';
|
|
88
|
-
const file = flags.get('--file') ?? '';
|
|
89
|
-
const featureFile = flags.get('--feature-file') ?? '';
|
|
90
|
-
const from = flags.get('--from') ?? '';
|
|
91
|
-
const optionsJson = flags.get('--options-json') ?? '';
|
|
92
|
-
const node = flags.get('--node') ?? '';
|
|
93
|
-
const kind = flags.get('--kind') ?? '';
|
|
94
|
-
const projectRoot = flags.get('--project-root') ?? '';
|
|
95
|
-
const status = flags.get('--status') ?? '';
|
|
96
|
-
const reason = flags.get('--reason') ?? '';
|
|
97
|
-
const okRaw = flags.get('--ok') ?? '';
|
|
98
|
-
const durationRaw = flags.get('--duration-ms') ?? '';
|
|
99
|
-
const actionsFile = flags.get('--actions-file') ?? '';
|
|
100
|
-
|
|
101
|
-
// `--estimated` labels a host-reported duration the driver did NOT time around the action
|
|
102
|
-
// (1070 R2). Only `--action` carries one: every other mode is refused here, before any write.
|
|
103
|
-
if (estimated && !action) usage();
|
|
104
|
-
// 1146 AC3: `--no-summary` belongs to the close step; any other mode is a usage error.
|
|
105
|
-
if (noSummary && !close) usage();
|
|
106
|
-
|
|
107
|
-
if (fingerprint) {
|
|
108
|
-
if (runId !== '' || file !== '' || taskFiles.length !== 1 || (taskFiles[0] ?? '').trim() === '') usage();
|
|
109
|
-
const { app } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunFingerprint']);
|
|
110
|
-
process.exit(await app.runInlineRunFingerprint({ taskFile: taskFiles[0] ?? '', featureFile }));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (decide) {
|
|
114
|
-
if (action || close || fingerprint || file !== '' || taskFiles.length > 0) usage();
|
|
115
|
-
if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
|
|
116
|
-
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
117
|
-
try {
|
|
118
|
-
const { app, portable } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunDecide']);
|
|
119
|
-
// Decide switch + catalog config, resolved once (1094, ADR-082), behind the skew guard (1120 R2).
|
|
120
|
-
const { app: lib } = await loadInlineApp<typeof import('../lib/inline-run.generated.mjs')>(
|
|
121
|
-
spurBin,
|
|
122
|
-
() => ({ entry: fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url)), portable }),
|
|
123
|
-
['resolveDecideDecisionMakerEnabled', 'loadSpurConfig'],
|
|
124
|
-
);
|
|
125
|
-
const loadOpts = portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined;
|
|
126
|
-
const [enabled, spurConfig] = await Promise.all([
|
|
127
|
-
lib.resolveDecideDecisionMakerEnabled(process.cwd(), loadOpts),
|
|
128
|
-
lib.loadSpurConfig(process.cwd(), loadOpts),
|
|
129
|
-
]);
|
|
130
|
-
process.exit(await app.runInlineRunDecide({ runId, node, optionsFile: optionsJson, enabled, spurConfig }));
|
|
131
|
-
} catch (error) {
|
|
132
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
-
process.stdout.write(`${JSON.stringify({ ok: false, runId, error: message })}\n`);
|
|
134
|
-
process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (persistOut) {
|
|
139
|
-
if (fingerprint || decide || action || close || runId !== '' || file !== '') usage();
|
|
140
|
-
if (from.trim() === '' || taskFiles.some((taskFile) => taskFile.trim() === '')) usage();
|
|
141
|
-
const { app } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunPersistOut']);
|
|
142
|
-
process.exit(await app.runInlineRunPersistOut({ from, taskFiles }));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (nodeEnter || actionsFile !== '' || action || close) {
|
|
146
|
-
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
147
|
-
const mode = nodeEnter ? 'node-enter' : actionsFile !== '' ? 'actions-file' : close ? 'close' : 'action';
|
|
148
|
-
const { app } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunTraceMode']);
|
|
149
|
-
const code = await app.runInlineRunTraceMode({
|
|
150
|
-
mode,
|
|
151
|
-
runId,
|
|
152
|
-
node,
|
|
153
|
-
kind,
|
|
154
|
-
status,
|
|
155
|
-
reason,
|
|
156
|
-
ok: okRaw,
|
|
157
|
-
durationMs: durationRaw,
|
|
158
|
-
actionsFile,
|
|
159
|
-
projectRoot,
|
|
160
|
-
estimated,
|
|
161
|
-
noSummary,
|
|
162
|
-
...(noSummary ? {} : { summarize: async (run) => writeCloseSummary(run) }),
|
|
163
|
-
});
|
|
164
|
-
if (code === 2) usage();
|
|
165
|
-
process.exit(code);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (runId.trim() === '' || file.trim() === '') usage();
|
|
169
|
-
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
170
|
-
const { app, portable } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, SETUP_EXPORTS);
|
|
171
|
-
let inventory: unknown;
|
|
172
|
-
try {
|
|
173
|
-
// The CLI walk lives in the app service (ADR-130 glue budget): the script passes only the
|
|
174
|
-
// paths it derives from its own module URL.
|
|
175
|
-
inventory = await app.readInstalledInventory({
|
|
176
|
-
file,
|
|
177
|
-
spurBin,
|
|
178
|
-
localCli: fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url)),
|
|
179
|
-
});
|
|
180
|
-
} catch (error) {
|
|
181
|
-
const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
|
|
182
|
-
app.writeInlineRunOutcome(runId, { ok: false, runId, error: message });
|
|
183
|
-
throw new Error(message);
|
|
184
|
-
}
|
|
185
|
-
process.exit(
|
|
186
|
-
await app.runInlineRunSetup({
|
|
187
|
-
runId,
|
|
188
|
-
file,
|
|
189
|
-
inventory,
|
|
190
|
-
...(portable ? { embeddedSchemas: app.EMBEDDED_SPUR_SCHEMAS } : {}),
|
|
191
|
-
}),
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
main().catch((e: unknown) => {
|
|
196
|
-
console.error(`inline-run-setup: FAIL — ${e instanceof Error ? e.message : String(e)}`);
|
|
197
|
-
process.exit(1);
|
|
198
|
-
});
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @bun
|
|
3
|
-
|
|
4
|
-
// plugins/sp/scripts/persist-out-check.ts
|
|
5
|
-
import { spawnSync } from "child_process";
|
|
6
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
7
|
-
import { basename, dirname, isAbsolute, join, relative } from "path";
|
|
8
|
-
var EVIDENCE_DIR = join(".spur", "memory", "evidence");
|
|
9
|
-
var RUN_DIR = join(".spur", "run");
|
|
10
|
-
var NAMED_CAP = 32;
|
|
11
|
-
var PREFIX_CAP = 64;
|
|
12
|
-
var DURABLE_FAMILY_RE = /^\d{4}-(?:verdict|feature-verification)\.json$/;
|
|
13
|
-
function wbsOfTaskFile(path) {
|
|
14
|
-
const m = /^(\d+)_/.exec(basename(path));
|
|
15
|
-
return m?.[1] ?? null;
|
|
16
|
-
}
|
|
17
|
-
function walkFiles(dir, root) {
|
|
18
|
-
if (!existsSync(dir))
|
|
19
|
-
return [];
|
|
20
|
-
const out = [];
|
|
21
|
-
const walk = (cur) => {
|
|
22
|
-
for (const e of readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
23
|
-
const p = join(cur, e.name);
|
|
24
|
-
if (e.isDirectory())
|
|
25
|
-
walk(p);
|
|
26
|
-
else if (e.isFile())
|
|
27
|
-
out.push(relative(root, p));
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
walk(dir);
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
33
|
-
function collectOwned(rels, prefixes, counts, out) {
|
|
34
|
-
for (const rel of rels) {
|
|
35
|
-
const prefix = prefixes.find((p) => basename(rel).startsWith(`${p}-`));
|
|
36
|
-
if (prefix === undefined)
|
|
37
|
-
continue;
|
|
38
|
-
const count = (counts.get(prefix) ?? 0) + 1;
|
|
39
|
-
if (count > PREFIX_CAP)
|
|
40
|
-
return false;
|
|
41
|
-
counts.set(prefix, count);
|
|
42
|
-
out.push(rel);
|
|
43
|
-
}
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
function listObligations(wtRoot, wbsList, runIds) {
|
|
47
|
-
const prefixes = [...new Set([...wbsList, ...runIds].filter(Boolean))];
|
|
48
|
-
if (prefixes.length === 0)
|
|
49
|
-
return null;
|
|
50
|
-
const evDir = join(wtRoot, EVIDENCE_DIR);
|
|
51
|
-
const runDir = join(wtRoot, RUN_DIR);
|
|
52
|
-
const evFiles = walkFiles(evDir, wtRoot);
|
|
53
|
-
const runFiles = walkFiles(runDir, wtRoot);
|
|
54
|
-
const files = [];
|
|
55
|
-
const counts = new Map;
|
|
56
|
-
if (!collectOwned(evFiles, prefixes, counts, files))
|
|
57
|
-
return null;
|
|
58
|
-
if (!collectOwned(runFiles, prefixes, counts, files))
|
|
59
|
-
return null;
|
|
60
|
-
const unowned = evFiles.length - files.filter((f) => f.startsWith(EVIDENCE_DIR)).length;
|
|
61
|
-
return { files: [...new Set(files)].sort(), unowned, planeFiles: [...evFiles, ...runFiles] };
|
|
62
|
-
}
|
|
63
|
-
function compareTrees(wtRoot, invokeRoot, files, foreignDivergent = new Set) {
|
|
64
|
-
const missing = [];
|
|
65
|
-
const divergent = [];
|
|
66
|
-
let ok = 0;
|
|
67
|
-
for (const rel of files) {
|
|
68
|
-
if (foreignDivergent.has(rel) || foreignDivergent.has(basename(rel))) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const wtPath = join(wtRoot, rel);
|
|
72
|
-
const invPath = join(invokeRoot, rel);
|
|
73
|
-
if (!existsSync(invPath)) {
|
|
74
|
-
missing.push(rel);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const same = readFileSync(wtPath).equals(readFileSync(invPath));
|
|
79
|
-
if (same)
|
|
80
|
-
ok++;
|
|
81
|
-
else
|
|
82
|
-
divergent.push(rel);
|
|
83
|
-
} catch {
|
|
84
|
-
divergent.push(rel);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return { missing, divergent, ok };
|
|
88
|
-
}
|
|
89
|
-
function persistOutCheckUsage() {
|
|
90
|
-
return "usage: persist-out-check --from <worktree> [--task-file <path>]\u2026 [--run-id <id>]\u2026 [--root <invoke-tree>] [--success-json <path>]";
|
|
91
|
-
}
|
|
92
|
-
function defaultInvokeRoot(cwd) {
|
|
93
|
-
try {
|
|
94
|
-
const run = spawnSync("git", ["-C", cwd, "rev-parse", "--git-common-dir"], { encoding: "utf8" });
|
|
95
|
-
const common = run.stdout.trim();
|
|
96
|
-
if (run.status === 0 && common)
|
|
97
|
-
return dirname(isAbsolute(common) ? common : join(cwd, common));
|
|
98
|
-
} catch {}
|
|
99
|
-
return cwd;
|
|
100
|
-
}
|
|
101
|
-
function main(argv, invokeRoot = defaultInvokeRoot(process.cwd())) {
|
|
102
|
-
let wtRoot = "";
|
|
103
|
-
let successJsonPath = "";
|
|
104
|
-
const wbsList = [];
|
|
105
|
-
const runIds = [];
|
|
106
|
-
for (let i = 0;i < argv.length; i++) {
|
|
107
|
-
const arg = argv[i];
|
|
108
|
-
if (arg === "--spur-bin") {
|
|
109
|
-
i++;
|
|
110
|
-
} else if (arg === "--from") {
|
|
111
|
-
wtRoot = argv[++i] ?? "";
|
|
112
|
-
} else if (arg === "--root") {
|
|
113
|
-
invokeRoot = argv[++i] ?? invokeRoot;
|
|
114
|
-
} else if (arg === "--task-file") {
|
|
115
|
-
const wbs = wbsOfTaskFile(argv[++i] ?? "");
|
|
116
|
-
if (wbs)
|
|
117
|
-
wbsList.push(wbs);
|
|
118
|
-
} else if (arg === "--run-id") {
|
|
119
|
-
const id = argv[++i] ?? "";
|
|
120
|
-
if (id)
|
|
121
|
-
runIds.push(id);
|
|
122
|
-
} else if (arg === "--success-json") {
|
|
123
|
-
successJsonPath = argv[++i] ?? "";
|
|
124
|
-
} else {
|
|
125
|
-
process.stderr.write(`${persistOutCheckUsage()}
|
|
126
|
-
`);
|
|
127
|
-
return 2;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (wbsList.length === 0 && runIds.length === 0) {
|
|
131
|
-
process.stderr.write(`${persistOutCheckUsage()}
|
|
132
|
-
persist-out-check: at least one --task-file or --run-id required
|
|
133
|
-
`);
|
|
134
|
-
return 2;
|
|
135
|
-
}
|
|
136
|
-
if (!wtRoot || !existsSync(wtRoot) || !statSync(wtRoot).isDirectory()) {
|
|
137
|
-
process.stderr.write(`${persistOutCheckUsage()}
|
|
138
|
-
persist-out-check: --from must be an existing directory
|
|
139
|
-
`);
|
|
140
|
-
return 2;
|
|
141
|
-
}
|
|
142
|
-
const foreignDivergent = new Set;
|
|
143
|
-
const skips = [];
|
|
144
|
-
const candidates = [
|
|
145
|
-
successJsonPath,
|
|
146
|
-
join(invokeRoot, ".spur", "run", "persist-out.json"),
|
|
147
|
-
join(wtRoot, ".spur", "run", "persist-out.json")
|
|
148
|
-
].filter(Boolean);
|
|
149
|
-
for (const cand of candidates) {
|
|
150
|
-
if (!existsSync(cand))
|
|
151
|
-
continue;
|
|
152
|
-
try {
|
|
153
|
-
const parsed = JSON.parse(readFileSync(cand, "utf8"));
|
|
154
|
-
if (Array.isArray(parsed.evidenceSkipped)) {
|
|
155
|
-
for (const item of parsed.evidenceSkipped) {
|
|
156
|
-
if (item.reason === "foreign-divergent" && typeof item.name === "string") {
|
|
157
|
-
foreignDivergent.add(item.name);
|
|
158
|
-
foreignDivergent.add(join(EVIDENCE_DIR, item.name));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (Array.isArray(parsed.skipped)) {
|
|
163
|
-
for (const item of parsed.skipped) {
|
|
164
|
-
if (typeof item.id === "string" && typeof item.reason === "string") {
|
|
165
|
-
skips.push({ id: item.id, reason: item.reason });
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
break;
|
|
170
|
-
} catch {}
|
|
171
|
-
}
|
|
172
|
-
const obligations = listObligations(wtRoot, wbsList, runIds);
|
|
173
|
-
if (obligations === null) {
|
|
174
|
-
process.stderr.write(`persist-out-check: BLOCKED \u2014 worktree evidence listing failed or exceeded caps (${PREFIX_CAP}/prefix) \u2014 inspect by hand
|
|
175
|
-
`);
|
|
176
|
-
return 1;
|
|
177
|
-
}
|
|
178
|
-
const { files, unowned, planeFiles } = obligations;
|
|
179
|
-
const unforwarded = planeFiles.filter((rel) => DURABLE_FAMILY_RE.test(basename(rel))).filter((rel) => !files.includes(rel) && !foreignDivergent.has(rel) && !foreignDivergent.has(basename(rel)));
|
|
180
|
-
const unforwardedFindings = unforwarded.map((f) => `UNFORWARDED ${f} \u2014 durable verdict/receipt outside the forwarded prefixes`);
|
|
181
|
-
const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files, foreignDivergent);
|
|
182
|
-
const findings = [
|
|
183
|
-
...missing.map((f) => `MISSING ${f}`),
|
|
184
|
-
...divergent.map((f) => `DIVERGENT ${f} \u2014 reconcile by hand (persist-out never overwrites)`),
|
|
185
|
-
...unforwardedFindings
|
|
186
|
-
];
|
|
187
|
-
for (const line of findings.slice(0, NAMED_CAP))
|
|
188
|
-
process.stdout.write(`${line}
|
|
189
|
-
`);
|
|
190
|
-
if (findings.length > NAMED_CAP)
|
|
191
|
-
process.stdout.write(`\u2026 +${findings.length - NAMED_CAP} more
|
|
192
|
-
`);
|
|
193
|
-
if (findings.length === 0) {
|
|
194
|
-
for (const skip of skips) {
|
|
195
|
-
process.stdout.write(`SKIP ${skip.id}: ${skip.reason}
|
|
196
|
-
`);
|
|
197
|
-
}
|
|
198
|
-
const abandonedMsg = skips.length > 0 ? `${skips.length} skipped` : "nothing abandoned";
|
|
199
|
-
process.stdout.write(`persist-out-check: ok \u2014 ${ok} evidence file(s) persisted, unowned: ${unowned}, ${abandonedMsg}
|
|
200
|
-
`);
|
|
201
|
-
return 0;
|
|
202
|
-
}
|
|
203
|
-
process.stderr.write(`persist-out-check: BLOCKED ${missing.length} missing, ${divergent.length} divergent` + `${unforwarded.length > 0 ? `, ${unforwarded.length} unforwarded durable verdict/receipt` : ""}` + ` \u2014 run inline-run-setup --persist-out --from <worktree> (--task-file <wbs>_<slug>.md) before worktree removal
|
|
204
|
-
`);
|
|
205
|
-
return 1;
|
|
206
|
-
}
|
|
207
|
-
{
|
|
208
|
-
process.exit(main(process.argv.slice(2)));
|
|
209
|
-
}
|
|
210
|
-
export {
|
|
211
|
-
compareTrees,
|
|
212
|
-
defaultInvokeRoot,
|
|
213
|
-
listObligations,
|
|
214
|
-
main,
|
|
215
|
-
persistOutCheckUsage,
|
|
216
|
-
wbsOfTaskFile
|
|
217
|
-
};
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* persist-out-check — WT-4 pre-removal assertion for E71 persist-out (task 1067 R1, 1148 R1).
|
|
4
|
-
* Before `git worktree remove`, verify the worktree's OWNED durable evidence would not be abandoned:
|
|
5
|
-
* every `.spur/run/` and `.spur/memory/evidence/` file starting with a forwarded `<wbs>-`/`<runId>-`
|
|
6
|
-
* prefix must exist in the invoking tree with identical bytes. Unowned evidence is counted
|
|
7
|
-
* (`unowned: N`) and does not block (1148 R1). Node builtins only; helpers are exported for tests.
|
|
8
|
-
* Exit 0 ok, 1 blocked/divergent, 2 usage error. Normal output is the finding lines themselves.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { spawnSync } from 'node:child_process';
|
|
12
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
13
|
-
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
14
|
-
|
|
15
|
-
const EVIDENCE_DIR = join('.spur', 'memory', 'evidence');
|
|
16
|
-
const RUN_DIR = join('.spur', 'run');
|
|
17
|
-
const NAMED_CAP = 32;
|
|
18
|
-
const PREFIX_CAP = 64;
|
|
19
|
-
/** The durable-verdict family persist-out itself transfers (1026/1043) — the one artifact class whose
|
|
20
|
-
* silent abandonment WT-4 exists to prevent, even when the driver forwarded no matching prefix. */
|
|
21
|
-
const DURABLE_FAMILY_RE = /^\d{4}-(?:verdict|feature-verification)\.json$/;
|
|
22
|
-
|
|
23
|
-
/** Task files own their `<wbs>-*` evidence: leading digits before `_` in the basename. */
|
|
24
|
-
export function wbsOfTaskFile(path: string): string | null {
|
|
25
|
-
const m = /^(\d+)_/.exec(basename(path));
|
|
26
|
-
return m?.[1] ?? null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ListObligationsResult {
|
|
30
|
-
files: string[];
|
|
31
|
-
unowned: number;
|
|
32
|
-
/** Every file seen in both planes (recursive) — lets callers audit outside the forwarded prefixes. */
|
|
33
|
-
planeFiles: string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Recursively list files under `dir`, relative to `root`. */
|
|
37
|
-
function walkFiles(dir: string, root: string): string[] {
|
|
38
|
-
if (!existsSync(dir)) return [];
|
|
39
|
-
const out: string[] = [];
|
|
40
|
-
const walk = (cur: string): void => {
|
|
41
|
-
for (const e of readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
42
|
-
const p = join(cur, e.name);
|
|
43
|
-
if (e.isDirectory()) walk(p);
|
|
44
|
-
else if (e.isFile()) out.push(relative(root, p));
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
walk(dir);
|
|
48
|
-
return out;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Owned evidence (prefix-matched) under one plane; null once a prefix exceeds PREFIX_CAP. */
|
|
52
|
-
function collectOwned(rels: string[], prefixes: string[], counts: Map<string, number>, out: string[]): boolean {
|
|
53
|
-
for (const rel of rels) {
|
|
54
|
-
const prefix = prefixes.find((p) => basename(rel).startsWith(`${p}-`));
|
|
55
|
-
if (prefix === undefined) continue;
|
|
56
|
-
const count = (counts.get(prefix) ?? 0) + 1;
|
|
57
|
-
if (count > PREFIX_CAP) return false;
|
|
58
|
-
counts.set(prefix, count);
|
|
59
|
-
out.push(rel);
|
|
60
|
-
}
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Evidence the worktree owns: `.spur/memory/evidence/` (recursive) and `.spur/run/` (recursive — the
|
|
66
|
-
* driver keeps run scratch under `.spur/run/<run-id>/`, which a flat scan silently missed) files whose
|
|
67
|
-
* basename starts with a forwarded `<wbs>-`/`<runId>-` prefix. Evidence matching no prefix is
|
|
68
|
-
* counted as `unowned` (1148 R1). Null when no prefix is forwarded or a prefix cap is exceeded.
|
|
69
|
-
*/
|
|
70
|
-
export function listObligations(wtRoot: string, wbsList: string[], runIds: string[]): ListObligationsResult | null {
|
|
71
|
-
const prefixes = [...new Set([...wbsList, ...runIds].filter(Boolean))];
|
|
72
|
-
if (prefixes.length === 0) return null;
|
|
73
|
-
|
|
74
|
-
const evDir = join(wtRoot, EVIDENCE_DIR);
|
|
75
|
-
const runDir = join(wtRoot, RUN_DIR);
|
|
76
|
-
const evFiles = walkFiles(evDir, wtRoot);
|
|
77
|
-
const runFiles = walkFiles(runDir, wtRoot);
|
|
78
|
-
|
|
79
|
-
const files: string[] = [];
|
|
80
|
-
const counts = new Map<string, number>();
|
|
81
|
-
if (!collectOwned(evFiles, prefixes, counts, files)) return null;
|
|
82
|
-
if (!collectOwned(runFiles, prefixes, counts, files)) return null;
|
|
83
|
-
const unowned = evFiles.length - files.filter((f) => f.startsWith(EVIDENCE_DIR)).length;
|
|
84
|
-
return { files: [...new Set(files)].sort(), unowned, planeFiles: [...evFiles, ...runFiles] };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Classify obligations against the invoking tree: missing, divergent (byte-differs), or ok. */
|
|
88
|
-
export function compareTrees(
|
|
89
|
-
wtRoot: string,
|
|
90
|
-
invokeRoot: string,
|
|
91
|
-
files: string[],
|
|
92
|
-
foreignDivergent = new Set<string>(),
|
|
93
|
-
): { missing: string[]; divergent: string[]; ok: number } {
|
|
94
|
-
const missing: string[] = [];
|
|
95
|
-
const divergent: string[] = [];
|
|
96
|
-
let ok = 0;
|
|
97
|
-
for (const rel of files) {
|
|
98
|
-
if (foreignDivergent.has(rel) || foreignDivergent.has(basename(rel))) {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const wtPath = join(wtRoot, rel);
|
|
102
|
-
const invPath = join(invokeRoot, rel);
|
|
103
|
-
if (!existsSync(invPath)) {
|
|
104
|
-
missing.push(rel);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
const same = readFileSync(wtPath).equals(readFileSync(invPath));
|
|
109
|
-
if (same) ok++;
|
|
110
|
-
else divergent.push(rel);
|
|
111
|
-
} catch {
|
|
112
|
-
divergent.push(rel);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return { missing, divergent, ok };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function persistOutCheckUsage(): string {
|
|
119
|
-
return 'usage: persist-out-check --from <worktree> [--task-file <path>]… [--run-id <id>]… [--root <invoke-tree>] [--success-json <path>]';
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* The invoking tree is the MAIN repo root — never default to cwd: WT-4 runs from the
|
|
124
|
-
* worktree, and comparing the worktree against itself is a vacuous pass (caught live in
|
|
125
|
-
* task 1067 R4). `--git-common-dir` resolves to the shared `.git`, whose parent is the
|
|
126
|
-
* main tree; inside the main tree itself this falls back to cwd, which is correct there.
|
|
127
|
-
*/
|
|
128
|
-
export function defaultInvokeRoot(cwd: string): string {
|
|
129
|
-
try {
|
|
130
|
-
const run = spawnSync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], { encoding: 'utf8' });
|
|
131
|
-
const common = run.stdout.trim();
|
|
132
|
-
if (run.status === 0 && common) return dirname(isAbsolute(common) ? common : join(cwd, common));
|
|
133
|
-
} catch {
|
|
134
|
-
// git unavailable — fall through to cwd
|
|
135
|
-
}
|
|
136
|
-
return cwd;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd())): number {
|
|
140
|
-
let wtRoot = '';
|
|
141
|
-
let successJsonPath = '';
|
|
142
|
-
const wbsList: string[] = [];
|
|
143
|
-
const runIds: string[] = [];
|
|
144
|
-
for (let i = 0; i < argv.length; i++) {
|
|
145
|
-
const arg = argv[i];
|
|
146
|
-
if (arg === '--spur-bin') {
|
|
147
|
-
i++;
|
|
148
|
-
} else if (arg === '--from') {
|
|
149
|
-
wtRoot = argv[++i] ?? '';
|
|
150
|
-
} else if (arg === '--root') {
|
|
151
|
-
invokeRoot = argv[++i] ?? invokeRoot;
|
|
152
|
-
} else if (arg === '--task-file') {
|
|
153
|
-
const wbs = wbsOfTaskFile(argv[++i] ?? '');
|
|
154
|
-
if (wbs) wbsList.push(wbs);
|
|
155
|
-
} else if (arg === '--run-id') {
|
|
156
|
-
const id = argv[++i] ?? '';
|
|
157
|
-
if (id) runIds.push(id);
|
|
158
|
-
} else if (arg === '--success-json') {
|
|
159
|
-
successJsonPath = argv[++i] ?? '';
|
|
160
|
-
} else {
|
|
161
|
-
process.stderr.write(`${persistOutCheckUsage()}\n`);
|
|
162
|
-
return 2;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (wbsList.length === 0 && runIds.length === 0) {
|
|
166
|
-
process.stderr.write(
|
|
167
|
-
`${persistOutCheckUsage()}\npersist-out-check: at least one --task-file or --run-id required\n`,
|
|
168
|
-
);
|
|
169
|
-
return 2;
|
|
170
|
-
}
|
|
171
|
-
if (!wtRoot || !existsSync(wtRoot) || !statSync(wtRoot).isDirectory()) {
|
|
172
|
-
process.stderr.write(`${persistOutCheckUsage()}\npersist-out-check: --from must be an existing directory\n`);
|
|
173
|
-
return 2;
|
|
174
|
-
}
|
|
175
|
-
const foreignDivergent = new Set<string>();
|
|
176
|
-
const skips: Array<{ id: string; reason: string }> = [];
|
|
177
|
-
const candidates = [
|
|
178
|
-
successJsonPath,
|
|
179
|
-
join(invokeRoot, '.spur', 'run', 'persist-out.json'),
|
|
180
|
-
join(wtRoot, '.spur', 'run', 'persist-out.json'),
|
|
181
|
-
].filter(Boolean);
|
|
182
|
-
for (const cand of candidates) {
|
|
183
|
-
if (!existsSync(cand)) continue;
|
|
184
|
-
try {
|
|
185
|
-
const parsed = JSON.parse(readFileSync(cand, 'utf8'));
|
|
186
|
-
if (Array.isArray(parsed.evidenceSkipped)) {
|
|
187
|
-
for (const item of parsed.evidenceSkipped) {
|
|
188
|
-
if (item.reason === 'foreign-divergent' && typeof item.name === 'string') {
|
|
189
|
-
foreignDivergent.add(item.name);
|
|
190
|
-
foreignDivergent.add(join(EVIDENCE_DIR, item.name));
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (Array.isArray(parsed.skipped)) {
|
|
195
|
-
for (const item of parsed.skipped) {
|
|
196
|
-
if (typeof item.id === 'string' && typeof item.reason === 'string') {
|
|
197
|
-
skips.push({ id: item.id, reason: item.reason });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
break;
|
|
202
|
-
} catch {}
|
|
203
|
-
}
|
|
204
|
-
const obligations = listObligations(wtRoot, wbsList, runIds);
|
|
205
|
-
if (obligations === null) {
|
|
206
|
-
process.stderr.write(
|
|
207
|
-
`persist-out-check: BLOCKED — worktree evidence listing failed or exceeded caps (${PREFIX_CAP}/prefix) — inspect by hand\n`,
|
|
208
|
-
);
|
|
209
|
-
return 1;
|
|
210
|
-
}
|
|
211
|
-
const { files, unowned, planeFiles } = obligations;
|
|
212
|
-
// Session-incident class (runall-1208-1209-bdcd): the driver forwarded only the batch run id, no
|
|
213
|
-
// prefix matched, and the check passed at 0 obligations while the worktree held its verdicts. Family
|
|
214
|
-
// files outside the prefixes (not foreign-divergent) block teardown; non-family scratch never does.
|
|
215
|
-
const unforwarded = planeFiles
|
|
216
|
-
.filter((rel) => DURABLE_FAMILY_RE.test(basename(rel)))
|
|
217
|
-
.filter((rel) => !files.includes(rel) && !foreignDivergent.has(rel) && !foreignDivergent.has(basename(rel)));
|
|
218
|
-
const unforwardedFindings = unforwarded.map(
|
|
219
|
-
(f) => `UNFORWARDED ${f} — durable verdict/receipt outside the forwarded prefixes`,
|
|
220
|
-
);
|
|
221
|
-
const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files, foreignDivergent);
|
|
222
|
-
const findings = [
|
|
223
|
-
...missing.map((f) => `MISSING ${f}`),
|
|
224
|
-
...divergent.map((f) => `DIVERGENT ${f} — reconcile by hand (persist-out never overwrites)`),
|
|
225
|
-
...unforwardedFindings,
|
|
226
|
-
];
|
|
227
|
-
for (const line of findings.slice(0, NAMED_CAP)) process.stdout.write(`${line}\n`);
|
|
228
|
-
if (findings.length > NAMED_CAP) process.stdout.write(`… +${findings.length - NAMED_CAP} more\n`);
|
|
229
|
-
if (findings.length === 0) {
|
|
230
|
-
for (const skip of skips) {
|
|
231
|
-
process.stdout.write(`SKIP ${skip.id}: ${skip.reason}\n`);
|
|
232
|
-
}
|
|
233
|
-
const abandonedMsg = skips.length > 0 ? `${skips.length} skipped` : 'nothing abandoned';
|
|
234
|
-
process.stdout.write(
|
|
235
|
-
`persist-out-check: ok — ${ok} evidence file(s) persisted, unowned: ${unowned}, ${abandonedMsg}\n`,
|
|
236
|
-
);
|
|
237
|
-
return 0;
|
|
238
|
-
}
|
|
239
|
-
process.stderr.write(
|
|
240
|
-
`persist-out-check: BLOCKED ${missing.length} missing, ${divergent.length} divergent` +
|
|
241
|
-
`${unforwarded.length > 0 ? `, ${unforwarded.length} unforwarded durable verdict/receipt` : ''}` +
|
|
242
|
-
' — run inline-run-setup --persist-out --from <worktree> (--task-file <wbs>_<slug>.md) before worktree removal\n',
|
|
243
|
-
);
|
|
244
|
-
return 1;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (import.meta.main) {
|
|
248
|
-
process.exit(main(process.argv.slice(2)));
|
|
249
|
-
}
|