@gobing-ai/spur 0.3.95 → 0.3.97

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/plugin-scripts.json +0 -54
  3. package/config/rules/boundary/sp-script-placement.yaml +19 -0
  4. package/config/rules/strict/runtime-boundaries.yaml +6 -0
  5. package/config/rules/structure/test-location.yaml +2 -0
  6. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
  7. package/config/rules/typescript/output-boundaries.yaml +15 -2
  8. package/config/script-placement-baseline.json +51 -0
  9. package/config/templates/AGENTS.md +3 -3
  10. package/config/workflows/feature-verification.yaml +1 -1
  11. package/config/workflows/history-anatomy.yaml +23 -11
  12. package/config/workflows/idea-pipeline.yaml +80 -85
  13. package/config/workflows/pr-review.yaml +18 -9
  14. package/config/workflows/task-pipeline.yaml +42 -95
  15. package/config/workflows/wrapup-pipeline.yaml +91 -38
  16. package/package.json +2 -2
  17. package/plugins/sp/README.md +14 -10
  18. package/plugins/sp/agents/super-reviewer.md +43 -18
  19. package/plugins/sp/commands/dev-fixgha.md +83 -0
  20. package/plugins/sp/commands/dev-gitmsg.md +8 -6
  21. package/plugins/sp/commands/dev-gtd.md +2 -2
  22. package/plugins/sp/commands/dev-idea.md +22 -12
  23. package/plugins/sp/commands/dev-job-dump.md +29 -0
  24. package/plugins/sp/commands/dev-job-resume.md +29 -0
  25. package/plugins/sp/commands/dev-plan.md +8 -9
  26. package/plugins/sp/commands/dev-review.md +22 -13
  27. package/plugins/sp/commands/dev-verifyall.md +1 -1
  28. package/plugins/sp/commands/spur-init.md +2 -2
  29. package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
  30. package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
  31. package/plugins/sp/lib/idea-handoff.generated.mjs +5 -4
  32. package/plugins/sp/lib/inline-run.generated.d.mts +11 -0
  33. package/plugins/sp/lib/inline-run.generated.mjs +39 -13
  34. package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
  35. package/plugins/sp/lib/quality-gate.generated.mjs +445 -0
  36. package/plugins/sp/lib/residual-scan.generated.d.mts +63 -0
  37. package/plugins/sp/lib/residual-scan.generated.mjs +226 -0
  38. package/plugins/sp/lib/spur-bin.ts +36 -0
  39. package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
  40. package/plugins/sp/lib/step-profile.generated.mjs +174 -0
  41. package/plugins/sp/plugin.json +1 -1
  42. package/plugins/sp/references/roles.md +3 -3
  43. package/plugins/sp/scripts/feature-verification-steps.mjs +14 -1
  44. package/plugins/sp/scripts/feature-verification-steps.ts +14 -1
  45. package/plugins/sp/scripts/history-anatomy-cache.mjs +20 -19
  46. package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
  47. package/plugins/sp/scripts/inline-run-setup.mjs +85 -320
  48. package/plugins/sp/scripts/inline-run-setup.ts +104 -667
  49. package/plugins/sp/scripts/quality-gate.mjs +46 -24
  50. package/plugins/sp/scripts/quality-gate.ts +13 -659
  51. package/plugins/sp/scripts/residual-scan.mjs +179 -174
  52. package/plugins/sp/scripts/residual-scan.ts +125 -517
  53. package/plugins/sp/scripts/script-root.mjs +5 -1
  54. package/plugins/sp/scripts/script-root.ts +5 -1
  55. package/plugins/sp/scripts/workflow-step-profile.mjs +25 -17
  56. package/plugins/sp/scripts/workflow-step-profile.ts +21 -315
  57. package/plugins/sp/scripts/wrapup-drift-probe.mjs +8 -2
  58. package/plugins/sp/scripts/wrapup-drift-probe.ts +3 -2
  59. package/plugins/sp/scripts/wrapup-steps.mjs +72 -33
  60. package/plugins/sp/scripts/wrapup-steps.ts +128 -37
  61. package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
  62. package/plugins/sp/skills/code-verification/SKILL.md +34 -9
  63. package/plugins/sp/skills/code-verification/references/verdict-schema.md +3 -3
  64. package/plugins/sp/skills/functional-review/SKILL.md +7 -4
  65. package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
  66. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
  67. package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
  68. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  69. package/plugins/sp/skills/source-driven-development/SKILL.md +11 -0
  70. package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
  71. package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
  72. package/plugins/sp/skills/spur-cli/references/features.md +17 -6
  73. package/plugins/sp/skills/spur-cli/references/init.md +17 -16
  74. package/plugins/sp/skills/spur-cli/references/self.md +3 -2
  75. package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
  76. package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +9 -5
  77. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +21 -6
  78. package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
  79. package/plugins/sp/skills/spur-cli/references/workflows.md +17 -14
  80. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  81. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
  82. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +14 -10
  83. package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
  84. package/plugins/sp/skills/spur-dev/references/dev-operations.md +165 -75
  85. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +7 -5
  86. package/plugins/sp/skills/spur-dev/references/execution-batch.md +84 -39
  87. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +8 -12
  88. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
  89. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +40 -21
  90. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
  91. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
  92. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +72 -15
  93. package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
  94. package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
  95. package/spur.js +4461 -2085
  96. package/web/_astro/{BoardApp.Cpxntzad.js → BoardApp.Bmv5WkJ9.js} +1 -1
  97. package/web/_astro/BoardApp.vveLOdDq.js +322 -0
  98. package/web/_astro/{TaskDetail.C5bW4WGV.js → TaskDetail.IKc3uDYV.js} +1 -1
  99. package/web/_astro/{arc.CyjRvNMY.js → arc.C63Ufg35.js} +1 -1
  100. package/web/_astro/{architectureDiagram-3BPJPVTR.B4lWRzJA.js → architectureDiagram-3BPJPVTR.CP8Hyldk.js} +1 -1
  101. package/web/_astro/{blockDiagram-GPEHLZMM.Be38USQ2.js → blockDiagram-GPEHLZMM.Bo_48MRA.js} +1 -1
  102. package/web/_astro/{c4Diagram-AAUBKEIU.DT9Fj5Qx.js → c4Diagram-AAUBKEIU.Ch61sw-O.js} +1 -1
  103. package/web/_astro/channel.BKCmJpWM.js +1 -0
  104. package/web/_astro/{chunk-2J33WTMH.SeSBWLg5.js → chunk-2J33WTMH.CHpzS7xf.js} +1 -1
  105. package/web/_astro/{chunk-4BX2VUAB.DoO4VE14.js → chunk-4BX2VUAB.DzMNNWIw.js} +1 -1
  106. package/web/_astro/{chunk-55IACEB6.DrmTFA53.js → chunk-55IACEB6.BqPrDw_Q.js} +1 -1
  107. package/web/_astro/{chunk-727SXJPM.Bi-TUb_V.js → chunk-727SXJPM.ZzBHhNw-.js} +1 -1
  108. package/web/_astro/{chunk-AQP2D5EJ.B-xp8Uhi.js → chunk-AQP2D5EJ.1_O_LLkx.js} +1 -1
  109. package/web/_astro/{chunk-FMBD7UC4.DGWrDnUU.js → chunk-FMBD7UC4.GS4UHg7_.js} +1 -1
  110. package/web/_astro/{chunk-ND2GUHAM._BagvPDy.js → chunk-ND2GUHAM.BnqotN4g.js} +1 -1
  111. package/web/_astro/{chunk-QZHKN3VN.VOboYWQ0.js → chunk-QZHKN3VN.Ghuh8zoJ.js} +1 -1
  112. package/web/_astro/{classDiagram-4FO5ZUOK.D_apfHS7.js → classDiagram-4FO5ZUOK.BW9vC3zg.js} +1 -1
  113. package/web/_astro/{classDiagram-v2-Q7XG4LA2.D_apfHS7.js → classDiagram-v2-Q7XG4LA2.BW9vC3zg.js} +1 -1
  114. package/web/_astro/{cose-bilkent-S5V4N54A.DNLU_L-x.js → cose-bilkent-S5V4N54A.D6jNC0ti.js} +1 -1
  115. package/web/_astro/{cynefin-OW5HDTMX.D8borhV-.js → cynefin-OW5HDTMX.BOGoCs4L.js} +1 -1
  116. package/web/_astro/{dagre-BM42HDAG.Uc0lvjcV.js → dagre-BM42HDAG.8iU5VcWi.js} +1 -1
  117. package/web/_astro/{diagram-2AECGRRQ.DDIxtDBo.js → diagram-2AECGRRQ.CVt88HwH.js} +1 -1
  118. package/web/_astro/{diagram-5GNKFQAL.DVoDnD3H.js → diagram-5GNKFQAL.C9HxUUMl.js} +1 -1
  119. package/web/_astro/{diagram-KO2AKTUF.CRubTJ-0.js → diagram-KO2AKTUF.4ywkQN3t.js} +1 -1
  120. package/web/_astro/{diagram-LMA3HP47.m9SeYcXQ.js → diagram-LMA3HP47.C48A6gpz.js} +1 -1
  121. package/web/_astro/{diagram-OG6HWLK6.C6s2ZjV5.js → diagram-OG6HWLK6.DxFieQW4.js} +1 -1
  122. package/web/_astro/{erDiagram-TEJ5UH35.nSzr5KTj.js → erDiagram-TEJ5UH35.CtjtQqYs.js} +1 -1
  123. package/web/_astro/{flowDiagram-I6XJVG4X.DrZcys16.js → flowDiagram-I6XJVG4X.C5zAiahU.js} +1 -1
  124. package/web/_astro/{ganttDiagram-6RSMTGT7.Do4M1b-K.js → ganttDiagram-6RSMTGT7.D_L6zsny.js} +1 -1
  125. package/web/_astro/{gitGraphDiagram-PVQCEYII.BWc9oJ4l.js → gitGraphDiagram-PVQCEYII.Ignnrk3V.js} +1 -1
  126. package/web/_astro/index.CbNz17Sx.css +1 -0
  127. package/web/_astro/{infoDiagram-5YYISTIA.BIdOctFk.js → infoDiagram-5YYISTIA.DLv8-P5u.js} +1 -1
  128. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CV5SuG2t.js → ishikawaDiagram-YF4QCWOH.p0OTpwFO.js} +1 -1
  129. package/web/_astro/{journeyDiagram-JHISSGLW.CxE-rEJ-.js → journeyDiagram-JHISSGLW.CIaBt-DD.js} +1 -1
  130. package/web/_astro/{kanban-definition-UN3LZRKU.BPRdKWs_.js → kanban-definition-UN3LZRKU.DGySIntq.js} +1 -1
  131. package/web/_astro/{linear.zRsuuDTE.js → linear.BATV4RXu.js} +1 -1
  132. package/web/_astro/{mermaid.core.jAJTcMKc.js → mermaid.core.DzkwM3VX.js} +4 -4
  133. package/web/_astro/{mindmap-definition-RKZ34NQL.BffJxfCr.js → mindmap-definition-RKZ34NQL.3piBRRzA.js} +1 -1
  134. package/web/_astro/{pieDiagram-4H26LBE5.CacwWaE3.js → pieDiagram-4H26LBE5.DUNueLub.js} +1 -1
  135. package/web/_astro/{quadrantDiagram-W4KKPZXB.hXELD06-.js → quadrantDiagram-W4KKPZXB.9bihVbrt.js} +1 -1
  136. package/web/_astro/{requirementDiagram-4Y6WPE33.DdEbPcqT.js → requirementDiagram-4Y6WPE33.CkjsC-YT.js} +1 -1
  137. package/web/_astro/{sankeyDiagram-5OEKKPKP.DgaDocKv.js → sankeyDiagram-5OEKKPKP.Dbq4vaoQ.js} +1 -1
  138. package/web/_astro/{sequenceDiagram-3UESZ5HK.B0KScnAu.js → sequenceDiagram-3UESZ5HK.BivrH99P.js} +1 -1
  139. package/web/_astro/{stateDiagram-AJRCARHV.CV9M_WNc.js → stateDiagram-AJRCARHV.CqLQ1Vzz.js} +1 -1
  140. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BOAo74Et.js → stateDiagram-v2-BHNVJYJU.DvvoLD3b.js} +1 -1
  141. package/web/_astro/{timeline-definition-PNZ67QCA.CqqepUMe.js → timeline-definition-PNZ67QCA.B-KR78gv.js} +1 -1
  142. package/web/_astro/{vennDiagram-CIIHVFJN.DDqe4dOG.js → vennDiagram-CIIHVFJN.Ck0Eieb5.js} +1 -1
  143. package/web/_astro/{wardleyDiagram-YWT4CUSO.B3R9OPdO.js → wardleyDiagram-YWT4CUSO.CRfdd_do.js} +1 -1
  144. package/web/_astro/{xychartDiagram-2RQKCTM6.B3SvgXpQ.js → xychartDiagram-2RQKCTM6.Ck_Jxk9C.js} +1 -1
  145. package/web/index.html +2 -2
  146. package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
  147. package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
  148. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -301
  149. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -481
  150. package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
  151. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
  152. package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
  153. package/plugins/sp/scripts/record-feature-sync.ts +0 -84
  154. package/plugins/sp/scripts/script-contract-check.ts +0 -506
  155. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  156. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
  157. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -189
  158. package/plugins/sp/scripts/task-size-precheck.ts +0 -212
  159. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  160. package/plugins/sp/scripts/validate-commands.ts +0 -689
  161. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -890
  162. package/plugins/sp/scripts/verify-answer-lint.ts +0 -549
  163. package/web/_astro/BoardApp.BxJuwD7I.js +0 -191
  164. package/web/_astro/channel.CI6N_tCg.js +0 -1
  165. package/web/_astro/index.CENnIEqT.css +0 -1
