@relayflows/sdk 2.0.0

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 (231) hide show
  1. package/dist/authored-flow-error.d.ts +9 -0
  2. package/dist/authored-flow-error.d.ts.map +1 -0
  3. package/dist/authored-flow-error.js +13 -0
  4. package/dist/authored-flow-error.js.map +1 -0
  5. package/dist/authored-flow-executor.d.ts +28 -0
  6. package/dist/authored-flow-executor.d.ts.map +1 -0
  7. package/dist/authored-flow-executor.js +203 -0
  8. package/dist/authored-flow-executor.js.map +1 -0
  9. package/dist/authored-flow-lifecycle.d.ts +61 -0
  10. package/dist/authored-flow-lifecycle.d.ts.map +1 -0
  11. package/dist/authored-flow-lifecycle.js +284 -0
  12. package/dist/authored-flow-lifecycle.js.map +1 -0
  13. package/dist/authored-flow-loader.d.ts +7 -0
  14. package/dist/authored-flow-loader.d.ts.map +1 -0
  15. package/dist/authored-flow-loader.js +39 -0
  16. package/dist/authored-flow-loader.js.map +1 -0
  17. package/dist/authored-flow-operation.d.ts +43 -0
  18. package/dist/authored-flow-operation.d.ts.map +1 -0
  19. package/dist/authored-flow-operation.js +176 -0
  20. package/dist/authored-flow-operation.js.map +1 -0
  21. package/dist/authored-flow.d.ts +12 -0
  22. package/dist/authored-flow.d.ts.map +1 -0
  23. package/dist/authored-flow.js +13 -0
  24. package/dist/authored-flow.js.map +1 -0
  25. package/dist/authored-promise-graph.d.ts +67 -0
  26. package/dist/authored-promise-graph.d.ts.map +1 -0
  27. package/dist/authored-promise-graph.js +206 -0
  28. package/dist/authored-promise-graph.js.map +1 -0
  29. package/dist/backlog-picker.d.ts +55 -0
  30. package/dist/backlog-picker.d.ts.map +1 -0
  31. package/dist/backlog-picker.js +117 -0
  32. package/dist/backlog-picker.js.map +1 -0
  33. package/dist/canonical.d.ts +18 -0
  34. package/dist/canonical.d.ts.map +1 -0
  35. package/dist/canonical.js +55 -0
  36. package/dist/canonical.js.map +1 -0
  37. package/dist/cli/check.d.ts +30 -0
  38. package/dist/cli/check.d.ts.map +1 -0
  39. package/dist/cli/check.js +324 -0
  40. package/dist/cli/check.js.map +1 -0
  41. package/dist/cli/direct-run.d.ts +3 -0
  42. package/dist/cli/direct-run.d.ts.map +1 -0
  43. package/dist/cli/direct-run.js +69 -0
  44. package/dist/cli/direct-run.js.map +1 -0
  45. package/dist/cli/hn-monitor.d.ts +107 -0
  46. package/dist/cli/hn-monitor.d.ts.map +1 -0
  47. package/dist/cli/hn-monitor.js +220 -0
  48. package/dist/cli/hn-monitor.js.map +1 -0
  49. package/dist/cli/interruptible-sleep.d.ts +13 -0
  50. package/dist/cli/interruptible-sleep.d.ts.map +1 -0
  51. package/dist/cli/interruptible-sleep.js +31 -0
  52. package/dist/cli/interruptible-sleep.js.map +1 -0
  53. package/dist/cli/run.d.ts +54 -0
  54. package/dist/cli/run.d.ts.map +1 -0
  55. package/dist/cli/run.js +316 -0
  56. package/dist/cli/run.js.map +1 -0
  57. package/dist/cli/tick-runner.d.ts +151 -0
  58. package/dist/cli/tick-runner.d.ts.map +1 -0
  59. package/dist/cli/tick-runner.js +258 -0
  60. package/dist/cli/tick-runner.js.map +1 -0
  61. package/dist/cli-adapter.d.ts +29 -0
  62. package/dist/cli-adapter.d.ts.map +1 -0
  63. package/dist/cli-adapter.js +92 -0
  64. package/dist/cli-adapter.js.map +1 -0
  65. package/dist/cli-executable.d.ts +3 -0
  66. package/dist/cli-executable.d.ts.map +1 -0
  67. package/dist/cli-executable.js +10 -0
  68. package/dist/cli-executable.js.map +1 -0
  69. package/dist/cli.d.ts +9 -0
  70. package/dist/cli.d.ts.map +1 -0
  71. package/dist/cli.js +348 -0
  72. package/dist/cli.js.map +1 -0
  73. package/dist/compile.d.ts +42 -0
  74. package/dist/compile.d.ts.map +1 -0
  75. package/dist/compile.js +515 -0
  76. package/dist/compile.js.map +1 -0
  77. package/dist/demo-hn-monitor.d.ts +2 -0
  78. package/dist/demo-hn-monitor.d.ts.map +1 -0
  79. package/dist/demo-hn-monitor.js +118 -0
  80. package/dist/demo-hn-monitor.js.map +1 -0
  81. package/dist/dir-watcher-poller.d.ts +73 -0
  82. package/dist/dir-watcher-poller.d.ts.map +1 -0
  83. package/dist/dir-watcher-poller.js +80 -0
  84. package/dist/dir-watcher-poller.js.map +1 -0
  85. package/dist/direct-input.d.ts +10 -0
  86. package/dist/direct-input.d.ts.map +1 -0
  87. package/dist/direct-input.js +61 -0
  88. package/dist/direct-input.js.map +1 -0
  89. package/dist/failure-kinds.d.ts +25 -0
  90. package/dist/failure-kinds.d.ts.map +1 -0
  91. package/dist/failure-kinds.js +64 -0
  92. package/dist/failure-kinds.js.map +1 -0
  93. package/dist/gate-contract.d.ts +24 -0
  94. package/dist/gate-contract.d.ts.map +1 -0
  95. package/dist/gate-contract.js +50 -0
  96. package/dist/gate-contract.js.map +1 -0
  97. package/dist/hn-poller.d.ts +50 -0
  98. package/dist/hn-poller.d.ts.map +1 -0
  99. package/dist/hn-poller.js +79 -0
  100. package/dist/hn-poller.js.map +1 -0
  101. package/dist/index.d.ts +21 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +30 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/journal-client.d.ts +122 -0
  106. package/dist/journal-client.d.ts.map +1 -0
  107. package/dist/journal-client.js +277 -0
  108. package/dist/journal-client.js.map +1 -0
  109. package/dist/json-schema-bound.d.ts +7 -0
  110. package/dist/json-schema-bound.d.ts.map +1 -0
  111. package/dist/json-schema-bound.js +353 -0
  112. package/dist/json-schema-bound.js.map +1 -0
  113. package/dist/json-schema.d.ts +5 -0
  114. package/dist/json-schema.d.ts.map +1 -0
  115. package/dist/json-schema.js +72 -0
  116. package/dist/json-schema.js.map +1 -0
  117. package/dist/json-value.d.ts +6 -0
  118. package/dist/json-value.d.ts.map +1 -0
  119. package/dist/json-value.js +94 -0
  120. package/dist/json-value.js.map +1 -0
  121. package/dist/model-name.d.ts +7 -0
  122. package/dist/model-name.d.ts.map +1 -0
  123. package/dist/model-name.js +21 -0
  124. package/dist/model-name.js.map +1 -0
  125. package/dist/output-schema.d.ts +8 -0
  126. package/dist/output-schema.d.ts.map +1 -0
  127. package/dist/output-schema.js +28 -0
  128. package/dist/output-schema.js.map +1 -0
  129. package/dist/preflight.d.ts +80 -0
  130. package/dist/preflight.d.ts.map +1 -0
  131. package/dist/preflight.js +361 -0
  132. package/dist/preflight.js.map +1 -0
  133. package/dist/protocol.d.ts +359 -0
  134. package/dist/protocol.d.ts.map +1 -0
  135. package/dist/protocol.js +15 -0
  136. package/dist/protocol.js.map +1 -0
  137. package/dist/spec.d.ts +294 -0
  138. package/dist/spec.d.ts.map +1 -0
  139. package/dist/spec.js +13 -0
  140. package/dist/spec.js.map +1 -0
  141. package/dist/step-dependencies.d.ts +2 -0
  142. package/dist/step-dependencies.d.ts.map +1 -0
  143. package/dist/step-dependencies.js +88 -0
  144. package/dist/step-dependencies.js.map +1 -0
  145. package/dist/step-fields.d.ts +17 -0
  146. package/dist/step-fields.d.ts.map +1 -0
  147. package/dist/step-fields.js +32 -0
  148. package/dist/step-fields.js.map +1 -0
  149. package/dist/tick-source.d.ts +205 -0
  150. package/dist/tick-source.d.ts.map +1 -0
  151. package/dist/tick-source.js +234 -0
  152. package/dist/tick-source.js.map +1 -0
  153. package/dist/unknown-keys.d.ts +7 -0
  154. package/dist/unknown-keys.d.ts.map +1 -0
  155. package/dist/unknown-keys.js +47 -0
  156. package/dist/unknown-keys.js.map +1 -0
  157. package/dist/validate.d.ts +7 -0
  158. package/dist/validate.d.ts.map +1 -0
  159. package/dist/validate.js +458 -0
  160. package/dist/validate.js.map +1 -0
  161. package/dist/work-package-consumer.d.ts +33 -0
  162. package/dist/work-package-consumer.d.ts.map +1 -0
  163. package/dist/work-package-consumer.js +44 -0
  164. package/dist/work-package-consumer.js.map +1 -0
  165. package/dist/work-package-validator.d.ts +15 -0
  166. package/dist/work-package-validator.d.ts.map +1 -0
  167. package/dist/work-package-validator.js +72 -0
  168. package/dist/work-package-validator.js.map +1 -0
  169. package/dist/worker-cli.d.ts +16 -0
  170. package/dist/worker-cli.d.ts.map +1 -0
  171. package/dist/worker-cli.js +77 -0
  172. package/dist/worker-cli.js.map +1 -0
  173. package/dist/worker.d.ts +57 -0
  174. package/dist/worker.d.ts.map +1 -0
  175. package/dist/worker.js +129 -0
  176. package/dist/worker.js.map +1 -0
  177. package/dist/wrapper-runtime.d.ts +10 -0
  178. package/dist/wrapper-runtime.d.ts.map +1 -0
  179. package/dist/wrapper-runtime.js +83 -0
  180. package/dist/wrapper-runtime.js.map +1 -0
  181. package/dist/wrapper-session.d.ts +16 -0
  182. package/dist/wrapper-session.d.ts.map +1 -0
  183. package/dist/wrapper-session.js +244 -0
  184. package/dist/wrapper-session.js.map +1 -0
  185. package/package.json +50 -0
  186. package/src/authored-flow-error.ts +31 -0
  187. package/src/authored-flow-executor.ts +365 -0
  188. package/src/authored-flow-lifecycle.ts +344 -0
  189. package/src/authored-flow-loader.ts +44 -0
  190. package/src/authored-flow-operation.ts +252 -0
  191. package/src/authored-flow.ts +21 -0
  192. package/src/authored-promise-graph.ts +196 -0
  193. package/src/backlog-picker.ts +153 -0
  194. package/src/canonical.ts +57 -0
  195. package/src/cli/check.ts +391 -0
  196. package/src/cli/direct-run.ts +86 -0
  197. package/src/cli/hn-monitor.ts +286 -0
  198. package/src/cli/interruptible-sleep.ts +27 -0
  199. package/src/cli/run.ts +439 -0
  200. package/src/cli/tick-runner.ts +378 -0
  201. package/src/cli-adapter.ts +115 -0
  202. package/src/cli-executable.ts +12 -0
  203. package/src/cli.ts +364 -0
  204. package/src/compile.ts +573 -0
  205. package/src/demo-hn-monitor.ts +132 -0
  206. package/src/dir-watcher-poller.ts +122 -0
  207. package/src/direct-input.ts +69 -0
  208. package/src/failure-kinds.ts +77 -0
  209. package/src/gate-contract.ts +72 -0
  210. package/src/hn-poller.ts +104 -0
  211. package/src/index.ts +195 -0
  212. package/src/journal-client.ts +379 -0
  213. package/src/json-schema-bound.ts +357 -0
  214. package/src/json-schema.ts +65 -0
  215. package/src/json-value.ts +110 -0
  216. package/src/model-name.ts +20 -0
  217. package/src/output-schema.ts +33 -0
  218. package/src/preflight.ts +482 -0
  219. package/src/protocol.ts +364 -0
  220. package/src/spec.ts +343 -0
  221. package/src/step-dependencies.ts +100 -0
  222. package/src/step-fields.ts +36 -0
  223. package/src/tick-source.ts +334 -0
  224. package/src/unknown-keys.ts +52 -0
  225. package/src/validate.ts +503 -0
  226. package/src/work-package-consumer.ts +73 -0
  227. package/src/work-package-validator.ts +90 -0
  228. package/src/worker-cli.ts +110 -0
  229. package/src/worker.ts +150 -0
  230. package/src/wrapper-runtime.ts +93 -0
  231. package/src/wrapper-session.ts +287 -0
