@gobing-ai/spur 0.3.94 → 0.3.96

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 (198) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +42 -0
  3. package/board/index.d.ts +38 -0
  4. package/config/plugin-scripts.json +3 -52
  5. package/config/rules/boundary/sp-script-placement.yaml +19 -0
  6. package/config/rules/strict/runtime-boundaries.yaml +5 -0
  7. package/config/rules/structure/test-location.yaml +2 -0
  8. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
  9. package/config/rules/typescript/output-boundaries.yaml +15 -2
  10. package/config/script-placement-baseline.json +51 -0
  11. package/config/workflows/feature-verification.yaml +7 -4
  12. package/config/workflows/history-anatomy.yaml +23 -11
  13. package/config/workflows/idea-pipeline.yaml +87 -86
  14. package/config/workflows/pr-review.yaml +18 -9
  15. package/config/workflows/task-pipeline.yaml +84 -109
  16. package/config/workflows/wrapup-pipeline.yaml +173 -46
  17. package/package.json +15 -11
  18. package/plugins/sp/README.md +8 -10
  19. package/plugins/sp/agents/super-reviewer.md +43 -18
  20. package/plugins/sp/commands/dev-fixgha.md +83 -0
  21. package/plugins/sp/commands/dev-gitmsg.md +8 -6
  22. package/plugins/sp/commands/dev-gtd.md +2 -2
  23. package/plugins/sp/commands/dev-idea.md +22 -12
  24. package/plugins/sp/commands/dev-plan.md +8 -9
  25. package/plugins/sp/commands/dev-review.md +22 -13
  26. package/plugins/sp/commands/dev-verify.md +7 -4
  27. package/plugins/sp/commands/dev-verifyall.md +5 -4
  28. package/plugins/sp/commands/dev-wrap.md +1 -1
  29. package/plugins/sp/commands/spur-init.md +2 -2
  30. package/plugins/sp/hooks/context-post-tool.ts +2 -2
  31. package/plugins/sp/hooks/context-session-start.ts +2 -1
  32. package/plugins/sp/hooks/context-session-stop.ts +22 -14
  33. package/plugins/sp/hooks/pi/guard-extension.ts +109 -150
  34. package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
  35. package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
  36. package/plugins/sp/lib/idea-handoff.generated.mjs +6 -5
  37. package/plugins/sp/lib/inline-run.generated.d.mts +13 -2
  38. package/plugins/sp/lib/inline-run.generated.mjs +33 -16
  39. package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
  40. package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
  41. package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
  42. package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
  43. package/plugins/sp/lib/spur-bin.ts +36 -0
  44. package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
  45. package/plugins/sp/lib/step-profile.generated.mjs +174 -0
  46. package/plugins/sp/plugin.json +1 -1
  47. package/plugins/sp/references/roles.md +1 -1
  48. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
  49. package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
  50. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
  51. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
  52. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
  53. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
  54. package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
  55. package/plugins/sp/scripts/history-anatomy-cache.mjs +23 -22
  56. package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
  57. package/plugins/sp/scripts/inline-run-setup.mjs +90 -315
  58. package/plugins/sp/scripts/inline-run-setup.ts +109 -639
  59. package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
  60. package/plugins/sp/scripts/pr-reviewing.ts +1 -0
  61. package/plugins/sp/scripts/quality-gate.mjs +38 -21
  62. package/plugins/sp/scripts/quality-gate.ts +20 -660
  63. package/plugins/sp/scripts/residual-scan.mjs +138 -159
  64. package/plugins/sp/scripts/residual-scan.ts +102 -499
  65. package/plugins/sp/scripts/script-root.mjs +129 -0
  66. package/plugins/sp/scripts/script-root.ts +199 -0
  67. package/plugins/sp/scripts/workflow-step-profile.mjs +27 -16
  68. package/plugins/sp/scripts/workflow-step-profile.ts +22 -314
  69. package/plugins/sp/scripts/wrapup-drift-probe.mjs +12 -3
  70. package/plugins/sp/scripts/wrapup-drift-probe.ts +6 -3
  71. package/plugins/sp/scripts/wrapup-steps.mjs +42 -40
  72. package/plugins/sp/scripts/wrapup-steps.ts +64 -46
  73. package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
  74. package/plugins/sp/skills/code-verification/SKILL.md +46 -16
  75. package/plugins/sp/skills/code-verification/references/verdict-schema.md +13 -2
  76. package/plugins/sp/skills/functional-review/SKILL.md +8 -5
  77. package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
  78. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
  79. package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
  80. package/plugins/sp/skills/next-router/references/routing-table.md +2 -2
  81. package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
  82. package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
  83. package/plugins/sp/skills/spur-cli/references/features.md +17 -6
  84. package/plugins/sp/skills/spur-cli/references/init.md +17 -16
  85. package/plugins/sp/skills/spur-cli/references/self.md +3 -2
  86. package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
  87. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +28 -5
  88. package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
  89. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
  90. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
  91. package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
  92. package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
  93. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
  94. package/plugins/sp/skills/spur-dev/references/execution-batch.md +107 -44
  95. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
  96. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
  97. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
  98. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
  99. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
  100. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +35 -6
  101. package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
  102. package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
  103. package/plugins/sp/tsconfig.json +8 -0
  104. package/spur.js +6638 -4775
  105. package/web/_astro/BoardApp.BIjMatT1.js +1 -0
  106. package/web/_astro/{BoardApp.FTEs3-N8.js → BoardApp.GvjIe9Z6.js} +150 -153
  107. package/web/_astro/TaskDetail.BZM3EAFx.js +1 -0
  108. package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
  109. package/web/_astro/{arc.uuAf51IT.js → arc.HkRiZnoI.js} +1 -1
  110. package/web/_astro/architectureDiagram-3BPJPVTR.BDdK-tgZ.js +36 -0
  111. package/web/_astro/{blockDiagram-GPEHLZMM.D1mGCq3p.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
  112. package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
  113. package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
  114. package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
  115. package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
  116. package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
  117. package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
  118. package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
  119. package/web/_astro/{c4Diagram-AAUBKEIU.CMsolcde.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
  120. package/web/_astro/channel.DlhX2MEt.js +1 -0
  121. package/web/_astro/{chunk-2J33WTMH.CrGA3fik.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
  122. package/web/_astro/{chunk-4BX2VUAB.DsLVVla0.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
  123. package/web/_astro/{chunk-55IACEB6.Dxc59Tfi.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
  124. package/web/_astro/{chunk-727SXJPM.CaEVE2Wy.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
  125. package/web/_astro/{chunk-AQP2D5EJ.BiJ4HXeI.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
  126. package/web/_astro/{chunk-FMBD7UC4.Mxf1fru5.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
  127. package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
  128. package/web/_astro/{chunk-ND2GUHAM.pyOWQixH.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
  129. package/web/_astro/{chunk-QZHKN3VN.BmEsg4vr.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
  130. package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
  131. package/web/_astro/{classDiagram-4FO5ZUOK.BHhhMFTO.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
  132. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BHhhMFTO.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
  133. package/web/_astro/client.BhluLQqe.js +1 -0
  134. package/web/_astro/client.DBpSRLnx.js +9 -0
  135. package/web/_astro/cose-bilkent-S5V4N54A.BraxQ2Nt.js +1 -0
  136. package/web/_astro/{cynefin-OW5HDTMX.C2j1_lKL.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
  137. package/web/_astro/{dagre-BM42HDAG.hZ2NCTdT.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
  138. package/web/_astro/{diagram-2AECGRRQ.Bfw5_EzK.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
  139. package/web/_astro/{diagram-5GNKFQAL.Bt1V_Tmk.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
  140. package/web/_astro/{diagram-KO2AKTUF.DEk-YFwp.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
  141. package/web/_astro/{diagram-LMA3HP47.CDjndtQm.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
  142. package/web/_astro/{diagram-OG6HWLK6.cFnUHScG.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
  143. package/web/_astro/{erDiagram-TEJ5UH35.DlhYp7NV.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
  144. package/web/_astro/{flowDiagram-I6XJVG4X.DNTpsxfx.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
  145. package/web/_astro/ganttDiagram-6RSMTGT7.Dmdxgd-M.js +292 -0
  146. package/web/_astro/{gitGraphDiagram-PVQCEYII.DkEqNI0P.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
  147. package/web/_astro/index.5dn-Zm-2.js +1 -0
  148. package/web/_astro/index.C-IqPpW2.js +1 -0
  149. package/web/_astro/index.Cyc_GA4f.js +1 -0
  150. package/web/_astro/index.EoZlzLK-.css +1 -0
  151. package/web/_astro/index.tZfqwSut.js +1 -0
  152. package/web/_astro/{infoDiagram-5YYISTIA.CrjioCTG.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
  153. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BcK0CN8n.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
  154. package/web/_astro/{journeyDiagram-JHISSGLW.p0CbBDX1.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
  155. package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
  156. package/web/_astro/{kanban-definition-UN3LZRKU.puHIFt6J.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
  157. package/web/_astro/{linear.Bb-3a1d7.js → linear.BnzMBgo_.js} +1 -1
  158. package/web/_astro/{mermaid.core.CJDgXOJs.js → mermaid.core.DSqeFu2Y.js} +4 -4
  159. package/web/_astro/{mindmap-definition-RKZ34NQL.BoAZEI9t.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
  160. package/web/_astro/{pieDiagram-4H26LBE5.CJNSdqY7.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
  161. package/web/_astro/{quadrantDiagram-W4KKPZXB.DtTy7_0Z.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
  162. package/web/_astro/{requirementDiagram-4Y6WPE33.BNYV98Xg.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
  163. package/web/_astro/{sankeyDiagram-5OEKKPKP.DRq9DGHp.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
  164. package/web/_astro/{sequenceDiagram-3UESZ5HK.B7KdBFCm.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
  165. package/web/_astro/{stateDiagram-AJRCARHV.CD62ZJ2H.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
  166. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CeVk1TAj.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
  167. package/web/_astro/{timeline-definition-PNZ67QCA.C_ZPA5tT.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
  168. package/web/_astro/{vennDiagram-CIIHVFJN.CrThO_FQ.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
  169. package/web/_astro/{wardleyDiagram-YWT4CUSO.DSZCA5nl.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
  170. package/web/_astro/{xychartDiagram-2RQKCTM6.KI7baTuk.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
  171. package/web/board-runtime.json +69 -0
  172. package/web/index.html +2 -2
  173. package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
  174. package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
  175. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -298
  176. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -479
  177. package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
  178. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
  179. package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
  180. package/plugins/sp/scripts/record-feature-sync.ts +0 -84
  181. package/plugins/sp/scripts/script-contract-check.ts +0 -377
  182. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  183. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
  184. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -187
  185. package/plugins/sp/scripts/task-size-precheck.ts +0 -210
  186. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  187. package/plugins/sp/scripts/validate-commands.ts +0 -689
  188. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
  189. package/plugins/sp/scripts/verify-answer-lint.ts +0 -547
  190. package/web/_astro/BoardApp.C02hAHPO.js +0 -1
  191. package/web/_astro/TaskDetail.C-GdsS-t.js +0 -1
  192. package/web/_astro/architectureDiagram-3BPJPVTR.CGe629A1.js +0 -36
  193. package/web/_astro/channel.fsgl7o5j.js +0 -1
  194. package/web/_astro/client.yhYJvxCU.js +0 -9
  195. package/web/_astro/cose-bilkent-S5V4N54A.2fH4YOlp.js +0 -1
  196. package/web/_astro/ganttDiagram-6RSMTGT7.DC_p36PI.js +0 -292
  197. package/web/_astro/index.De90oHcH.js +0 -1
  198. package/web/_astro/index.Hjbr15fG.css +0 -1
@@ -1,176 +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> [--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
- * Idempotent; collisions are reported as `skipped[{id,reason}]`, never overwrites. Exit 0 =
60
- * persisted (possibly with skips); exit 1 = fail closed — the driver routes to WT-5 and
61
- * retains the worktree; usage errors exit 2.
62
- *
63
- * Exit 2 is reserved for usage errors.
64
- *
65
- * Env: SPUR_BIN
66
- */
67
-
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. */
68
5
  import { spawnSync } from 'node:child_process';
69
- import {
70
- appendFileSync,
71
- closeSync,
72
- existsSync,
73
- mkdirSync,
74
- openSync,
75
- readFileSync,
76
- renameSync,
77
- unlinkSync,
78
- writeFileSync,
79
- writeSync,
80
- } from 'node:fs';
6
+ import { existsSync } from 'node:fs';
81
7
  import { dirname, join, resolve } from 'node:path';
82
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';
83
11
  import { getEnvVar } from '../lib/env';
84
12
 
85
- /** Setup outcome projected into the run-record state `.spur/run/<run-id>.state.json` (0927 R1). */
86
- interface SetupOutcome {
87
- readonly ok: boolean;
88
- readonly runId: string;
89
- readonly attached?: boolean;
90
- readonly definitionDigest?: string;
91
- readonly workflowName?: string;
92
- readonly workflowVersion?: string | null;
93
- readonly resolvedPath?: string;
94
- readonly layer?: string;
95
- readonly workdir?: string;
96
- readonly status?: string;
97
- readonly error?: string;
98
- }
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> };
99
16
 
100
17
  function usage(): never {
101
18
  console.error(
102
- 'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
103
- );
104
- console.error(
105
- ' bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]',
106
- );
107
- console.error(
108
- ' bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> ' +
109
- '--status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]',
110
- );
111
- console.error(
112
- ' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
113
- );
114
- console.error(
115
- ' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--spur-bin <path>]',
116
- );
117
- console.error(
118
- ' terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, ' +
119
- 'failed-timeout, failed-guard, cancelled, interrupted, retry-exhausted',
120
- );
121
- console.error(
122
- ' 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'),
123
29
  );
124
30
  process.exit(2);
125
31
  }
126
32
 
127
- /**
128
- * The run id becomes filenames under `.spur/run/` (`<run-id>.state.json` + `<run-id>.md`),
129
- * so it must be a single safe filename component before anything is written — the same guard class the
130
- * task-pipeline.yaml route-reason action applies to `$__runId` (task 0804 R8). The allowlist
131
- * refuses path separators, dot traversal (leading `.`), unresolved interpolation (`$`/`{`/`}`) and
132
- * every other shell/unspecified metachar; valid UUID/timestamp-slug ids pass.
133
- */
33
+ // Run ids become filenames under `.spur/run/` — single safe component only (0804 R8).
134
34
  const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
135
35
 
136
36
  function refuseUnsafeRunId(runId: string): never {
137
- // Refuse BEFORE any outcome write: an unsafe id must never reach
138
- // `.spur/run/<run-id>.state.json` / `.md` (no traversal, no unintended file).
139
37
  console.error(`inline-run-setup: refusing unsafe run id: ${runId}`);
140
38
  console.error(
141
- ' The run id must be a single safe filename component (alphanumeric/._-, no leading dot, ' +
142
- 'no path separators, interpolation or traversal; same class as the task-pipeline ' +
143
- '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.',
144
40
  );
145
41
  process.exit(1);
146
42
  }
147
43
 
148
- /**
149
- * Resolve the spur repo checkout the same way the other prechecks resolve the CLI
150
- * (--spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`), then derive the repo
151
- * root from the resolved main module. `bun <repo>/apps/cli/src/index.ts` → repo root is
152
- * three levels up. A bundle-only install (`spur` on PATH, a bundled `spur.js`, or a spur
153
- * binary without the app workspace) uses the adjacent generated application bundle.
154
- */
44
+ /** Resolve the app entry: --spur-bin/SPUR_BIN main module if it proves a repo checkout, else the adjacent bundle. */
155
45
  function resolveAppEntry(spurBin: string): { entry: string; portable: boolean } {
156
- let candidates: string[] = [];
157
- if (spurBin !== '') {
158
- candidates = [spurBin];
159
- } else {
160
- // scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw
161
- // pathname breaks on %-encoded paths, e.g. spaces in the checkout directory).
162
- candidates = [fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url))];
163
- }
46
+ const fallback = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
47
+ const candidates = spurBin !== '' ? [spurBin] : [fallback];
164
48
  for (const candidate of candidates) {
165
49
  const tokens = candidate.split(/\s+/).filter(Boolean);
166
- // The main module is the last path-like token (tolerates `bun <path>` /
167
- // `bun run <path>` lead tokens). Only a TypeScript source entry proves a repo
168
- // 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.
169
51
  const mainModule = [...tokens].reverse().find((t) => t.endsWith('.ts'));
170
52
  if (mainModule === undefined || !existsSync(mainModule)) continue;
171
- // <repo>/apps/cli/src/index.ts → repo root; then require the app package source.
172
- const srcDir = dirname(mainModule);
173
- const repoRoot = resolve(srcDir, '..', '..', '..');
53
+ const repoRoot = resolve(dirname(mainModule), '..', '..', '..');
174
54
  const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
175
55
  if (existsSync(appEntry)) return { entry: appEntry, portable: false };
176
56
  }
@@ -204,139 +84,12 @@ async function readInstalledInventory(file: string, spurBin: string): Promise<un
204
84
  }
205
85
  const value: unknown = JSON.parse(result.stdout);
206
86
  // Honor the existing optional JSON envelope without inventing another projection format.
207
- if (value && typeof value === 'object' && 'ok' in value && 'data' in value && value.ok === true) {
208
- return value.data;
209
- }
87
+ if (value && typeof value === 'object' && 'ok' in value && 'data' in value && value.ok === true) return value.data;
210
88
  return value;
211
89
  }
212
90
 
213
- /**
214
- * Project the setup outcome into the two-file run record (task 0927 R1): the machine state
215
- * merges into `.spur/run/<run-id>.state.json` (atomic same-directory temp + rename, the
216
- * 0925 R1 pattern) and `.spur/run/<run-id>.md` receives its run-start header exactly once.
217
- * A re-setup of the same run id rewrites the state from the current outcome but keeps the
218
- * prior `startedAt` instead of resurrecting setup time. Identity/provenance fields only —
219
- * never prompt bodies — so the record carries no secret-bearing content by construction
220
- * (0927 AC2). The retired `<run-id>-inline-setup.json` sidecar is not written (0927 R4);
221
- * its readers were migrated to the pair state in the same change.
222
- */
223
- function writeOutcome(runId: string, outcome: SetupOutcome): void {
224
- const runDir = join(process.cwd(), '.spur', 'run');
225
- if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
226
- const statePath = join(runDir, `${runId}.state.json`);
227
- const markdownPath = join(runDir, `${runId}.md`);
228
- let prior: Record<string, unknown> = {};
229
- try {
230
- const parsed: unknown = JSON.parse(readFileSync(statePath, 'utf8'));
231
- if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
232
- prior = parsed as Record<string, unknown>;
233
- }
234
- } catch {
235
- // No prior state (new run) or unreadable file — the outcome alone defines the state.
236
- }
237
- const at = new Date().toISOString();
238
- const state = {
239
- schemaVersion: 1 as const,
240
- runId,
241
- ...(outcome.workflowName !== undefined ? { workflowName: outcome.workflowName } : {}),
242
- ...(outcome.status !== undefined ? { status: outcome.status } : {}),
243
- startedAt: typeof prior.startedAt === 'string' ? prior.startedAt : at,
244
- updatedAt: at,
245
- ...(outcome.attached !== undefined ? { attached: outcome.attached } : {}),
246
- ...(outcome.definitionDigest !== undefined ? { definitionDigest: outcome.definitionDigest } : {}),
247
- ...(outcome.workflowVersion !== undefined ? { workflowVersion: outcome.workflowVersion } : {}),
248
- ...(outcome.resolvedPath !== undefined ? { resolvedPath: outcome.resolvedPath } : {}),
249
- ...(outcome.layer !== undefined ? { layer: outcome.layer } : {}),
250
- ...(outcome.workdir !== undefined ? { workdir: outcome.workdir } : {}),
251
- // 0948 R7: project the setup outcome itself, so the retired `-inline-setup.json`
252
- // sidecar's `ok` is still readable from the pair, and a SUCCESSFUL re-setup
253
- // explicitly drops any prior `error` — a stale failure message must not outlive
254
- // the failure it described (the state object is rebuilt, never merged with `prior`).
255
- ok: outcome.ok,
256
- ...(outcome.ok === false && outcome.error !== undefined ? { error: outcome.error } : {}),
257
- };
258
- const temp = `${statePath}.tmp`;
259
- try {
260
- writeFileSync(temp, `${JSON.stringify(state, null, 4)}\n`);
261
- renameSync(temp, statePath);
262
- } catch {
263
- // Best-effort: a failing record write must not wedge setup reporting — and never
264
- // leaves `.tmp` residue behind (0926 R1).
265
- try {
266
- unlinkSync(temp);
267
- } catch {
268
- // Nothing to clean (temp was never created).
269
- }
270
- }
271
- // 0948 R7: create the header with `wx` (O_EXCL) instead of `existsSync` → `appendFileSync`.
272
- // The check-then-append pair could append a SECOND header when two setups raced, and it
273
- // kept "header present" as a separate fact from the identity write. One exclusive create
274
- // makes exactly-one-header atomic; EEXIST just means it is already there.
275
- let headerFd: number | undefined;
276
- try {
277
- headerFd = openSync(markdownPath, 'wx');
278
- writeSync(
279
- headerFd,
280
- `# spur inline run ${runId} — ${outcome.workflowName ?? 'unknown workflow'} — setup ${at}\n`,
281
- );
282
- } catch {
283
- // EEXIST (already written) or an unwritable path — the human header is not the setup
284
- // identity, so this stays best-effort exactly like the state write above.
285
- } finally {
286
- if (headerFd !== undefined) closeSync(headerFd);
287
- }
288
- }
289
-
290
- /**
291
- * `--fingerprint` mode (task 0862 R5): print the engine's proof-input digest for a task file
292
- * (plus optional feature file) instead of creating a run, so the inline driver can capture the
293
- * fresh digest bound `run.artifact` registration checks against.
294
- *
295
- * The digest itself comes from the exported app functions — never reimplemented here — and the
296
- * options mirror `ProofFingerprintActionRunner` (cwd, spec contents, fileSystem; no `--expect`).
297
- * Returns the process exit code: 0 printed, 1 read/resolve failure.
298
- */
299
- async function printFingerprint(taskFile: string, featureFile: string, spurBin: string): Promise<number> {
300
- const { entry } = resolveAppEntry(spurBin);
301
- const app = (await import(entry)) as {
302
- computeProofInputFingerprint: (options: Record<string, unknown>) => Promise<string>;
303
- readProofInputContents: (
304
- fileSystem: unknown,
305
- workdir: string,
306
- options: { taskFile?: unknown; featureFile?: unknown },
307
- ) => Promise<{ ok: true; taskContent?: string; featureContent?: string } | { ok: false; error: string }>;
308
- };
309
-
310
- const workdir = process.cwd();
311
- // `undefined` fs takes readProofInputContents' node-filesystem default — the same default its
312
- // sibling createGitAlternateTree applies, and the same Node FS the CLI's runner injects.
313
- const inputs = await app.readProofInputContents(undefined, workdir, {
314
- taskFile,
315
- ...(featureFile.trim() !== '' ? { featureFile } : {}),
316
- });
317
- if (!inputs.ok) {
318
- console.error(`inline-run-setup: FAIL — ${inputs.error}`);
319
- return 1;
320
- }
321
-
322
- const digest = await app.computeProofInputFingerprint({
323
- cwd: workdir,
324
- ...(inputs.taskContent !== undefined ? { taskContent: inputs.taskContent } : {}),
325
- ...(inputs.featureContent !== undefined ? { featureContent: inputs.featureContent } : {}),
326
- });
327
- process.stdout.write(`${digest}\n`);
328
- return 0;
329
- }
330
-
331
- /** Terminal statuses the inline driver may declare when closing its run row. */
332
- const CLOSE_STATUSES = new Set(['done', 'failed', 'paused']);
333
-
334
- /**
335
- * 0937 R2: closed terminal-reason vocabulary the driver may declare on `--close`.
336
- * COPIED from packages/app/src/workflow/terminal-reason.ts — the plugin standalone
337
- * contract forbids a value import of app code; the parity test asserts the copy
338
- * equals the app export.
339
- */
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.
340
93
  const TERMINAL_REASONS = new Set([
341
94
  'done',
342
95
  'paused-operator',
@@ -349,257 +102,6 @@ const TERMINAL_REASONS = new Set([
349
102
  'retry-exhausted',
350
103
  ]);
351
104
 
352
- /** Finalize statuses — a finish emission is terminal, so only done|failed are valid (0868 #4). */
353
- const ACTION_STATUSES = new Set(['done', 'failed']);
354
-
355
- /** Input for the ADR-117 emission modes (`--action` / `--close`). */
356
- import type { WorkflowActionTraceWriter } from '@gobing-ai/spur-app';
357
-
358
- interface TraceModeInput {
359
- readonly runId: string;
360
- readonly close: boolean;
361
- readonly node: string;
362
- readonly kind: string;
363
- /** Declared terminal reason (0937 R2) — validated against TERMINAL_REASONS before this point. */
364
- readonly reason?: string;
365
- readonly status: string;
366
- readonly ok: boolean;
367
- readonly durationMs: number;
368
- readonly spurBin: string;
369
- }
370
-
371
- /**
372
- * The run-record file for appended driver lines (task 0927 R1): `.spur/run/<run-id>.md`
373
- * for pair-based runs. A legacy run that predates the pair keeps appending to its declared
374
- * `.spur/run/<run-id>.log` — the same precedence as `readWorkflowRunRecord` (the pair wins,
375
- * legacy stays readable in place).
376
- */
377
- function runRecordLogPath(runDir: string, runId: string): string {
378
- const markdownPath = join(runDir, `${runId}.md`);
379
- const legacyLogPath = join(runDir, `${runId}.log`);
380
- if (existsSync(legacyLogPath) && !existsSync(markdownPath)) return legacyLogPath;
381
- return markdownPath;
382
- }
383
-
384
- /**
385
- * Append one emission-failure line to the run record markdown — `.spur/run/<run-id>.md`,
386
- * the run log the inline driver already owns. Best-effort and synchronous (the process may
387
- * exit immediately after), and never throws: an unwritable log must not wedge the run
388
- * (ADR-117 R3).
389
- */
390
- function appendRunLogLine(runId: string, detail: string): void {
391
- try {
392
- const runDir = join(process.cwd(), '.spur', 'run');
393
- if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
394
- const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, '_');
395
- const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
396
- appendFileSync(runRecordLogPath(runDir, safeRunId), `[${stamp}] ${detail}\n`);
397
- } catch {
398
- // Best-effort (R3): the run continues even when the failure cannot be recorded.
399
- }
400
- }
401
-
402
- /**
403
- * Emit one trace write through the SHARED `WorkflowActionTraceWriter` (task 0868 R5/R7).
404
- * `--action` is best-effort: an emission failure is recorded to the run log and reported
405
- * on stdout as `{"ok":false}`, and the script exits 0 so the run still reaches its declared
406
- * terminal state (R3/R12). `--close` is bookkeeping, so a missing run row or a persistence
407
- * failure fails loudly with exit 1 (review findings #1/#4).
408
- */
409
- async function runTraceMode(input: TraceModeInput): Promise<number> {
410
- const operation = input.close ? 'run.close' : 'action.finish';
411
- const fail = (error: string): number => {
412
- appendRunLogLine(
413
- input.runId,
414
- `trace-emission-failed operation=${operation} run=${input.runId}` +
415
- `${input.node === '' ? '' : ` node=${input.node}`}${input.kind === '' ? '' : ` kind=${input.kind}`}: ${error}`,
416
- );
417
- process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}\n`);
418
- // The action boundary is best-effort (exit 0); the run-row closure fails loudly (exit 1).
419
- return input.close ? 1 : 0;
420
- };
421
-
422
- // Compile-time link (0868 finding #5): the writer half is typed by the real packages/app
423
- // export (type-only import, erased at runtime) so a signature drift breaks THIS file's
424
- // typecheck instead of hiding behind the hand-declared cast.
425
- let projectDb: { adapter: unknown; close: () => void } | undefined;
426
- try {
427
- const { entry } = resolveAppEntry(input.spurBin);
428
- const app = (await import(entry)) as {
429
- openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
430
- createWorkflowActionTraceWriter: (
431
- db: unknown,
432
- recordFailure?: (failure: unknown) => void,
433
- ) => WorkflowActionTraceWriter;
434
- };
435
-
436
- projectDb = await app.openInlineRunProjectDb(process.cwd());
437
- const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure: unknown) => {
438
- const detail = failure as { operation?: string; error?: string };
439
- appendRunLogLine(
440
- input.runId,
441
- `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? 'unknown error'}`,
442
- );
443
- });
444
- const result = (
445
- input.close
446
- ? await writer.closeRun(input.runId, input.status, undefined, input.reason)
447
- : await writer.recordAction({
448
- runId: input.runId,
449
- node: input.node,
450
- kind: input.kind,
451
- status: input.status,
452
- ok: input.ok,
453
- durationMs: input.durationMs,
454
- })
455
- ) as Record<string, unknown>;
456
- if (result.ok !== true && result.failure !== undefined) {
457
- // One stdout shape for emission failures (0868 finding #1): flatten the guard's
458
- // nested failure object to the same `{ok, runId, error}` the direct paths emit.
459
- const failure = result.failure as { error?: string };
460
- return fail(failure.error ?? 'unknown trace emission failure');
461
- }
462
- if (input.close && input.status === 'done' && result.actionRows === 0) {
463
- // A run finalized `done` with ZERO recorded action rows is a bookkeeping defect
464
- // (task 0975 R2): the row is already terminal — closeRun ran above — but the
465
- // driver must surface this instead of reporting a clean close, and must never
466
- // backfill rows. Exit 1 with the named code; the run record carries the finding.
467
- const error = `run ${input.runId} closed done with zero action_runs rows`;
468
- appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${error}`);
469
- process.stdout.write(
470
- `${JSON.stringify({ ok: false, runId: input.runId, error, code: 'NO_ACTION_ROWS', actionRows: 0 })}\n`,
471
- );
472
- return 1;
473
- }
474
- process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}\n`);
475
- return 0;
476
- } catch (error) {
477
- if (input.close && (error as { name?: string }).name === 'RunRowNotFoundError') {
478
- // The run row must exist before --close can mark it terminal (R6); a missing row
479
- // is a loud correctness failure, not a best-effort emission failure (finding #4).
480
- const message = error instanceof Error ? error.message : String(error);
481
- appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
482
- process.stdout.write(
483
- `${JSON.stringify({ ok: false, runId: input.runId, error: message, code: 'RUN_NOT_FOUND' })}\n`,
484
- );
485
- return 1;
486
- }
487
- return fail(error instanceof Error ? error.message : String(error));
488
- } finally {
489
- projectDb?.close();
490
- }
491
- }
492
-
493
- /**
494
- * `--decide` mode (task 0941 R5): execute the non-pausing decide action through the same app
495
- * function the engine registers (`runDecideForInlineRun` → `DecideActionRunner`), then record
496
- * the `action_runs` trace row through the shared writer. The decide-enabled switch
497
- * (`workflow.decideDecisionMaker`) is resolved HERE at the driver boundary (ADR-082) through
498
- * the bridged facade derivation and passed into the app runner as an explicit parameter —
499
- * app services never load Spur config. Degraded outcomes (`disabled`,
500
- * `no-backend`, `error`, `timeout`, `low-confidence`) are normal — the value is the declared
501
- * default and the run continues — so they print `ok: true` and exit 0. Only an invalid options
502
- * schema, an unreadable options file, or a failed config load fails closed with exit 1.
503
- */
504
- async function runDecideMode(input: {
505
- runId: string;
506
- node: string;
507
- optionsFile: string;
508
- spurBin: string;
509
- }): Promise<number> {
510
- type DecideOutcome = {
511
- ok: boolean;
512
- error?: string;
513
- value?: string;
514
- degraded?: boolean;
515
- reason?: string;
516
- source?: 'model' | 'default';
517
- backend?: string | null;
518
- confidence?: number | null;
519
- resultFile?: string;
520
- durationMs?: number;
521
- };
522
- const decideFailed = (error: string): number => {
523
- process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}\n`);
524
- return 1;
525
- };
526
- let outcome: DecideOutcome;
527
- try {
528
- const { entry, portable } = resolveAppEntry(input.spurBin);
529
- const app = (await import(entry)) as {
530
- runDecideForInlineRun: (input2: {
531
- workdir: string;
532
- optionsFile: string;
533
- enabled: boolean;
534
- }) => Promise<DecideOutcome>;
535
- };
536
- // Config switch at the driver boundary (ADR-082 / task 0941 gate fix): the flag comes
537
- // from the bridged facade derivation in the generated lib — the same committed bundle
538
- // the inventory flow imports — never a @gobing-ai/* value import. Embedded schemas
539
- // ride along only in the portable layout, mirroring the setup mode's load posture.
540
- const lib = (await import(
541
- fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url))
542
- )) as typeof import('../lib/inline-run.generated.mjs');
543
- const enabled = await lib.resolveDecideDecisionMakerEnabled(
544
- process.cwd(),
545
- portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined,
546
- );
547
- outcome = await app.runDecideForInlineRun({
548
- workdir: process.cwd(),
549
- optionsFile: input.optionsFile,
550
- enabled,
551
- });
552
- } catch (error) {
553
- return decideFailed(error instanceof Error ? error.message : String(error));
554
- }
555
- if (!outcome.ok) return decideFailed(outcome.error ?? 'decide failed without an error message');
556
- process.stdout.write(`${JSON.stringify({ ok: true, runId: input.runId, node: input.node, ...outcome })}\n`);
557
- // 0976 R2: the run log names the decision's provenance, so a declared-default fallback is
558
- // never read as a model decision. Best-effort through the same run-log appender.
559
- appendRunLogLine(
560
- input.runId,
561
- `decide node=${input.node} value=${outcome.value ?? ''} source=${outcome.source ?? 'default'} reason=${outcome.reason ?? ''}`,
562
- );
563
- // Trace row is best-effort, exactly like --action: an emission failure never wedges the run.
564
- return await runTraceMode({
565
- runId: input.runId,
566
- close: false,
567
- node: input.node,
568
- kind: 'decide',
569
- status: 'done',
570
- ok: true,
571
- durationMs: outcome.durationMs ?? 0,
572
- spurBin: input.spurBin,
573
- });
574
- }
575
-
576
- /**
577
- * `--persist-out --from <worktree-path>` (task 0975 R1): resolve the app through the shared
578
- * chain and copy the worktree's inline-run provenance into THIS tree. No persistence policy
579
- * lives here — the app's `persistWorktreeRuns` (→ domain `transferRunTables`) owns the DB
580
- * transfer, the idempotence and the conflict skips. Success prints
581
- * `{ok:true,persisted:<n>,skipped:[…]}` and exits 0; any failure prints `{ok:false,error}`
582
- * and exits 1 (the driver routes to WT-5 and retains the worktree).
583
- */
584
- async function runPersistOutMode(input: { from: string; spurBin: string }): Promise<number> {
585
- try {
586
- const { entry } = resolveAppEntry(input.spurBin);
587
- const app = (await import(entry)) as {
588
- persistWorktreeRuns: (i: {
589
- fromWorkdir: string;
590
- toWorkdir: string;
591
- }) => Promise<{ ok: true; persisted: number; skipped: ReadonlyArray<{ id: string; reason: string }> }>;
592
- };
593
- const result = await app.persistWorktreeRuns({ fromWorkdir: input.from, toWorkdir: process.cwd() });
594
- process.stdout.write(`${JSON.stringify({ ok: true, persisted: result.persisted, skipped: result.skipped })}\n`);
595
- return 0;
596
- } catch (error) {
597
- const message = error instanceof Error ? error.message : String(error);
598
- process.stdout.write(`${JSON.stringify({ ok: false, error: message })}\n`);
599
- return 1;
600
- }
601
- }
602
-
603
105
  async function main(): Promise<void> {
604
106
  // The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
605
107
  if (!process.versions.bun) {
@@ -609,89 +111,95 @@ async function main(): Promise<void> {
609
111
  if (child.error) console.error(`inline-run-setup requires Bun on PATH: ${child.error.message}`);
610
112
  process.exit(child.status ?? 1);
611
113
  }
612
- let runId = '';
613
- let file = '';
614
- let fingerprint = false;
615
- let taskFile = '';
616
- let featureFile = '';
617
- let action = false;
618
- let close = false;
619
- let decide = false;
620
- let persistOut = false;
621
- let from = '';
622
- let optionsJson = '';
623
- let node = '';
624
- let kind = '';
625
- let status = '';
626
- let reason = '';
627
- let okRaw = '';
628
- let durationRaw = '';
114
+ const flags = new Map<string, string>();
115
+ let fingerprint = false,
116
+ action = false,
117
+ close = false,
118
+ decide = false,
119
+ persistOut = false;
120
+ const taskFiles: string[] = [];
629
121
  let spurBin = getEnvVar('SPUR_BIN') ?? '';
630
122
  const argv = process.argv.slice(2);
631
123
  for (let i = 0; i < argv.length; i++) {
632
- if (argv[i] === '--run-id') runId = argv[++i] ?? '';
633
- else if (argv[i] === '--file') file = argv[++i] ?? '';
634
- else if (argv[i] === '--fingerprint') fingerprint = true;
635
- else if (argv[i] === '--task-file') taskFile = argv[++i] ?? '';
636
- else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
637
- else if (argv[i] === '--action') action = true;
638
- else if (argv[i] === '--close') close = true;
639
- else if (argv[i] === '--decide') decide = true;
640
- else if (argv[i] === '--persist-out') persistOut = true;
641
- else if (argv[i] === '--from') from = argv[++i] ?? '';
642
- else if (argv[i] === '--options-json') optionsJson = argv[++i] ?? '';
643
- else if (argv[i] === '--node') node = argv[++i] ?? '';
644
- else if (argv[i] === '--kind') kind = argv[++i] ?? '';
645
- else if (argv[i] === '--status') status = argv[++i] ?? '';
646
- else if (argv[i] === '--reason') reason = argv[++i] ?? '';
647
- else if (argv[i] === '--ok') okRaw = argv[++i] ?? '';
648
- else if (argv[i] === '--duration-ms') durationRaw = argv[++i] ?? '';
649
- else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
650
- }
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') ?? '';
651
147
 
652
- // Two mutually exclusive modes share this entry point: create/attach a run (run-id + file), or
653
- // print the proof digest for the inline driver (task 0862 R5). Mixing them is a usage error.
654
148
  if (fingerprint) {
655
- if (runId !== '' || file !== '' || taskFile.trim() === '') usage();
656
- process.exit(await printFingerprint(taskFile, featureFile, spurBin));
149
+ if (runId !== '' || file !== '' || taskFiles.length !== 1 || (taskFiles[0] ?? '').trim() === '') usage();
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 }));
657
161
  }
658
162
 
659
- // ADR-117 emission modes (task 0868): the inline driver reports one completed action
660
- // boundary, or closes its run row at the declared terminal state. Both share the run-id
661
- // filename guard, the app-entry resolution chain and the best-effort failure contract.
662
- // Non-pausing decide action (0941 R5): --decide is mutually exclusive with the run-setup,
663
- // fingerprint, and trace modes; it requires the run id (filename-guarded), the state id
664
- // for the trace row, and the options JSON file.
665
163
  if (decide) {
666
- if (action || close || fingerprint || file !== '' || taskFile !== '') usage();
164
+ if (action || close || fingerprint || file !== '' || taskFiles.length > 0) usage();
667
165
  if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
668
166
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
669
- 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
+ }
670
183
  }
671
184
 
672
- // Worktree provenance persist-out (task 0975 R1): mutually exclusive with every other
673
- // mode; requires the source worktree path. Runs with cwd = the invoking tree.
674
185
  if (persistOut) {
675
- if (fingerprint || decide || action || close || runId !== '' || file !== '' || taskFile !== '') usage();
676
- if (from.trim() === '') usage();
677
- process.exit(await runPersistOutMode({ from, spurBin }));
186
+ if (fingerprint || decide || action || close || runId !== '' || file !== '') usage();
187
+ if (from.trim() === '' || taskFiles.some((taskFile) => taskFile.trim() === '')) usage();
188
+ const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
189
+ process.exit(await app.runInlineRunPersistOut({ from, taskFiles }));
678
190
  }
679
191
 
680
192
  if (action || close) {
681
193
  if (action && close) usage();
682
194
  if (runId.trim() === '' || status.trim() === '') usage();
683
195
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
196
+ const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
684
197
  if (close) {
685
- if (!CLOSE_STATUSES.has(status)) usage();
686
- // 0937 R2: a failed close requires a declared reason, and any declared reason
687
- // must be a closed-enum value — both fail loudly BEFORE any write happens.
688
- if (reason.trim() === '') {
689
- if (status === 'failed') usage();
690
- } else if (!TERMINAL_REASONS.has(reason)) {
691
- usage();
692
- }
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();
693
201
  process.exit(
694
- await runTraceMode({
202
+ await app.runInlineRunTrace({
695
203
  runId,
696
204
  close: true,
697
205
  node: '',
@@ -699,79 +207,41 @@ async function main(): Promise<void> {
699
207
  status,
700
208
  ok: true,
701
209
  durationMs: 0,
702
- spurBin,
703
210
  ...(reason.trim() === '' ? {} : { reason }),
704
211
  }),
705
212
  );
706
213
  }
707
214
  if (node.trim() === '' || kind.trim() === '') usage();
708
- if (!ACTION_STATUSES.has(status)) usage();
709
- // `--ok` and `--duration-ms` are required and exact for the action mode
710
- // (review finding #2): a miscased `--ok True` or an omitted `--duration-ms`
711
- // must be a loud usage error, never a silently-defaulted `ok=0` /
712
- // `duration_ms=0` row.
215
+ if (!app.isInlineRunActionStatus(status)) usage();
216
+ // `--ok` and `--duration-ms` are required and exact (0868 #2): no silent defaults.
713
217
  if (okRaw !== 'true' && okRaw !== 'false') usage();
714
- const ok = okRaw === 'true';
715
218
  const durationMs = Number(durationRaw);
716
219
  if (durationRaw.trim() === '' || !Number.isFinite(durationMs) || durationMs < 0) usage();
717
- 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
+ );
718
223
  }
719
224
 
720
225
  if (runId.trim() === '' || file.trim() === '') usage();
721
226
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
722
-
723
227
  const { entry, portable } = resolveAppEntry(spurBin);
724
-
725
- // Dynamic import by absolute path: the app source graph resolves its own workspace
726
- // dependencies from the repo checkout, never from this plugin script's location.
727
- const app = (await import(entry)) as {
728
- createOrAttachInlineRun: (input: {
729
- workdir: string;
730
- getDb: () => Promise<unknown>;
731
- file: string;
732
- runId: string;
733
- inventory?: unknown;
734
- embeddedSchemas?: ReadonlyMap<string, string>;
735
- }) => Promise<SetupOutcome & { ok: boolean }>;
736
- openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
737
- EMBEDDED_SPUR_SCHEMAS?: ReadonlyMap<string, string>;
738
- };
739
-
740
- const workdir = process.cwd();
228
+ const app = (await import(entry)) as InlineApp;
741
229
  let inventory: unknown;
742
230
  try {
743
231
  inventory = await readInstalledInventory(file, spurBin);
744
232
  } catch (error) {
745
233
  const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
746
- writeOutcome(runId, { ok: false, runId, error: message });
234
+ app.writeInlineRunOutcome(runId, { ok: false, runId, error: message });
747
235
  throw new Error(message);
748
236
  }
749
- const projectDb = await app.openInlineRunProjectDb(workdir);
750
- let exitCode = 0;
751
- try {
752
- const result = await app.createOrAttachInlineRun({
753
- workdir,
754
- getDb: async () => projectDb.adapter,
755
- file,
237
+ process.exit(
238
+ await app.runInlineRunSetup({
756
239
  runId,
240
+ file,
757
241
  inventory,
758
242
  ...(portable ? { embeddedSchemas: app.EMBEDDED_SPUR_SCHEMAS } : {}),
759
- });
760
- writeOutcome(runId, result);
761
- if (!result.ok) {
762
- console.error(`inline-run-setup: FAIL for run ${runId}`);
763
- console.error(` ${result.error}`);
764
- exitCode = 1;
765
- } else {
766
- console.error(
767
- `inline-run-setup: ${result.attached ? 'attached' : 'created'} run ${runId} ` +
768
- `(${result.workflowName}, layer ${result.layer}, digest ${result.definitionDigest}, status ${result.status})`,
769
- );
770
- }
771
- } finally {
772
- projectDb.close();
773
- }
774
- process.exit(exitCode);
243
+ }),
244
+ );
775
245
  }
776
246
 
777
247
  main().catch((e: unknown) => {