@@ -1,180 +1,56 @@
1
1
  #!/usr/bin/env bun
2
- /**
3
- * inline-run-setup — authoritative inline full-pipeline run identity (task 0804 R1).
4
- *
5
- * Unlike the subprocess path (`spur workflow run`), the interactive inline driver
6
- * allocated a run id but never persisted an authoritative `runs` row, so bound
7
- * `run.artifact` registration (0785 R3) correctly refused every inline record. This
8
- * script is the thin delegate the driver now runs at Run setup: it loads the source
9
- * app service when the SPUR_BIN chain identifies a checkout, otherwise its generated bundle
10
- * (`createOrAttachInlineRun` / `openInlineRunProjectDb` from packages/app), and lets it
11
- * resolve the SAME project/registered/shared definition the engine would launch, compute the
12
- * canonical definition digest with the exported hash machinery, and create-or-attach the
13
- * run row through the existing engine persistence adapter.
14
- *
15
- * The script itself contains NO direct SQL, NO second hasher and NO persistence policy —
16
- * every rule lives in packages/app (0804 D1). On a bundle-only install, the existing
17
- * workflow-show projection selects the CLI's definition; the bundled app revalidates its
18
- * schema and digest before recording the selected layer. It never creates an unbound run.
19
- *
20
- * The setup outcome is written into the two-file run record (task 0927 R1) — machine state
21
- * at `.spur/run/<run-id>.state.json` (atomic replace) plus the `.spur/run/<run-id>.md`
22
- * run-start header — so the driver can seed the inline var overlay (`__runId`,
23
- * `__definitionDigest`) that proof capture and bound registration verify against. The
24
- * pre-0927 `<run-id>-inline-setup.json` sidecar is no longer written. Exit 0 = authoritative
25
- * identity ready (created or idempotently attached); exit 1 = fail closed, the driver must stop.
26
- *
27
- * Standard script (ADR-065): the generated Node-runnable twin re-enters Bun for the
28
- * existing SQLite runtime. Bun on PATH is required; a monorepo checkout is not.
29
- *
30
- * Usage:
31
- * bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
32
- * bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]
33
- * bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> \
34
- * bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]
35
- * --status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]
36
- * bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]
37
- * bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]
38
- *
39
- * The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
40
- * creates nothing (task 0862 R5).
41
- *
42
- * The `--action` / `--close` modes are the inline driver's ADR-117 emission boundary (task
43
- * 0868): `--action` records one completed action boundary as an `action_runs` row through the
44
- * shared `WorkflowActionTraceWriter`, `--close` marks the run row terminal through the same
45
- * writer. `--action` is best-effort: a persistence failure is appended to
46
- * `.spur/run/<run-id>.md` and the script still exits 0 with `{"ok":false}` on stdout, so
47
- * observation never wedges the run. `--close` is NOT best-effort: the run-row closure is
48
- * bookkeeping, so a missing run row or a persistence failure exits 1 with a named error.
49
- * The `--decide` mode (task 0941 R5) executes the non-pausing decide action through the SAME
50
- * app runner the engine registers (`runDecideForInlineRun`), writing the same resultFile row
51
- * and recording the same `action_runs` trace row (`kind=decide`, best-effort). A degraded
52
- * decision is still `ok: true` — the run continues — so exit 0 covers every model-level
53
- * outcome; only an invalid options schema exits 1 (fail closed).
54
- *
55
- * The `--persist-out --from <worktree-path>` mode (task 0975 R1) is the WT-4a step a
56
- * `--worktree` run executes BEFORE teardown: it copies the worktree's run provenance — the
57
- * `runs`/`action_runs`/`phase_runs`/`transition_runs`/`workflow_states` rows and the two-file
58
- * run records — into THIS tree's project DB and `.spur/run/` (cwd = the invoking tree).
59
- * Repeatable `--task-file <path>` (0984 R2) forwards each merged task file so the app service
60
- * also copies/verifies the `.spur/run/<file>` evidence that file cites; citation selection,
61
- * safe-path validation and conflict behavior live entirely in the app service. Idempotent;
62
- * collisions are reported as `skipped[{id,reason}]`, never overwrites. Exit 0 = persisted
63
- * (possibly with skips); exit 1 = fail closed — an unreadable/unsafe/over-cap/unresolved
64
- * citation or a divergent cited file fails the pass too — the driver routes to WT-5 and
65
- * retains the worktree; usage errors exit 2.
66
- *
67
- * Exit 2 is reserved for usage errors.
68
- *
69
- * Env: SPUR_BIN
70
- */
71
-
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. */
72
5
  import { spawnSync } from 'node:child_process';
