@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
@@ -0,0 +1,482 @@
1
+ import type { FlowSpec, StepSpec, TriggerSpec } from './spec.js';
2
+ import { acceptsAnyOutput, inspectStepGate, type StepGateInspection } from './gate-contract.js';
3
+ import { compileSpec, CompileError } from './compile.js';
4
+ import type {
5
+ PreflightFailureKind,
6
+ PreflightWarningKind,
7
+ } from './failure-kinds.js';
8
+
9
+ export type CliResolutionSource = 'step' | 'named' | 'flow' | 'project';
10
+
11
+ export interface CliResolution {
12
+ stepId: string;
13
+ cli: string;
14
+ source: CliResolutionSource;
15
+ /** Model the step or selected named agent declared, probed with the CLI. */
16
+ model?: string;
17
+ }
18
+
19
+ export interface CliProbeResult {
20
+ exists: boolean;
21
+ authenticated: boolean;
22
+ /** False when a custom executable did not identify as a wrapper adapter. */
23
+ supported?: boolean;
24
+ /** Exact declared model passed the CLI's model-scoped readiness probe. */
25
+ modelAvailable?: boolean;
26
+ authCommand?: string;
27
+ modelCommand?: string;
28
+ }
29
+
30
+ export type CliProbeFailureDetail =
31
+ | 'spawn_failed'
32
+ | `timeout:${number}ms`
33
+ | `signal:${string}`;
34
+
35
+ export class CliProbeError extends Error {
36
+ constructor(readonly detail: CliProbeFailureDetail) {
37
+ super('CLI probe failed');
38
+ }
39
+ }
40
+
41
+ type CliProbeOutcome =
42
+ | { result: CliProbeResult }
43
+ | { failure: CliProbeFailureDetail | null };
44
+
45
+ /**
46
+ * Environment facts are injected; this module performs no I/O. A probe may
47
+ * throw when its fact cannot be collected. Preflight catches that boundary and
48
+ * emits `probe_failed` (or `command_unprovable` for a deterministic command).
49
+ */
50
+ export interface PreflightProbes {
51
+ /**
52
+ * Resolve relative paths against the file implied by `source`, then probe
53
+ * `auth status`. When the step declared a `model`, the probe runs with that
54
+ * model in scope, so readiness answers "can this CLI use THIS model" rather
55
+ * than the weaker "is this CLI authenticated at all".
56
+ */
57
+ cli(cli: string, source: CliResolutionSource, model?: string): CliProbeResult;
58
+ executor(trigger: TriggerSpec): boolean;
59
+ command(binary: string): boolean;
60
+ }
61
+
62
+ export interface PreflightOptions {
63
+ projectCli?: string;
64
+ projectConfigPath?: string;
65
+ projectSearchStart?: string;
66
+ /** Exact, project-owned model allowlist from the nearest flows.json. */
67
+ models?: readonly string[];
68
+ modelRegistryPath?: string;
69
+ probes: PreflightProbes;
70
+ }
71
+
72
+ export interface PreflightRefusal {
73
+ severity: 'refusal';
74
+ kind: PreflightFailureKind;
75
+ message: string;
76
+ stepId?: string;
77
+ cli?: string;
78
+ agent?: string;
79
+ model?: string;
80
+ triggerId?: string;
81
+ executor?: string;
82
+ detail?: CliProbeFailureDetail;
83
+ /** Author-facing validation errors when kind is `invalid_spec`. */
84
+ errors?: string[];
85
+ }
86
+
87
+ export interface PreflightWarning {
88
+ severity: 'warning';
89
+ kind: PreflightWarningKind;
90
+ message: string;
91
+ stepId?: string;
92
+ }
93
+
94
+ export type PreflightDiagnostic = PreflightRefusal | PreflightWarning;
95
+
96
+ export interface PreflightResult {
97
+ ok: boolean;
98
+ gates: StepGateInspection[];
99
+ resolutions: CliResolution[];
100
+ diagnostics: PreflightDiagnostic[];
101
+ }
102
+
103
+ export function preflight(flow: FlowSpec, options: PreflightOptions): PreflightResult {
104
+ // Compile before touching any environment fact. `compileSpec` snapshots raw
105
+ // input into inert data, validates it against the closed authoring schema,
106
+ // and lowers `output` sugar into its json_schema gate — so the gate plan
107
+ // below describes what the kernel will actually judge, and no probe or gate
108
+ // inspection ever reads a live accessor. The failure is a named refusal
109
+ // rather than a thrown error (RFC covenant 2), which is the contract main
110
+ // settled for this boundary.
111
+ let compiled: FlowSpec;
112
+ try {
113
+ compiled = compileSpec(flow);
114
+ } catch (error) {
115
+ const errors = error instanceof CompileError
116
+ ? error.errors
117
+ : [error instanceof Error ? error.message : 'spec: expected JSON-compatible data'];
118
+ return {
119
+ ok: false,
120
+ gates: [],
121
+ resolutions: [],
122
+ diagnostics: [{
123
+ severity: 'refusal',
124
+ kind: 'invalid_spec',
125
+ message: `Relayflow spec is invalid: ${errors.join('; ')}`,
126
+ errors,
127
+ }],
128
+ };
129
+ }
130
+ const diagnostics: PreflightDiagnostic[] = [];
131
+ const resolutions: CliResolution[] = [];
132
+ const resolutionByStep = new Map<string, CliResolution>();
133
+ const cliProbeResults = new Map<string, CliProbeOutcome>();
134
+
135
+ diagnostics.push(...unknownModelDiagnostics(compiled, options));
136
+ // Resolve the complete flow before touching any environment fact. A later
137
+ // statically unresolved CLI makes the whole submission impossible, so no
138
+ // earlier command, provider/model, or trigger probe may run first.
139
+ for (const step of compiled.steps) {
140
+ if (step.type === 'deterministic') continue;
141
+ const resolution = resolveCli(step, compiled, options.projectCli);
142
+ if (resolution === undefined) {
143
+ diagnostics.push({
144
+ severity: 'refusal',
145
+ kind: 'cli_unresolved',
146
+ stepId: step.id,
147
+ message: unresolvedCliMessage(step.id, options),
148
+ });
149
+ } else {
150
+ resolutions.push(resolution);
151
+ resolutionByStep.set(step.id, resolution);
152
+ }
153
+ }
154
+ if (diagnostics.length > 0) {
155
+ return { ok: false, gates: compiled.steps.map(inspectStepGate), resolutions, diagnostics };
156
+ }
157
+
158
+ for (const step of compiled.steps) {
159
+ warnOnVacuousGate(step, diagnostics);
160
+ warnOnUnprovableEffects(step, options.probes, diagnostics);
161
+ if (step.type === 'deterministic') continue;
162
+ const resolution = resolutionByStep.get(step.id)!;
163
+ probeResolvedCli(resolution, options.probes, cliProbeResults, diagnostics);
164
+ }
165
+
166
+ for (const trigger of compiled.triggers ?? []) {
167
+ probeTrigger(trigger, options.probes, diagnostics);
168
+ }
169
+
170
+ return {
171
+ ok: !diagnostics.some((diagnostic) => diagnostic.severity === 'refusal'),
172
+ gates: compiled.steps.map(inspectStepGate),
173
+ resolutions,
174
+ diagnostics,
175
+ };
176
+ }
177
+
178
+ /** Pure authoring validation: no executable, command, trigger, or daemon probe. */
179
+ function unknownModelDiagnostics(
180
+ flow: FlowSpec,
181
+ options: PreflightOptions,
182
+ ): PreflightRefusal[] {
183
+ const diagnostics: PreflightRefusal[] = [];
184
+
185
+ // Named declarations remain in the normalized authoring object until this
186
+ // boundary so even unused or step-shadowed models are checked. toKernelSpec
187
+ // erases the map and selector only after this pass has had a chance to fail.
188
+ for (const [agent, declaration] of Object.entries(flow.agents ?? {})) {
189
+ if (isKnownModel(declaration.model, options.models)) continue;
190
+ diagnostics.push({
191
+ severity: 'refusal',
192
+ kind: 'model_unknown',
193
+ agent,
194
+ cli: declaration.cli,
195
+ model: declaration.model,
196
+ message: unknownNamedAgentModelMessage(agent, declaration.cli, declaration.model, options.modelRegistryPath),
197
+ });
198
+ }
199
+
200
+ for (const step of flow.steps) {
201
+ if (step.type === 'deterministic' || step.model === undefined) continue;
202
+ if (isKnownModel(step.model, options.models)) continue;
203
+ const resolution = resolveCli(step, flow, options.projectCli);
204
+ diagnostics.push({
205
+ severity: 'refusal',
206
+ kind: 'model_unknown',
207
+ stepId: step.id,
208
+ ...(resolution === undefined ? {} : { cli: resolution.cli }),
209
+ model: step.model,
210
+ message: unknownModelMessage(step.id, step.model, resolution?.cli, options.modelRegistryPath),
211
+ });
212
+ }
213
+
214
+ return diagnostics;
215
+ }
216
+
217
+ function unknownNamedAgentModelMessage(
218
+ agent: string,
219
+ cli: string,
220
+ model: string,
221
+ registryPath: string | undefined,
222
+ ): string {
223
+ const source = registryPath === undefined
224
+ ? 'the nearest project config (no model registry was found)'
225
+ : `project model registry "${registryPath}"`;
226
+ return `Named agent "${agent}" declares model "${model}" for CLI "${cli}", but it is not listed in ${source}; add the exact model only after verifying that project is allowed to use it.`;
227
+ }
228
+
229
+ function isKnownModel(model: string, models: readonly string[] | undefined): boolean {
230
+ return models?.includes(model) === true;
231
+ }
232
+
233
+ function unknownModelMessage(
234
+ stepId: string,
235
+ model: string,
236
+ cli: string | undefined,
237
+ registryPath: string | undefined,
238
+ ): string {
239
+ const source = registryPath === undefined
240
+ ? 'the nearest project config (no model registry was found)'
241
+ : `project model registry "${registryPath}"`;
242
+ const cliContext = cli === undefined ? '' : ` for CLI "${cli}"`;
243
+ return `Step "${stepId}" declares model "${model}"${cliContext}, but it is not listed in ${source}; add the exact model only after verifying that project is allowed to use it.`;
244
+ }
245
+
246
+ function unresolvedCliMessage(stepId: string, options: PreflightOptions): string {
247
+ const context = options.projectConfigPath !== undefined
248
+ ? ` Nearest project config "${options.projectConfigPath}" declares no cli; outer configs are shadowed.`
249
+ : options.projectSearchStart !== undefined
250
+ ? ` No flows.json was found from "${options.projectSearchStart}" to the filesystem root.`
251
+ : '';
252
+ return `Step "${stepId}" has no CLI at step, flow, or project level.${context}`;
253
+ }
254
+
255
+ function resolveCli(
256
+ step: Extract<StepSpec, { type: 'llm' | 'agent' }>,
257
+ flow: FlowSpec,
258
+ projectCli: string | undefined,
259
+ ): CliResolution | undefined {
260
+ const named = step.type === 'agent' && step.agent !== undefined
261
+ ? flow.agents?.[step.agent]
262
+ : undefined;
263
+ // Model comes only from the step or its explicitly selected declaration.
264
+ // There is deliberately no flow/project or host default.
265
+ const effectiveModel = step.model ?? named?.model;
266
+ const model = effectiveModel !== undefined ? { model: effectiveModel } : {};
267
+ if (step.cli !== undefined) return { stepId: step.id, cli: step.cli, source: 'step', ...model };
268
+ if (named !== undefined) return { stepId: step.id, cli: named.cli, source: 'named', ...model };
269
+ if (flow.cli !== undefined) return { stepId: step.id, cli: flow.cli, source: 'flow', ...model };
270
+ if (projectCli !== undefined) return { stepId: step.id, cli: projectCli, source: 'project', ...model };
271
+ return undefined;
272
+ }
273
+
274
+ function probeResolvedCli(
275
+ resolution: CliResolution,
276
+ probes: PreflightProbes,
277
+ cache: Map<string, CliProbeOutcome>,
278
+ diagnostics: PreflightDiagnostic[],
279
+ ): void {
280
+ // Source is load-bearing: the same relative CLI string resolves from the
281
+ // flow directory for step/named/flow declarations and the config directory for
282
+ // project declarations.
283
+ // Model is part of the key: the same CLI probed with two different models
284
+ // is two different questions, and caching on the CLI alone would let a
285
+ // model that the CLI cannot resolve inherit an earlier model's pass.
286
+ const cacheKey = JSON.stringify([resolution.cli, resolution.source, resolution.model ?? null]);
287
+ let outcome = cache.get(cacheKey);
288
+ if (outcome === undefined) {
289
+ try {
290
+ outcome = { result: probes.cli(resolution.cli, resolution.source, resolution.model) };
291
+ } catch (error) {
292
+ const detail = error instanceof CliProbeError ? error.detail : undefined;
293
+ outcome = { failure: detail ?? null };
294
+ }
295
+ cache.set(cacheKey, outcome);
296
+ }
297
+ if ('failure' in outcome) {
298
+ const detail = outcome.failure ?? undefined;
299
+ diagnostics.push({
300
+ severity: 'refusal',
301
+ kind: 'probe_failed',
302
+ stepId: resolution.stepId,
303
+ cli: resolution.cli,
304
+ ...(detail !== undefined ? { detail } : {}),
305
+ message: probeFailedMessage(resolution, detail),
306
+ });
307
+ return;
308
+ }
309
+ const { result } = outcome;
310
+ if (!result.exists) {
311
+ diagnostics.push({
312
+ severity: 'refusal',
313
+ kind: 'cli_missing',
314
+ stepId: resolution.stepId,
315
+ cli: resolution.cli,
316
+ message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but it does not resolve as an executable.`,
317
+ });
318
+ } else if (result.supported === false) {
319
+ diagnostics.push({
320
+ severity: 'refusal',
321
+ kind: 'cli_unsupported',
322
+ stepId: resolution.stepId,
323
+ cli: resolution.cli,
324
+ message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but it is neither a supported raw Claude/Codex executable nor a conforming Relayflows wrapper; custom wrappers must identify with the relayflows-agent-cli-v1 contract.`,
325
+ });
326
+ } else if (!result.authenticated) {
327
+ const command = result.authCommand ?? `${resolution.cli} auth status`;
328
+ diagnostics.push({
329
+ severity: 'refusal',
330
+ kind: 'cli_unauthenticated',
331
+ stepId: resolution.stepId,
332
+ cli: resolution.cli,
333
+ message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero; authenticate it or repair that adapter's authentication probe.`,
334
+ });
335
+ } else if (resolution.model !== undefined && result.modelAvailable !== true) {
336
+ diagnostics.push({
337
+ severity: 'refusal',
338
+ kind: 'model_unavailable',
339
+ stepId: resolution.stepId,
340
+ cli: resolution.cli,
341
+ model: resolution.model,
342
+ message: `Step "${resolution.stepId}" declares model "${resolution.model}" for CLI "${resolution.cli}", but its model-scoped "${result.modelCommand ?? `${resolution.cli} auth status`}" probe exited non-zero; verify the model name and this credential's access.`,
343
+ });
344
+ }
345
+ }
346
+
347
+ function probeFailedMessage(
348
+ resolution: CliResolution,
349
+ detail?: CliProbeFailureDetail,
350
+ ): string {
351
+ const prefix = `Could not verify CLI "${resolution.cli}" for step "${resolution.stepId}"`;
352
+ if (detail === undefined) return `${prefix}.`;
353
+ if (detail === 'spawn_failed') return `${prefix}: the probe process could not be started.`;
354
+ if (detail.startsWith('signal:')) {
355
+ return `${prefix}: the probe was terminated by signal "${detail.slice('signal:'.length)}".`;
356
+ }
357
+ return `${prefix}: the probe timed out after ${detail.slice('timeout:'.length)}.`;
358
+ }
359
+
360
+ function probeTrigger(
361
+ trigger: TriggerSpec,
362
+ probes: PreflightProbes,
363
+ diagnostics: PreflightDiagnostic[],
364
+ ): void {
365
+ let registered: boolean;
366
+ try {
367
+ registered = probes.executor(trigger);
368
+ } catch {
369
+ diagnostics.push({
370
+ severity: 'refusal',
371
+ kind: 'probe_failed',
372
+ triggerId: trigger.id,
373
+ executor: trigger.executor,
374
+ message: `Could not verify executor "${trigger.executor}" for trigger "${trigger.id}".`,
375
+ });
376
+ return;
377
+ }
378
+ if (!registered) {
379
+ diagnostics.push({
380
+ severity: 'refusal',
381
+ kind: 'no_executor',
382
+ triggerId: trigger.id,
383
+ executor: trigger.executor,
384
+ message: `Trigger "${trigger.id}" has no registered executor "${trigger.executor}".`,
385
+ });
386
+ }
387
+ }
388
+
389
+ /**
390
+ * A deterministic step is never silently accepted. An unresolved bare command
391
+ * may still be a shell builtin, function, or assignment, so it warns. A command
392
+ * containing `/` names a path rather than relying on shell resolution, so a
393
+ * failed existence probe refuses the flow.
394
+ */
395
+ /**
396
+ * A declared `json_schema` gate that accepts every output is legal and stays
397
+ * legal — but it is indistinguishable in the gate plan from one that judges
398
+ * something, which is exactly the confusion AGENTS.md's "never edit a gate that
399
+ * judges your own work" rail exists to prevent.
400
+ */
401
+ function warnOnVacuousGate(step: StepSpec, diagnostics: PreflightDiagnostic[]): void {
402
+ if (step.verification?.type !== 'json_schema') return;
403
+ if (!acceptsAnyOutput(step.verification.schema)) return;
404
+ diagnostics.push({
405
+ severity: 'warning',
406
+ kind: 'vacuous_gate',
407
+ stepId: step.id,
408
+ message: `Step "${step.id}" declares a json_schema gate that accepts every possible output, so it judges nothing.`,
409
+ });
410
+ }
411
+
412
+ function warnOnUnprovableEffects(
413
+ step: StepSpec,
414
+ probes: PreflightProbes,
415
+ diagnostics: PreflightDiagnostic[],
416
+ ): void {
417
+ if (step.type !== 'deterministic') return;
418
+ const binary = firstCommandWord(step.command);
419
+ if (binary === undefined) {
420
+ diagnostics.push({
421
+ severity: 'warning',
422
+ kind: 'command_unprovable',
423
+ stepId: step.id,
424
+ message: `Step "${step.id}" has no command word to check, so nothing about it can be proven before execution.`,
425
+ });
426
+ return;
427
+ }
428
+ let exists: boolean;
429
+ try {
430
+ exists = probes.command(binary);
431
+ } catch {
432
+ diagnostics.push({
433
+ severity: 'warning',
434
+ kind: 'command_unprovable',
435
+ stepId: step.id,
436
+ message: `Step "${step.id}" command "${binary}" could not be probed, so its presence is unproven before execution.`,
437
+ });
438
+ return;
439
+ }
440
+ diagnostics.push(exists
441
+ ? {
442
+ severity: 'warning',
443
+ kind: 'unprovable_effects',
444
+ stepId: step.id,
445
+ message: `Step "${step.id}" command "${binary}" resolves, but its effects cannot be proven before execution.`,
446
+ }
447
+ : binary.includes('/')
448
+ ? {
449
+ severity: 'refusal',
450
+ kind: 'command_missing',
451
+ stepId: step.id,
452
+ message: `Step "${step.id}" command path "${binary}" does not exist.`,
453
+ }
454
+ : {
455
+ severity: 'warning',
456
+ kind: 'command_unresolved',
457
+ stepId: step.id,
458
+ message: `Step "${step.id}" command "${binary}" does not resolve as an executable; it runs only if the shell supplies it.`,
459
+ });
460
+ }
461
+
462
+ function firstCommandWord(command: string): string | undefined {
463
+ // Skip the shell prefixes that can legally precede the command word.
464
+ //
465
+ // Review caught this on PR #47: the new path-like refusal keys on the first
466
+ // word containing a slash, and `TMPDIR=/tmp printf ok`, `>/tmp/out echo hi`
467
+ // and `PATH=/usr/bin:$PATH mkdir x` all have one — but none of them names a
468
+ // path to execute. All three are valid and were being refused outright,
469
+ // which is exactly the "refusing would reject valid flows" failure the warn
470
+ // behaviour exists to avoid.
471
+ //
472
+ // An assignment is NAME=value with a shell-legal name; a redirection starts
473
+ // with < or > (optionally with a leading fd number). Neither is the command.
474
+ let rest = command.trim();
475
+ for (;;) {
476
+ const prefix = rest.match(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]*)|[0-9]*[<>]{1,2}\s*[^\s]+)\s+/);
477
+ if (prefix === null) break;
478
+ rest = rest.slice(prefix[0].length);
479
+ }
480
+ const match = rest.match(/^(?:"([^"]+)"|'([^']+)'|([^\s]+))/);
481
+ return match?.[1] ?? match?.[2] ?? match?.[3];
482
+ }