@gobing-ai/spur 0.3.91 → 0.3.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -9
  3. package/config/plugin-scripts.json +27 -1
  4. package/config/templates/AGENTS.md +4 -0
  5. package/config/templates/docs/02_ROADMAP.md +2 -0
  6. package/config/templates/docs/03_ARCHITECTURE.md +3 -1
  7. package/config/templates/docs/04_DESIGN.md +2 -0
  8. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +10 -2
  9. package/config/workflow-candidates.json +72 -1
  10. package/config/workflows/feature-lifecycle.yaml +14 -5
  11. package/config/workflows/feature-verification.yaml +36 -27
  12. package/config/workflows/history-anatomy.yaml +16 -25
  13. package/config/workflows/idea-pipeline.yaml +76 -23
  14. package/config/workflows/pr-review.yaml +8 -0
  15. package/config/workflows/task-pipeline.yaml +362 -40
  16. package/config/workflows/wayfinder-resolution.yaml +5 -0
  17. package/config/workflows/wrapup-pipeline.yaml +102 -19
  18. package/package.json +9 -9
  19. package/plugins/sp/README.md +7 -2
  20. package/plugins/sp/agents/super-planner.md +14 -5
  21. package/plugins/sp/commands/dev-dogfood.md +4 -4
  22. package/plugins/sp/commands/dev-fixall.md +8 -5
  23. package/plugins/sp/commands/dev-run.md +8 -2
  24. package/plugins/sp/commands/dev-runall.md +14 -8
  25. package/plugins/sp/commands/dev-verify.md +9 -0
  26. package/plugins/sp/commands/dev-verifyall.md +5 -0
  27. package/plugins/sp/lib/idea-handoff.generated.mjs +306 -301
  28. package/plugins/sp/lib/inline-run.generated.d.mts +18 -0
  29. package/plugins/sp/lib/inline-run.generated.mjs +1468 -0
  30. package/plugins/sp/plugin.json +1 -1
  31. package/plugins/sp/references/environment-lens.md +1 -1
  32. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
  33. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
  34. package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
  35. package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
  36. package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
  37. package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
  38. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +2 -1
  39. package/plugins/sp/scripts/inline-run-setup.mjs +421 -0
  40. package/plugins/sp/scripts/inline-run-setup.ts +325 -78
  41. package/plugins/sp/scripts/quality-gate.mjs +248 -6
  42. package/plugins/sp/scripts/quality-gate.ts +410 -9
  43. package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
  44. package/plugins/sp/scripts/record-feature-sync.ts +84 -0
  45. package/plugins/sp/scripts/residual-scan.mjs +484 -0
  46. package/plugins/sp/scripts/residual-scan.ts +640 -0
  47. package/plugins/sp/scripts/task-diffstat.mjs +156 -0
  48. package/plugins/sp/scripts/task-diffstat.ts +229 -0
  49. package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
  50. package/plugins/sp/scripts/task-size-precheck.ts +8 -3
  51. package/plugins/sp/scripts/wrapup-drift-probe.mjs +181 -0
  52. package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
  53. package/plugins/sp/scripts/wrapup-steps.mjs +60 -1
  54. package/plugins/sp/scripts/wrapup-steps.ts +89 -4
  55. package/plugins/sp/skills/brainstorm/SKILL.md +2 -0
  56. package/plugins/sp/skills/brainstorm/references/workflows.md +17 -2
  57. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  58. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
  59. package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
  60. package/plugins/sp/skills/code-verification/SKILL.md +21 -0
  61. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -2
  62. package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
  63. package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
  64. package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
  65. package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
  66. package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
  67. package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
  68. package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
  69. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  70. package/plugins/sp/skills/session-review/SKILL.md +12 -2
  71. package/plugins/sp/skills/spur-check/SKILL.md +112 -0
  72. package/plugins/sp/skills/spur-cli/references/features.md +1 -1
  73. package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
  74. package/plugins/sp/skills/spur-cli/references/workflows.md +39 -19
  75. package/plugins/sp/skills/spur-dev/SKILL.md +13 -5
  76. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +18 -3
  77. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
  78. package/plugins/sp/skills/spur-dev/references/document-authoring.md +85 -0
  79. package/plugins/sp/skills/spur-dev/references/execution-batch.md +286 -58
  80. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +1 -1
  81. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +27 -4
  82. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
  83. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +61 -16
  84. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +10 -5
  85. package/plugins/sp/skills/spur-dev/templates/design.md +31 -0
  86. package/plugins/sp/skills/spur-dev/templates/plan.md +32 -0
  87. package/plugins/sp/skills/spur-doctor/SKILL.md +60 -14
  88. package/schemas/state-machine-workflow.schema.json +4 -0
  89. package/spur.js +22501 -19644
  90. package/web/_astro/BoardApp.CerSBgis.js +192 -0
  91. package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
  92. package/web/_astro/{TaskDetail.CXGltuT_.js → TaskDetail.DCqiC-OZ.js} +1 -1
  93. package/web/_astro/arc.CPwg6Rw0.js +1 -0
  94. package/web/_astro/{architectureDiagram-3BPJPVTR.DJ8DHkWE.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
  95. package/web/_astro/{blockDiagram-GPEHLZMM.D8DHK3Jl.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
  96. package/web/_astro/{c4Diagram-AAUBKEIU.BugQbX9u.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
  97. package/web/_astro/channel.MYZLKNwy.js +1 -0
  98. package/web/_astro/{chunk-2J33WTMH.Mv26KlVn.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
  99. package/web/_astro/{chunk-4BX2VUAB.CD51JoT_.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
  100. package/web/_astro/{chunk-55IACEB6.D_PFaIEe.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
  101. package/web/_astro/{chunk-727SXJPM.DemMW1ao.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
  102. package/web/_astro/{chunk-AQP2D5EJ.Iu2V5-ex.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
  103. package/web/_astro/{chunk-FMBD7UC4.MsNgSP-E.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
  104. package/web/_astro/{chunk-ND2GUHAM.DfbRaAlm.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
  105. package/web/_astro/{chunk-QZHKN3VN.BbJAQ4h-.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
  106. package/web/_astro/{classDiagram-4FO5ZUOK.CPurtiC2.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
  107. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CPurtiC2.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
  108. package/web/_astro/{cose-bilkent-S5V4N54A.CKKdx1bM.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
  109. package/web/_astro/{cynefin-OW5HDTMX.CoKMTg-R.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
  110. package/web/_astro/{dagre-BM42HDAG.D4h4_k56.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
  111. package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
  112. package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
  113. package/web/_astro/{diagram-KO2AKTUF.BFoCkiCr.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
  114. package/web/_astro/{diagram-LMA3HP47.exHn9OVx.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
  115. package/web/_astro/{diagram-OG6HWLK6.CeqO34nN.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
  116. package/web/_astro/{erDiagram-TEJ5UH35.D_v7HqxR.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
  117. package/web/_astro/{flowDiagram-I6XJVG4X.EUmrpbwh.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
  118. package/web/_astro/{ganttDiagram-6RSMTGT7.BOCF5lII.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
  119. package/web/_astro/{gitGraphDiagram-PVQCEYII.Di7otYZD.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
  120. package/web/_astro/index.Hjbr15fG.css +1 -0
  121. package/web/_astro/{infoDiagram-5YYISTIA.TcBkCAJk.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
  122. package/web/_astro/{ishikawaDiagram-YF4QCWOH.D-2y4M0c.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
  123. package/web/_astro/{journeyDiagram-JHISSGLW.DPbJI_n2.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
  124. package/web/_astro/{kanban-definition-UN3LZRKU.EFxhQ9Fj.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
  125. package/web/_astro/{linear.DSAsQLzs.js → linear.C2_IkbZT.js} +1 -1
  126. package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
  127. package/web/_astro/{mindmap-definition-RKZ34NQL.CJY1N_7V.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
  128. package/web/_astro/{pieDiagram-4H26LBE5.567ZNoL2.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
  129. package/web/_astro/{quadrantDiagram-W4KKPZXB.mqfz9-MY.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
  130. package/web/_astro/{requirementDiagram-4Y6WPE33.Bv1Gv9In.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
  131. package/web/_astro/{sankeyDiagram-5OEKKPKP.B6Gs4X4r.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
  132. package/web/_astro/{sequenceDiagram-3UESZ5HK.BhYj4v-m.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
  133. package/web/_astro/{stateDiagram-AJRCARHV.BPbBnkpw.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
  134. package/web/_astro/{stateDiagram-v2-BHNVJYJU.C4squMNK.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
  135. package/web/_astro/{timeline-definition-PNZ67QCA.C_SwIHgl.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
  136. package/web/_astro/{vennDiagram-CIIHVFJN.Bz4NZGpQ.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
  137. package/web/_astro/{wardleyDiagram-YWT4CUSO.CozMVZ3i.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
  138. package/web/_astro/{xychartDiagram-2RQKCTM6.BwMGBwjB.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
  139. package/web/index.html +2 -2
  140. package/config/workflows/decision-routing-example.yaml +0 -134
  141. package/web/_astro/BoardApp.BEDWpzsr.js +0 -188
  142. package/web/_astro/BoardApp.DQG2xfEz.js +0 -1
  143. package/web/_astro/arc.C0rrflm_.js +0 -1
  144. package/web/_astro/channel.SRrg1P-w.js +0 -1
  145. package/web/_astro/diagram-2AECGRRQ.DJ0h9zgw.js +0 -43
  146. package/web/_astro/diagram-5GNKFQAL.DvPk1jYd.js +0 -10
  147. package/web/_astro/index.Bx6GY4RH.css +0 -1
  148. package/web/_astro/mermaid.core.kAZjgJHG.js +0 -301
@@ -5,34 +5,35 @@
5
5
  * Unlike the subprocess path (`spur workflow run`), the interactive inline driver
6
6
  * allocated a run id but never persisted an authoritative `runs` row, so bound
7
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 resolves the spur
9
- * repo checkout from the SPUR_BIN chain, imports the real app service
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
10
  * (`createOrAttachInlineRun` / `openInlineRunProjectDb` from packages/app), and lets it
11
- * resolve the SAME project-or-bundled definition the engine would launch, compute the
11
+ * resolve the SAME project/registered/shared definition the engine would launch, compute the
12
12
  * canonical definition digest with the exported hash machinery, and create-or-attach the
13
13
  * run row through the existing engine persistence adapter.
14
14
  *
15
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 there is no repo
17
- * checkout to import the app service from, so the setup fails closed with actionable
18
- * remediation guidance (point SPUR_BIN at a repo checkout); it never falls back to an
19
- * unbound run (0804 R1 failure 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.
20
19
  *
21
- * Outcome JSON is written to `.spur/run/<run-id>-inline-setup.json` so the driver can
22
- * seed the inline var overlay (`__runId`, `__definitionDigest`) that proof capture and
23
- * bound registration verify against. Exit 0 = authoritative identity ready (created or
24
- * idempotently attached); exit 1 = fail closed, the driver must stop.
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.
25
26
  *
26
- * Repo-only script (ADR-065): it imports the app workspace source, so it runs under bun
27
- * against a monorepo checkout only — the same posture as task-size-precheck.ts and
28
- * task-evidence-precheck.ts.
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
29
  *
30
30
  * Usage:
31
31
  * bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
32
32
  * bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]
33
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>]
34
35
  * --status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]
35
- * bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--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>]
36
37
  *
37
38
  * The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
38
39
  * creates nothing (task 0862 R5).
@@ -41,20 +42,38 @@
41
42
  * 0868): `--action` records one completed action boundary as an `action_runs` row through the
42
43
  * shared `WorkflowActionTraceWriter`, `--close` marks the run row terminal through the same
43
44
  * writer. `--action` is best-effort: a persistence failure is appended to
44
- * `.spur/run/<run-id>.log` and the script still exits 0 with `{"ok":false}` on stdout, so
45
+ * `.spur/run/<run-id>.md` and the script still exits 0 with `{"ok":false}` on stdout, so
45
46
  * observation never wedges the run. `--close` is NOT best-effort: the run-row closure is
46
47
  * bookkeeping, so a missing run row or a persistence failure exits 1 with a named error.
48
+ * The `--decide` mode (task 0941 R5) executes the non-pausing decide action through the SAME
49
+ * app runner the engine registers (`runDecideForInlineRun`), writing the same resultFile row
50
+ * and recording the same `action_runs` trace row (`kind=decide`, best-effort). A degraded
51
+ * decision is still `ok: true` — the run continues — so exit 0 covers every model-level
52
+ * outcome; only an invalid options schema exits 1 (fail closed).
53
+ *
47
54
  * Exit 2 is reserved for usage errors.
48
55
  *
49
56
  * Env: SPUR_BIN
50
57
  */
51
58
 
52
- import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
59
+ import { spawnSync } from 'node:child_process';
60
+ import {
61
+ appendFileSync,
62
+ closeSync,
63
+ existsSync,
64
+ mkdirSync,
65
+ openSync,
66
+ readFileSync,
67
+ renameSync,
68
+ unlinkSync,
69
+ writeFileSync,
70
+ writeSync,
71
+ } from 'node:fs';
53
72
  import { dirname, join, resolve } from 'node:path';
54
73
  import { fileURLToPath } from 'node:url';
55
74
  import { getEnvVar } from '../lib/env';
56
75
 
57
- /** Outcome document written to `.spur/run/<run-id>-inline-setup.json`. */
76
+ /** Setup outcome projected into the run-record state `.spur/run/<run-id>.state.json` (0927 R1). */
58
77
  interface SetupOutcome {
59
78
  readonly ok: boolean;
60
79
  readonly runId: string;
@@ -81,14 +100,17 @@ function usage(): never {
81
100
  '--status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]',
82
101
  );
83
102
  console.error(
84
- ' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--spur-bin <path>]',
103
+ ' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
104
+ );
105
+ console.error(
106
+ ' bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]',
85
107
  );
86
108
  process.exit(2);
87
109
  }
88
110
 
89
111
  /**
90
- * The run id becomes a filename under `.spur/run/` (`<run-id>-inline-setup.json`), so it must be a
91
- * single safe filename component before anything is written — the same guard class the
112
+ * The run id becomes filenames under `.spur/run/` (`<run-id>.state.json` + `<run-id>.md`),
113
+ * so it must be a single safe filename component before anything is written — the same guard class the
92
114
  * task-pipeline.yaml route-reason action applies to `$__runId` (task 0804 R8). The allowlist
93
115
  * refuses path separators, dot traversal (leading `.`), unresolved interpolation (`$`/`{`/`}`) and
94
116
  * every other shell/unspecified metachar; valid UUID/timestamp-slug ids pass.
@@ -97,7 +119,7 @@ const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
97
119
 
98
120
  function refuseUnsafeRunId(runId: string): never {
99
121
  // Refuse BEFORE any outcome write: an unsafe id must never reach
100
- // `.spur/run/<run-id>-inline-setup.json` (no traversal, no unintended file).
122
+ // `.spur/run/<run-id>.state.json` / `.md` (no traversal, no unintended file).
101
123
  console.error(`inline-run-setup: refusing unsafe run id: ${runId}`);
102
124
  console.error(
103
125
  ' The run id must be a single safe filename component (alphanumeric/._-, no leading dot, ' +
@@ -112,12 +134,9 @@ function refuseUnsafeRunId(runId: string): never {
112
134
  * (--spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`), then derive the repo
113
135
  * root from the resolved main module. `bun <repo>/apps/cli/src/index.ts` → repo root is
114
136
  * three levels up. A bundle-only install (`spur` on PATH, a bundled `spur.js`, or a spur
115
- * binary without the app workspace) has no app entry to import — the caller fails that
116
- * closed with remediation guidance.
137
+ * binary without the app workspace) uses the adjacent generated application bundle.
117
138
  */
118
- function resolveAppEntry(
119
- spurBin: string,
120
- ): { entry: string; repoRoot: string } | { entry: null; repoRoot: null; chain: string } {
139
+ function resolveAppEntry(spurBin: string): { entry: string; portable: boolean } {
121
140
  let candidates: string[] = [];
122
141
  if (spurBin !== '') {
123
142
  candidates = [spurBin];
@@ -137,16 +156,119 @@ function resolveAppEntry(
137
156
  const srcDir = dirname(mainModule);
138
157
  const repoRoot = resolve(srcDir, '..', '..', '..');
139
158
  const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
140
- if (existsSync(appEntry)) return { entry: appEntry, repoRoot };
141
- return { entry: null, repoRoot: null, chain: `${candidate} (no ${appEntry})` };
159
+ if (existsSync(appEntry)) return { entry: appEntry, portable: false };
160
+ }
161
+ const entry = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
162
+ if (!existsSync(entry)) {
163
+ throw new Error('inline application bundle is missing — rebuild/install the sp plugin before running inline');
164
+ }
165
+ return { entry, portable: true };
166
+ }
167
+
168
+ /** Let the selected CLI own config and layer resolution, then revalidate its snapshot in the app. */
169
+ async function readInstalledInventory(file: string, spurBin: string): Promise<unknown> {
170
+ const localCli = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
171
+ const bundle = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
172
+ const { splitLaunchCommand } = (await import(bundle)) as typeof import('../lib/inline-run.generated.mjs');
173
+ const launch = spurBin
174
+ ? splitLaunchCommand(spurBin, 'inline-run-setup "spurBin"')
175
+ : existsSync(localCli)
176
+ ? { command: 'bun', leadingArgs: [localCli] }
177
+ : { command: 'spur', leadingArgs: [] };
178
+ if ('error' in launch) throw new Error(launch.error);
179
+ const result = spawnSync(
180
+ launch.command,
181
+ [...launch.leadingArgs, 'workflow', 'show', file, '--format', 'todo', '--json'],
182
+ { cwd: process.cwd(), encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024 },
183
+ );
184
+ if (result.status !== 0) {
185
+ throw new Error(
186
+ `could not resolve the workflow definition with the installed CLI: ${result.error?.message ?? result.stderr}`,
187
+ );
142
188
  }
143
- return { entry: null, repoRoot: null, chain: spurBin === '' ? 'PATH spur (bundle-only install)' : spurBin };
189
+ const value: unknown = JSON.parse(result.stdout);
190
+ // Honor the existing optional JSON envelope without inventing another projection format.
191
+ if (value && typeof value === 'object' && 'ok' in value && 'data' in value && value.ok === true) {
192
+ return value.data;
193
+ }
194
+ return value;
144
195
  }
145
196
 
197
+ /**
198
+ * Project the setup outcome into the two-file run record (task 0927 R1): the machine state
199
+ * merges into `.spur/run/<run-id>.state.json` (atomic same-directory temp + rename, the
200
+ * 0925 R1 pattern) and `.spur/run/<run-id>.md` receives its run-start header exactly once.
201
+ * A re-setup of the same run id rewrites the state from the current outcome but keeps the
202
+ * prior `startedAt` instead of resurrecting setup time. Identity/provenance fields only —
203
+ * never prompt bodies — so the record carries no secret-bearing content by construction
204
+ * (0927 AC2). The retired `<run-id>-inline-setup.json` sidecar is not written (0927 R4);
205
+ * its readers were migrated to the pair state in the same change.
206
+ */
146
207
  function writeOutcome(runId: string, outcome: SetupOutcome): void {
147
208
  const runDir = join(process.cwd(), '.spur', 'run');
148
209
  if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
149
- writeFileSync(join(runDir, `${runId}-inline-setup.json`), `${JSON.stringify(outcome, null, 4)}\n`);
210
+ const statePath = join(runDir, `${runId}.state.json`);
211
+ const markdownPath = join(runDir, `${runId}.md`);
212
+ let prior: Record<string, unknown> = {};
213
+ try {
214
+ const parsed: unknown = JSON.parse(readFileSync(statePath, 'utf8'));
215
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
216
+ prior = parsed as Record<string, unknown>;
217
+ }
218
+ } catch {
219
+ // No prior state (new run) or unreadable file — the outcome alone defines the state.
220
+ }
221
+ const at = new Date().toISOString();
222
+ const state = {
223
+ schemaVersion: 1 as const,
224
+ runId,
225
+ ...(outcome.workflowName !== undefined ? { workflowName: outcome.workflowName } : {}),
226
+ ...(outcome.status !== undefined ? { status: outcome.status } : {}),
227
+ startedAt: typeof prior.startedAt === 'string' ? prior.startedAt : at,
228
+ updatedAt: at,
229
+ ...(outcome.attached !== undefined ? { attached: outcome.attached } : {}),
230
+ ...(outcome.definitionDigest !== undefined ? { definitionDigest: outcome.definitionDigest } : {}),
231
+ ...(outcome.workflowVersion !== undefined ? { workflowVersion: outcome.workflowVersion } : {}),
232
+ ...(outcome.resolvedPath !== undefined ? { resolvedPath: outcome.resolvedPath } : {}),
233
+ ...(outcome.layer !== undefined ? { layer: outcome.layer } : {}),
234
+ ...(outcome.workdir !== undefined ? { workdir: outcome.workdir } : {}),
235
+ // 0948 R7: project the setup outcome itself, so the retired `-inline-setup.json`
236
+ // sidecar's `ok` is still readable from the pair, and a SUCCESSFUL re-setup
237
+ // explicitly drops any prior `error` — a stale failure message must not outlive
238
+ // the failure it described (the state object is rebuilt, never merged with `prior`).
239
+ ok: outcome.ok,
240
+ ...(outcome.ok === false && outcome.error !== undefined ? { error: outcome.error } : {}),
241
+ };
242
+ const temp = `${statePath}.tmp`;
243
+ try {
244
+ writeFileSync(temp, `${JSON.stringify(state, null, 4)}\n`);
245
+ renameSync(temp, statePath);
246
+ } catch {
247
+ // Best-effort: a failing record write must not wedge setup reporting — and never
248
+ // leaves `.tmp` residue behind (0926 R1).
249
+ try {
250
+ unlinkSync(temp);
251
+ } catch {
252
+ // Nothing to clean (temp was never created).
253
+ }
254
+ }
255
+ // 0948 R7: create the header with `wx` (O_EXCL) instead of `existsSync` → `appendFileSync`.
256
+ // The check-then-append pair could append a SECOND header when two setups raced, and it
257
+ // kept "header present" as a separate fact from the identity write. One exclusive create
258
+ // makes exactly-one-header atomic; EEXIST just means it is already there.
259
+ let headerFd: number | undefined;
260
+ try {
261
+ headerFd = openSync(markdownPath, 'wx');
262
+ writeSync(
263
+ headerFd,
264
+ `# spur inline run ${runId} — ${outcome.workflowName ?? 'unknown workflow'} — setup ${at}\n`,
265
+ );
266
+ } catch {
267
+ // EEXIST (already written) or an unwritable path — the human header is not the setup
268
+ // identity, so this stays best-effort exactly like the state write above.
269
+ } finally {
270
+ if (headerFd !== undefined) closeSync(headerFd);
271
+ }
150
272
  }
151
273
 
152
274
  /**
@@ -159,14 +281,7 @@ function writeOutcome(runId: string, outcome: SetupOutcome): void {
159
281
  * Returns the process exit code: 0 printed, 1 read/resolve failure.
160
282
  */
161
283
  async function printFingerprint(taskFile: string, featureFile: string, spurBin: string): Promise<number> {
162
- const { entry, repoRoot, chain } = resolveAppEntry(spurBin);
163
- if (entry === null || repoRoot === null) {
164
- console.error(
165
- `inline-run-setup: FAIL — no monorepo checkout of spur is reachable via ${chain}. ` +
166
- 'The proof-input digest must be computed by the app service; a bundle-only install cannot do it.',
167
- );
168
- return 1;
169
- }
284
+ const { entry } = resolveAppEntry(spurBin);
170
285
  const app = (await import(entry)) as {
171
286
  computeProofInputFingerprint: (options: Record<string, unknown>) => Promise<string>;
172
287
  readProofInputContents: (
@@ -200,17 +315,37 @@ async function printFingerprint(taskFile: string, featureFile: string, spurBin:
200
315
  /** Terminal statuses the inline driver may declare when closing its run row. */
201
316
  const CLOSE_STATUSES = new Set(['done', 'failed', 'paused']);
202
317
 
318
+ /**
319
+ * 0937 R2: closed terminal-reason vocabulary the driver may declare on `--close`.
320
+ * COPIED from packages/app/src/workflow/terminal-reason.ts — the plugin standalone
321
+ * contract forbids a value import of app code; the parity test asserts the copy
322
+ * equals the app export.
323
+ */
324
+ const TERMINAL_REASONS = new Set([
325
+ 'done',
326
+ 'paused-operator',
327
+ 'failed-check',
328
+ 'failed-agent',
329
+ 'failed-timeout',
330
+ 'failed-guard',
331
+ 'cancelled',
332
+ 'interrupted',
333
+ 'retry-exhausted',
334
+ ]);
335
+
203
336
  /** Finalize statuses — a finish emission is terminal, so only done|failed are valid (0868 #4). */
204
337
  const ACTION_STATUSES = new Set(['done', 'failed']);
205
338
 
206
339
  /** Input for the ADR-117 emission modes (`--action` / `--close`). */
207
- import type { WorkflowActionTraceWriter } from '@gobing-ai/app';
340
+ import type { WorkflowActionTraceWriter } from '@gobing-ai/spur-app';
208
341
 
209
342
  interface TraceModeInput {
210
343
  readonly runId: string;
211
344
  readonly close: boolean;
212
345
  readonly node: string;
213
346
  readonly kind: string;
347
+ /** Declared terminal reason (0937 R2) — validated against TERMINAL_REASONS before this point. */
348
+ readonly reason?: string;
214
349
  readonly status: string;
215
350
  readonly ok: boolean;
216
351
  readonly durationMs: number;
@@ -218,9 +353,23 @@ interface TraceModeInput {
218
353
  }
219
354
 
220
355
  /**
221
- * Append one emission-failure line to `.spur/run/<run-id>.log` — the run log the inline
222
- * driver already owns. Best-effort and synchronous (the process may exit immediately
223
- * after), and never throws: an unwritable log must not wedge the run (ADR-117 R3).
356
+ * The run-record file for appended driver lines (task 0927 R1): `.spur/run/<run-id>.md`
357
+ * for pair-based runs. A legacy run that predates the pair keeps appending to its declared
358
+ * `.spur/run/<run-id>.log` — the same precedence as `readWorkflowRunRecord` (the pair wins,
359
+ * legacy stays readable in place).
360
+ */
361
+ function runRecordLogPath(runDir: string, runId: string): string {
362
+ const markdownPath = join(runDir, `${runId}.md`);
363
+ const legacyLogPath = join(runDir, `${runId}.log`);
364
+ if (existsSync(legacyLogPath) && !existsSync(markdownPath)) return legacyLogPath;
365
+ return markdownPath;
366
+ }
367
+
368
+ /**
369
+ * Append one emission-failure line to the run record markdown — `.spur/run/<run-id>.md`,
370
+ * the run log the inline driver already owns. Best-effort and synchronous (the process may
371
+ * exit immediately after), and never throws: an unwritable log must not wedge the run
372
+ * (ADR-117 R3).
224
373
  */
225
374
  function appendTraceFailureLine(runId: string, detail: string): void {
226
375
  try {
@@ -228,7 +377,7 @@ function appendTraceFailureLine(runId: string, detail: string): void {
228
377
  if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
229
378
  const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, '_');
230
379
  const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
231
- appendFileSync(join(runDir, `${safeRunId}.log`), `[${stamp}] ${detail}\n`);
380
+ appendFileSync(runRecordLogPath(runDir, safeRunId), `[${stamp}] ${detail}\n`);
232
381
  } catch {
233
382
  // Best-effort (R3): the run continues even when the failure cannot be recorded.
234
383
  }
@@ -254,27 +403,20 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
254
403
  return input.close ? 1 : 0;
255
404
  };
256
405
 
257
- const { entry, repoRoot, chain } = resolveAppEntry(input.spurBin);
258
- if (entry === null || repoRoot === null) {
259
- return fail(
260
- `no monorepo checkout of spur is reachable via ${chain} — the shared trace writer ` +
261
- 'lives in packages/app (point SPUR_BIN at a repo checkout)',
262
- );
263
- }
264
-
265
406
  // Compile-time link (0868 finding #5): the writer half is typed by the real packages/app
266
407
  // export (type-only import, erased at runtime) so a signature drift breaks THIS file's
267
408
  // typecheck instead of hiding behind the hand-declared cast.
268
- const app = (await import(entry)) as {
269
- openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
270
- createWorkflowActionTraceWriter: (
271
- db: unknown,
272
- recordFailure?: (failure: unknown) => void,
273
- ) => WorkflowActionTraceWriter;
274
- };
275
-
276
409
  let projectDb: { adapter: unknown; close: () => void } | undefined;
277
410
  try {
411
+ const { entry } = resolveAppEntry(input.spurBin);
412
+ const app = (await import(entry)) as {
413
+ openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
414
+ createWorkflowActionTraceWriter: (
415
+ db: unknown,
416
+ recordFailure?: (failure: unknown) => void,
417
+ ) => WorkflowActionTraceWriter;
418
+ };
419
+
278
420
  projectDb = await app.openInlineRunProjectDb(process.cwd());
279
421
  const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure: unknown) => {
280
422
  const detail = failure as { operation?: string; error?: string };
@@ -285,7 +427,7 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
285
427
  });
286
428
  const result = (
287
429
  input.close
288
- ? await writer.closeRun(input.runId, input.status)
430
+ ? await writer.closeRun(input.runId, input.status, undefined, input.reason)
289
431
  : await writer.recordAction({
290
432
  runId: input.runId,
291
433
  node: input.node,
@@ -320,7 +462,91 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
320
462
  }
321
463
  }
322
464
 
465
+ /**
466
+ * `--decide` mode (task 0941 R5): execute the non-pausing decide action through the same app
467
+ * function the engine registers (`runDecideForInlineRun` → `DecideActionRunner`), then record
468
+ * the `action_runs` trace row through the shared writer. The decide-enabled switch
469
+ * (`workflow.decideDecisionMaker`) is resolved HERE at the driver boundary (ADR-082) through
470
+ * the bridged facade derivation and passed into the app runner as an explicit parameter —
471
+ * app services never load Spur config. Degraded outcomes (`disabled`,
472
+ * `no-backend`, `error`, `timeout`, `low-confidence`) are normal — the value is the declared
473
+ * default and the run continues — so they print `ok: true` and exit 0. Only an invalid options
474
+ * schema, an unreadable options file, or a failed config load fails closed with exit 1.
475
+ */
476
+ async function runDecideMode(input: {
477
+ runId: string;
478
+ node: string;
479
+ optionsFile: string;
480
+ spurBin: string;
481
+ }): Promise<number> {
482
+ type DecideOutcome = {
483
+ ok: boolean;
484
+ error?: string;
485
+ value?: string;
486
+ degraded?: boolean;
487
+ reason?: string;
488
+ backend?: string | null;
489
+ confidence?: number | null;
490
+ resultFile?: string;
491
+ durationMs?: number;
492
+ };
493
+ const decideFailed = (error: string): number => {
494
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}\n`);
495
+ return 1;
496
+ };
497
+ let outcome: DecideOutcome;
498
+ try {
499
+ const { entry, portable } = resolveAppEntry(input.spurBin);
500
+ const app = (await import(entry)) as {
501
+ runDecideForInlineRun: (input2: {
502
+ workdir: string;
503
+ optionsFile: string;
504
+ enabled: boolean;
505
+ }) => Promise<DecideOutcome>;
506
+ };
507
+ // Config switch at the driver boundary (ADR-082 / task 0941 gate fix): the flag comes
508
+ // from the bridged facade derivation in the generated lib — the same committed bundle
509
+ // the inventory flow imports — never a @gobing-ai/* value import. Embedded schemas
510
+ // ride along only in the portable layout, mirroring the setup mode's load posture.
511
+ const lib = (await import(
512
+ fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url))
513
+ )) as typeof import('../lib/inline-run.generated.mjs');
514
+ const enabled = await lib.resolveDecideDecisionMakerEnabled(
515
+ process.cwd(),
516
+ portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined,
517
+ );
518
+ outcome = await app.runDecideForInlineRun({
519
+ workdir: process.cwd(),
520
+ optionsFile: input.optionsFile,
521
+ enabled,
522
+ });
523
+ } catch (error) {
524
+ return decideFailed(error instanceof Error ? error.message : String(error));
525
+ }
526
+ if (!outcome.ok) return decideFailed(outcome.error ?? 'decide failed without an error message');
527
+ process.stdout.write(`${JSON.stringify({ ok: true, runId: input.runId, node: input.node, ...outcome })}\n`);
528
+ // Trace row is best-effort, exactly like --action: an emission failure never wedges the run.
529
+ return await runTraceMode({
530
+ runId: input.runId,
531
+ close: false,
532
+ node: input.node,
533
+ kind: 'decide',
534
+ status: 'done',
535
+ ok: true,
536
+ durationMs: outcome.durationMs ?? 0,
537
+ spurBin: input.spurBin,
538
+ });
539
+ }
540
+
323
541
  async function main(): Promise<void> {
542
+ // The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
543
+ if (!process.versions.bun) {
544
+ const child = spawnSync('bun', [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
545
+ stdio: 'inherit',
546
+ });
547
+ if (child.error) console.error(`inline-run-setup requires Bun on PATH: ${child.error.message}`);
548
+ process.exit(child.status ?? 1);
549
+ }
324
550
  let runId = '';
325
551
  let file = '';
326
552
  let fingerprint = false;
@@ -328,9 +554,12 @@ async function main(): Promise<void> {
328
554
  let featureFile = '';
329
555
  let action = false;
330
556
  let close = false;
557
+ let decide = false;
558
+ let optionsJson = '';
331
559
  let node = '';
332
560
  let kind = '';
333
561
  let status = '';
562
+ let reason = '';
334
563
  let okRaw = '';
335
564
  let durationRaw = '';
336
565
  let spurBin = getEnvVar('SPUR_BIN') ?? '';
@@ -343,9 +572,12 @@ async function main(): Promise<void> {
343
572
  else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
344
573
  else if (argv[i] === '--action') action = true;
345
574
  else if (argv[i] === '--close') close = true;
575
+ else if (argv[i] === '--decide') decide = true;
576
+ else if (argv[i] === '--options-json') optionsJson = argv[++i] ?? '';
346
577
  else if (argv[i] === '--node') node = argv[++i] ?? '';
347
578
  else if (argv[i] === '--kind') kind = argv[++i] ?? '';
348
579
  else if (argv[i] === '--status') status = argv[++i] ?? '';
580
+ else if (argv[i] === '--reason') reason = argv[++i] ?? '';
349
581
  else if (argv[i] === '--ok') okRaw = argv[++i] ?? '';
350
582
  else if (argv[i] === '--duration-ms') durationRaw = argv[++i] ?? '';
351
583
  else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
@@ -361,12 +593,29 @@ async function main(): Promise<void> {
361
593
  // ADR-117 emission modes (task 0868): the inline driver reports one completed action
362
594
  // boundary, or closes its run row at the declared terminal state. Both share the run-id
363
595
  // filename guard, the app-entry resolution chain and the best-effort failure contract.
596
+ // Non-pausing decide action (0941 R5): --decide is mutually exclusive with the run-setup,
597
+ // fingerprint, and trace modes; it requires the run id (filename-guarded), the state id
598
+ // for the trace row, and the options JSON file.
599
+ if (decide) {
600
+ if (action || close || fingerprint || file !== '' || taskFile !== '') usage();
601
+ if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
602
+ if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
603
+ process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
604
+ }
605
+
364
606
  if (action || close) {
365
607
  if (action && close) usage();
366
608
  if (runId.trim() === '' || status.trim() === '') usage();
367
609
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
368
610
  if (close) {
369
611
  if (!CLOSE_STATUSES.has(status)) usage();
612
+ // 0937 R2: a failed close requires a declared reason, and any declared reason
613
+ // must be a closed-enum value — both fail loudly BEFORE any write happens.
614
+ if (reason.trim() === '') {
615
+ if (status === 'failed') usage();
616
+ } else if (!TERMINAL_REASONS.has(reason)) {
617
+ usage();
618
+ }
370
619
  process.exit(
371
620
  await runTraceMode({
372
621
  runId,
@@ -377,6 +626,7 @@ async function main(): Promise<void> {
377
626
  ok: true,
378
627
  durationMs: 0,
379
628
  spurBin,
629
+ ...(reason.trim() === '' ? {} : { reason }),
380
630
  }),
381
631
  );
382
632
  }
@@ -396,23 +646,7 @@ async function main(): Promise<void> {
396
646
  if (runId.trim() === '' || file.trim() === '') usage();
397
647
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
398
648
 
399
- const { entry, repoRoot, chain } = resolveAppEntry(spurBin);
400
- if (entry === null || repoRoot === null) {
401
- const outcome: SetupOutcome = {
402
- ok: false,
403
- runId,
404
- error:
405
- `inline run setup failed closed: no monorepo checkout of spur is reachable via ${chain}. ` +
406
- 'The authoritative run identity must be persisted by the app service ' +
407
- '(packages/app/src/services/inline-run-setup.ts); a bundle-only install cannot do this. ' +
408
- 'Remediation: point SPUR_BIN at a repo checkout, e.g. ' +
409
- 'SPUR_BIN="bun /path/to/spur/apps/cli/src/index.ts". The pipeline must not run unbound.',
410
- };
411
- writeOutcome(runId, outcome);
412
- console.error(`inline-run-setup: FAIL for run ${runId}`);
413
- console.error(` ${outcome.error}`);
414
- process.exit(1);
415
- }
649
+ const { entry, portable } = resolveAppEntry(spurBin);
416
650
 
417
651
  // Dynamic import by absolute path: the app source graph resolves its own workspace
418
652
  // dependencies from the repo checkout, never from this plugin script's location.
@@ -422,11 +656,22 @@ async function main(): Promise<void> {
422
656
  getDb: () => Promise<unknown>;
423
657
  file: string;
424
658
  runId: string;
659
+ inventory?: unknown;
660
+ embeddedSchemas?: ReadonlyMap<string, string>;
425
661
  }) => Promise<SetupOutcome & { ok: boolean }>;
426
662
  openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
663
+ EMBEDDED_SPUR_SCHEMAS?: ReadonlyMap<string, string>;
427
664
  };
428
665
 
429
666
  const workdir = process.cwd();
667
+ let inventory: unknown;
668
+ try {
669
+ inventory = await readInstalledInventory(file, spurBin);
670
+ } catch (error) {
671
+ const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
672
+ writeOutcome(runId, { ok: false, runId, error: message });
673
+ throw new Error(message);
674
+ }
430
675
  const projectDb = await app.openInlineRunProjectDb(workdir);
431
676
  let exitCode = 0;
432
677
  try {
@@ -435,6 +680,8 @@ async function main(): Promise<void> {
435
680
  getDb: async () => projectDb.adapter,
436
681
  file,
437
682
  runId,
683
+ inventory,
684
+ ...(portable ? { embeddedSchemas: app.EMBEDDED_SPUR_SCHEMAS } : {}),
438
685
  });
439
686
  writeOutcome(runId, result);
440
687
  if (!result.ok) {