73
- import {
74
- appendFileSync,
75
- closeSync,
76
- existsSync,
77
- mkdirSync,
78
- openSync,
79
- readFileSync,
80
- renameSync,
81
- unlinkSync,
82
- writeFileSync,
83
- writeSync,
84
- } from 'node:fs';
6
+ import { existsSync } from 'node:fs';
85
7
  import { dirname, join, resolve } from 'node:path';
86
8
  import { fileURLToPath } from 'node:url';
9
+ // Type-only namespace import (erased before bundling; the smoke scanner exempts `import type`).
10
+ import type * as spurApp from '@gobing-ai/spur-app';
87
11
  import { getEnvVar } from '../lib/env';
88
12
 
89
- /** Setup outcome projected into the run-record state `.spur/run/<run-id>.state.json` (0927 R1). */
90
- interface SetupOutcome {
91
- readonly ok: boolean;
92
- readonly runId: string;
93
- readonly attached?: boolean;
94
- readonly definitionDigest?: string;
95
- readonly workflowName?: string;
96
- readonly workflowVersion?: string | null;
97
- readonly resolvedPath?: string;
98
- readonly layer?: string;
99
- readonly workdir?: string;
100
- readonly status?: string;
101
- readonly error?: string;
102
- }
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> };
103
16
 
104
17
  function usage(): never {
105
18
  console.error(
106
- 'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
107
- );
108
- console.error(
109
- ' bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]',
110
- );
111
- console.error(
112
- ' bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> ' +
113
- '--status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]',
114
- );
115
- console.error(
116
- ' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
117
- );
118
- console.error(
119
- ' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]',
120
- );
121
- console.error(
122
- ' terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, ' +
123
- 'failed-timeout, failed-guard, cancelled, interrupted, retry-exhausted',
124
- );
125
- console.error(
126
- ' bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]',
19
+ [
20
+ 'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
21
+ ' bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]',
22
+ ' 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> [--spur-bin <path>]',
23
+ ' bun plugins/sp/scripts/inline-run-setup.ts --actions-file <json-file> --run-id <id> [--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>] [--spur-bin <path>]',
25
+ ' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]',
26
+ ' terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, failed-timeout, failed-guard, cancelled, interrupted, retry-exhausted',
27
+ ' bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]',
28
+ ].join('\n'),
127
29
  );
128
30
  process.exit(2);
129
31
  }
130
32
 
131
- /**
132
- * The run id becomes filenames under `.spur/run/` (`<run-id>.state.json` + `<run-id>.md`),
133
- * so it must be a single safe filename component before anything is written — the same guard class the
134
- * task-pipeline.yaml route-reason action applies to `$__runId` (task 0804 R8). The allowlist
135
- * refuses path separators, dot traversal (leading `.`), unresolved interpolation (`$`/`{`/`}`) and
136
- * every other shell/unspecified metachar; valid UUID/timestamp-slug ids pass.
137
- */
33
+ // Run ids become filenames under `.spur/run/` — single safe component only (0804 R8).
138
34
  const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
139
35
 