package/src/compile.ts ADDED
@@ -0,0 +1,573 @@
1
+ // The YAML -> spec compiler. Authoring surface in, the kernel's spec dialect
2
+ // out (RFC settled decision #5: the composable unit is the spec, not any
3
+ // language). Compilation is two mappings:
4
+ //
5
+ // compileYaml / compileSpec — parse + validate (fail-closed) + authoring
6
+ // defaults, staying in the authoring shape.
7
+ // toKernelSpec — authoring shape -> the ONE boundary dialect
8
+ // the kernel parses, journals, and hashes
9
+ // (snake_case, flat v0 verification, defaults
10
+ // materialized).
11
+ //
12
+ // `compileYamlToCanonicalJson` / `specHash` operate on the kernel dialect, so
13
+ // sha256(canonical JSON) equals the kernel's `spec_hash`. That claim is
14
+ // proven, not asserted: `tests/spec-parity.test.ts` and the kernel's
15
+ // `tests/spec_parity.rs` pin both sides to the same `testdata/` fixture.
16
+
17
+ import { parse as parseYaml } from 'yaml';
18
+ import type {
19
+ AgentStepSpec,
20
+ DeterministicStepSpec,
21
+ FlowSpec,
22
+ KernelAgentStep,
23
+ KernelRunSpec,
24
+ KernelStepCommon,
25
+ KernelStepSpec,
26
+ KernelTriggerSpec,
27
+ KernelVerificationSpec,
28
+ LlmStepSpec,
29
+ NamedAgentSpec,
30
+ OutputVerificationSpec,
31
+ StepSpec,
32
+ StepType,
33
+ TriggerSpec,
34
+ VerificationSpec,
35
+ } from './spec.js';
36
+ import { SPEC_SCHEMA_VERSION } from './spec.js';
37
+ import { canonicalize, specHash } from './canonical.js';
38
+ import { validateOutputDeclaration } from './output-schema.js';
39
+ import { validateSpec, type ValidationResult } from './validate.js';
40
+ import { snapshotJsonValue } from './json-value.js';
41
+
42
+ export class CompileError extends Error {
43
+ readonly errors: string[];
44
+ constructor(errors: string[]) {
45
+ super('spec compile failed:\n - ' + errors.join('\n - '));
46
+ this.name = 'CompileError';
47
+ this.errors = errors;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Compile a YAML string into a validated authoring `FlowSpec`.
53
+ * Throws `CompileError` on a YAML parse error or any validation failure.
54
+ */
55
+ export function compileYaml(yaml: string): FlowSpec {
56
+ const parsed = parseYaml(yaml);
57
+ if (parsed === null || typeof parsed !== 'object') {
58
+ throw new CompileError(['YAML: expected a mapping at the top level']);
59
+ }
60
+ return compileSpec(parsed);
61
+ }
62
+
63
+ /** Kernel-dialect canonical JSON of `compileYaml` (sorted keys, no whitespace). */
64
+ export function compileYamlToCanonicalJson(yaml: string): string {
65
+ return canonicalize(toKernelSpec(compileYaml(yaml)));
66
+ }
67
+
68
+ /**
69
+ * Validate a parsed spec object and apply authoring defaults, returning a
70
+ * normalized `FlowSpec`. Throws `CompileError` on validation failure.
71
+ */
72
+ export function compileSpec(spec: unknown): FlowSpec {
73
+ let snapshot: unknown;
74
+ try {
75
+ snapshot = snapshotJsonValue(spec, 'spec');
76
+ } catch (error) {
77
+ throw new CompileError([
78
+ error instanceof Error ? error.message : 'spec: expected JSON-compatible data',
79
+ ]);
80
+ }
81
+ const validation: ValidationResult = validateSpec(snapshot);
82
+ if (!validation.ok) throw new CompileError(validation.errors);
83
+
84
+ const input = snapshot as FlowSpec;
85
+ // Preserve named declarations and selectors through authoring normalization.
86
+ // They are resolved exactly once at the kernel boundary, after public
87
+ // preflight has validated every declaration with truthful provenance.
88
+ const steps = input.steps.map(compileStep);
89
+ const flow: FlowSpec = {
90
+ version: input.version,
91
+ ...(input.name !== undefined ? { name: input.name } : {}),
92
+ ...(input.description !== undefined ? { description: input.description } : {}),
93
+ ...(input.cli !== undefined ? { cli: input.cli } : {}),
94
+ ...(input.agents !== undefined ? { agents: input.agents } : {}),
95
+ // The kernel omits an empty trigger list when serializing RunSpec. Normalize
96
+ // it here so the authoring shape and boundary shape retain one hashable form.
97
+ ...(input.triggers?.length ? { triggers: input.triggers } : {}),
98
+ steps,
99
+ ...(input.budget !== undefined ? { budget: input.budget } : {}),
100
+ };
101
+ return flow;
102
+ }
103
+
104
+ function compileStep(step: StepSpec): StepSpec {
105
+ const maxIterations = step.maxIterations ?? 1;
106
+ const verification = typedOutputVerification(step);
107
+ const base = {
108
+ id: step.id,
109
+ type: step.type,
110
+ ...(step.dependsOn !== undefined ? { dependsOn: step.dependsOn } : {}),
111
+ maxIterations,
112
+ };
113
+
114
+ switch (step.type as StepType) {
115
+ case 'deterministic': {
116
+ const s = step as DeterministicStepSpec;
117
+ // A deterministic step with no verification gets the implicit exit_code
118
+ // gate. Read the SHARED `verification`, never `s.verification`: for this
119
+ // verb the two are equal today (typedOutputVerification returns
120
+ // `step.verification` unchanged, because `output` is not authorable on a
121
+ // deterministic step), but reading the shared binding is what keeps every
122
+ // branch of this switch on the lowered gate rather than the raw authored
123
+ // one. See ops/reviews/20260903-pr139-repair-0903.md §10, trap 2.
124
+ return {
125
+ ...base,
126
+ type: 'deterministic',
127
+ command: s.command,
128
+ verification: verification ?? { type: 'exit_code' as const },
129
+ // #138: `timeoutMs` is deterministic-only — worker-backed verbs own
130
+ // their dispatch timeout. It must be spread HERE and nowhere in `base`.
131
+ ...(s.timeoutMs !== undefined ? { timeoutMs: s.timeoutMs } : {}),
132
+ };
133
+ }
134
+ case 'llm': {
135
+ const s = step as LlmStepSpec;
136
+ return {
137
+ ...base,
138
+ type: 'llm',
139
+ prompt: s.prompt,
140
+ // From the shared `verification` — which may have been lowered from an
141
+ // `output` declaration — never from `s.verification`, which would drop
142
+ // that lowering.
143
+ ...(verification !== undefined ? { verification: outputGate(verification, s.id) } : {}),
144
+ ...(s.model !== undefined ? { model: s.model } : {}),
145
+ ...(s.cli !== undefined ? { cli: s.cli } : {}),
146
+ };
147
+ }
148
+ case 'agent': {
149
+ const s = step as AgentStepSpec;
150
+ const recoveryMode = s.recoveryMode ?? 'reset';
151
+ return {
152
+ ...base,
153
+ type: 'agent',
154
+ instruction: s.instruction,
155
+ ...(verification !== undefined ? { verification: outputGate(verification, s.id) } : {}),
156
+ ...(s.agent !== undefined ? { agent: s.agent } : {}),
157
+ ...(s.cli !== undefined ? { cli: s.cli } : {}),
158
+ ...(s.model !== undefined ? { model: s.model } : {}),
159
+ recoveryMode,
160
+ ...(s.surfaces !== undefined ? { surfaces: s.surfaces } : {}),
161
+ ...(s.permissions !== undefined ? { permissions: s.permissions } : {}),
162
+ };
163
+ }
164
+ default:
165
+ // validateSpec already gated this; unreachable.
166
+ throw new CompileError([`step "${step.id}": unknown type "${String((step as { type: unknown }).type)}"`]);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Narrow a gate to the ones an `llm`/`agent` step may carry. `validateSpec`
172
+ * already refuses `exit_code` off a deterministic step, so this is the
173
+ * fail-closed backstop for a runtime value cast past the authoring types.
174
+ */
175
+ function outputGate(gate: VerificationSpec, stepId: string): OutputVerificationSpec {
176
+ if (gate.type === 'exit_code') {
177
+ throw new CompileError([
178
+ `step "${stepId}": exit_code is supported only on deterministic steps`,
179
+ ]);
180
+ }
181
+ return gate;
182
+ }
183
+
184
+ function typedOutputVerification(step: StepSpec): StepSpec['verification'] {
185
+ if (step.type !== 'deterministic' && step.output !== undefined) {
186
+ const errors = validateOutputDeclaration(step, `step "${step.id}"`);
187
+ if (errors.length > 0) throw new CompileError(errors);
188
+ return { type: 'json_schema', schema: step.output };
189
+ }
190
+ return step.verification;
191
+ }
192
+
193
+ /**
194
+ * Resolve declarative named-agent sugar at kernel lowering. Explicit step
195
+ * fields win independently, so an author may override
196
+ * only the CLI or only the model. The selector and declaration map never
197
+ * cross the journal boundary.
198
+ */
199
+ function resolveNamedAgent(
200
+ step: StepSpec,
201
+ agents: Record<string, NamedAgentSpec> | undefined,
202
+ ): StepSpec {
203
+ if (step.type !== 'agent' || step.agent === undefined) return step;
204
+ const declaration = agents !== undefined && Object.hasOwn(agents, step.agent)
205
+ ? agents[step.agent]
206
+ : undefined;
207
+ if (declaration === undefined) {
208
+ throw new CompileError([
209
+ `step "${step.id}": unknown named agent "${step.agent}"`,
210
+ ]);
211
+ }
212
+ return {
213
+ ...step,
214
+ ...(step.cli === undefined ? { cli: declaration.cli } : {}),
215
+ ...(step.model === undefined ? { model: declaration.model } : {}),
216
+ };
217
+ }
218
+
219
+ // Kernel defaults, materialized at compile time so the emitted spec is
220
+ // byte-identical to the kernel's own serialization of it (spec.rs defaults).
221
+ const KERNEL_RETRY_DEFAULTS = {
222
+ initial_backoff_ms: 100,
223
+ max_backoff_ms: 60_000,
224
+ multiplier: 2,
225
+ jitter_percent: 20,
226
+ } as const;
227
+
228
+ /**
229
+ * Map an authoring `FlowSpec` to the kernel spec dialect — the single shape at
230
+ * the SDK↔kernel boundary (`kernel/relayflowd-core/src/spec.rs`). Authoring
231
+ * sugar that the dialect cannot carry is a `CompileError`, never a silent drop.
232
+ */
233
+ export function toKernelSpec(flow: FlowSpec): KernelRunSpec {
234
+ // This public boundary is callable without compileSpec. Compile again so
235
+ // runtime casts are validated and all returned schema data is snapshotted.
236
+ const compiled = compileSpec(flow);
237
+
238
+ return {
239
+ version: compiled.version,
240
+ ...(compiled.name !== undefined ? { name: compiled.name } : {}),
241
+ ...(compiled.description !== undefined ? { description: compiled.description } : {}),
242
+ ...(compiled.cli !== undefined ? { cli: compiled.cli } : {}),
243
+ // Both halves are load-bearing, and this hunk is trap 2's shape a second
244
+ // time. `compiled.*` is #139's snapshot guard: this boundary is callable
245
+ // without compileSpec, so it recompiles and reads the validated, snapshotted
246
+ // spec rather than the caller's raw object. `.map(toKernelTrigger)` is
247
+ // #151's LOWERING of authoring trigger keys into the kernel dialect --
248
+ // authoring sugar that becomes a different object at the boundary, exactly
249
+ // like `output:`. Taking either side of this conflict wholesale silently
250
+ // reverts the other, and `validateSpec` and `flows check` would both still
251
+ // look correct. See ops/reviews/20260903-pr139-repair-0903.md section 10.
252
+ ...(compiled.triggers?.length ? { triggers: compiled.triggers.map(toKernelTrigger) } : {}),
253
+ steps: compiled.steps.map((step) => toKernelStep(resolveNamedAgent(step, compiled.agents))),
254
+ ...(compiled.budget !== undefined
255
+ ? {
256
+ budget: {
257
+ ...(compiled.budget.maxTokensIn !== undefined ? { max_tokens_in: compiled.budget.maxTokensIn } : {}),
258
+ ...(compiled.budget.maxTokensOut !== undefined ? { max_tokens_out: compiled.budget.maxTokensOut } : {}),
259
+ ...(compiled.budget.maxDollars !== undefined ? { max_dollars: compiled.budget.maxDollars } : {}),
260
+ },
261
+ }
262
+ : {}),
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Map the kernel boundary dialect back to the normalized authoring shape.
268
+ * This is the inverse of `toKernelSpec` over specs this compiler emits.
269
+ * Kernel-only values with no authoring representation are refused.
270
+ */
271
+ export function kernelToAuthoring(value: unknown): unknown {
272
+ let snapshot: unknown;
273
+ try {
274
+ snapshot = snapshotJsonValue(value, 'spec');
275
+ } catch (error) {
276
+ throw new CompileError([
277
+ error instanceof Error ? error.message : 'spec: expected JSON-compatible data',
278
+ ]);
279
+ }
280
+ const root = requireKernelObject(
281
+ snapshot,
282
+ ['version', 'name', 'description', 'cli', 'triggers', 'steps', 'budget'],
283
+ 'spec',
284
+ );
285
+ const steps = requireKernelArray(root['steps'], 'spec.steps')
286
+ .map((step, index) => kernelStepToAuthoring(step, `spec.steps[${index}]`));
287
+ const triggers = root['triggers'];
288
+ return {
289
+ ...copyDefined(root, ['version', 'name', 'description', 'cli']),
290
+ ...(triggers !== undefined
291
+ ? {
292
+ triggers: requireKernelArray(triggers, 'spec.triggers')
293
+ .map((trigger, index) => kernelTriggerToAuthoring(trigger, `spec.triggers[${index}]`)),
294
+ }
295
+ : {}),
296
+ steps,
297
+ ...(root['budget'] !== undefined
298
+ ? { budget: kernelBudgetToAuthoring(root['budget'], 'spec.budget') }
299
+ : {}),
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Lower one authoring trigger into the kernel dialect.
305
+ *
306
+ * This mapping was missing entirely: `toKernelSpec` used to spread
307
+ * `flow.triggers` through untouched, so every event subscription reached the
308
+ * kernel in camelCase and `relayflowd` — whose `TriggerSpec` is
309
+ * `#[serde(deny_unknown_fields)]` over snake_case — refused the spec outright:
310
+ *
311
+ * malformed run spec: unknown field `dedupeKeyTemplate`, expected one of
312
+ * `id`, `executor`, `event_type`, `pattern`, `dedupe_key_template`,
313
+ * `stale_after_ms`
314
+ *
315
+ * The committed `testdata/*.spec.canonical.json` fixtures are snake_case and
316
+ * the kernel accepts them, which is why nothing noticed: no test compiled a
317
+ * triggered flow through this function and compared it to a fixture. Every
318
+ * triggered flow in `testdata/` was therefore unauthorable through the
319
+ * supported SDK path. `tests/spec-parity.test.ts` now pins the mapping.
320
+ */
321
+ function toKernelTrigger(trigger: TriggerSpec): KernelTriggerSpec {
322
+ return {
323
+ id: trigger.id,
324
+ executor: trigger.executor,
325
+ ...(trigger.eventType !== undefined ? { event_type: trigger.eventType } : {}),
326
+ ...(trigger.pattern !== undefined ? { pattern: trigger.pattern } : {}),
327
+ ...(trigger.dedupeKeyTemplate !== undefined
328
+ ? { dedupe_key_template: trigger.dedupeKeyTemplate }
329
+ : {}),
330
+ ...(trigger.staleAfterMs !== undefined ? { stale_after_ms: trigger.staleAfterMs } : {}),
331
+ };
332
+ }
333
+
334
+ /** Inverse of `toKernelTrigger`. Kernel-only keys are refused, never dropped. */
335
+ function kernelTriggerToAuthoring(value: unknown, at: string): unknown {
336
+ const trigger = requireKernelObject(
337
+ value,
338
+ ['id', 'executor', 'event_type', 'pattern', 'dedupe_key_template', 'stale_after_ms'],
339
+ at,
340
+ );
341
+ return {
342
+ id: trigger['id'],
343
+ executor: trigger['executor'],
344
+ ...(trigger['event_type'] !== undefined ? { eventType: trigger['event_type'] } : {}),
345
+ ...(trigger['pattern'] !== undefined ? { pattern: trigger['pattern'] } : {}),
346
+ ...(trigger['dedupe_key_template'] !== undefined
347
+ ? { dedupeKeyTemplate: trigger['dedupe_key_template'] }
348
+ : {}),
349
+ ...(trigger['stale_after_ms'] !== undefined ? { staleAfterMs: trigger['stale_after_ms'] } : {}),
350
+ };
351
+ }
352
+
353
+ function kernelStepToAuthoring(value: unknown, at: string): unknown {
354
+ const unionKeys = [
355
+ 'id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification',
356
+ 'command', 'timeout_ms', 'prompt', 'model', 'cli', 'instruction',
357
+ 'recovery_mode', 'surfaces', 'permissions',
358
+ ] as const;
359
+ const step = requireKernelObject(value, unionKeys, at);
360
+ const type = step['type'];
361
+ const commonKeys = ['id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification'] as const;
362
+ const typeKeys = type === 'deterministic'
363
+ ? ['command', 'timeout_ms'] as const
364
+ : type === 'llm'
365
+ ? ['prompt', 'model', 'cli'] as const
366
+ : type === 'agent'
367
+ ? ['instruction', 'cli', 'model', 'recovery_mode', 'surfaces', 'permissions'] as const
368
+ : [];
369
+ assertKernelKeys(step, [...commonKeys, ...typeKeys], at);
370
+ if (step['retry'] !== undefined) validateKernelRetry(step['retry'], `${at}.retry`);
371
+ const dependsOn = step['depends_on'];
372
+ const common = {
373
+ id: step['id'],
374
+ type,
375
+ ...(dependsOn !== undefined && (!Array.isArray(dependsOn) || dependsOn.length > 0)
376
+ ? { dependsOn }
377
+ : {}),
378
+ ...(step['max_iterations'] !== undefined ? { maxIterations: step['max_iterations'] } : {}),
379
+ ...kernelVerificationToAuthoring(type, step['verification'], `${at}.verification`),
380
+ };
381
+ if (type === 'deterministic') {
382
+ return {
383
+ ...common,
384
+ command: step['command'],
385
+ ...(step['timeout_ms'] !== undefined ? { timeoutMs: step['timeout_ms'] } : {}),
386
+ };
387
+ }
388
+ if (type === 'llm') {
389
+ return { ...common, prompt: step['prompt'], ...copyDefined(step, ['model', 'cli']) };
390
+ }
391
+ if (type === 'agent') {
392
+ return {
393
+ ...common,
394
+ instruction: step['instruction'],
395
+ ...(step['recovery_mode'] !== undefined ? { recoveryMode: step['recovery_mode'] } : {}),
396
+ ...copyDefined(step, ['cli', 'model', 'surfaces']),
397
+ ...(step['permissions'] !== undefined
398
+ ? { permissions: kernelPermissionsToAuthoring(step['permissions'], `${at}.permissions`) }
399
+ : {}),
400
+ };
401
+ }
402
+ return common;
403
+ }
404
+
405
+ function validateKernelRetry(value: unknown, at: string): void {
406
+ const retry = requireKernelObject(value, [
407
+ 'initial_backoff_ms', 'max_backoff_ms', 'multiplier', 'jitter_percent',
408
+ ], at);
409
+ for (const [field, expected] of Object.entries(KERNEL_RETRY_DEFAULTS)) {
410
+ if (retry[field] !== expected) {
411
+ throw new CompileError([
412
+ `${at}.${field} must equal the authoring default ${expected}`,
413
+ ]);
414
+ }
415
+ }
416
+ }
417
+
418
+ function kernelVerificationToAuthoring(
419
+ type: unknown,
420
+ value: unknown,
421
+ at: string,
422
+ ): Record<string, unknown> {
423
+ if (value === undefined) return {};
424
+ const verification = requireKernelObject(value, ['output_contains', 'json_schema'], at);
425
+ if (verification['output_contains'] !== undefined && verification['json_schema'] !== undefined) {
426
+ throw new CompileError([`${at} may not contain two gates in spec v0.1.0`]);
427
+ }
428
+ if (verification['output_contains'] !== undefined) {
429
+ return { verification: { type: 'output_contains', value: verification['output_contains'] } };
430
+ }
431
+ if (verification['json_schema'] !== undefined) {
432
+ return { verification: { type: 'json_schema', schema: verification['json_schema'] } };
433
+ }
434
+ return type === 'deterministic' ? { verification: { type: 'exit_code' } } : {};
435
+ }
436
+
437
+ function kernelBudgetToAuthoring(value: unknown, at: string): unknown {
438
+ const budget = requireKernelObject(value, ['max_tokens_in', 'max_tokens_out', 'max_dollars'], at);
439
+ return {
440
+ ...(budget['max_tokens_in'] !== undefined ? { maxTokensIn: budget['max_tokens_in'] } : {}),
441
+ ...(budget['max_tokens_out'] !== undefined ? { maxTokensOut: budget['max_tokens_out'] } : {}),
442
+ ...(budget['max_dollars'] !== undefined ? { maxDollars: budget['max_dollars'] } : {}),
443
+ };
444
+ }
445
+
446
+ function kernelPermissionsToAuthoring(value: unknown, at: string): unknown {
447
+ const permissions = requireKernelObject(value, ['file_globs', 'network_allowlist', 'access_preset'], at);
448
+ return {
449
+ ...(permissions['file_globs'] !== undefined ? { fileGlobs: permissions['file_globs'] } : {}),
450
+ ...(permissions['network_allowlist'] !== undefined ? { networkAllowlist: permissions['network_allowlist'] } : {}),
451
+ ...(permissions['access_preset'] !== undefined ? { accessPreset: permissions['access_preset'] } : {}),
452
+ };
453
+ }
454
+
455
+ function requireKernelObject(
456
+ value: unknown,
457
+ allowed: readonly string[],
458
+ at: string,
459
+ ): Record<string, unknown> {
460
+ if (!isObject(value)) {
461
+ throw new CompileError([`${at}: expected an object`]);
462
+ }
463
+ assertKernelKeys(value, allowed, at);
464
+ return value;
465
+ }
466
+
467
+ function assertKernelKeys(
468
+ value: Record<string, unknown>,
469
+ allowed: readonly string[],
470
+ at: string,
471
+ ): void {
472
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
473
+ if (unknown.length > 0) {
474
+ throw new CompileError([
475
+ `${at}: unknown ${unknown.length === 1 ? 'key' : 'keys'} ${unknown.map((key) => `"${key}"`).join(', ')} (expected one of ${allowed.join(' | ')})`,
476
+ ]);
477
+ }
478
+ }
479
+
480
+ function requireKernelArray(value: unknown, at: string): unknown[] {
481
+ if (!Array.isArray(value)) throw new CompileError([`${at}: expected an array`]);
482
+ return value;
483
+ }
484
+
485
+ function copyDefined(value: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {
486
+ return Object.fromEntries(keys.filter((key) => value[key] !== undefined).map((key) => [key, value[key]]));
487
+ }
488
+
489
+ function isObject(value: unknown): value is Record<string, unknown> {
490
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
491
+ }
492
+
493
+ function toKernelStep(step: StepSpec): KernelStepSpec {
494
+ const common: KernelStepCommon = {
495
+ id: step.id,
496
+ depends_on: step.dependsOn ?? [],
497
+ max_iterations: step.maxIterations ?? 1,
498
+ retry: { ...KERNEL_RETRY_DEFAULTS },
499
+ verification: toKernelVerification(step),
500
+ };
501
+ switch (step.type) {
502
+ case 'deterministic':
503
+ return {
504
+ ...common,
505
+ type: 'deterministic',
506
+ command: step.command,
507
+ ...(step.timeoutMs !== undefined ? { timeout_ms: step.timeoutMs } : {}),
508
+ };
509
+ case 'llm': {
510
+ return {
511
+ ...common,
512
+ type: 'llm',
513
+ prompt: step.prompt,
514
+ ...(step.model !== undefined ? { model: step.model } : {}),
515
+ ...(step.cli !== undefined ? { cli: step.cli } : {}),
516
+ };
517
+ }
518
+ case 'agent': {
519
+ const out: KernelAgentStep = {
520
+ ...common,
521
+ type: 'agent',
522
+ instruction: step.instruction,
523
+ ...(step.cli !== undefined ? { cli: step.cli } : {}),
524
+ ...(step.model !== undefined ? { model: step.model } : {}),
525
+ recovery_mode: step.recoveryMode ?? 'reset',
526
+ };
527
+ const surfaces = {
528
+ ...(step.surfaces?.workspace?.length ? { workspace: step.surfaces.workspace.map((w) => ({ surface: w.surface })) } : {}),
529
+ ...(step.surfaces?.streams?.length ? { streams: step.surfaces.streams.map((s) => ({ stream: s.stream })) } : {}),
530
+ ...(step.surfaces?.external?.length ? { external: step.surfaces.external } : {}),
531
+ };
532
+ if (Object.keys(surfaces).length > 0) out.surfaces = surfaces;
533
+ if (step.permissions !== undefined) {
534
+ out.permissions = {
535
+ ...(step.permissions.fileGlobs !== undefined ? { file_globs: step.permissions.fileGlobs } : {}),
536
+ ...(step.permissions.networkAllowlist !== undefined ? { network_allowlist: step.permissions.networkAllowlist } : {}),
537
+ ...(step.permissions.accessPreset !== undefined ? { access_preset: step.permissions.accessPreset } : {}),
538
+ };
539
+ }
540
+ return out;
541
+ }
542
+ }
543
+ }
544
+
545
+ function toKernelVerification(step: StepSpec): KernelVerificationSpec {
546
+ const output = step.type === 'deterministic' ? undefined : step.output;
547
+ if (output !== undefined) {
548
+ const errors = validateOutputDeclaration(step, `step "${step.id}"`);
549
+ if (errors.length > 0) throw new CompileError(errors);
550
+ return { json_schema: output };
551
+ }
552
+ const gate = step.verification;
553
+ // Validation permits explicit exit_code only on deterministic steps, where
554
+ // {} selects the kernel's implicit exit_code == 0 gate (DESIGN.md §4).
555
+ if (gate === undefined || gate.type === 'exit_code') return {};
556
+ if (gate.type === 'output_contains') return { output_contains: gate.value };
557
+ if (gate.type === 'json_schema') return { json_schema: gate.schema };
558
+ throw new CompileError([
559
+ `step "${step.id}".verification.type: expected exit_code | output_contains | json_schema`,
560
+ ]);
561
+ }
562
+
563
+ /**
564
+ * Compile + hash in one call. `hash` is sha256 of the kernel-dialect canonical
565
+ * JSON — the spec identity the kernel stamps as `spec_hash` in `run.spawned`.
566
+ */
567
+ export function compileAndHash(yaml: string): { spec: FlowSpec; kernelSpec: KernelRunSpec; hash: string } {
568
+ const spec = compileYaml(yaml);
569
+ const kernelSpec = toKernelSpec(spec);
570
+ return { spec, kernelSpec, hash: specHash(kernelSpec) };
571
+ }
572
+
573
+ export { SPEC_SCHEMA_VERSION, canonicalize, specHash };