140
36
  function refuseUnsafeRunId(runId: string): never {
141
- // Refuse BEFORE any outcome write: an unsafe id must never reach
142
- // `.spur/run/<run-id>.state.json` / `.md` (no traversal, no unintended file).
143
37
  console.error(`inline-run-setup: refusing unsafe run id: ${runId}`);
144
38
  console.error(
145
- ' The run id must be a single safe filename component (alphanumeric/._-, no leading dot, ' +
146
- 'no path separators, interpolation or traversal; same class as the task-pipeline ' +
147
- 'route-reason guard, task 0804 R8). Allocate a fresh run id (uuid or timestamp slug) and retry.',
39
+ ' 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.',
148
40
  );
149
41
  process.exit(1);
150
42
  }
151
43
 
152
- /**
153
- * Resolve the spur repo checkout the same way the other prechecks resolve the CLI
154
- * (--spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`), then derive the repo
155
- * root from the resolved main module. `bun <repo>/apps/cli/src/index.ts` → repo root is
156
- * three levels up. A bundle-only install (`spur` on PATH, a bundled `spur.js`, or a spur
157
- * binary without the app workspace) uses the adjacent generated application bundle.
158
- */
44
+ /** Resolve the app entry: --spur-bin/SPUR_BIN main module if it proves a repo checkout, else the adjacent bundle. */
159
45
  function resolveAppEntry(spurBin: string): { entry: string; portable: boolean } {
160
- let candidates: string[] = [];
161
- if (spurBin !== '') {
162
- candidates = [spurBin];
163
- } else {
164
- // scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw
165
- // pathname breaks on %-encoded paths, e.g. spaces in the checkout directory).
166
- candidates = [fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url))];
167
- }
46
+ const fallback = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
47
+ const candidates = spurBin !== '' ? [spurBin] : [fallback];
168
48
  for (const candidate of candidates) {
169
49
  const tokens = candidate.split(/\s+/).filter(Boolean);
170
- // The main module is the last path-like token (tolerates `bun <path>` /
171
- // `bun run <path>` lead tokens). Only a TypeScript source entry proves a repo
172
- // checkout; a bundled `spur.js` or a bare `spur` binary does not.
50
+ // Main module = last path-like token; only a .ts source entry proves a repo checkout.
173
51
  const mainModule = [...tokens].reverse().find((t) => t.endsWith('.ts'));
174
52
  if (mainModule === undefined || !existsSync(mainModule)) continue;
175
- // <repo>/apps/cli/src/index.ts → repo root; then require the app package source.
176
- const srcDir = dirname(mainModule);
177
- const repoRoot = resolve(srcDir, '..', '..', '..');
53
+ const repoRoot = resolve(dirname(mainModule), '..', '..', '..');
178
54
  const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
179
55
  if (existsSync(appEntry)) return { entry: appEntry, portable: false };
180
56
  }
@@ -208,144 +84,12 @@ async function readInstalledInventory(file: string, spurBin: string): Promise<un
208
84
  }
209
85
  const value: unknown = JSON.parse(result.stdout);
210
86
  // Honor the existing optional JSON envelope without inventing another projection format.
211
- if (value && typeof value === 'object' && 'ok' in value && 'data' in value && value.ok === true) {
212
- return value.data;
213
- }
87
+ if (value && typeof value === 'object' && 'ok' in value && 'data' in value && value.ok === true) return value.data;
214
88
  return value;
215
89
  }
216
90
 
217
- /**
218
- * Project the setup outcome into the two-file run record (task 0927 R1): the machine state
219
- * merges into `.spur/run/<run-id>.state.json` (atomic same-directory temp + rename, the
220
- * 0925 R1 pattern) and `.spur/run/<run-id>.md` receives its run-start header exactly once.
221
- * A re-setup of the same run id rewrites the state from the current outcome but keeps the
222
- * prior `startedAt` instead of resurrecting setup time. Identity/provenance fields only —
223
- * never prompt bodies — so the record carries no secret-bearing content by construction
224
- * (0927 AC2). The retired `<run-id>-inline-setup.json` sidecar is not written (0927 R4);
225
- * its readers were migrated to the pair state in the same change.
226
- */
227
- function writeOutcome(runId: string, outcome: SetupOutcome): void {
228
- const runDir = join(process.cwd(), '.spur', 'run');
229
- if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
230
- const statePath = join(runDir, `${runId}.state.json`);
231
- const markdownPath = join(runDir, `${runId}.md`);
232
- let prior: Record<string, unknown> = {};
233
- try {
234
- const parsed: unknown = JSON.parse(readFileSync(statePath, 'utf8'));
235
- if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
236
- prior = parsed as Record<string, unknown>;
237
- }
238
- } catch {
239
- // No prior state (new run) or unreadable file — the outcome alone defines the state.
240
- }
241
- const at = new Date().toISOString();
242
- const state = {
243
- schemaVersion: 1 as const,
244
- runId,
245
- ...(outcome.workflowName !== undefined ? { workflowName: outcome.workflowName } : {}),
246
- ...(outcome.status !== undefined ? { status: outcome.status } : {}),
247
- startedAt: typeof prior.startedAt === 'string' ? prior.startedAt : at,
248
- updatedAt: at,
249
- ...(outcome.attached !== undefined ? { attached: outcome.attached } : {}),
250
- ...(outcome.definitionDigest !== undefined ? { definitionDigest: outcome.definitionDigest } : {}),
251
- ...(outcome.workflowVersion !== undefined ? { workflowVersion: outcome.workflowVersion } : {}),
252
- ...(outcome.resolvedPath !== undefined ? { resolvedPath: outcome.resolvedPath } : {}),
253
- ...(outcome.layer !== undefined ? { layer: outcome.layer } : {}),
254
- ...(outcome.workdir !== undefined ? { workdir: outcome.workdir } : {}),
255
- // 0948 R7: project the setup outcome itself, so the retired `-inline-setup.json`
256
- // sidecar's `ok` is still readable from the pair, and a SUCCESSFUL re-setup
257
- // explicitly drops any prior `error` — a stale failure message must not outlive
258
- // the failure it described (the state object is rebuilt, never merged with `prior`).
259
- ok: outcome.ok,
260
- ...(outcome.ok === false && outcome.error !== undefined ? { error: outcome.error } : {}),
261
- };
262
- const temp = `${statePath}.tmp`;
263
- try {
264
- writeFileSync(temp, `${JSON.stringify(state, null, 4)}\n`);
265
- renameSync(temp, statePath);
266
- } catch {
267
- // Best-effort: a failing record write must not wedge setup reporting — and never
268
- // leaves `.tmp` residue behind (0926 R1).
269
- try {
270
- unlinkSync(temp);
271
- } catch {
272
- // Nothing to clean (temp was never created).
273
- }
274
- }
275
- // 0948 R7: create the header with `wx` (O_EXCL) instead of `existsSync` → `appendFileSync`.
276
- // The check-then-append pair could append a SECOND header when two setups raced, and it
277
- // kept "header present" as a separate fact from the identity write. One exclusive create
278
- // makes exactly-one-header atomic; EEXIST just means it is already there.
279
- let headerFd: number | undefined;
280
- try {
281
- headerFd = openSync(markdownPath, 'wx');
282
- writeSync(
283
- headerFd,
284
- `# spur inline run ${runId} — ${outcome.workflowName ?? 'unknown workflow'} — setup ${at}\n`,
285
- );
286
- } catch {
287
- // EEXIST (already written) or an unwritable path — the human header is not the setup
288
- // identity, so this stays best-effort exactly like the state write above.
289
- } finally {
290
- if (headerFd !== undefined) closeSync(headerFd);
291
- }
292
- }
293
-
294
- /**
295
- * `--fingerprint` mode (task 0862 R5): print the engine's proof-input digest for a task file
296
- * (plus optional feature file) instead of creating a run, so the inline driver can capture the
297
- * fresh digest bound `run.artifact` registration checks against.
298
- *
299
- * The digest itself comes from the exported app functions — never reimplemented here — and the
300
- * options mirror `ProofFingerprintActionRunner` (cwd, spec contents, fileSystem; no `--expect`).
301
- * Returns the process exit code: 0 printed, 1 read/resolve failure.
302
- */
303
- async function printFingerprint(taskFile: string, featureFile: string, spurBin: string): Promise<number> {
304
- const { entry } = resolveAppEntry(spurBin);
305
- const app = (await import(entry)) as {
306
- computeProofInputFingerprint: (options: Record<string, unknown>) => Promise<string>;
307
- readProofInputContents: (
308
- fileSystem: unknown,
309
- workdir: string,
310
- options: { taskFile?: unknown; featureFile?: unknown },
311
- ) => Promise<{ ok: true; taskContent?: string; featureContent?: string } | { ok: false; error: string }>;
312
- };
313
-
314
- const workdir = process.cwd();
315
- // `undefined` fs takes readProofInputContents' node-filesystem default — the same default its
316
- // sibling createGitAlternateTree applies, and the same Node FS the CLI's runner injects.
317
- const inputs = await app.readProofInputContents(undefined, workdir, {
318
- taskFile,
319
- ...(featureFile.trim() !== '' ? { featureFile } : {}),
320
- });
321
- if (!inputs.ok) {
322
- console.error(`inline-run-setup: FAIL — ${inputs.error}`);
323
- return 1;
324
- }
325
-
326
- const digest = await app.computeProofInputFingerprint({
327
- cwd: workdir,
328
- ...(inputs.taskContent !== undefined ? { taskContent: inputs.taskContent } : {}),
329
- ...(inputs.featureContent !== undefined ? { featureContent: inputs.featureContent } : {}),
330
- });
331
- process.stdout.write(`${digest}\n`);
332
- return 0;
333
- }
334
-
335
- /** Terminal statuses the inline driver may declare when closing its run row. */
336
- const CLOSE_STATUSES: ReadonlySet<string> = new Set<'done' | 'failed' | 'paused'>(['done', 'failed', 'paused']);
337
-
338
- /** Narrow an argv `--status` to the close vocabulary (`CLOSE_STATUSES`). */
339
- function isCloseStatus(status: string): status is 'done' | 'failed' | 'paused' {
340
- return CLOSE_STATUSES.has(status);
341
- }
342
-
343
- /**
344
- * 0937 R2: closed terminal-reason vocabulary the driver may declare on `--close`.
345
- * COPIED from packages/app/src/workflow/terminal-reason.ts — the plugin standalone
346
- * contract forbids a value import of app code; the parity test asserts the copy
347
- * equals the app export.
348
- */
91
+ // 0937 R2 closed terminal-reason vocabulary for `--close`. COPIED from
92
+ // packages/app/src/workflow/terminal-reason.ts (no value import); parity test asserts equality.
349
93
  const TERMINAL_REASONS = new Set([
350
94
  'done',
351
95
  'paused-operator',
@@ -358,278 +102,6 @@ const TERMINAL_REASONS = new Set([
358
102
  'retry-exhausted',
359
103
  ]);
360
104
 
361
- /** Finalize statuses — a finish emission is terminal, so only done|failed are valid (0868 #4). */
362
- const ACTION_STATUSES: ReadonlySet<string> = new Set<'done' | 'failed'>(['done', 'failed']);
363
-
364
- /** Narrow an argv `--status` to the finalize vocabulary (`ACTION_STATUSES`). */
365
- function isActionStatus(status: string): status is 'done' | 'failed' {
366
- return ACTION_STATUSES.has(status);
367
- }
368
-
369
- /** Input for the ADR-117 emission modes (`--action` / `--close`). */
370
- import type { WorkflowActionTraceWriter } from '@gobing-ai/spur-app';
371
-
372
- interface TraceModeInput {
373
- readonly runId: string;
374
- readonly close: boolean;
375
- readonly node: string;
376
- readonly kind: string;
377
- /** Declared terminal reason (0937 R2) — validated against TERMINAL_REASONS before this point. */
378
- readonly reason?: string;
379
- /**
380
- * Trace status: the finalize vocabulary plus the close-only `paused` (`CLOSE_STATUSES`).
381
- * Both are assignable to the engine's `WorkflowStatus`; the plugin cannot import that
382
- * type (standalone contract), so the literal union stands in for it.
383
- */
384
- readonly status: 'done' | 'failed' | 'paused';
385
- readonly ok: boolean;
386
- readonly durationMs: number;
387
- readonly spurBin: string;
388
- }
389
-
390
- /**
391
- * The run-record file for appended driver lines (task 0927 R1): `.spur/run/<run-id>.md`
392
- * for pair-based runs. A legacy run that predates the pair keeps appending to its declared
393
- * `.spur/run/<run-id>.log` — the same precedence as `readWorkflowRunRecord` (the pair wins,
394
- * legacy stays readable in place).
395
- */
396
- function runRecordLogPath(runDir: string, runId: string): string {
397
- const markdownPath = join(runDir, `${runId}.md`);
398
- const legacyLogPath = join(runDir, `${runId}.log`);
399
- if (existsSync(legacyLogPath) && !existsSync(markdownPath)) return legacyLogPath;
400
- return markdownPath;
401
- }
402
-
403
- /**
404
- * Append one emission-failure line to the run record markdown — `.spur/run/<run-id>.md`,
405
- * the run log the inline driver already owns. Best-effort and synchronous (the process may
406
- * exit immediately after), and never throws: an unwritable log must not wedge the run
407
- * (ADR-117 R3).
408
- */
409
- function appendRunLogLine(runId: string, detail: string): void {
410
- try {
411
- const runDir = join(process.cwd(), '.spur', 'run');
412
- if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
413
- const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, '_');
414
- const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
415
- appendFileSync(runRecordLogPath(runDir, safeRunId), `[${stamp}] ${detail}\n`);
416
- } catch {
417
- // Best-effort (R3): the run continues even when the failure cannot be recorded.
418
- }
419
- }
420
-
421
- /**
422
- * Emit one trace write through the SHARED `WorkflowActionTraceWriter` (task 0868 R5/R7).
423
- * `--action` is best-effort: an emission failure is recorded to the run log and reported
424
- * on stdout as `{"ok":false}`, and the script exits 0 so the run still reaches its declared
425
- * terminal state (R3/R12). `--close` is bookkeeping, so a missing run row or a persistence
426
- * failure fails loudly with exit 1 (review findings #1/#4).
427
- */
428
- async function runTraceMode(input: TraceModeInput): Promise<number> {
429
- const operation = input.close ? 'run.close' : 'action.finish';
430
- const fail = (error: string): number => {
431
- appendRunLogLine(
432
- input.runId,
433
- `trace-emission-failed operation=${operation} run=${input.runId}` +
434
- `${input.node === '' ? '' : ` node=${input.node}`}${input.kind === '' ? '' : ` kind=${input.kind}`}: ${error}`,
435
- );
436
- process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}\n`);
437
- // The action boundary is best-effort (exit 0); the run-row closure fails loudly (exit 1).
438
- return input.close ? 1 : 0;
439
- };
440
-
441
- // Compile-time link (0868 finding #5): the writer half is typed by the real packages/app
442
- // export (type-only import, erased at runtime) so a signature drift breaks THIS file's
443
- // typecheck instead of hiding behind the hand-declared cast.
444
- let projectDb: { adapter: unknown; close: () => void } | undefined;
445
- try {
446
- const { entry } = resolveAppEntry(input.spurBin);
447
- const app = (await import(entry)) as {
448
- openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
449
- createWorkflowActionTraceWriter: (
450
- db: unknown,
451
- recordFailure?: (failure: unknown) => void,
452
- ) => WorkflowActionTraceWriter;
453
- };
454
-
455
- projectDb = await app.openInlineRunProjectDb(process.cwd());
456
- const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure: unknown) => {
457
- const detail = failure as { operation?: string; error?: string };
458
- appendRunLogLine(
459
- input.runId,
460
- `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? 'unknown error'}`,
461
- );
462
- });
463
- const result = (
464
- input.close
465
- ? await writer.closeRun(input.runId, input.status, undefined, input.reason)
466
- : await writer.recordAction({
467
- runId: input.runId,
468
- node: input.node,
469
- kind: input.kind,
470
- status: input.status,
471
- ok: input.ok,
472
- durationMs: input.durationMs,
473
- })
474
- ) as Record<string, unknown>;
475
- if (result.ok !== true && result.failure !== undefined) {
476
- // One stdout shape for emission failures (0868 finding #1): flatten the guard's
477
- // nested failure object to the same `{ok, runId, error}` the direct paths emit.
478
- const failure = result.failure as { error?: string };
479
- return fail(failure.error ?? 'unknown trace emission failure');
480
- }
481
- if (input.close && input.status === 'done' && result.actionRows === 0) {
482
- // A run finalized `done` with ZERO recorded action rows is a bookkeeping defect
483
- // (task 0975 R2): the row is already terminal — closeRun ran above — but the
484
- // driver must surface this instead of reporting a clean close, and must never
485
- // backfill rows. Exit 1 with the named code; the run record carries the finding.
486
- const error = `run ${input.runId} closed done with zero action_runs rows`;
487
- appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${error}`);
488
- process.stdout.write(
489
- `${JSON.stringify({ ok: false, runId: input.runId, error, code: 'NO_ACTION_ROWS', actionRows: 0 })}\n`,
490
- );
491
- return 1;
492
- }
493
- process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}\n`);
494
- return 0;
495
- } catch (error) {
496
- if (input.close && (error as { name?: string }).name === 'RunRowNotFoundError') {
497
- // The run row must exist before --close can mark it terminal (R6); a missing row
498
- // is a loud correctness failure, not a best-effort emission failure (finding #4).
499
- const message = error instanceof Error ? error.message : String(error);
500
- appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
501
- process.stdout.write(
502
- `${JSON.stringify({ ok: false, runId: input.runId, error: message, code: 'RUN_NOT_FOUND' })}\n`,
503
- );
504
- return 1;
505
- }
506
- return fail(error instanceof Error ? error.message : String(error));
507
- } finally {
508
- projectDb?.close();
509
- }
510
- }
511
-
512
- /**
513
- * `--decide` mode (task 0941 R5): execute the non-pausing decide action through the same app
514
- * function the engine registers (`runDecideForInlineRun` → `DecideActionRunner`), then record
515
- * the `action_runs` trace row through the shared writer. The decide-enabled switch
516
- * (`workflow.decideDecisionMaker`) is resolved HERE at the driver boundary (ADR-082) through
517
- * the bridged facade derivation and passed into the app runner as an explicit parameter —
518
- * app services never load Spur config. Degraded outcomes (`disabled`,
519
- * `no-backend`, `error`, `timeout`, `low-confidence`) are normal — the value is the declared
520
- * default and the run continues — so they print `ok: true` and exit 0. Only an invalid options
521
- * schema, an unreadable options file, or a failed config load fails closed with exit 1.
522
- */
523
- async function runDecideMode(input: {
524
- runId: string;
525
- node: string;
526
- optionsFile: string;
527
- spurBin: string;
528
- }): Promise<number> {
529
- type DecideOutcome = {
530
- ok: boolean;
531
- error?: string;
532
- value?: string;
533
- degraded?: boolean;
534
- reason?: string;
535
- source?: 'model' | 'default';
536
- backend?: string | null;
537
- confidence?: number | null;
538
- resultFile?: string;
539
- durationMs?: number;
540
- };
541
- const decideFailed = (error: string): number => {
542
- process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}\n`);
543
- return 1;
544
- };
545
- let outcome: DecideOutcome;
546
- try {
547
- const { entry, portable } = resolveAppEntry(input.spurBin);
548
- const app = (await import(entry)) as {
549
- runDecideForInlineRun: (input2: {
550
- workdir: string;
551
- optionsFile: string;
552
- enabled: boolean;
553
- }) => Promise<DecideOutcome>;
554
- };
555
- // Config switch at the driver boundary (ADR-082 / task 0941 gate fix): the flag comes
556
- // from the bridged facade derivation in the generated lib — the same committed bundle
557
- // the inventory flow imports — never a @gobing-ai/* value import. Embedded schemas
558
- // ride along only in the portable layout, mirroring the setup mode's load posture.
559
- const lib = (await import(
560
- fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url))
561
- )) as typeof import('../lib/inline-run.generated.mjs');
562
- const enabled = await lib.resolveDecideDecisionMakerEnabled(
563
- process.cwd(),
564
- portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined,
565
- );
566
- outcome = await app.runDecideForInlineRun({
567
- workdir: process.cwd(),
568
- optionsFile: input.optionsFile,
569
- enabled,
570
- });
571
- } catch (error) {
572
- return decideFailed(error instanceof Error ? error.message : String(error));
573
- }
574
- if (!outcome.ok) return decideFailed(outcome.error ?? 'decide failed without an error message');
575
- process.stdout.write(`${JSON.stringify({ runId: input.runId, node: input.node, ...outcome, ok: true })}\n`);
576
- // 0976 R2: the run log names the decision's provenance, so a declared-default fallback is
577
- // never read as a model decision. Best-effort through the same run-log appender.
578
- appendRunLogLine(
579
- input.runId,
580
- `decide node=${input.node} value=${outcome.value ?? ''} source=${outcome.source ?? 'default'} reason=${outcome.reason ?? ''}`,
581
- );
582
- // Trace row is best-effort, exactly like --action: an emission failure never wedges the run.
583
- return await runTraceMode({
584
- runId: input.runId,
585
- close: false,
586
- node: input.node,
587
- kind: 'decide',
588
- status: 'done',
589
- ok: true,
590
- durationMs: outcome.durationMs ?? 0,
591
- spurBin: input.spurBin,
592
- });
593
- }
594
-
595
- /**
596
- * `--persist-out --from <worktree-path>` (task 0975 R1; `--task-file` per 0984 R2): resolve
597
- * the app through the shared chain and copy the worktree's inline-run provenance into THIS
598
- * tree. No persistence policy lives here — the app's `persistWorktreeRuns` (→ domain
599
- * `transferRunTables`) owns the DB transfer, the citation selection/copy rules, the
600
- * idempotence and the conflict skips; this delegate only forwards the merged task file
601
- * paths. Success prints `{ok:true,persisted:<n>,skipped:[…]}` and exits 0; any failure
602
- * prints `{ok:false,error}` and exits 1 (the driver routes to WT-5 and retains the
603
- * worktree).
604
- */
605
- async function runPersistOutMode(input: {
606
- from: string;
607
- taskFiles: readonly string[];
608
- spurBin: string;
609
- }): Promise<number> {
610
- try {
611
- const { entry } = resolveAppEntry(input.spurBin);
612
- const app = (await import(entry)) as {
613
- persistWorktreeRuns: (i: {
614
- fromWorkdir: string;
615
- toWorkdir: string;
616
- taskFiles?: readonly string[];
617
- }) => Promise<{ ok: true; persisted: number; skipped: ReadonlyArray<{ id: string; reason: string }> }>;
618
- };
619
- const result = await app.persistWorktreeRuns({
620
- fromWorkdir: input.from,
621
- toWorkdir: process.cwd(),
622
- ...(input.taskFiles.length > 0 ? { taskFiles: input.taskFiles } : {}),
623
- });
624
- process.stdout.write(`${JSON.stringify({ ok: true, persisted: result.persisted, skipped: result.skipped })}\n`);
625
- return 0;
626
- } catch (error) {
627
- const message = error instanceof Error ? error.message : String(error);
628
- process.stdout.write(`${JSON.stringify({ ok: false, error: message })}\n`);
629
- return 1;
630
- }
631
- }
632
-
633
105
  async function main(): Promise<void> {
634
106
  // The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
635
107
  if (!process.versions.bun) {
@@ -639,92 +111,95 @@ async function main(): Promise<void> {
639
111
  if (child.error) console.error(`inline-run-setup requires Bun on PATH: ${child.error.message}`);
640
112
  process.exit(child.status ?? 1);
641
113
  }
642
- let runId = '';
643
- let file = '';
644
- let fingerprint = false;
114
+ const flags = new Map<string, string>();
115
+ let fingerprint = false,
116
+ action = false,
117
+ close = false,
118
+ decide = false,
119
+ persistOut = false;
645
120
  const taskFiles: string[] = [];
646
- let featureFile = '';
647
- let action = false;
648
- let close = false;
649
- let decide = false;
650
- let persistOut = false;
651
- let from = '';
652
- let optionsJson = '';
653
- let node = '';
654
- let kind = '';
655
- let status = '';
656
- let reason = '';
657
- let okRaw = '';
658
- let durationRaw = '';
659
121
  let spurBin = getEnvVar('SPUR_BIN') ?? '';
660
122
  const argv = process.argv.slice(2);
661
123
  for (let i = 0; i < argv.length; i++) {
662
- if (argv[i] === '--run-id') runId = argv[++i] ?? '';
663
- else if (argv[i] === '--file') file = argv[++i] ?? '';
664
- else if (argv[i] === '--fingerprint') fingerprint = true;
665
- else if (argv[i] === '--task-file') taskFiles.push(argv[++i] ?? '');
666
- else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
667
- else if (argv[i] === '--action') action = true;
668
- else if (argv[i] === '--close') close = true;
669
- else if (argv[i] === '--decide') decide = true;
670
- else if (argv[i] === '--persist-out') persistOut = true;
671
- else if (argv[i] === '--from') from = argv[++i] ?? '';
672
- else if (argv[i] === '--options-json') optionsJson = argv[++i] ?? '';
673
- else if (argv[i] === '--node') node = argv[++i] ?? '';
674
- else if (argv[i] === '--kind') kind = argv[++i] ?? '';
675
- else if (argv[i] === '--status') status = argv[++i] ?? '';
676
- else if (argv[i] === '--reason') reason = argv[++i] ?? '';
677
- else if (argv[i] === '--ok') okRaw = argv[++i] ?? '';
678
- else if (argv[i] === '--duration-ms') durationRaw = argv[++i] ?? '';
679
- else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
680
- }
124
+ const flag = argv[i];
125
+ if (flag === '--fingerprint') fingerprint = true;
126
+ else if (flag === '--action') action = true;
127
+ else if (flag === '--close') close = true;
128
+ else if (flag === '--decide') decide = true;
129
+ else if (flag === '--persist-out') persistOut = true;
130
+ else if (flag === '--task-file') taskFiles.push(argv[++i] ?? '');
131
+ else if (flag === '--spur-bin') spurBin = argv[++i] ?? spurBin;
132
+ else if (flag !== undefined) flags.set(flag, argv[++i] ?? '');
133
+ }
134
+ // Repeated value flags: last occurrence wins (Map.set), matching the previous assignment chain.
135
+ const runId = flags.get('--run-id') ?? '';
136
+ const file = flags.get('--file') ?? '';
137
+ const featureFile = flags.get('--feature-file') ?? '';
138
+ const from = flags.get('--from') ?? '';
139
+ const optionsJson = flags.get('--options-json') ?? '';
140
+ const node = flags.get('--node') ?? '';
141
+ const kind = flags.get('--kind') ?? '';
142
+ const status = flags.get('--status') ?? '';
143
+ const reason = flags.get('--reason') ?? '';
144
+ const okRaw = flags.get('--ok') ?? '';
145
+ const durationRaw = flags.get('--duration-ms') ?? '';
146
+ const actionsFile = flags.get('--actions-file') ?? '';
681
147
 
682
- // Two mutually exclusive modes share this entry point: create/attach a run (run-id + file), or
683
- // print the proof digest for the inline driver (task 0862 R5). Mixing them is a usage error.
684
- // `--task-file` belongs to the fingerprint (exactly one) and persist-out (zero or more) modes.
685
148
  if (fingerprint) {
686
149
  if (runId !== '' || file !== '' || taskFiles.length !== 1 || (taskFiles[0] ?? '').trim() === '') usage();
687
- process.exit(await printFingerprint(taskFiles[0] ?? '', featureFile, spurBin));
150
+ const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
151
+ process.exit(await app.runInlineRunFingerprint({ taskFile: taskFiles[0] ?? '', featureFile }));
152
+ }
153
+
154
+ if (actionsFile !== '') {
155
+ if (action || close || fingerprint || decide || persistOut || file !== '' || taskFiles.length > 0) usage();
156
+ if (runId.trim() === '' || status !== '' || node !== '' || kind !== '') usage();
157
+ if (okRaw !== '' || durationRaw !== '') usage();
158
+ if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
159
+ const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
160
+ process.exit(await app.runInlineRunTraceBatch({ runId, actionsFile }));
688
161
  }
689
162
 
690
- // ADR-117 emission modes (task 0868): the inline driver reports one completed action
691
- // boundary, or closes its run row at the declared terminal state. Both share the run-id
692
- // filename guard, the app-entry resolution chain and the best-effort failure contract.
693
- // Non-pausing decide action (0941 R5): --decide is mutually exclusive with the run-setup,
694
- // fingerprint, and trace modes; it requires the run id (filename-guarded), the state id
695
- // for the trace row, and the options JSON file.
696
163
  if (decide) {
697
164
  if (action || close || fingerprint || file !== '' || taskFiles.length > 0) usage();
698
165
  if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
699
166
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
700
- process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
167
+ try {
168
+ const { entry, portable } = resolveAppEntry(spurBin);
169
+ const app = (await import(entry)) as InlineApp;
170
+ const bundlePath = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
171
+ const lib = (await import(bundlePath)) as typeof import('../lib/inline-run.generated.mjs');
172
+ // Decide-enabled switch resolved at the driver boundary (ADR-082), passed as a param.
173
+ const enabled = await lib.resolveDecideDecisionMakerEnabled(
174
+ process.cwd(),
175
+ portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined,
176
+ );
177
+ process.exit(await app.runInlineRunDecide({ runId, node, optionsFile: optionsJson, enabled }));
178
+ } catch (error) {
179
+ const message = error instanceof Error ? error.message : String(error);
180
+ process.stdout.write(`${JSON.stringify({ ok: false, runId, error: message })}\n`);
181
+ process.exit(1);
182
+ }
701
183
  }
702
184
 
703
- // Worktree provenance persist-out (task 0975 R1): mutually exclusive with every other
704
- // mode; requires the source worktree path and accepts zero or more merged task files
705
- // (0984 R2) whose cited `.spur/run/` evidence the app service copies/verifies. Runs
706
- // with cwd = the invoking tree.
707
185
  if (persistOut) {
708
186
  if (fingerprint || decide || action || close || runId !== '' || file !== '') usage();
709
187
  if (from.trim() === '' || taskFiles.some((taskFile) => taskFile.trim() === '')) usage();
710
- process.exit(await runPersistOutMode({ from, taskFiles, spurBin }));
188
+ const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
189
+ process.exit(await app.runInlineRunPersistOut({ from, taskFiles }));
711
190
  }
712
191
 
713
192
  if (action || close) {
714
193
  if (action && close) usage();
715
194
  if (runId.trim() === '' || status.trim() === '') usage();
716
195
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
196
+ const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
717
197
  if (close) {
718
- if (!isCloseStatus(status)) usage();
719
- // 0937 R2: a failed close requires a declared reason, and any declared reason
720
- // must be a closed-enum value — both fail loudly BEFORE any write happens.
721
- if (reason.trim() === '') {
722
- if (status === 'failed') usage();
723
- } else if (!TERMINAL_REASONS.has(reason)) {
724
- usage();
725
- }
198
+ if (!app.isInlineRunCloseStatus(status)) usage();
199
+ // 0937 R2: failed close needs a declared closed-enum reason — before any write.
200
+ if (reason.trim() === '' ? status === 'failed' : !TERMINAL_REASONS.has(reason)) usage();
726
201
  process.exit(
727
- await runTraceMode({
202
+ await app.runInlineRunTrace({
728
203
  runId,
729
204
  close: true,
730
205
  node: '',
@@ -732,79 +207,41 @@ async function main(): Promise<void> {
732
207
  status,
733
208
  ok: true,
734
209
  durationMs: 0,
735
- spurBin,
736
210
  ...(reason.trim() === '' ? {} : { reason }),
737
211
  }),
738
212
  );
739
213
  }
740
214
  if (node.trim() === '' || kind.trim() === '') usage();
741
- if (!isActionStatus(status)) usage();
742
- // `--ok` and `--duration-ms` are required and exact for the action mode
743
- // (review finding #2): a miscased `--ok True` or an omitted `--duration-ms`
744
- // must be a loud usage error, never a silently-defaulted `ok=0` /
745
- // `duration_ms=0` row.
215
+ if (!app.isInlineRunActionStatus(status)) usage();
216
+ // `--ok` and `--duration-ms` are required and exact (0868 #2): no silent defaults.
746
217
  if (okRaw !== 'true' && okRaw !== 'false') usage();
747
- const ok = okRaw === 'true';
748
218
  const durationMs = Number(durationRaw);
749
219
  if (durationRaw.trim() === '' || !Number.isFinite(durationMs) || durationMs < 0) usage();
750
- process.exit(await runTraceMode({ runId, close: false, node, kind, status, ok, durationMs, spurBin }));
220
+ process.exit(
221
+ await app.runInlineRunTrace({ runId, close: false, node, kind, status, ok: okRaw === 'true', durationMs }),
222
+ );
751
223
  }
752
224
 
753
225
  if (runId.trim() === '' || file.trim() === '') usage();
754
226
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
755
-
756
227
  const { entry, portable } = resolveAppEntry(spurBin);
757
-
758
- // Dynamic import by absolute path: the app source graph resolves its own workspace
759
- // dependencies from the repo checkout, never from this plugin script's location.
760
- const app = (await import(entry)) as {
761
- createOrAttachInlineRun: (input: {
762
- workdir: string;
763
- getDb: () => Promise<unknown>;
764
- file: string;
765
- runId: string;
766
- inventory?: unknown;
767
- embeddedSchemas?: ReadonlyMap<string, string>;
768
- }) => Promise<SetupOutcome & { ok: boolean }>;
769
- openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
770
- EMBEDDED_SPUR_SCHEMAS?: ReadonlyMap<string, string>;
771
- };
772
-
773
- const workdir = process.cwd();
228
+ const app = (await import(entry)) as InlineApp;
774
229
  let inventory: unknown;
775
230
  try {
776
231
  inventory = await readInstalledInventory(file, spurBin);
777
232
  } catch (error) {
778
233
  const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
779
- writeOutcome(runId, { ok: false, runId, error: message });
234
+ app.writeInlineRunOutcome(runId, { ok: false, runId, error: message });
780
235
  throw new Error(message);
781
236
  }
782
- const projectDb = await app.openInlineRunProjectDb(workdir);
783
- let exitCode = 0;
784
- try {
785
- const result = await app.createOrAttachInlineRun({
786
- workdir,
787
- getDb: async () => projectDb.adapter,
788
- file,
237
+ process.exit(
238
+ await app.runInlineRunSetup({
789
239
  runId,
240
+ file,
790
241
  inventory,
791
242
  ...(portable ? { embeddedSchemas: app.EMBEDDED_SPUR_SCHEMAS } : {}),
792
- });
793
- writeOutcome(runId, result);
794
- if (!result.ok) {
795
- console.error(`inline-run-setup: FAIL for run ${runId}`);
796
- console.error(` ${result.error}`);
797
- exitCode = 1;
798
- } else {
799
- console.error(
800
- `inline-run-setup: ${result.attached ? 'attached' : 'created'} run ${runId} ` +
801
- `(${result.workflowName}, layer ${result.layer}, digest ${result.definitionDigest}, status ${result.status})`,
802
- );
803
- }
804
- } finally {
805
- projectDb.close();
806
- }
807
- process.exit(exitCode);
243
+ }),
244
+ );
808
245
  }
809
246
 
810
247
  main().catch((e: unknown) => {