@warble/codex-local 0.4.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.
package/dist/cli.js ADDED
@@ -0,0 +1,4113 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
5
+ import { resolve as resolve3 } from "path";
6
+ import { parseArgs } from "util";
7
+
8
+ // src/ask_prepare.ts
9
+ import { isAbsolute } from "path";
10
+
11
+ // src/error.ts
12
+ var CodexDispatchError = class extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "CodexDispatchError";
16
+ }
17
+ };
18
+
19
+ // src/dispatch_registry.ts
20
+ function assertDispatchableComponentIdentity(node) {
21
+ if (node.realization_kind !== "skill") {
22
+ throw new CodexDispatchError(
23
+ `component '${node.id}' is host-executed and cannot be dispatched by codex:local: realization_kind '${node.realization_kind}' is not 'skill'`
24
+ );
25
+ }
26
+ }
27
+
28
+ // src/ir.ts
29
+ var TARGET = "codex:local";
30
+ var SUPPORTED_IR_VERSION = "0.6";
31
+ function isRecord(value) {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33
+ }
34
+ function stringArray(value, field) {
35
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
36
+ throw new CodexDispatchError(`${field} must be an array of strings`);
37
+ }
38
+ return value;
39
+ }
40
+ function parseCall(value, componentId) {
41
+ if (!isRecord(value)) {
42
+ throw new CodexDispatchError(`component '${componentId}' has a malformed llm_call`);
43
+ }
44
+ const { name, tier, prompt } = value;
45
+ if (typeof name !== "string" || typeof tier !== "string" || typeof prompt !== "string" || typeof value["conditional"] !== "boolean" || value["produces"] !== null && typeof value["produces"] !== "string") {
46
+ throw new CodexDispatchError(
47
+ `component '${componentId}' llm_call has malformed name/tier/prompt/conditional/produces`
48
+ );
49
+ }
50
+ return {
51
+ name,
52
+ tier,
53
+ prompt,
54
+ consumes: stringArray(value["consumes"] ?? [], `${componentId}.${name}.consumes`),
55
+ produces: value["produces"],
56
+ conditional: value["conditional"],
57
+ when: value["when"] ?? null
58
+ };
59
+ }
60
+ function parseGuardrail(value, componentId) {
61
+ if (!isRecord(value) || typeof value["name"] !== "string" || typeof value["locked"] !== "boolean") {
62
+ throw new CodexDispatchError(`component '${componentId}' has a malformed guardrail`);
63
+ }
64
+ return {
65
+ name: value["name"],
66
+ locked: value["locked"],
67
+ ...typeof value["scope"] === "string" ? { scope: value["scope"] } : {},
68
+ ...typeof value["threshold"] === "number" ? { threshold: value["threshold"] } : {}
69
+ };
70
+ }
71
+ function parseComponent(value) {
72
+ if (!isRecord(value) || typeof value["id"] !== "string") {
73
+ throw new CodexDispatchError("IR component must be an object with a string id");
74
+ }
75
+ const id = value["id"];
76
+ const trigger = value["trigger"];
77
+ const effect = value["effect"];
78
+ const outcome = isRecord(effect) ? effect["outcome"] : null;
79
+ const context = value["context_binding"];
80
+ if (typeof value["verb"] !== "string" || typeof value["type"] !== "string" || typeof value["realization_kind"] !== "string" || !Array.isArray(value["llm_calls"]) || !Array.isArray(value["guardrails"]) || !isRecord(trigger) || typeof trigger["kind"] !== "string" || !isRecord(effect) || !isRecord(outcome) || typeof outcome["kind"] !== "string" || !Array.isArray(effect["render_blocks"]) || !isRecord(context) || typeof context["binding_mode"] !== "string" || typeof context["project"] !== "string") {
81
+ throw new CodexDispatchError(`component '${id}' is missing required IR fields`);
82
+ }
83
+ return {
84
+ id,
85
+ verb: value["verb"],
86
+ type: value["type"],
87
+ realization_kind: value["realization_kind"],
88
+ llm_calls: value["llm_calls"].map((call) => parseCall(call, id)),
89
+ required_capabilities: stringArray(
90
+ value["required_capabilities"] ?? [],
91
+ `${id}.required_capabilities`
92
+ ),
93
+ guardrails: value["guardrails"].map((guard) => parseGuardrail(guard, id)),
94
+ trigger: { kind: trigger["kind"] },
95
+ effect: {
96
+ outcome: { kind: outcome["kind"] },
97
+ render_blocks: effect["render_blocks"]
98
+ },
99
+ context_binding: {
100
+ binding_mode: context["binding_mode"],
101
+ project: context["project"]
102
+ }
103
+ };
104
+ }
105
+ function parseIr(raw) {
106
+ let value;
107
+ try {
108
+ value = JSON.parse(raw);
109
+ } catch (error) {
110
+ throw new CodexDispatchError(`invalid IR JSON: ${String(error)}`);
111
+ }
112
+ if (!isRecord(value) || typeof value["warble_ir_version"] !== "string" || typeof value["profile"] !== "string" || !Array.isArray(value["components"])) {
113
+ throw new CodexDispatchError("IR requires warble_ir_version, profile, and components");
114
+ }
115
+ if (value["warble_ir_version"] !== SUPPORTED_IR_VERSION) {
116
+ throw new CodexDispatchError(
117
+ `unsupported warble_ir_version '${value["warble_ir_version"]}' (supported: ${SUPPORTED_IR_VERSION})`
118
+ );
119
+ }
120
+ return {
121
+ warble_ir_version: value["warble_ir_version"],
122
+ profile: value["profile"],
123
+ components: value["components"].map(parseComponent)
124
+ };
125
+ }
126
+
127
+ // src/render_contract.ts
128
+ function isRecord2(value) {
129
+ return typeof value === "object" && value !== null && !Array.isArray(value);
130
+ }
131
+ function parseDashboardRenderBlockContracts(renderBlocks) {
132
+ const contracts = /* @__PURE__ */ new Map();
133
+ for (const entry of renderBlocks) {
134
+ if (!isRecord2(entry) || typeof entry["type"] !== "string" || !isRecord2(entry["fields"])) {
135
+ throw new CodexDispatchError("dashboard IR contains a malformed render block contract");
136
+ }
137
+ const fields = entry["fields"];
138
+ if (!Object.values(fields).every((field) => typeof field === "string")) {
139
+ throw new CodexDispatchError("dashboard IR contains a malformed render field contract");
140
+ }
141
+ contracts.set(entry["type"], fields);
142
+ }
143
+ return contracts;
144
+ }
145
+ function validatePrimitive(value, type, context) {
146
+ if (type.endsWith("?")) {
147
+ if (value === void 0 || value === null) return;
148
+ validatePrimitive(value, type.slice(0, -1), context);
149
+ return;
150
+ }
151
+ if (type.endsWith("[]")) {
152
+ if (!Array.isArray(value)) throw new CodexDispatchError(`${context} must be an array`);
153
+ const itemType2 = type.slice(0, -2);
154
+ for (const [index, item] of value.entries()) {
155
+ validatePrimitive(item, itemType2, `${context}[${index}]`);
156
+ }
157
+ return;
158
+ }
159
+ if (type.includes("|")) {
160
+ const alternatives = type.split("|");
161
+ if (alternatives.includes("string") && typeof value === "string") return;
162
+ if (alternatives.includes("number") && typeof value === "number" && Number.isFinite(value)) return;
163
+ if (typeof value === "string" && alternatives.includes(value)) return;
164
+ throw new CodexDispatchError(`${context} does not match '${type}'`);
165
+ }
166
+ if (type === "string" && typeof value === "string") return;
167
+ if (type === "number" && typeof value === "number" && Number.isFinite(value)) return;
168
+ if (type === "boolean" && typeof value === "boolean") return;
169
+ if (type === "row" && isRecord2(value)) return;
170
+ throw new CodexDispatchError(`${context} does not match '${type}'`);
171
+ }
172
+ function validateDashboardRenderEnvelope(value, node) {
173
+ if (!isRecord2(value)) throw new CodexDispatchError("dashboard output must be a JSON object");
174
+ const keys = Object.keys(value);
175
+ if (keys.some((key) => !(/* @__PURE__ */ new Set(["blocks", "summary", "verified"])).has(key)) || !Array.isArray(value["blocks"]) || value["blocks"].length === 0 || typeof value["verified"] !== "boolean" || value["summary"] !== void 0 && typeof value["summary"] !== "string") {
176
+ throw new CodexDispatchError(
177
+ "dashboard output requires only non-empty blocks, optional summary, and boolean verified"
178
+ );
179
+ }
180
+ const contracts = parseDashboardRenderBlockContracts(node.effect.render_blocks);
181
+ const blocks = value["blocks"].map((entry, index) => {
182
+ if (!isRecord2(entry) || typeof entry["type"] !== "string") {
183
+ throw new CodexDispatchError(`dashboard block[${index}] requires a string type`);
184
+ }
185
+ const fields = contracts.get(entry["type"]);
186
+ if (!fields) {
187
+ throw new CodexDispatchError(`dashboard block[${index}] uses undeclared type '${entry["type"]}'`);
188
+ }
189
+ const allowed = /* @__PURE__ */ new Set(["type", ...Object.keys(fields)]);
190
+ if (Object.keys(entry).some((key) => !allowed.has(key))) {
191
+ throw new CodexDispatchError(`dashboard block[${index}] contains undeclared fields`);
192
+ }
193
+ const normalized = { ...entry };
194
+ for (const [field, type] of Object.entries(fields)) {
195
+ validatePrimitive(normalized[field], type, `dashboard block[${index}].${field}`);
196
+ if (type.endsWith("?") && normalized[field] === null) delete normalized[field];
197
+ }
198
+ return normalized;
199
+ });
200
+ return {
201
+ blocks,
202
+ ...typeof value["summary"] === "string" ? { summary: value["summary"] } : {},
203
+ verified: value["verified"]
204
+ };
205
+ }
206
+
207
+ // src/request_transport.ts
208
+ var REQUEST_TRANSPORT_SERVER = "warble_request_transport";
209
+ var REQUEST_TRANSPORT_TOOL = "get_original_request";
210
+ var STEP_TRANSPORT_TOOL = "get_step_request";
211
+
212
+ // src/target_profile.ts
213
+ var mcpVia = (mcpName) => `mcp:${mcpName}`;
214
+ var CAPABILITY_REALIZATION = {
215
+ "llm:strong": { outcome: "native", via: null },
216
+ "llm:cheap": { outcome: "native", via: null },
217
+ "llm:per_step_tier": { outcome: "native", via: null },
218
+ source_connect: { outcome: "realize-via", via: mcpVia },
219
+ context_build: { outcome: "realize-via", via: mcpVia },
220
+ semantic_introspection: { outcome: "realize-via", via: mcpVia },
221
+ raw_material_read: { outcome: "realize-via", via: mcpVia },
222
+ "sql_execution:read_only": { outcome: "realize-via", via: mcpVia },
223
+ genbi_build: { outcome: "native", via: "validated-render-envelope" },
224
+ render_contract: { outcome: "native", via: "validated-render-envelope" },
225
+ artifact_write: { outcome: "realize-via", via: "consumer-persisted-render-envelope" }
226
+ };
227
+ function resolveCapabilities(requiredCapabilities, mcpName) {
228
+ return requiredCapabilities.map((capability) => {
229
+ const entry = CAPABILITY_REALIZATION[capability];
230
+ if (!entry) {
231
+ throw new CodexDispatchError(`capability '${capability}' has no realization on codex:local`);
232
+ }
233
+ return {
234
+ capability,
235
+ outcome: entry.outcome,
236
+ via: typeof entry.via === "function" ? entry.via(mcpName) : entry.via
237
+ };
238
+ });
239
+ }
240
+ function hasExactCapabilities(requiredCapabilities, expected) {
241
+ return requiredCapabilities.length === expected.size && requiredCapabilities.every((capability) => expected.has(capability));
242
+ }
243
+ var SETUP_DOMAIN_CAPABILITIES = ["source_connect", "context_build"];
244
+ var SETUP_DOMAIN_CAPABILITY_SET = new Set(SETUP_DOMAIN_CAPABILITIES);
245
+ function isSetupDomainCapability(value) {
246
+ return SETUP_DOMAIN_CAPABILITY_SET.has(value);
247
+ }
248
+ var ASK_ANSWER_CAPABILITIES = /* @__PURE__ */ new Set([
249
+ "sql_execution:read_only",
250
+ "llm:per_step_tier",
251
+ "llm:strong",
252
+ "llm:cheap"
253
+ ]);
254
+ var ASK_DASHBOARD_CAPABILITIES = /* @__PURE__ */ new Set([
255
+ "sql_execution:read_only",
256
+ "genbi_build",
257
+ "render_contract",
258
+ "artifact_write",
259
+ "llm:per_step_tier",
260
+ "llm:strong",
261
+ "llm:cheap"
262
+ ]);
263
+ var ENRICH_DOMAIN_CAPABILITIES = ["semantic_introspection", "raw_material_read"];
264
+ var ENRICH_DOMAIN_CAPABILITY_SET = new Set(ENRICH_DOMAIN_CAPABILITIES);
265
+ function isEnrichDomainCapability(value) {
266
+ return ENRICH_DOMAIN_CAPABILITY_SET.has(value);
267
+ }
268
+ var ENRICH_ALLOWED_CAPABILITIES = /* @__PURE__ */ new Set([
269
+ ...ENRICH_DOMAIN_CAPABILITIES,
270
+ "llm:cheap",
271
+ "llm:strong"
272
+ ]);
273
+ var GUARDRAIL_ENFORCEMENT = {
274
+ setup_execution: { locked: true, scope: "." },
275
+ read_only_execution: { locked: true },
276
+ deterministic_gate: { locked: true },
277
+ row_limit: { locked: false, threshold: 1e3 },
278
+ statement_timeout: { locked: false, threshold: 30 },
279
+ artifact_write: { locked: true, scope: "." }
280
+ };
281
+ function guardrailMatches(guard, name, options) {
282
+ const requirement = GUARDRAIL_ENFORCEMENT[name];
283
+ if (!requirement || !guard || guard.name !== name || guard.locked !== requirement.locked) {
284
+ return false;
285
+ }
286
+ if (requirement.scope !== void 0 && guard.scope !== requirement.scope) {
287
+ return false;
288
+ }
289
+ if (requirement.threshold !== void 0 && guard.threshold !== requirement.threshold) {
290
+ return false;
291
+ }
292
+ if (options?.requireScopeAbsent && guard.scope !== void 0) {
293
+ return false;
294
+ }
295
+ return true;
296
+ }
297
+
298
+ // src/ask_prepare.ts
299
+ function unique(values) {
300
+ return [...new Set(values)];
301
+ }
302
+ var TOOLS_BY_EXECUTION_KIND = {
303
+ answer_query: [["get_context"], ["run_sql"], ["run_sql"]],
304
+ generate_dashboard: [["get_context"], ["run_sql"]]
305
+ };
306
+ function requireNonEmpty(value, field) {
307
+ if (value.trim().length === 0) throw new CodexDispatchError(`${field} must not be empty`);
308
+ }
309
+ function parseWhen(step) {
310
+ if (!step.conditional) {
311
+ if (step.when !== null) {
312
+ throw new CodexDispatchError(`step '${step.name}' is unconditional but has a when guard`);
313
+ }
314
+ return null;
315
+ }
316
+ if (typeof step.when !== "object" || step.when === null || Array.isArray(step.when) || step.when["guard"] !== "on_failure" || typeof step.when["target"] !== "string") {
317
+ throw new CodexDispatchError(
318
+ `step '${step.name}' wall-hit: Ask repair requires on_failure(target)`
319
+ );
320
+ }
321
+ return {
322
+ guard: "on_failure",
323
+ target: step.when["target"]
324
+ };
325
+ }
326
+ function validateCommonAnalyticalShape(node) {
327
+ if (node.type !== "analytical" || node.realization_kind !== "skill" || node.trigger.kind !== "one_shot" || node.effect.outcome.kind !== "none") {
328
+ throw new CodexDispatchError(
329
+ `component '${node.id}' wall-hit: Codex analytical execution requires analytical/skill/one_shot/none`
330
+ );
331
+ }
332
+ if (node.context_binding.binding_mode !== "runtime_selected") {
333
+ throw new CodexDispatchError(
334
+ `component '${node.id}' wall-hit: Codex analytical execution requires runtime_selected context binding`
335
+ );
336
+ }
337
+ }
338
+ function validateStepChain(node) {
339
+ const calls = node.llm_calls;
340
+ if (calls.length === 0) {
341
+ throw new CodexDispatchError(`component '${node.id}' wall-hit: Ask requires at least one llm_call`);
342
+ }
343
+ let sawConditional = false;
344
+ calls.forEach((call, index) => {
345
+ if (call.tier !== "cheap" && call.tier !== "strong") {
346
+ throw new CodexDispatchError(
347
+ `component '${node.id}' wall-hit: step '${call.name}' has unsupported tier '${call.tier}'`
348
+ );
349
+ }
350
+ if (call.produces === null) {
351
+ throw new CodexDispatchError(
352
+ `component '${node.id}' wall-hit: step '${call.name}' must produce a named output`
353
+ );
354
+ }
355
+ const when = parseWhen(call);
356
+ if (index === 0) {
357
+ if (call.conditional || call.consumes.length !== 0) {
358
+ throw new CodexDispatchError(
359
+ `component '${node.id}' wall-hit: first Ask step must be unconditional with no consumes and one output`
360
+ );
361
+ }
362
+ return;
363
+ }
364
+ const previous = calls[index - 1];
365
+ if (call.conditional) {
366
+ if (when?.target !== previous.name || call.consumes.length !== 1 || call.consumes[0] !== previous.produces) {
367
+ throw new CodexDispatchError(
368
+ `component '${node.id}' wall-hit: step '${call.name}' must be an on_failure repair of the immediately preceding step '${previous.name}'`
369
+ );
370
+ }
371
+ sawConditional = true;
372
+ return;
373
+ }
374
+ if (sawConditional) {
375
+ throw new CodexDispatchError(
376
+ `component '${node.id}' wall-hit: an unconditional step cannot follow a repair step`
377
+ );
378
+ }
379
+ if (call.consumes.length !== 1 || call.consumes[0] !== previous.produces) {
380
+ throw new CodexDispatchError(
381
+ `component '${node.id}' wall-hit: step '${call.name}' must consume exactly the preceding step's output`
382
+ );
383
+ }
384
+ });
385
+ }
386
+ function validateAnswerShape(node) {
387
+ validateCommonAnalyticalShape(node);
388
+ validateStepChain(node);
389
+ if (!hasExactCapabilities(node.required_capabilities, ASK_ANSWER_CAPABILITIES)) {
390
+ throw new CodexDispatchError(
391
+ `component '${node.id}' wall-hit: Ask capability set must be read-only SQL plus cheap/strong per-step tiering`
392
+ );
393
+ }
394
+ const guards = new Map(node.guardrails.map((guard) => [guard.name, guard]));
395
+ if (guards.size !== 4 || !guardrailMatches(guards.get("read_only_execution"), "read_only_execution") || !guardrailMatches(guards.get("deterministic_gate"), "deterministic_gate") || !guardrailMatches(guards.get("row_limit"), "row_limit") || !guardrailMatches(guards.get("statement_timeout"), "statement_timeout")) {
396
+ throw new CodexDispatchError(
397
+ `component '${node.id}' wall-hit: Ask guardrails must match the locked read-only/deterministic and bounded row/timeout contract`
398
+ );
399
+ }
400
+ }
401
+ function validateDashboardShape(node) {
402
+ validateCommonAnalyticalShape(node);
403
+ validateStepChain(node);
404
+ if (!hasExactCapabilities(node.required_capabilities, ASK_DASHBOARD_CAPABILITIES)) {
405
+ throw new CodexDispatchError(
406
+ `component '${node.id}' wall-hit: dashboard capability set must match read-only SQL, build, render, artifact, and cheap/strong per-step tiering`
407
+ );
408
+ }
409
+ const guards = new Map(node.guardrails.map((guard) => [guard.name, guard]));
410
+ if (guards.size !== 2 || !guardrailMatches(guards.get("read_only_execution"), "read_only_execution") || !guardrailMatches(guards.get("artifact_write"), "artifact_write")) {
411
+ throw new CodexDispatchError(
412
+ `component '${node.id}' wall-hit: dashboard guardrails must be locked read-only execution plus scoped artifact_write`
413
+ );
414
+ }
415
+ if (node.effect.render_blocks.length === 0) {
416
+ throw new CodexDispatchError(
417
+ `component '${node.id}' wall-hit: dashboard render contract must declare at least one render block type`
418
+ );
419
+ }
420
+ parseDashboardRenderBlockContracts(node.effect.render_blocks);
421
+ }
422
+ function executionKind(node) {
423
+ const capabilities = new Set(node.required_capabilities);
424
+ if (capabilities.has("render_contract") || capabilities.has("artifact_write")) {
425
+ validateDashboardShape(node);
426
+ return "generate_dashboard";
427
+ }
428
+ validateAnswerShape(node);
429
+ return "answer_query";
430
+ }
431
+ function matchesAskContractShape(node) {
432
+ try {
433
+ executionKind(node);
434
+ return true;
435
+ } catch (error) {
436
+ if (error instanceof CodexDispatchError) return false;
437
+ throw error;
438
+ }
439
+ }
440
+ function askContractMismatchReason(node) {
441
+ try {
442
+ executionKind(node);
443
+ return null;
444
+ } catch (error) {
445
+ if (error instanceof CodexDispatchError) return error.message;
446
+ throw error;
447
+ }
448
+ }
449
+ function roleName(stepName) {
450
+ const value = `warble_${stepName}`.replace(/[^A-Za-z0-9_-]/g, "_");
451
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(value)) {
452
+ throw new CodexDispatchError(`step '${stepName}' cannot be mapped to a Codex agent role`);
453
+ }
454
+ return value;
455
+ }
456
+ function prepareAsk(input) {
457
+ const ir = typeof input.ir === "string" ? parseIr(input.ir) : input.ir;
458
+ if (ir.warble_ir_version !== SUPPORTED_IR_VERSION) {
459
+ throw new CodexDispatchError(
460
+ `unsupported warble_ir_version '${ir.warble_ir_version}' (supported: ${SUPPORTED_IR_VERSION})`
461
+ );
462
+ }
463
+ const node = ir.components.find((candidate) => candidate.id === input.component);
464
+ if (!node) {
465
+ throw new CodexDispatchError(
466
+ `component '${input.component}' was not found in profile '${ir.profile}'`
467
+ );
468
+ }
469
+ assertDispatchableComponentIdentity(node);
470
+ const kind = executionKind(node);
471
+ if (!/^[A-Za-z0-9_-]+$/.test(input.mcp.name)) {
472
+ throw new CodexDispatchError(
473
+ `MCP server name '${input.mcp.name}' must contain only letters, digits, '_' or '-'`
474
+ );
475
+ }
476
+ if (input.mcp.name === REQUEST_TRANSPORT_SERVER) {
477
+ throw new CodexDispatchError(`MCP server name '${input.mcp.name}' is reserved by the Ask request transport`);
478
+ }
479
+ if (!isAbsolute(input.mcp.command)) {
480
+ throw new CodexDispatchError("Ask MCP server command must be absolute");
481
+ }
482
+ requireNonEmpty(input.models.orchestrator, "orchestrator model binding");
483
+ requireNonEmpty(input.models.cheap, "cheap-tier model binding");
484
+ requireNonEmpty(input.models.strong, "strong-tier model binding");
485
+ const steps = node.llm_calls.map((step, index) => {
486
+ const tier = step.tier;
487
+ if (tier !== "cheap" && tier !== "strong") {
488
+ throw new CodexDispatchError(`step '${step.name}' has unsupported tier '${tier}'`);
489
+ }
490
+ const enabledTools = unique(input.mcp.toolsByStep[step.name] ?? []);
491
+ const expectedTools = TOOLS_BY_EXECUTION_KIND[kind][index];
492
+ if (expectedTools === void 0) {
493
+ throw new CodexDispatchError(
494
+ `step '${step.name}' has no declared MCP tool allowlist for target index ${index}`
495
+ );
496
+ }
497
+ if (enabledTools.length !== expectedTools.length || enabledTools.some((tool, toolIndex) => tool !== expectedTools[toolIndex])) {
498
+ throw new CodexDispatchError(
499
+ `step '${step.name}' requires exact MCP tools: ${expectedTools.join(", ")}`
500
+ );
501
+ }
502
+ if (step.produces === null) {
503
+ throw new CodexDispatchError(`step '${step.name}' must produce a named slot`);
504
+ }
505
+ return {
506
+ name: step.name,
507
+ role: roleName(step.name),
508
+ tier,
509
+ model: input.models[tier],
510
+ prompt: step.prompt,
511
+ consumes: [...step.consumes],
512
+ produces: step.produces,
513
+ conditional: step.conditional,
514
+ when: parseWhen(step),
515
+ enabledTools,
516
+ requireSuccessfulTool: kind === "generate_dashboard" || index > 0
517
+ };
518
+ });
519
+ return {
520
+ target: TARGET,
521
+ profile: ir.profile,
522
+ node,
523
+ componentId: node.id,
524
+ steps,
525
+ capabilities: resolveCapabilities(node.required_capabilities, input.mcp.name),
526
+ mcp: input.mcp,
527
+ models: input.models,
528
+ executionKind: kind,
529
+ maxRepairAttempts: steps.filter((step) => step.conditional).length
530
+ };
531
+ }
532
+
533
+ // src/app_server_transport.ts
534
+ import { spawn } from "child_process";
535
+ import { existsSync, realpathSync } from "fs";
536
+ import { homedir } from "os";
537
+ import { isAbsolute as isAbsolute2, join, relative, resolve } from "path";
538
+ import { createInterface } from "readline";
539
+
540
+ // src/config.ts
541
+ var API_BILLING_ENV_KEYS = /* @__PURE__ */ new Set([
542
+ "OPENAI_API_KEY",
543
+ "CODEX_API_KEY",
544
+ "AZURE_OPENAI_API_KEY",
545
+ "OPENAI_ORGANIZATION",
546
+ "OPENAI_ORG_ID",
547
+ "OPENAI_PROJECT",
548
+ "OPENAI_PROJECT_ID"
549
+ ]);
550
+ var DISABLED_FEATURES = [
551
+ "shell_tool",
552
+ "unified_exec",
553
+ "shell_zsh_fork",
554
+ "unified_exec_zsh_fork",
555
+ "standalone_web_search",
556
+ "apps",
557
+ "plugins",
558
+ "in_app_browser",
559
+ "browser_use",
560
+ "computer_use",
561
+ "image_generation",
562
+ "skill_search",
563
+ "multi_agent"
564
+ ];
565
+ function tomlString(value) {
566
+ return JSON.stringify(value);
567
+ }
568
+ function tomlStringArray(values) {
569
+ return `[${values.map(tomlString).join(",")}]`;
570
+ }
571
+ function sanitizeCodexToolName(value) {
572
+ return value.replace(/[^A-Za-z0-9_]/g, "_");
573
+ }
574
+ function codexMcpCallableNamespace(server) {
575
+ return `mcp__${sanitizeCodexToolName(server)}`;
576
+ }
577
+ function codexMcpCallableName(server, tool) {
578
+ return `${codexMcpCallableNamespace(server)}__${sanitizeCodexToolName(tool)}`;
579
+ }
580
+ function sanitizeCodexEnvironment(source = process.env) {
581
+ const clean = {};
582
+ for (const [key, value] of Object.entries(source)) {
583
+ if (!API_BILLING_ENV_KEYS.has(key.toUpperCase()) && value !== void 0) clean[key] = value;
584
+ }
585
+ return clean;
586
+ }
587
+ function buildIsolationArgs(prepared) {
588
+ const serverKey = `mcp_servers.${prepared.mcp.name}`;
589
+ const args = [
590
+ "-c",
591
+ "shell_environment_policy.inherit=none",
592
+ "-c",
593
+ "project_doc_max_bytes=0",
594
+ "-c",
595
+ "project_root_markers=[]",
596
+ "-c",
597
+ `web_search=${tomlString("disabled")}`,
598
+ "-c",
599
+ "features.code_mode.enabled=false",
600
+ "-c",
601
+ `features.code_mode.direct_only_tool_namespaces=${tomlStringArray([codexMcpCallableNamespace(prepared.mcp.name)])}`,
602
+ "-c",
603
+ `${serverKey}.command=${tomlString(prepared.mcp.command)}`,
604
+ "-c",
605
+ `${serverKey}.args=${tomlStringArray(prepared.mcp.args ?? [])}`,
606
+ "-c",
607
+ `${serverKey}.enabled_tools=${tomlStringArray(prepared.enabledTools)}`,
608
+ "-c",
609
+ `${serverKey}.default_tools_approval_mode=${tomlString("approve")}`,
610
+ "-c",
611
+ `${serverKey}.required=true`
612
+ ];
613
+ for (const feature of DISABLED_FEATURES) args.push("--disable", feature);
614
+ return args;
615
+ }
616
+ function buildIsolationConfig(prepared) {
617
+ const serverKey = `mcp_servers.${prepared.mcp.name}`;
618
+ return {
619
+ "shell_environment_policy.inherit": "none",
620
+ project_doc_max_bytes: 0,
621
+ project_root_markers: [],
622
+ web_search: "disabled",
623
+ "features.code_mode.enabled": false,
624
+ "features.code_mode.direct_only_tool_namespaces": [
625
+ codexMcpCallableNamespace(prepared.mcp.name)
626
+ ],
627
+ [`${serverKey}.command`]: prepared.mcp.command,
628
+ [`${serverKey}.args`]: prepared.mcp.args ?? [],
629
+ [`${serverKey}.enabled_tools`]: prepared.enabledTools,
630
+ [`${serverKey}.default_tools_approval_mode`]: "approve",
631
+ [`${serverKey}.required`]: true,
632
+ ...Object.fromEntries(DISABLED_FEATURES.map((feature) => [`features.${feature}`, false]))
633
+ };
634
+ }
635
+ function buildCodexArgs(prepared, step, options) {
636
+ const args = [
637
+ ...options.codexArgsPrefix ?? [],
638
+ "--ask-for-approval",
639
+ "never",
640
+ "exec",
641
+ "--json",
642
+ "--ephemeral",
643
+ "--ignore-user-config",
644
+ "--ignore-rules",
645
+ "--strict-config",
646
+ "--skip-git-repo-check",
647
+ "--sandbox",
648
+ "read-only",
649
+ "--cd",
650
+ options.cwd,
651
+ "--model",
652
+ step.model,
653
+ ...buildIsolationArgs(prepared)
654
+ ];
655
+ args.push("-");
656
+ return args;
657
+ }
658
+ function buildPrompt(prepared, step, request, inputs = {}, options = {}) {
659
+ const tools = prepared.enabledTools.map(
660
+ (tool) => `${prepared.mcp.name}.${tool} -> ${codexMcpCallableName(prepared.mcp.name, tool)}`
661
+ ).join(", ");
662
+ const terminalContract = [
663
+ `The final answer must be one JSON object with exactly the produced field '${step.produces}'.`,
664
+ ...options.producedValue === "string" ? [`The value of '${step.produces}' must be a JSON string, not an object, array, number, boolean, or null.`] : [],
665
+ "Do not wrap the JSON in Markdown or include prose."
666
+ ];
667
+ const inputSection = step.consumes.length === 0 ? [] : [
668
+ "",
669
+ "Inputs from earlier steps (JSON):",
670
+ JSON.stringify(Object.fromEntries(step.consumes.map((name) => [name, inputs[name]])))
671
+ ];
672
+ return [
673
+ `You are executing Warble target ${prepared.target}.`,
674
+ `Run exactly one profile step: ${prepared.componentId}.${step.name}.`,
675
+ `Only use the allowlisted MCP tools (raw identity -> Codex callable name): ${tools}.`,
676
+ "The raw and qualified names identify the same MCP tool; call the qualified Codex name, not a fallback.",
677
+ "Do not use shell, file mutation, web, browser, apps, plugins, skills, or delegation.",
678
+ "If the required MCP tool is unavailable or fails, fail loudly; do not substitute another mechanism.",
679
+ ...terminalContract,
680
+ ...inputSection,
681
+ "",
682
+ "Step contract:",
683
+ step.prompt,
684
+ "",
685
+ "User request:",
686
+ request
687
+ ].join("\n");
688
+ }
689
+
690
+ // src/app_server_transport.ts
691
+ function isRecord3(value) {
692
+ return typeof value === "object" && value !== null && !Array.isArray(value);
693
+ }
694
+ function isWithin(parent, candidate) {
695
+ const path = relative(parent, candidate);
696
+ return path === "" || !path.startsWith("..") && !isAbsolute2(path);
697
+ }
698
+ function validateSessionIsolation(options) {
699
+ if (options.externalAuthentication !== "provisioned") {
700
+ throw new CodexDispatchError(
701
+ "persistent session authentication must be provisioned externally"
702
+ );
703
+ }
704
+ if (!isAbsolute2(options.codexHome) || !isAbsolute2(options.cwd)) {
705
+ throw new CodexDispatchError("session codexHome and cwd must be absolute");
706
+ }
707
+ if (!existsSync(options.codexHome)) {
708
+ throw new CodexDispatchError("dedicated session codexHome must be provisioned before start");
709
+ }
710
+ if (existsSync(join(options.codexHome, "config.toml"))) {
711
+ throw new CodexDispatchError("dedicated session codexHome must not contain config.toml");
712
+ }
713
+ const codexHome = realpathSync(options.codexHome);
714
+ const cwd = realpathSync(options.cwd);
715
+ const inheritedCodexHome = options.env === void 0 ? process.env["CODEX_HOME"] : options.env["CODEX_HOME"];
716
+ const defaultHome = resolve(inheritedCodexHome ?? join(homedir(), ".codex"));
717
+ const comparableDefault = existsSync(defaultHome) ? realpathSync(defaultHome) : defaultHome;
718
+ if (codexHome === comparableDefault) {
719
+ throw new CodexDispatchError("persistent sessions require a dedicated non-default codexHome");
720
+ }
721
+ if (isWithin(cwd, codexHome) || isWithin(codexHome, cwd)) {
722
+ throw new CodexDispatchError(
723
+ "dedicated session codexHome and project cwd must not overlap"
724
+ );
725
+ }
726
+ return { codexHome, cwd };
727
+ }
728
+ function buildAppServerArgs(prepared, options) {
729
+ return [
730
+ ...options.codexArgsPrefix ?? [],
731
+ "app-server",
732
+ "--stdio",
733
+ "--strict-config",
734
+ ...buildIsolationArgs(prepared)
735
+ ];
736
+ }
737
+ function validateCatalogTransport(options) {
738
+ if (!isAbsolute2(options.cwd) || !existsSync(options.cwd)) {
739
+ throw new CodexDispatchError("model catalog cwd must be an existing absolute path");
740
+ }
741
+ if (options.codexHome !== void 0 && (!isAbsolute2(options.codexHome) || !existsSync(options.codexHome))) {
742
+ throw new CodexDispatchError("model catalog codexHome must be an existing absolute path");
743
+ }
744
+ return {
745
+ cwd: realpathSync(options.cwd),
746
+ codexHome: options.codexHome === void 0 ? void 0 : realpathSync(options.codexHome)
747
+ };
748
+ }
749
+ var CodexAppServerTransport = class _CodexAppServerTransport {
750
+ constructor(child, timeoutMs, terminationGraceMs, onNotification, onDisconnect) {
751
+ this.timeoutMs = timeoutMs;
752
+ this.terminationGraceMs = terminationGraceMs;
753
+ this.onNotification = onNotification;
754
+ this.onDisconnect = onDisconnect;
755
+ this.child = child;
756
+ if (child.stdout === null || child.stdin === null || child.stderr === null) {
757
+ throw new CodexDispatchError("app-server requires piped stdio");
758
+ }
759
+ child.stderr.resume();
760
+ this.lines = createInterface({ input: child.stdout });
761
+ this.lines.on("line", (line) => this.onLine(line));
762
+ this.closePromise = new Promise((resolveClose) => {
763
+ child.once("close", (code, signal) => {
764
+ this.closed = true;
765
+ this.lines.close();
766
+ const detail = signal !== null ? `signal ${signal}` : `exit ${code ?? "unknown"}`;
767
+ this.rejectPending(`app-server transport disconnected (${detail})`);
768
+ if (!this.closing) this.onDisconnect();
769
+ resolveClose();
770
+ });
771
+ child.once("error", () => {
772
+ this.rejectPending("failed to start app-server");
773
+ });
774
+ });
775
+ }
776
+ timeoutMs;
777
+ terminationGraceMs;
778
+ onNotification;
779
+ onDisconnect;
780
+ nextId = 1;
781
+ pending = /* @__PURE__ */ new Map();
782
+ lines;
783
+ child;
784
+ closing = false;
785
+ closed = false;
786
+ killTimer;
787
+ closePromise;
788
+ static async start(prepared, options, onNotification, onDisconnect) {
789
+ return _CodexAppServerTransport.startWithArgs(
790
+ buildAppServerArgs(prepared, options),
791
+ options,
792
+ onNotification,
793
+ onDisconnect
794
+ );
795
+ }
796
+ static async startWithArgs(args, options, onNotification, onDisconnect) {
797
+ const isolated = validateSessionIsolation(options);
798
+ const child = spawn(options.codexBin ?? "codex", args, {
799
+ cwd: isolated.cwd,
800
+ env: {
801
+ ...sanitizeCodexEnvironment(options.env),
802
+ CODEX_HOME: isolated.codexHome
803
+ },
804
+ stdio: ["pipe", "pipe", "pipe"],
805
+ detached: process.platform !== "win32"
806
+ });
807
+ const transport = new _CodexAppServerTransport(
808
+ child,
809
+ options.timeoutMs ?? 1e4,
810
+ options.terminationGraceMs ?? 1e3,
811
+ onNotification,
812
+ onDisconnect
813
+ );
814
+ try {
815
+ const initialized = await transport.request("initialize", {
816
+ clientInfo: { name: "warble_codex_local", title: "Warble Codex Local", version: "0.1.0" },
817
+ capabilities: { experimentalApi: true, requestAttestation: false }
818
+ });
819
+ if (!isRecord3(initialized) || resolve(String(initialized["codexHome"] ?? "")) !== isolated.codexHome) {
820
+ throw new CodexDispatchError("app-server initialize returned an unexpected codexHome");
821
+ }
822
+ transport.notify("initialized");
823
+ return transport;
824
+ } catch (error) {
825
+ await transport.close();
826
+ throw error;
827
+ }
828
+ }
829
+ /**
830
+ * Start a narrowly read-only app-server transport for `model/list`. Unlike persistent sessions,
831
+ * catalog discovery may use the caller's normal logged-in Codex identity, but it never starts a
832
+ * thread or applies the session's MCP/tool isolation configuration.
833
+ */
834
+ static async startCatalog(options) {
835
+ const catalog = validateCatalogTransport(options);
836
+ const child = spawn(options.codexBin ?? "codex", [
837
+ ...options.codexArgsPrefix ?? [],
838
+ "app-server",
839
+ "--stdio"
840
+ ], {
841
+ cwd: catalog.cwd,
842
+ env: {
843
+ ...sanitizeCodexEnvironment(options.env),
844
+ ...catalog.codexHome === void 0 ? {} : { CODEX_HOME: catalog.codexHome }
845
+ },
846
+ stdio: ["pipe", "pipe", "pipe"],
847
+ detached: process.platform !== "win32"
848
+ });
849
+ const transport = new _CodexAppServerTransport(
850
+ child,
851
+ options.timeoutMs ?? 1e4,
852
+ options.terminationGraceMs ?? 1e3,
853
+ () => void 0,
854
+ () => void 0
855
+ );
856
+ try {
857
+ const initialized = await transport.request("initialize", {
858
+ clientInfo: { name: "warble_codex_local_catalog", title: "Warble Codex Model Catalog", version: "0.1.0" },
859
+ capabilities: { experimentalApi: true, requestAttestation: false }
860
+ });
861
+ const returnedCodexHome = isRecord3(initialized) ? initialized["codexHome"] : void 0;
862
+ if (typeof returnedCodexHome !== "string" || !isAbsolute2(returnedCodexHome) || catalog.codexHome !== void 0 && resolve(returnedCodexHome) !== catalog.codexHome) {
863
+ throw new CodexDispatchError("app-server initialize returned an invalid catalog response");
864
+ }
865
+ transport.notify("initialized");
866
+ return transport;
867
+ } catch (error) {
868
+ await transport.close();
869
+ throw error;
870
+ }
871
+ }
872
+ request(method, params = {}) {
873
+ if (this.closed || this.closing || this.child.stdin === null) {
874
+ return Promise.reject(new CodexDispatchError("app-server transport is not available"));
875
+ }
876
+ const id = this.nextId++;
877
+ return new Promise((resolveRequest, rejectRequest) => {
878
+ const timer = setTimeout(() => {
879
+ this.pending.delete(id);
880
+ rejectRequest(new CodexDispatchError(`app-server request '${method}' timed out`));
881
+ void this.close();
882
+ }, this.timeoutMs);
883
+ this.pending.set(id, { method, resolve: resolveRequest, reject: rejectRequest, timer });
884
+ this.write({ jsonrpc: "2.0", id, method, params });
885
+ });
886
+ }
887
+ notify(method, params) {
888
+ this.write({ jsonrpc: "2.0", method, ...params === void 0 ? {} : { params } });
889
+ }
890
+ async close() {
891
+ if (this.closing || this.closed) return this.closePromise;
892
+ this.closing = true;
893
+ this.signalTree("SIGTERM");
894
+ this.killTimer = setTimeout(() => {
895
+ if (!this.closed) this.signalTree("SIGKILL");
896
+ }, this.terminationGraceMs);
897
+ await this.closePromise;
898
+ if (this.killTimer !== void 0) clearTimeout(this.killTimer);
899
+ }
900
+ write(message) {
901
+ if (this.child.stdin === null || this.child.stdin.destroyed) {
902
+ throw new CodexDispatchError("app-server stdin is closed");
903
+ }
904
+ this.child.stdin.write(`${JSON.stringify(message)}
905
+ `);
906
+ }
907
+ onLine(line) {
908
+ let message;
909
+ try {
910
+ message = JSON.parse(line);
911
+ } catch {
912
+ this.protocolFailure("app-server emitted non-JSON output");
913
+ return;
914
+ }
915
+ if (!isRecord3(message)) {
916
+ this.protocolFailure("app-server emitted a non-object message");
917
+ return;
918
+ }
919
+ if (typeof message["id"] === "number" && ("result" in message || "error" in message)) {
920
+ const pending = this.pending.get(message["id"]);
921
+ if (!pending) {
922
+ this.protocolFailure("app-server emitted a response for an unknown request");
923
+ return;
924
+ }
925
+ this.pending.delete(message["id"]);
926
+ clearTimeout(pending.timer);
927
+ if (message["error"] !== void 0) {
928
+ if (pending.method === "model/list" && isRecord3(message["error"]) && typeof message["error"]["message"] === "string" && /not authenticated|unauthenticated|authentication|login required|sign in/i.test(message["error"]["message"])) {
929
+ pending.reject(new CodexDispatchError("app-server model catalog is not authenticated"));
930
+ } else {
931
+ pending.reject(new CodexDispatchError(`app-server request '${pending.method}' failed`));
932
+ }
933
+ } else {
934
+ pending.resolve(message["result"]);
935
+ }
936
+ return;
937
+ }
938
+ if (typeof message["method"] === "string" && message["id"] === void 0) {
939
+ try {
940
+ this.onNotification(message["method"], message["params"]);
941
+ } catch {
942
+ this.protocolFailure("app-server notification violated the session contract");
943
+ }
944
+ return;
945
+ }
946
+ if (typeof message["method"] === "string" && message["id"] !== void 0) {
947
+ this.write({
948
+ jsonrpc: "2.0",
949
+ id: message["id"],
950
+ error: { code: -32601, message: "client request not supported" }
951
+ });
952
+ return;
953
+ }
954
+ this.protocolFailure("app-server emitted an invalid JSON-RPC message");
955
+ }
956
+ protocolFailure(message) {
957
+ this.rejectPending(message);
958
+ this.onDisconnect(new CodexDispatchError(message));
959
+ void this.close();
960
+ }
961
+ rejectPending(message) {
962
+ for (const pending of this.pending.values()) {
963
+ clearTimeout(pending.timer);
964
+ pending.reject(new CodexDispatchError(message));
965
+ }
966
+ this.pending.clear();
967
+ }
968
+ signalTree(signal) {
969
+ if (this.closed || this.child.pid === void 0) return;
970
+ if (process.platform !== "win32") {
971
+ try {
972
+ process.kill(-this.child.pid, signal);
973
+ return;
974
+ } catch (error) {
975
+ if (error.code === "ESRCH") return;
976
+ }
977
+ }
978
+ this.child.kill(signal);
979
+ }
980
+ };
981
+
982
+ // src/ask_config.ts
983
+ import { existsSync as existsSync2, mkdtempSync, rmSync, writeFileSync } from "fs";
984
+ import { tmpdir } from "os";
985
+ import { join as join2 } from "path";
986
+ import { fileURLToPath } from "url";
987
+ var ASK_DISABLED_FEATURES = DISABLED_FEATURES.filter(
988
+ (feature) => feature !== "multi_agent"
989
+ );
990
+ function renderConfigValue(value) {
991
+ if (typeof value === "string") return tomlString(value);
992
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
993
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
994
+ return tomlStringArray(value);
995
+ }
996
+ throw new Error("Ask app-server config contains an unsupported value");
997
+ }
998
+ function buildAskAppServerArgs(bundle) {
999
+ const args = ["app-server", "--stdio", "--strict-config"];
1000
+ for (const [key, value] of Object.entries(bundle.parentConfig)) {
1001
+ args.push("-c", `${key}=${renderConfigValue(value)}`);
1002
+ }
1003
+ return args;
1004
+ }
1005
+ function childInstructions(prepared, step) {
1006
+ const toolNames = step.enabledTools.map(
1007
+ (tool) => `${prepared.mcp.name}.${tool} -> ${codexMcpCallableName(prepared.mcp.name, tool)}`
1008
+ ).join(", ");
1009
+ const requestTransportCallable = codexMcpCallableName(
1010
+ REQUEST_TRANSPORT_SERVER,
1011
+ REQUEST_TRANSPORT_TOOL
1012
+ );
1013
+ const stepTransportCallable = codexMcpCallableName(
1014
+ REQUEST_TRANSPORT_SERVER,
1015
+ STEP_TRANSPORT_TOOL
1016
+ );
1017
+ const dashboardContract = prepared.executionKind === "generate_dashboard" ? [
1018
+ `The exact allowed dashboard block contract is ${JSON.stringify(prepared.node.effect.render_blocks)}.`,
1019
+ "Each contract entry's fields object is schema metadata, not an output wrapper: emit each declared field directly beside type at the block top level and never emit a fields key.",
1020
+ "A field whose type ends in ? is optional: omit it when unavailable and never emit null for it.",
1021
+ "Use every required field declared for a chosen block type, use no undeclared fields, and represent each row as a JSON object keyed by its column names."
1022
+ ] : [];
1023
+ const dashboardOutput = prepared.executionKind === "generate_dashboard" && step.name === prepared.steps.at(-1)?.name ? [
1024
+ "The value in the successful step envelope must be the dashboard render artifact: a JSON object with non-empty blocks, optional summary, and boolean verified.",
1025
+ "Blocks may use only the block types and fields declared in the exact allowed dashboard block contract above; include at least one data panel and one definition block.",
1026
+ "Set verified=true only when the required MCP queries completed successfully and the returned values were validated."
1027
+ ] : [];
1028
+ const requiredTool = step.requireSuccessfulTool ? [
1029
+ "This step requires at least one successful call to an enabled MCP tool. The configured tool is available: attempt the call before reporting any tool availability failure."
1030
+ ] : [];
1031
+ const wrenToolArguments = step.enabledTools.includes("get_context") ? [
1032
+ "For wren.get_context, pass exactly one argument named question whose value is the authoritative original request text returned by the request transport call."
1033
+ ] : [];
1034
+ const queryCardinality = step.enabledTools.includes("run_sql") ? [
1035
+ "Before claiming verified=true, check join cardinality and fanout. Never compute independent table counts over a raw CROSS JOIN; use scalar subqueries or independently aggregated CTEs. For joined facts, use declared semantic relationships and distinct entity keys where needed so row multiplication cannot inflate aggregates."
1036
+ ] : [];
1037
+ return [
1038
+ `You are the named Warble step agent '${step.role}'.`,
1039
+ `Execute only IR step '${step.name}' and produce slot '${step.produces}'.`,
1040
+ `Before any reasoning or business MCP call, call ${REQUEST_TRANSPORT_SERVER}.${REQUEST_TRANSPORT_TOOL} through its exact qualified Codex callable ${requestTransportCallable} exactly once. Its returned text is the authoritative original user request for this turn.`,
1041
+ `Then call ${REQUEST_TRANSPORT_SERVER}.${STEP_TRANSPORT_TOOL} through its exact qualified Codex callable ${stepTransportCallable} exactly once. Its returned WARBLE_STEP_REQUEST envelope is the authoritative step and input slots; ignore any task-message copy of those inputs.`,
1042
+ `When MCP tools are exposed through code-mode exec, invoke exactly await tools.${requestTransportCallable}({}); do not guess, shorten, or rename the callable.`,
1043
+ "Never ask the parent to copy, summarize, or reconstruct the original request, and never continue if the request transport call fails.",
1044
+ `Use only these MCP tools when needed (raw identity -> exact qualified Codex callable): ${toolNames}.`,
1045
+ "Under code-mode exec, invoke the qualified callable shown above through tools; do not guess an alias or use exec for any non-MCP operation.",
1046
+ "Do not use shell, file mutation, web, browser, apps, plugins, skills, or child agents.",
1047
+ "Return exactly one JSON object with keys warble_step, produces, ok, value, and error.",
1048
+ `warble_step must equal '${step.name}' and produces must equal '${step.produces}'.`,
1049
+ "On success set ok=true, put the produced slot value in value, and set error=null exactly; never use an empty error string.",
1050
+ "On failure set ok=false, keep the produced slot value with any diagnostics needed by a declared repair step, and use a non-empty stable non-secret error string.",
1051
+ "Do not wrap the JSON in markdown and do not add prose.",
1052
+ ...requiredTool,
1053
+ ...wrenToolArguments,
1054
+ ...queryCardinality,
1055
+ ...dashboardContract,
1056
+ ...dashboardOutput,
1057
+ "",
1058
+ "Step contract:",
1059
+ step.prompt
1060
+ ].join("\n");
1061
+ }
1062
+ function renderAskAgentToml(prepared, step, requestFile, stepRequestFile) {
1063
+ const serverKey = `mcp_servers.${prepared.mcp.name}`;
1064
+ const requestServerKey = `mcp_servers.${REQUEST_TRANSPORT_SERVER}`;
1065
+ const builtRequestMcp = fileURLToPath(new URL("./request_mcp.js", import.meta.url));
1066
+ const sourceRequestMcp = fileURLToPath(new URL("./request_mcp.ts", import.meta.url));
1067
+ const sourceTsx = fileURLToPath(new URL("../node_modules/.bin/tsx", import.meta.url));
1068
+ const requestMcp = existsSync2(builtRequestMcp) ? builtRequestMcp : sourceRequestMcp;
1069
+ const requestMcpCommand = existsSync2(builtRequestMcp) ? process.execPath : sourceTsx;
1070
+ if (!existsSync2(requestMcp) || !existsSync2(requestMcpCommand)) {
1071
+ throw new Error("Ask request transport executable is unavailable");
1072
+ }
1073
+ const lines = [
1074
+ `name = ${tomlString(step.role)}`,
1075
+ `description = ${tomlString(`Executes Warble IR step ${step.name}`)}`,
1076
+ `developer_instructions = ${tomlString(childInstructions(prepared, step))}`,
1077
+ `model = ${tomlString(step.model)}`,
1078
+ `approval_policy = ${tomlString("never")}`,
1079
+ `sandbox_mode = ${tomlString("read-only")}`,
1080
+ "",
1081
+ "[agents]",
1082
+ "enabled = false",
1083
+ "",
1084
+ `[${serverKey}]`,
1085
+ `command = ${tomlString(prepared.mcp.command)}`,
1086
+ `args = ${tomlStringArray(prepared.mcp.args ?? [])}`,
1087
+ `enabled_tools = ${tomlStringArray(step.enabledTools)}`,
1088
+ `default_tools_approval_mode = ${tomlString("approve")}`,
1089
+ "required = true",
1090
+ "",
1091
+ `[${requestServerKey}]`,
1092
+ `command = ${tomlString(requestMcpCommand)}`,
1093
+ `args = ${tomlStringArray([requestMcp, "--request-file", requestFile, "--step-file", stepRequestFile])}`,
1094
+ `enabled_tools = ${tomlStringArray([REQUEST_TRANSPORT_TOOL, STEP_TRANSPORT_TOOL])}`,
1095
+ `default_tools_approval_mode = ${tomlString("approve")}`,
1096
+ "required = true",
1097
+ ""
1098
+ ];
1099
+ return lines.join("\n");
1100
+ }
1101
+ function createAskAgentConfigBundle(prepared) {
1102
+ const directory = mkdtempSync(join2(tmpdir(), "warble-codex-agents-"));
1103
+ try {
1104
+ const requestFile = join2(directory, "original-request.txt");
1105
+ const stepRequestFile = join2(directory, "step-request.txt");
1106
+ writeFileSync(requestFile, "", { encoding: "utf8", mode: 384 });
1107
+ writeFileSync(stepRequestFile, "", { encoding: "utf8", mode: 384 });
1108
+ const agents = prepared.steps.map((step) => {
1109
+ const path = join2(directory, `${step.role}.toml`);
1110
+ writeFileSync(path, renderAskAgentToml(prepared, step, requestFile, stepRequestFile), { encoding: "utf8", mode: 384 });
1111
+ return { role: step.role, path, model: step.model, tools: [...step.enabledTools] };
1112
+ });
1113
+ const parentConfig = {
1114
+ "shell_environment_policy.inherit": "none",
1115
+ project_doc_max_bytes: 0,
1116
+ project_root_markers: [],
1117
+ web_search: "disabled",
1118
+ // Current Codex collaboration tools are invoked through code-mode exec.
1119
+ // The parent has no business MCP servers and every non-collaboration
1120
+ // surface remains disabled below, so this only exposes the IR driver.
1121
+ "features.code_mode.enabled": true,
1122
+ "features.multi_agent": true,
1123
+ "agents.enabled": true,
1124
+ // Codex applies this as the total spawned-thread capacity for the session.
1125
+ // Warble enforces sequential spawn -> wait ordering in the event validator.
1126
+ "agents.max_concurrent_threads_per_session": prepared.steps.length,
1127
+ ...Object.fromEntries(
1128
+ ASK_DISABLED_FEATURES.map((feature) => [`features.${feature}`, false])
1129
+ )
1130
+ };
1131
+ for (const agent of agents) {
1132
+ parentConfig[`agents.${agent.role}.description`] = `Execute only the Warble step mapped to ${agent.role}`;
1133
+ parentConfig[`agents.${agent.role}.config_file`] = agent.path;
1134
+ }
1135
+ return {
1136
+ directory,
1137
+ requestFile,
1138
+ stepRequestFile,
1139
+ agents,
1140
+ parentConfig,
1141
+ bindRequest: (request) => writeFileSync(requestFile, request, { encoding: "utf8", mode: 384 }),
1142
+ bindStepRequest: (request) => writeFileSync(stepRequestFile, request, { encoding: "utf8", mode: 384 }),
1143
+ cleanup: () => rmSync(directory, { recursive: true, force: true })
1144
+ };
1145
+ } catch (error) {
1146
+ rmSync(directory, { recursive: true, force: true });
1147
+ throw error;
1148
+ }
1149
+ }
1150
+
1151
+ // src/session_types.ts
1152
+ var SESSION_REFERENCE_VERSION = "0.1";
1153
+
1154
+ // src/ask_runtime.ts
1155
+ var PASSIVE_PARENT_ITEMS = /* @__PURE__ */ new Set([
1156
+ "userMessage",
1157
+ "agentMessage",
1158
+ "reasoning",
1159
+ "plan",
1160
+ "subAgentActivity",
1161
+ "contextCompaction"
1162
+ ]);
1163
+ var IGNORED_NOTIFICATIONS = /* @__PURE__ */ new Set([
1164
+ "thread/started",
1165
+ "thread/status/changed",
1166
+ "thread/tokenUsage/updated",
1167
+ "turn/plan/updated",
1168
+ "item/agentMessage/delta",
1169
+ "item/plan/delta",
1170
+ "item/reasoning/summaryTextDelta",
1171
+ "item/reasoning/summaryPartAdded",
1172
+ "item/reasoning/textDelta",
1173
+ "skills/changed",
1174
+ "mcpServer/startupStatus/updated",
1175
+ "account/updated",
1176
+ "account/rateLimits/updated",
1177
+ "remoteControl/status/changed",
1178
+ "model/rerouted",
1179
+ "configWarning",
1180
+ "warning"
1181
+ ]);
1182
+ var CHILD_THREAD_NOTIFICATIONS = /* @__PURE__ */ new Set([
1183
+ "turn/started",
1184
+ "item/started",
1185
+ "item/completed",
1186
+ "turn/completed"
1187
+ ]);
1188
+ function isRecord4(value) {
1189
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1190
+ }
1191
+ function record(value, context) {
1192
+ if (!isRecord4(value)) throw new CodexDispatchError(`${context} requires an object`);
1193
+ return value;
1194
+ }
1195
+ function string(recordValue, key, context) {
1196
+ const value = recordValue[key];
1197
+ if (typeof value !== "string" || value.length === 0) {
1198
+ throw new CodexDispatchError(`${context} requires string ${key}`);
1199
+ }
1200
+ return value;
1201
+ }
1202
+ function sessionReference(thread) {
1203
+ return {
1204
+ version: SESSION_REFERENCE_VERSION,
1205
+ target: "codex:local",
1206
+ threadId: string(thread, "id", "thread"),
1207
+ forkedFromThreadId: typeof thread["forkedFromId"] === "string" ? thread["forkedFromId"] : null
1208
+ };
1209
+ }
1210
+ function turnStatus(value) {
1211
+ switch (value) {
1212
+ case "inProgress":
1213
+ return "in_progress";
1214
+ case "completed":
1215
+ case "interrupted":
1216
+ case "failed":
1217
+ return value;
1218
+ default:
1219
+ throw new CodexDispatchError("turn requires a recognized status");
1220
+ }
1221
+ }
1222
+ function turnReference(threadId, value) {
1223
+ const turn = record(value, "turn");
1224
+ return { threadId, turnId: string(turn, "id", "turn"), status: turnStatus(turn["status"]) };
1225
+ }
1226
+ function validateReference(reference) {
1227
+ if (reference.version !== SESSION_REFERENCE_VERSION || reference.target !== "codex:local" || reference.threadId.length === 0) {
1228
+ throw new CodexDispatchError("invalid codex session reference");
1229
+ }
1230
+ }
1231
+ function canonical(value) {
1232
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1233
+ if (isRecord4(value)) {
1234
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
1235
+ }
1236
+ return JSON.stringify(value);
1237
+ }
1238
+ function parseEnvelope(text2, step) {
1239
+ let value;
1240
+ try {
1241
+ value = JSON.parse(text2);
1242
+ } catch {
1243
+ throw new CodexDispatchError(`agent '${step.role}' returned a non-JSON step envelope`);
1244
+ }
1245
+ const envelope = record(value, `agent '${step.role}' envelope`);
1246
+ const keys = Object.keys(envelope).sort();
1247
+ const expectedKeys = ["error", "ok", "produces", "value", "warble_step"];
1248
+ if (canonical(keys) !== canonical(expectedKeys)) {
1249
+ throw new CodexDispatchError(`agent '${step.role}' returned an unexpected envelope shape`);
1250
+ }
1251
+ if (envelope["warble_step"] !== step.name || envelope["produces"] !== step.produces || typeof envelope["ok"] !== "boolean" || envelope["error"] !== null && typeof envelope["error"] !== "string") {
1252
+ throw new CodexDispatchError(`agent '${step.role}' returned a mismatched step envelope`);
1253
+ }
1254
+ if (envelope["ok"] === true && envelope["error"] !== null) {
1255
+ throw new CodexDispatchError(`agent '${step.role}' marked success with an error`);
1256
+ }
1257
+ if (envelope["ok"] === false && (typeof envelope["error"] !== "string" || envelope["error"].trim().length === 0)) {
1258
+ throw new CodexDispatchError(`agent '${step.role}' marked failure without an error`);
1259
+ }
1260
+ return envelope;
1261
+ }
1262
+ function validateAnswerQueryValue(value) {
1263
+ const answer = record(value, "answer_query final value");
1264
+ if (canonical(Object.keys(answer).sort()) !== canonical(["columns", "definition", "rows", "summary", "verified"])) {
1265
+ throw new CodexDispatchError("answer_query success requires the canonical rich result shape");
1266
+ }
1267
+ const definition = record(answer["definition"], "answer_query definition");
1268
+ if (canonical(Object.keys(definition).sort()) !== canonical(["filters", "source_tables", "sql"])) {
1269
+ throw new CodexDispatchError("answer_query success requires complete run provenance");
1270
+ }
1271
+ if (!Array.isArray(answer["columns"]) || !answer["columns"].every((column) => typeof column === "string" && column.length > 0) || !Array.isArray(answer["rows"]) || typeof answer["summary"] !== "string" || answer["summary"].trim().length === 0 || answer["verified"] !== true || typeof definition["sql"] !== "string" || definition["sql"].trim().length === 0 || !Array.isArray(definition["source_tables"]) || !definition["source_tables"].every(
1272
+ (table) => typeof table === "string" && table.length > 0
1273
+ ) || !Array.isArray(definition["filters"])) {
1274
+ throw new CodexDispatchError(
1275
+ "answer_query success requires a grounded summary, verification, and complete run provenance"
1276
+ );
1277
+ }
1278
+ return answer;
1279
+ }
1280
+ function parseStepRequest(text2, step) {
1281
+ const prefix = "WARBLE_STEP_REQUEST\n";
1282
+ if (!text2.startsWith(prefix)) {
1283
+ throw new CodexDispatchError(`agent '${step.role}' input lacks the Warble step envelope`);
1284
+ }
1285
+ let value;
1286
+ try {
1287
+ value = JSON.parse(text2.slice(prefix.length));
1288
+ } catch {
1289
+ throw new CodexDispatchError(`agent '${step.role}' input has malformed JSON`);
1290
+ }
1291
+ const request = record(value, `agent '${step.role}' input`);
1292
+ const keys = Object.keys(request).sort();
1293
+ if (canonical(keys) !== canonical(["inputs", "step"]) || request["step"] !== step.name || !isRecord4(request["inputs"])) {
1294
+ throw new CodexDispatchError(`agent '${step.role}' input does not match its IR step`);
1295
+ }
1296
+ return request;
1297
+ }
1298
+ function buildStepRequest(step, slots) {
1299
+ return `WARBLE_STEP_REQUEST
1300
+ ${JSON.stringify({
1301
+ step: step.name,
1302
+ inputs: Object.fromEntries(step.consumes.map((slot) => [slot, slots[slot]]))
1303
+ })}`;
1304
+ }
1305
+ function stepCountBounds(steps) {
1306
+ return {
1307
+ minimumSteps: steps.filter((step) => !step.conditional).length,
1308
+ maximumSteps: steps.length
1309
+ };
1310
+ }
1311
+ function repairersByTarget(steps) {
1312
+ const map = /* @__PURE__ */ new Map();
1313
+ for (let index = 1; index < steps.length; index += 1) {
1314
+ const step = steps[index];
1315
+ const previous = steps[index - 1];
1316
+ if (step.conditional && step.when?.target === previous.name) {
1317
+ map.set(previous.name, step);
1318
+ }
1319
+ }
1320
+ return map;
1321
+ }
1322
+ function buildAskDriverPrompt(prepared) {
1323
+ const steps = prepared.steps.map((step, index) => {
1324
+ const inputDescription = step.consumes.length === 0 ? "an empty inputs object" : `inputs containing only ${step.consumes.join(", ")} copied exactly from the prior agent value`;
1325
+ return `${index + 1}. Spawn agent_type=${step.role} for step=${step.name} with ${inputDescription}. Wait for it before any later spawn.`;
1326
+ });
1327
+ const repairers = repairersByTarget(prepared.steps);
1328
+ const repairRules = prepared.steps.flatMap((step) => {
1329
+ const repairer = repairers.get(step.name);
1330
+ if (repairer === void 0) return [];
1331
+ return [
1332
+ `If '${step.name}' returns ok=true, do not spawn '${repairer.role}'.`,
1333
+ `If it returns ok=false, spawn '${repairer.role}' exactly once; if repair fails, fail loudly.`
1334
+ ];
1335
+ });
1336
+ const producesRenderEnvelope = prepared.executionKind === "generate_dashboard";
1337
+ const executionRules = producesRenderEnvelope ? [
1338
+ "Every declared step is required. If any child returns ok=false, fail loudly and stop.",
1339
+ "Do not write files in the parent or children; the final validated render envelope is the consumer-persistable artifact output.",
1340
+ ...repairRules
1341
+ ] : repairRules;
1342
+ return [
1343
+ `Execute Warble component '${prepared.componentId}' by named child-agent delegation only.`,
1344
+ "Do not perform any IR step in the parent and do not use business MCP tools in the parent.",
1345
+ "Use Codex's direct collaboration tools for every spawn and wait. Call spawn_agent and wait_agent as tool calls; do not invoke collaboration through exec or code mode.",
1346
+ "Select the exact custom agent type named for each step and send the exact child message below. Give the spawn a short unique task name when the current tool schema requires one.",
1347
+ "Do not override the child model or reasoning effort, and do not fork the parent conversation into the child. After each spawn, wait for that child to complete before any later spawn.",
1348
+ `The dispatcher supplies the authoritative original request directly to each child through ${REQUEST_TRANSPORT_SERVER}.${REQUEST_TRANSPORT_TOOL}; never copy, summarize, or include the request in a child message.`,
1349
+ "For every child, send exactly this message:",
1350
+ "WARBLE_STEP_REQUEST",
1351
+ '{"step":"<step>","inputs":{"<slot>":<prior value>}}',
1352
+ "The JSON object must contain only step and inputs. Never add the original request, a request summary, or any extra field.",
1353
+ "Each child returns a JSON envelope. Copy its value exactly into the next declared input slot.",
1354
+ "Spawn without an explicit model override: the named custom-agent config owns the model.",
1355
+ "",
1356
+ ...steps,
1357
+ "",
1358
+ ...executionRules,
1359
+ "Do not copy the final child value into the parent response; large structured values must remain authoritative in the child thread.",
1360
+ 'Your final message must be exactly {"warble_final_step":"<actual final successful step name>","ok":true} with no prose.'
1361
+ ].join("\n");
1362
+ }
1363
+ var CodexAskRuntime = class _CodexAskRuntime {
1364
+ constructor(prepared, options) {
1365
+ this.prepared = prepared;
1366
+ this.options = options;
1367
+ }
1368
+ prepared;
1369
+ options;
1370
+ transport;
1371
+ bundle;
1372
+ session = null;
1373
+ active = null;
1374
+ startingTurn = false;
1375
+ pendingTurnNotifications = [];
1376
+ disconnected = false;
1377
+ static async connect(prepared, options) {
1378
+ const runtime = new _CodexAskRuntime(prepared, options);
1379
+ runtime.bundle = createAskAgentConfigBundle(prepared);
1380
+ try {
1381
+ runtime.transport = await CodexAppServerTransport.startWithArgs(
1382
+ [...options.codexArgsPrefix ?? [], ...buildAskAppServerArgs(runtime.bundle)],
1383
+ options,
1384
+ (method, params) => runtime.onNotification(method, params),
1385
+ (error) => runtime.onDisconnect(error)
1386
+ );
1387
+ return runtime;
1388
+ } catch (error) {
1389
+ runtime.bundle.cleanup();
1390
+ throw error;
1391
+ }
1392
+ }
1393
+ async start() {
1394
+ this.ensureConnected();
1395
+ if (this.session !== null) throw new CodexDispatchError("an Ask session is already loaded");
1396
+ const result = record(
1397
+ await this.transport.request("thread/start", {
1398
+ model: this.prepared.models.orchestrator,
1399
+ cwd: this.options.cwd,
1400
+ approvalPolicy: "never",
1401
+ sandbox: "read-only",
1402
+ config: this.bundle.parentConfig,
1403
+ ephemeral: false,
1404
+ historyMode: "legacy",
1405
+ environments: [],
1406
+ runtimeWorkspaceRoots: [],
1407
+ selectedCapabilityRoots: [],
1408
+ dynamicTools: [],
1409
+ experimentalRawEvents: false
1410
+ }),
1411
+ "thread/start response"
1412
+ );
1413
+ this.session = sessionReference(record(result["thread"], "thread/start thread"));
1414
+ this.emit({ t: "session_started", session: this.session });
1415
+ return this.session;
1416
+ }
1417
+ async resume(reference) {
1418
+ validateReference(reference);
1419
+ this.ensureConnected();
1420
+ if (this.active !== null) throw new CodexDispatchError("cannot resume while an Ask turn is active");
1421
+ const result = record(
1422
+ await this.transport.request("thread/resume", {
1423
+ threadId: reference.threadId,
1424
+ model: this.prepared.models.orchestrator,
1425
+ cwd: this.options.cwd,
1426
+ approvalPolicy: "never",
1427
+ sandbox: "read-only",
1428
+ config: this.bundle.parentConfig,
1429
+ runtimeWorkspaceRoots: []
1430
+ }),
1431
+ "thread/resume response"
1432
+ );
1433
+ const resumed = sessionReference(record(result["thread"], "thread/resume thread"));
1434
+ if (resumed.threadId !== reference.threadId) {
1435
+ throw new CodexDispatchError("thread/resume returned a different thread id");
1436
+ }
1437
+ this.session = resumed;
1438
+ this.emit({ t: "session_resumed", session: resumed });
1439
+ return resumed;
1440
+ }
1441
+ async run(reference, request, signal) {
1442
+ validateReference(reference);
1443
+ this.ensureConnected();
1444
+ if (this.session?.threadId !== reference.threadId) {
1445
+ throw new CodexDispatchError("Ask session reference is not loaded; resume it first");
1446
+ }
1447
+ if (this.active !== null) throw new CodexDispatchError("an Ask turn is already active");
1448
+ if (request.trim().length === 0) throw new CodexDispatchError("Ask request must not be empty");
1449
+ if (signal?.aborted) throw new CodexDispatchError("Ask turn was cancelled before start");
1450
+ let resolveRun;
1451
+ let rejectRun;
1452
+ const completion = new Promise((resolve4, reject) => {
1453
+ resolveRun = resolve4;
1454
+ rejectRun = reject;
1455
+ });
1456
+ let turn;
1457
+ this.startingTurn = true;
1458
+ this.pendingTurnNotifications = [];
1459
+ try {
1460
+ this.bundle.bindRequest(request);
1461
+ const initialStepRequest = buildStepRequest(this.prepared.steps[0], {});
1462
+ this.bundle.bindStepRequest(initialStepRequest);
1463
+ const result = record(
1464
+ await this.transport.request("turn/start", {
1465
+ threadId: reference.threadId,
1466
+ input: [{ type: "text", text: buildAskDriverPrompt(this.prepared), text_elements: [] }],
1467
+ approvalPolicy: "never",
1468
+ environments: [],
1469
+ runtimeWorkspaceRoots: []
1470
+ }),
1471
+ "turn/start response"
1472
+ );
1473
+ turn = turnReference(reference.threadId, result["turn"]);
1474
+ if (turn.status !== "in_progress") {
1475
+ throw new CodexDispatchError("turn/start did not return an in-progress turn");
1476
+ }
1477
+ this.active = {
1478
+ threadId: reference.threadId,
1479
+ turnId: turn.turnId,
1480
+ started: false,
1481
+ completed: false,
1482
+ status: "in_progress",
1483
+ spawns: [],
1484
+ pendingItems: /* @__PURE__ */ new Map(),
1485
+ pendingChildThreadIds: /* @__PURE__ */ new Set(),
1486
+ pendingChildCompletedIds: /* @__PURE__ */ new Set(),
1487
+ deferredWaitItems: [],
1488
+ stepRequests: [initialStepRequest],
1489
+ slots: {},
1490
+ childAnswers: /* @__PURE__ */ new Map(),
1491
+ finalText: null,
1492
+ deferredTurnCompletion: null,
1493
+ stopReason: null,
1494
+ stopCompleted: null,
1495
+ resolve: resolveRun,
1496
+ reject: rejectRun
1497
+ };
1498
+ } catch (error) {
1499
+ this.startingTurn = false;
1500
+ this.pendingTurnNotifications = [];
1501
+ throw error;
1502
+ }
1503
+ this.startingTurn = false;
1504
+ const pendingNotifications = this.pendingTurnNotifications;
1505
+ this.pendingTurnNotifications = [];
1506
+ for (const [method, params] of pendingNotifications) {
1507
+ this.onNotification(method, params);
1508
+ }
1509
+ const timeoutMs = this.options.turnTimeoutMs ?? 12e4;
1510
+ const timer = setTimeout(() => {
1511
+ void this.stopTurn(turn, "turn_timeout");
1512
+ }, timeoutMs);
1513
+ const cancel = () => {
1514
+ void this.stopTurn(turn, "turn_cancelled");
1515
+ };
1516
+ signal?.addEventListener("abort", cancel, { once: true });
1517
+ if (signal?.aborted) cancel();
1518
+ try {
1519
+ await completion;
1520
+ clearTimeout(timer);
1521
+ signal?.removeEventListener("abort", cancel);
1522
+ const active = this.active;
1523
+ if (active === null || active.turnId !== turn.turnId) {
1524
+ throw new CodexDispatchError("Ask turn state was displaced");
1525
+ }
1526
+ const steps = await this.validateChildren(active);
1527
+ const finalStep = steps.at(-1);
1528
+ if (!finalStep?.ok) throw new CodexDispatchError("Ask run has no successful final step");
1529
+ if (!isRecord4(finalStep.value)) {
1530
+ throw new CodexDispatchError("Ask final value must be a JSON object");
1531
+ }
1532
+ let parentFinal;
1533
+ try {
1534
+ parentFinal = JSON.parse(active.finalText ?? "");
1535
+ } catch {
1536
+ throw new CodexDispatchError("Ask parent final message is not JSON");
1537
+ }
1538
+ const expectedReceipt = { warble_final_step: finalStep.step, ok: true };
1539
+ if (canonical(parentFinal) !== canonical(expectedReceipt)) {
1540
+ throw new CodexDispatchError("Ask parent final message does not match the final child receipt");
1541
+ }
1542
+ let artifact = null;
1543
+ let renderDegraded = false;
1544
+ let finalValue = finalStep.value;
1545
+ if (this.prepared.executionKind === "answer_query") {
1546
+ finalValue = validateAnswerQueryValue(finalStep.value);
1547
+ finalStep.value = finalValue;
1548
+ } else {
1549
+ try {
1550
+ const envelope = validateDashboardRenderEnvelope(finalStep.value, this.prepared.node);
1551
+ finalValue = envelope;
1552
+ finalStep.value = envelope;
1553
+ artifact = {
1554
+ version: SESSION_REFERENCE_VERSION,
1555
+ kind: "render_envelope",
1556
+ parentThreadId: active.threadId,
1557
+ parentTurnId: active.turnId,
1558
+ agentThreadId: finalStep.agentThreadId,
1559
+ step: finalStep.step,
1560
+ agentRole: finalStep.agentRole,
1561
+ verified: envelope.verified,
1562
+ blockTypes: envelope.blocks.map((block) => String(block["type"]))
1563
+ };
1564
+ this.emit({ t: "render_artifact", reference: artifact });
1565
+ } catch (error) {
1566
+ if (!(error instanceof CodexDispatchError)) throw error;
1567
+ renderDegraded = true;
1568
+ this.emit({
1569
+ t: "render_degraded",
1570
+ parentThreadId: active.threadId,
1571
+ parentTurnId: active.turnId,
1572
+ reason: "invalid_render_envelope"
1573
+ });
1574
+ }
1575
+ }
1576
+ const completed = {
1577
+ threadId: active.threadId,
1578
+ turnId: active.turnId,
1579
+ status: active.status
1580
+ };
1581
+ this.emit({ t: "turn_completed", turn: completed });
1582
+ return {
1583
+ target: "codex:local",
1584
+ component: this.prepared.componentId,
1585
+ session: reference,
1586
+ turn: completed,
1587
+ finalText: JSON.stringify(finalValue),
1588
+ value: finalValue,
1589
+ steps,
1590
+ artifact,
1591
+ renderDegraded
1592
+ };
1593
+ } finally {
1594
+ clearTimeout(timer);
1595
+ signal?.removeEventListener("abort", cancel);
1596
+ this.startingTurn = false;
1597
+ this.pendingTurnNotifications = [];
1598
+ this.active = null;
1599
+ }
1600
+ }
1601
+ async restartAndResume(reference) {
1602
+ if (this.active !== null) throw new CodexDispatchError("cannot restart while an Ask turn is active");
1603
+ await this.transport.close();
1604
+ this.transport = await CodexAppServerTransport.startWithArgs(
1605
+ [...this.options.codexArgsPrefix ?? [], ...buildAskAppServerArgs(this.bundle)],
1606
+ this.options,
1607
+ (method, params) => this.onNotification(method, params),
1608
+ (error) => this.onDisconnect(error)
1609
+ );
1610
+ this.disconnected = false;
1611
+ try {
1612
+ return await this.resume(reference);
1613
+ } catch (error) {
1614
+ this.disconnected = true;
1615
+ await this.transport.close();
1616
+ throw error;
1617
+ }
1618
+ }
1619
+ async close() {
1620
+ this.disconnected = true;
1621
+ this.active?.reject(new CodexDispatchError("Ask runtime closed during an active turn"));
1622
+ this.active = null;
1623
+ await this.transport.close();
1624
+ this.bundle.cleanup();
1625
+ }
1626
+ onNotification(method, paramsValue) {
1627
+ try {
1628
+ if (IGNORED_NOTIFICATIONS.has(method)) return;
1629
+ const params = record(paramsValue, `${method} notification`);
1630
+ if (this.active === null && this.startingTurn) {
1631
+ this.pendingTurnNotifications.push([method, paramsValue]);
1632
+ return;
1633
+ }
1634
+ if (method === "error") {
1635
+ if (params["willRetry"] === true) return;
1636
+ throw new CodexDispatchError("app-server reported a terminal Ask error");
1637
+ }
1638
+ const active = this.active;
1639
+ if (active === null) throw new CodexDispatchError(`unexpected '${method}' without an active Ask turn`);
1640
+ const notificationThreadId = params["threadId"];
1641
+ if (typeof notificationThreadId === "string" && notificationThreadId !== active.threadId) {
1642
+ const knownChild = active.spawns.some(
1643
+ (spawn3) => spawn3.agentThreadId === notificationThreadId
1644
+ );
1645
+ if (knownChild && CHILD_THREAD_NOTIFICATIONS.has(method)) {
1646
+ this.observeChildNotification(method, params, active, notificationThreadId);
1647
+ return;
1648
+ }
1649
+ if (CHILD_THREAD_NOTIFICATIONS.has(method) && active.spawns.length < this.prepared.steps.length) {
1650
+ active.pendingChildThreadIds.add(notificationThreadId);
1651
+ this.observeChildNotification(method, params, active, notificationThreadId);
1652
+ if (method === "turn/completed") {
1653
+ active.pendingChildCompletedIds.add(notificationThreadId);
1654
+ }
1655
+ if (active.pendingChildThreadIds.size > this.prepared.steps.length - active.spawns.length) {
1656
+ throw new CodexDispatchError("Ask received too many unattributed child threads");
1657
+ }
1658
+ return;
1659
+ }
1660
+ throw new CodexDispatchError("Ask notification belongs to an unknown thread");
1661
+ }
1662
+ if (method === "turn/started") {
1663
+ const turn = turnReference(string(params, "threadId", method), params["turn"]);
1664
+ if (turn.threadId !== active.threadId || turn.turnId !== active.turnId || active.started) {
1665
+ throw new CodexDispatchError("Ask turn start notification does not match active state");
1666
+ }
1667
+ active.started = true;
1668
+ this.emit({ t: "turn_started", turn });
1669
+ return;
1670
+ }
1671
+ if (method === "item/started" || method === "item/completed") {
1672
+ this.onItem(method, params, active);
1673
+ this.tryFinalizeTurn(active);
1674
+ return;
1675
+ }
1676
+ if (method === "turn/completed") {
1677
+ const turn = turnReference(string(params, "threadId", method), params["turn"]);
1678
+ if (!active.started || turn.threadId !== active.threadId || turn.turnId !== active.turnId) {
1679
+ throw new CodexDispatchError("Ask turn completion does not match active state");
1680
+ }
1681
+ if (active.stopReason !== null && turn.status === "interrupted") {
1682
+ active.completed = true;
1683
+ active.status = turn.status;
1684
+ active.stopCompleted?.();
1685
+ return;
1686
+ }
1687
+ if (turn.status !== "completed" || active.finalText === null) {
1688
+ throw new CodexDispatchError("Ask parent turn did not complete with a final answer");
1689
+ }
1690
+ this.synthesizeDirectCollaboration(active);
1691
+ active.deferredTurnCompletion = turn;
1692
+ this.tryFinalizeTurn(active);
1693
+ return;
1694
+ }
1695
+ throw new CodexDispatchError(`unsupported app-server notification '${method}'`);
1696
+ } catch (error) {
1697
+ const failure = error instanceof Error ? error : new Error(String(error));
1698
+ this.active?.reject(failure);
1699
+ this.onDisconnect(
1700
+ failure instanceof CodexDispatchError ? failure : new CodexDispatchError(failure.message)
1701
+ );
1702
+ void this.transport.close();
1703
+ }
1704
+ }
1705
+ onItem(method, params, active) {
1706
+ if (string(params, "threadId", method) !== active.threadId || string(params, "turnId", method) !== active.turnId) {
1707
+ throw new CodexDispatchError("Ask item belongs to a different parent turn");
1708
+ }
1709
+ const item = record(params["item"], `${method} item`);
1710
+ const type = string(item, "type", `${method} item`);
1711
+ if (type === "collabAgentToolCall") {
1712
+ this.onCollabItem(method, item, active);
1713
+ return;
1714
+ }
1715
+ if (!PASSIVE_PARENT_ITEMS.has(type)) {
1716
+ throw new CodexDispatchError(`isolation violation: Ask parent emitted forbidden '${type}'`);
1717
+ }
1718
+ if (method === "item/completed" && type === "agentMessage") {
1719
+ active.finalText = string(item, "text", type);
1720
+ }
1721
+ }
1722
+ observeChildNotification(method, params, active, childThreadId) {
1723
+ if (method !== "item/completed") return;
1724
+ const item = record(params["item"], "child item/completed item");
1725
+ if (item["type"] !== "agentMessage") return;
1726
+ if (active.childAnswers.has(childThreadId)) {
1727
+ throw new CodexDispatchError("Ask child emitted more than one final answer");
1728
+ }
1729
+ const answer = string(item, "text", "child agentMessage");
1730
+ active.childAnswers.set(childThreadId, answer);
1731
+ const knownIndex = active.spawns.findIndex((spawn3) => spawn3.agentThreadId === childThreadId);
1732
+ const pendingIndex = [...active.pendingChildThreadIds].indexOf(childThreadId);
1733
+ const stepIndex = knownIndex >= 0 ? knownIndex : active.spawns.length + pendingIndex;
1734
+ const step = this.prepared.steps[stepIndex];
1735
+ if (!step) throw new CodexDispatchError("Ask child answer has no IR step attribution");
1736
+ const envelope = parseEnvelope(answer, step);
1737
+ active.slots[step.produces] = envelope.value;
1738
+ const next = this.prepared.steps[stepIndex + 1];
1739
+ const repairers = repairersByTarget(this.prepared.steps);
1740
+ const isRecoverable = repairers.has(step.name);
1741
+ const shouldPrepareNext = next !== void 0 && (isRecoverable ? !envelope.ok : envelope.ok);
1742
+ if (!shouldPrepareNext || next === void 0) return;
1743
+ const request = buildStepRequest(next, active.slots);
1744
+ active.stepRequests[stepIndex + 1] = request;
1745
+ this.bundle.bindStepRequest(request);
1746
+ }
1747
+ onCollabItem(method, item, active) {
1748
+ const id = string(item, "id", "collaboration item");
1749
+ const tool = string(item, "tool", "collaboration item");
1750
+ if (tool !== "spawnAgent" && tool !== "wait") {
1751
+ throw new CodexDispatchError(`Ask parent used unsupported collaboration tool '${tool}'`);
1752
+ }
1753
+ if (method === "item/started") {
1754
+ if (item["status"] !== "inProgress" || active.pendingItems.has(id)) {
1755
+ throw new CodexDispatchError("collaboration item has an invalid start state");
1756
+ }
1757
+ active.pendingItems.set(id, tool);
1758
+ return;
1759
+ }
1760
+ if (active.pendingItems.get(id) !== tool) {
1761
+ throw new CodexDispatchError("collaboration item completed without a matching start");
1762
+ }
1763
+ active.pendingItems.delete(id);
1764
+ if (item["status"] !== "completed") {
1765
+ throw new CodexDispatchError(`collaboration '${tool}' failed`);
1766
+ }
1767
+ if (tool === "spawnAgent") {
1768
+ const previous = active.spawns.at(-1);
1769
+ if (previous && !previous.waited) {
1770
+ throw new CodexDispatchError("Ask parent spawned the next agent before waiting for the prior one");
1771
+ }
1772
+ const expected = this.prepared.steps[active.spawns.length];
1773
+ if (!expected) throw new CodexDispatchError("Ask parent spawned too many agents");
1774
+ const receiverIds = item["receiverThreadIds"];
1775
+ if (!Array.isArray(receiverIds) || receiverIds.length !== 1 || typeof receiverIds[0] !== "string") {
1776
+ throw new CodexDispatchError("spawnAgent must return exactly one child thread");
1777
+ }
1778
+ const firstPendingChild = active.pendingChildThreadIds.values().next().value;
1779
+ if (firstPendingChild !== void 0 && firstPendingChild !== receiverIds[0]) {
1780
+ throw new CodexDispatchError("spawnAgent attributed a different child thread than its notifications");
1781
+ }
1782
+ if (firstPendingChild !== void 0) active.pendingChildThreadIds.delete(firstPendingChild);
1783
+ active.pendingChildCompletedIds.delete(receiverIds[0]);
1784
+ const requestedModel = item["model"];
1785
+ if (requestedModel !== null && requestedModel !== expected.model) {
1786
+ throw new CodexDispatchError(`agent '${expected.role}' ran on the wrong model`);
1787
+ }
1788
+ const stepRequest = active.stepRequests[active.spawns.length];
1789
+ if (stepRequest === void 0) {
1790
+ throw new CodexDispatchError(`agent '${expected.role}' spawned before its host input was ready`);
1791
+ }
1792
+ const spawn3 = {
1793
+ callId: id,
1794
+ expected,
1795
+ agentThreadId: receiverIds[0],
1796
+ model: expected.model,
1797
+ prompt: item["prompt"] === null ? null : string(item, "prompt", "spawnAgent"),
1798
+ stepRequest,
1799
+ waited: false
1800
+ };
1801
+ active.spawns.push(spawn3);
1802
+ const deferred = active.deferredWaitItems.shift();
1803
+ if (deferred !== void 0) this.completeWait(deferred, active);
1804
+ return;
1805
+ }
1806
+ const current = active.spawns.at(-1);
1807
+ if (!current?.agentThreadId) {
1808
+ if (active.deferredWaitItems.length >= this.prepared.steps.length - active.spawns.length) {
1809
+ throw new CodexDispatchError("too many waits completed before child attribution");
1810
+ }
1811
+ active.deferredWaitItems.push(item);
1812
+ return;
1813
+ }
1814
+ this.completeWait(item, active);
1815
+ }
1816
+ completeWait(item, active) {
1817
+ const current = active.spawns.at(-1);
1818
+ if (!current?.agentThreadId || current.waited) {
1819
+ throw new CodexDispatchError("wait did not follow exactly one active child spawn");
1820
+ }
1821
+ const receiverIds = item["receiverThreadIds"];
1822
+ if (!Array.isArray(receiverIds) || receiverIds.length !== 1 || receiverIds[0] !== current.agentThreadId) {
1823
+ throw new CodexDispatchError("wait targeted a different child thread");
1824
+ }
1825
+ const states = record(item["agentsStates"], "wait agentsStates");
1826
+ const childState = record(states[current.agentThreadId], "wait child state");
1827
+ if (childState["status"] !== "completed") {
1828
+ throw new CodexDispatchError("wait completed before the child agent succeeded");
1829
+ }
1830
+ current.waited = true;
1831
+ }
1832
+ tryFinalizeTurn(active) {
1833
+ const turn = active.deferredTurnCompletion;
1834
+ if (turn === null || active.pendingItems.size > 0 || active.pendingChildThreadIds.size > 0 || active.deferredWaitItems.length > 0) {
1835
+ return;
1836
+ }
1837
+ active.deferredTurnCompletion = null;
1838
+ active.completed = true;
1839
+ active.status = turn.status;
1840
+ active.resolve();
1841
+ }
1842
+ synthesizeDirectCollaboration(active) {
1843
+ if (active.spawns.length > 0 || active.pendingChildThreadIds.size === 0) return;
1844
+ const childIds = [...active.pendingChildThreadIds];
1845
+ const { minimumSteps, maximumSteps } = stepCountBounds(this.prepared.steps);
1846
+ if (childIds.length < minimumSteps || childIds.length > maximumSteps || childIds.some((id) => !active.pendingChildCompletedIds.has(id))) {
1847
+ throw new CodexDispatchError("direct collaboration children did not complete in the required sequence");
1848
+ }
1849
+ if (active.deferredWaitItems.length !== childIds.length) {
1850
+ throw new CodexDispatchError("direct collaboration did not wait once for every child");
1851
+ }
1852
+ active.spawns = childIds.map((agentThreadId, index) => {
1853
+ const stepRequest = active.stepRequests[index];
1854
+ if (stepRequest === void 0) {
1855
+ throw new CodexDispatchError("direct collaboration child spawned before its host input was ready");
1856
+ }
1857
+ return {
1858
+ callId: `direct-${agentThreadId}`,
1859
+ expected: this.prepared.steps[index],
1860
+ agentThreadId,
1861
+ model: this.prepared.steps[index].model,
1862
+ prompt: null,
1863
+ stepRequest,
1864
+ waited: true
1865
+ };
1866
+ });
1867
+ active.pendingChildThreadIds.clear();
1868
+ active.pendingChildCompletedIds.clear();
1869
+ active.deferredWaitItems = [];
1870
+ }
1871
+ async validateChildren(active) {
1872
+ const { minimumSteps, maximumSteps } = stepCountBounds(this.prepared.steps);
1873
+ if (active.spawns.length < minimumSteps || active.spawns.length > maximumSteps || active.spawns.some((spawn3) => !spawn3.waited)) {
1874
+ throw new CodexDispatchError("Ask parent did not complete the required named-agent sequence");
1875
+ }
1876
+ const results = [];
1877
+ const slots = {};
1878
+ const repairers = repairersByTarget(this.prepared.steps);
1879
+ for (const [index, spawn3] of active.spawns.entries()) {
1880
+ const step = this.prepared.steps[index];
1881
+ if (spawn3.expected !== step || spawn3.agentThreadId === null || spawn3.model !== step.model) {
1882
+ throw new CodexDispatchError("Ask child sequence does not match the IR");
1883
+ }
1884
+ const child = record(
1885
+ await this.transport.request("thread/read", {
1886
+ threadId: spawn3.agentThreadId,
1887
+ includeTurns: true
1888
+ }),
1889
+ "child thread/read response"
1890
+ );
1891
+ const thread = record(child["thread"], "child thread/read thread");
1892
+ if (thread["id"] !== spawn3.agentThreadId || thread["parentThreadId"] !== active.threadId || thread["agentRole"] !== step.role) {
1893
+ throw new CodexDispatchError(`child thread attribution failed for agent '${step.role}'`);
1894
+ }
1895
+ this.emit({
1896
+ t: "agent_started",
1897
+ parentThreadId: active.threadId,
1898
+ parentTurnId: active.turnId,
1899
+ step: step.name,
1900
+ agentRole: step.role,
1901
+ agentThreadId: spawn3.agentThreadId,
1902
+ model: step.model
1903
+ });
1904
+ const turns = thread["turns"];
1905
+ if (!Array.isArray(turns) || turns.length !== 1) {
1906
+ throw new CodexDispatchError(`agent '${step.role}' must have exactly one turn`);
1907
+ }
1908
+ const turn = record(turns[0], `agent '${step.role}' turn`);
1909
+ if (turn["status"] !== "completed" || !Array.isArray(turn["items"])) {
1910
+ throw new CodexDispatchError(`agent '${step.role}' turn did not complete`);
1911
+ }
1912
+ let inputText = null;
1913
+ let answerText = null;
1914
+ const artifacts = [];
1915
+ let originalRequestCalls = 0;
1916
+ let stepRequestCalls = 0;
1917
+ let businessToolSeen = false;
1918
+ for (const itemValue of turn["items"]) {
1919
+ const item = record(itemValue, `agent '${step.role}' item`);
1920
+ const type = string(item, "type", `agent '${step.role}' item`);
1921
+ if (type === "userMessage") {
1922
+ const content = item["content"];
1923
+ if (!Array.isArray(content) || !isRecord4(content[0]) || typeof content[0]["text"] !== "string") {
1924
+ throw new CodexDispatchError(`agent '${step.role}' user input is malformed`);
1925
+ }
1926
+ inputText = content[0]["text"];
1927
+ } else if (type === "agentMessage") {
1928
+ answerText = string(item, "text", `agent '${step.role}' answer`);
1929
+ } else if (type === "mcpToolCall") {
1930
+ const server = string(item, "server", "child MCP item");
1931
+ const tool = string(item, "tool", "child MCP item");
1932
+ const status = string(item, "status", "child MCP item");
1933
+ if (status !== "completed" && status !== "failed") {
1934
+ throw new CodexDispatchError(`agent '${step.role}' has an unfinished MCP tool`);
1935
+ }
1936
+ if (server === REQUEST_TRANSPORT_SERVER) {
1937
+ const successful = !businessToolSeen && status === "completed" && (item["error"] === null || item["error"] === void 0);
1938
+ if (tool === REQUEST_TRANSPORT_TOOL) {
1939
+ if (!successful || originalRequestCalls !== 0 || stepRequestCalls !== 0) {
1940
+ throw new CodexDispatchError(`agent '${step.role}' violated the original request transport contract`);
1941
+ }
1942
+ originalRequestCalls += 1;
1943
+ } else if (tool === STEP_TRANSPORT_TOOL) {
1944
+ if (!successful || originalRequestCalls !== 1 || stepRequestCalls !== 0) {
1945
+ throw new CodexDispatchError(`agent '${step.role}' violated the step request transport contract`);
1946
+ }
1947
+ stepRequestCalls += 1;
1948
+ } else {
1949
+ throw new CodexDispatchError(`agent '${step.role}' used an unknown request transport tool`);
1950
+ }
1951
+ continue;
1952
+ }
1953
+ if (server !== this.prepared.mcp.name || !step.enabledTools.includes(tool)) {
1954
+ throw new CodexDispatchError(`agent '${step.role}' used a non-allowlisted MCP tool`);
1955
+ }
1956
+ businessToolSeen = true;
1957
+ const reference = {
1958
+ version: SESSION_REFERENCE_VERSION,
1959
+ kind: "mcp_tool_result",
1960
+ parentThreadId: active.threadId,
1961
+ parentTurnId: active.turnId,
1962
+ agentThreadId: spawn3.agentThreadId,
1963
+ step: step.name,
1964
+ agentRole: step.role,
1965
+ itemId: string(item, "id", "child MCP item"),
1966
+ server,
1967
+ tool,
1968
+ ok: status === "completed" && (item["error"] === null || item["error"] === void 0)
1969
+ };
1970
+ artifacts.push(reference);
1971
+ this.emit({ t: "artifact", reference });
1972
+ } else if (!(/* @__PURE__ */ new Set(["reasoning", "plan"])).has(type)) {
1973
+ throw new CodexDispatchError(`agent '${step.role}' emitted forbidden '${type}'`);
1974
+ }
1975
+ }
1976
+ if (answerText === null) {
1977
+ throw new CodexDispatchError(`agent '${step.role}' lacks a final answer`);
1978
+ }
1979
+ if (originalRequestCalls !== 1) {
1980
+ throw new CodexDispatchError(`agent '${step.role}' did not load the authoritative original request`);
1981
+ }
1982
+ if (stepRequestCalls !== 1) {
1983
+ throw new CodexDispatchError(`agent '${step.role}' did not load the authoritative step request`);
1984
+ }
1985
+ const requestTexts = [spawn3.stepRequest, inputText, spawn3.prompt].filter(
1986
+ (value) => value !== null
1987
+ );
1988
+ const requests = requestTexts.map((text2) => parseStepRequest(text2, step));
1989
+ if (requests.some((request2) => canonical(request2) !== canonical(requests[0]))) {
1990
+ throw new CodexDispatchError(`agent '${step.role}' has conflicting step inputs`);
1991
+ }
1992
+ const request = requests[0];
1993
+ const inputs = request["inputs"];
1994
+ if (Object.keys(inputs).sort().join(",") !== [...step.consumes].sort().join(",")) {
1995
+ throw new CodexDispatchError(`agent '${step.role}' received the wrong input slots`);
1996
+ }
1997
+ for (const consumed of step.consumes) {
1998
+ if (canonical(inputs[consumed]) !== canonical(slots[consumed])) {
1999
+ throw new CodexDispatchError(`agent '${step.role}' input '${consumed}' was not marshalled exactly`);
2000
+ }
2001
+ }
2002
+ const envelope = parseEnvelope(answerText, step);
2003
+ const repairer = repairers.get(step.name);
2004
+ if (repairer === void 0) {
2005
+ if (!envelope.ok) {
2006
+ throw new CodexDispatchError(
2007
+ step.conditional ? "bounded repair attempt did not recover generation" : `required step '${step.name}' failed`
2008
+ );
2009
+ }
2010
+ } else {
2011
+ const repairerSpawned = active.spawns.length > index + 1;
2012
+ if (!envelope.ok && !repairerSpawned) {
2013
+ throw new CodexDispatchError(
2014
+ `step '${step.name}' failure did not trigger repair step '${repairer.name}'`
2015
+ );
2016
+ }
2017
+ if (envelope.ok && repairerSpawned) {
2018
+ throw new CodexDispatchError(
2019
+ `repair step '${repairer.name}' ran even though '${step.name}' succeeded`
2020
+ );
2021
+ }
2022
+ }
2023
+ if (step.requireSuccessfulTool && artifacts.length === 0) {
2024
+ throw new CodexDispatchError(`agent '${step.role}' completed without its required MCP tool attempt`);
2025
+ }
2026
+ if (envelope.ok && step.requireSuccessfulTool && !artifacts.some((artifact) => artifact.ok)) {
2027
+ throw new CodexDispatchError(`agent '${step.role}' claimed success without a successful MCP tool`);
2028
+ }
2029
+ slots[step.produces] = envelope.value;
2030
+ const result = {
2031
+ step: step.name,
2032
+ agentRole: step.role,
2033
+ agentThreadId: spawn3.agentThreadId,
2034
+ model: step.model,
2035
+ produced: step.produces,
2036
+ ok: envelope.ok,
2037
+ value: envelope.value,
2038
+ artifacts
2039
+ };
2040
+ results.push(result);
2041
+ this.emit({
2042
+ t: "step_finished",
2043
+ parentThreadId: active.threadId,
2044
+ parentTurnId: active.turnId,
2045
+ step: step.name,
2046
+ agentRole: step.role,
2047
+ agentThreadId: spawn3.agentThreadId,
2048
+ ok: envelope.ok
2049
+ });
2050
+ }
2051
+ return results;
2052
+ }
2053
+ async stopTurn(turn, reason) {
2054
+ if (this.active?.turnId !== turn.turnId || this.active.stopReason !== null) return;
2055
+ this.active.stopReason = reason;
2056
+ const transport = this.transport;
2057
+ let resolveStopped;
2058
+ const stopped = new Promise((resolve4) => {
2059
+ resolveStopped = resolve4;
2060
+ });
2061
+ this.active.stopCompleted = resolveStopped;
2062
+ try {
2063
+ await transport.request("turn/interrupt", {
2064
+ threadId: turn.threadId,
2065
+ turnId: turn.turnId
2066
+ });
2067
+ } catch {
2068
+ }
2069
+ let graceTimer;
2070
+ await Promise.race([
2071
+ stopped,
2072
+ new Promise((resolve4) => {
2073
+ graceTimer = setTimeout(resolve4, this.options.terminationGraceMs ?? 1e3);
2074
+ })
2075
+ ]);
2076
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
2077
+ await transport.close();
2078
+ if (this.active?.turnId !== turn.turnId) return;
2079
+ const error = new CodexDispatchError(
2080
+ reason === "turn_timeout" ? `Ask turn '${turn.turnId}' timed out` : `Ask turn '${turn.turnId}' was cancelled`
2081
+ );
2082
+ this.active.reject(error);
2083
+ this.onDisconnect(void 0, reason);
2084
+ }
2085
+ onDisconnect(protocolError, reasonOverride) {
2086
+ if (this.disconnected) return;
2087
+ this.disconnected = true;
2088
+ if (protocolError) {
2089
+ this.emit({ t: "session_failed", threadId: this.session?.threadId ?? null, reason: "protocol_violation" });
2090
+ this.active?.reject(protocolError);
2091
+ } else {
2092
+ this.emit({
2093
+ t: "session_recoverable",
2094
+ threadId: this.session?.threadId ?? null,
2095
+ reason: reasonOverride ?? (this.active ? "app_server_crash" : "transport_disconnect")
2096
+ });
2097
+ this.active?.reject(new CodexDispatchError("app-server disconnected during an Ask turn"));
2098
+ }
2099
+ }
2100
+ ensureConnected() {
2101
+ if (this.disconnected) throw new CodexDispatchError("app-server transport disconnected; resume required");
2102
+ }
2103
+ emit(event) {
2104
+ this.options.onAskEvent?.(event);
2105
+ }
2106
+ };
2107
+
2108
+ // src/enrich_prepare.ts
2109
+ import { isAbsolute as isAbsolute3 } from "path";
2110
+
2111
+ // src/step_engine.ts
2112
+ function parseStepWhen(step) {
2113
+ if (!step.conditional) {
2114
+ if (step.when !== null) {
2115
+ throw new CodexDispatchError(`step '${step.name}' is unconditional but has a when guard`);
2116
+ }
2117
+ return null;
2118
+ }
2119
+ if (typeof step.when !== "object" || step.when === null || Array.isArray(step.when) || step.when["guard"] !== "on_failure" || typeof step.when["target"] !== "string") {
2120
+ throw new CodexDispatchError(`step '${step.name}' wall-hit: repair requires on_failure(target)`);
2121
+ }
2122
+ return {
2123
+ guard: "on_failure",
2124
+ target: step.when["target"]
2125
+ };
2126
+ }
2127
+ function validateStepTopology(node) {
2128
+ const names = /* @__PURE__ */ new Set();
2129
+ for (const step of node.llm_calls) {
2130
+ if (names.has(step.name)) {
2131
+ throw new CodexDispatchError(
2132
+ `component '${node.id}' wall-hit: step name '${step.name}' is declared more than once`
2133
+ );
2134
+ }
2135
+ names.add(step.name);
2136
+ }
2137
+ const topology = [];
2138
+ const produced = /* @__PURE__ */ new Set();
2139
+ for (let index = 0; index < node.llm_calls.length; index += 1) {
2140
+ const step = node.llm_calls[index];
2141
+ for (const consumed of step.consumes) {
2142
+ if (!produced.has(consumed)) {
2143
+ throw new CodexDispatchError(
2144
+ `component '${node.id}' wall-hit: step '${step.name}' consumes '${consumed}' but no earlier step produces it`
2145
+ );
2146
+ }
2147
+ }
2148
+ if (step.produces === null) {
2149
+ throw new CodexDispatchError(
2150
+ `component '${node.id}' wall-hit: this transport requires a produced slot; step '${step.name}' produces none`
2151
+ );
2152
+ }
2153
+ produced.add(step.produces);
2154
+ const when = parseStepWhen(step);
2155
+ if (when !== null) {
2156
+ if (index !== node.llm_calls.length - 1) {
2157
+ throw new CodexDispatchError(
2158
+ `component '${node.id}' wall-hit: conditional step '${step.name}' must be the last step; a step nothing downstream can safely consume from must not have output others rely on`
2159
+ );
2160
+ }
2161
+ if (!names.has(when.target) || !node.llm_calls.slice(0, index).some((earlier) => earlier.name === when.target)) {
2162
+ throw new CodexDispatchError(
2163
+ `component '${node.id}' wall-hit: step '${step.name}' on_failure target '${when.target}' is not an earlier step`
2164
+ );
2165
+ }
2166
+ }
2167
+ topology.push({ when });
2168
+ }
2169
+ return topology;
2170
+ }
2171
+ function parseStepTerminal(text2, produces) {
2172
+ let parsed;
2173
+ try {
2174
+ parsed = JSON.parse(text2);
2175
+ } catch {
2176
+ throw new CodexDispatchError("step terminal is not JSON");
2177
+ }
2178
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2179
+ throw new CodexDispatchError("step terminal must be a JSON object");
2180
+ }
2181
+ const record2 = parsed;
2182
+ const keys = Object.keys(record2);
2183
+ if (keys.length !== 1 || keys[0] !== produces || record2[produces] === null) {
2184
+ throw new CodexDispatchError(`step terminal requires exactly the produced field '${produces}'`);
2185
+ }
2186
+ return record2;
2187
+ }
2188
+ function shouldRunStep(when, outcomes) {
2189
+ if (when === null) return true;
2190
+ const target = outcomes.get(when.target);
2191
+ return target !== void 0 && target.ran && !target.ok;
2192
+ }
2193
+ function resolveStepModel(model, tier, componentId) {
2194
+ const resolved = typeof model === "string" ? model : model[tier];
2195
+ if (resolved === void 0 || resolved.trim().length === 0) {
2196
+ throw new CodexDispatchError(
2197
+ `component '${componentId}' wall-hit: no model binding for tier '${tier}'`
2198
+ );
2199
+ }
2200
+ return resolved;
2201
+ }
2202
+
2203
+ // src/enrich_prepare.ts
2204
+ function unique2(values) {
2205
+ return [...new Set(values)];
2206
+ }
2207
+ function validateEnrichShape(node) {
2208
+ assertDispatchableComponentIdentity(node);
2209
+ for (const capability of node.required_capabilities) {
2210
+ if (!ENRICH_ALLOWED_CAPABILITIES.has(capability)) {
2211
+ throw new CodexDispatchError(
2212
+ `component '${node.id}' cannot be dispatched by codex:local: required capability '${capability}' has no honest realization on this target`
2213
+ );
2214
+ }
2215
+ }
2216
+ if (node.type !== "analytical" || node.realization_kind !== "skill" || node.trigger.kind !== "one_shot" || node.effect.outcome.kind !== "none") {
2217
+ throw new CodexDispatchError(
2218
+ `component '${node.id}' wall-hit: requires analytical/skill/one_shot/none`
2219
+ );
2220
+ }
2221
+ if (node.context_binding.binding_mode !== "pinned") {
2222
+ throw new CodexDispatchError(
2223
+ `component '${node.id}' wall-hit: requires a pinned context binding`
2224
+ );
2225
+ }
2226
+ if (node.llm_calls.length === 0) {
2227
+ throw new CodexDispatchError(`component '${node.id}' wall-hit: at least one llm_call is required`);
2228
+ }
2229
+ validateStepTopology(node);
2230
+ if (node.guardrails.length !== 1 || !guardrailMatches(node.guardrails[0], "read_only_execution", { requireScopeAbsent: true })) {
2231
+ throw new CodexDispatchError(
2232
+ `component '${node.id}' wall-hit: exactly one locked read_only_execution guardrail with no scope is required`
2233
+ );
2234
+ }
2235
+ const domainCapabilities = node.required_capabilities.filter(isEnrichDomainCapability);
2236
+ if (domainCapabilities.length === 0) {
2237
+ throw new CodexDispatchError(
2238
+ `component '${node.id}' wall-hit: at least one of semantic_introspection/raw_material_read is required`
2239
+ );
2240
+ }
2241
+ const tiers = unique2(node.llm_calls.map((step) => step.tier));
2242
+ if (tiers.length !== 1) {
2243
+ throw new CodexDispatchError(
2244
+ `component '${node.id}' wall-hit: this transport's persistent session supports exactly one tier per component; found '${tiers.join("', '")}'`
2245
+ );
2246
+ }
2247
+ const expectedLlm = `llm:${tiers[0]}`;
2248
+ if (!node.required_capabilities.includes(expectedLlm)) {
2249
+ throw new CodexDispatchError(
2250
+ `component '${node.id}' wall-hit: required capability '${expectedLlm}' is missing`
2251
+ );
2252
+ }
2253
+ const expectedCapabilities = /* @__PURE__ */ new Set([...domainCapabilities, expectedLlm]);
2254
+ if (!hasExactCapabilities(node.required_capabilities, expectedCapabilities)) {
2255
+ throw new CodexDispatchError(
2256
+ `component '${node.id}' wall-hit: supports exactly '${domainCapabilities.join("', '")}' and '${expectedLlm}' capabilities`
2257
+ );
2258
+ }
2259
+ return domainCapabilities;
2260
+ }
2261
+ function matchesEnrichContractShape(node) {
2262
+ try {
2263
+ validateEnrichShape(node);
2264
+ return true;
2265
+ } catch (error) {
2266
+ if (error instanceof CodexDispatchError) return false;
2267
+ throw error;
2268
+ }
2269
+ }
2270
+ function enrichContractMismatchReason(node) {
2271
+ try {
2272
+ validateEnrichShape(node);
2273
+ return null;
2274
+ } catch (error) {
2275
+ if (error instanceof CodexDispatchError) return error.message;
2276
+ throw error;
2277
+ }
2278
+ }
2279
+ function prepareEnrich(input) {
2280
+ const ir = typeof input.ir === "string" ? parseIr(input.ir) : input.ir;
2281
+ if (ir.warble_ir_version !== SUPPORTED_IR_VERSION) {
2282
+ throw new CodexDispatchError(
2283
+ `unsupported warble_ir_version '${ir.warble_ir_version}' (supported: ${SUPPORTED_IR_VERSION})`
2284
+ );
2285
+ }
2286
+ const node = ir.components.find((candidate) => candidate.id === input.component);
2287
+ if (!node) {
2288
+ throw new CodexDispatchError(
2289
+ `component '${input.component}' was not found in profile '${ir.profile}'`
2290
+ );
2291
+ }
2292
+ const domainCapabilities = validateEnrichShape(node);
2293
+ const componentId = node.id;
2294
+ if (!/^[A-Za-z0-9_-]+$/.test(input.mcp.name)) {
2295
+ throw new CodexDispatchError(
2296
+ `MCP server name '${input.mcp.name}' must contain only letters, digits, '_' or '-'`
2297
+ );
2298
+ }
2299
+ if (!isAbsolute3(input.mcp.command)) {
2300
+ throw new CodexDispatchError(
2301
+ `MCP server command must be absolute when shell_environment_policy.inherit=none`
2302
+ );
2303
+ }
2304
+ const enabledTools = unique2(
2305
+ domainCapabilities.flatMap((capability) => input.mcp.toolsByCapability[capability] ?? [])
2306
+ );
2307
+ if (enabledTools.length === 0) {
2308
+ throw new CodexDispatchError(
2309
+ `component '${componentId}' has no allowlisted MCP tools for '${domainCapabilities.join("', '")}'`
2310
+ );
2311
+ }
2312
+ const topology = validateStepTopology(node);
2313
+ const steps = node.llm_calls.map((call, index) => ({
2314
+ name: call.name,
2315
+ tier: call.tier,
2316
+ model: resolveStepModel(input.model, call.tier, componentId),
2317
+ prompt: call.prompt,
2318
+ consumes: call.consumes,
2319
+ produces: call.produces,
2320
+ when: topology[index].when
2321
+ }));
2322
+ return {
2323
+ target: TARGET,
2324
+ profile: ir.profile,
2325
+ node,
2326
+ componentId,
2327
+ domainCapabilities,
2328
+ steps,
2329
+ capabilities: resolveCapabilities(node.required_capabilities, input.mcp.name),
2330
+ enabledTools,
2331
+ mcp: input.mcp
2332
+ };
2333
+ }
2334
+
2335
+ // src/prepare.ts
2336
+ import { isAbsolute as isAbsolute4 } from "path";
2337
+ function unique3(values) {
2338
+ return [...new Set(values)];
2339
+ }
2340
+ function validateSetupShape(node) {
2341
+ if (node.type !== "analytical" || node.realization_kind !== "skill" || node.trigger.kind !== "one_shot" || node.effect.outcome.kind !== "none" || node.effect.render_blocks.length !== 0) {
2342
+ throw new CodexDispatchError(
2343
+ `component '${node.id}' wall-hit: requires analytical/skill/one_shot/none with no render blocks`
2344
+ );
2345
+ }
2346
+ if (node.llm_calls.length === 0) {
2347
+ throw new CodexDispatchError(`component '${node.id}' wall-hit: at least one llm_call is required`);
2348
+ }
2349
+ validateStepTopology(node);
2350
+ if (node.guardrails.length !== 1 || !guardrailMatches(node.guardrails[0], "setup_execution")) {
2351
+ throw new CodexDispatchError(
2352
+ `component '${node.id}' wall-hit: exactly one locked setup_execution guardrail with scope '.' is required`
2353
+ );
2354
+ }
2355
+ const domainCapabilities = node.required_capabilities.filter(isSetupDomainCapability);
2356
+ if (domainCapabilities.length !== 1) {
2357
+ throw new CodexDispatchError(
2358
+ `component '${node.id}' wall-hit: exactly one of source_connect/context_build is required`
2359
+ );
2360
+ }
2361
+ const tiers = unique3(node.llm_calls.map((step) => step.tier));
2362
+ const expectedLlm = tiers.length === 1 ? `llm:${tiers[0]}` : "llm:per_step_tier";
2363
+ if (!node.required_capabilities.includes(expectedLlm)) {
2364
+ throw new CodexDispatchError(
2365
+ `component '${node.id}' wall-hit: required capability '${expectedLlm}' is missing`
2366
+ );
2367
+ }
2368
+ const expectedCapabilities = /* @__PURE__ */ new Set([domainCapabilities[0], expectedLlm]);
2369
+ if (!hasExactCapabilities(node.required_capabilities, expectedCapabilities)) {
2370
+ throw new CodexDispatchError(
2371
+ `component '${node.id}' wall-hit: supports exactly '${domainCapabilities[0]}' and '${expectedLlm}' capabilities`
2372
+ );
2373
+ }
2374
+ return domainCapabilities[0];
2375
+ }
2376
+ function matchesSetupContractShape(node) {
2377
+ try {
2378
+ validateSetupShape(node);
2379
+ return true;
2380
+ } catch (error) {
2381
+ if (error instanceof CodexDispatchError) return false;
2382
+ throw error;
2383
+ }
2384
+ }
2385
+ function setupContractMismatchReason(node) {
2386
+ try {
2387
+ validateSetupShape(node);
2388
+ return null;
2389
+ } catch (error) {
2390
+ if (error instanceof CodexDispatchError) return error.message;
2391
+ throw error;
2392
+ }
2393
+ }
2394
+ function prepareSetup(input) {
2395
+ const ir = typeof input.ir === "string" ? parseIr(input.ir) : input.ir;
2396
+ if (ir.warble_ir_version !== SUPPORTED_IR_VERSION) {
2397
+ throw new CodexDispatchError(
2398
+ `unsupported warble_ir_version '${ir.warble_ir_version}' (supported: ${SUPPORTED_IR_VERSION})`
2399
+ );
2400
+ }
2401
+ const node = ir.components.find((candidate) => candidate.id === input.component);
2402
+ if (!node) {
2403
+ throw new CodexDispatchError(`component '${input.component}' was not found in profile '${ir.profile}'`);
2404
+ }
2405
+ assertDispatchableComponentIdentity(node);
2406
+ const domainCapability = validateSetupShape(node);
2407
+ const componentId = node.id;
2408
+ if (!/^[A-Za-z0-9_-]+$/.test(input.mcp.name)) {
2409
+ throw new CodexDispatchError(
2410
+ `MCP server name '${input.mcp.name}' must contain only letters, digits, '_' or '-'`
2411
+ );
2412
+ }
2413
+ if (!isAbsolute4(input.mcp.command)) {
2414
+ throw new CodexDispatchError(
2415
+ `MCP server command must be absolute when shell_environment_policy.inherit=none`
2416
+ );
2417
+ }
2418
+ const enabledTools = unique3(input.mcp.toolsByCapability[domainCapability]);
2419
+ if (enabledTools.length === 0) {
2420
+ throw new CodexDispatchError(
2421
+ `component '${componentId}' has no allowlisted MCP tools for '${domainCapability}'`
2422
+ );
2423
+ }
2424
+ const topology = validateStepTopology(node);
2425
+ const steps = node.llm_calls.map((call, index) => ({
2426
+ name: call.name,
2427
+ tier: call.tier,
2428
+ model: resolveStepModel(input.model, call.tier, componentId),
2429
+ prompt: call.prompt,
2430
+ consumes: call.consumes,
2431
+ produces: call.produces,
2432
+ when: topology[index].when
2433
+ }));
2434
+ return {
2435
+ target: TARGET,
2436
+ profile: ir.profile,
2437
+ node,
2438
+ componentId,
2439
+ domainCapability,
2440
+ steps,
2441
+ capabilities: resolveCapabilities(node.required_capabilities, input.mcp.name),
2442
+ enabledTools,
2443
+ mcp: input.mcp
2444
+ };
2445
+ }
2446
+ function prepareAllSetup(raw, config) {
2447
+ const ir = parseIr(raw);
2448
+ for (const node of ir.components) assertDispatchableComponentIdentity(node);
2449
+ return ir.components.map(
2450
+ (node) => prepareSetup({ ...config, ir, component: node.id })
2451
+ );
2452
+ }
2453
+
2454
+ // src/dispatch_contract.ts
2455
+ function selectedComponent(ir, component) {
2456
+ const node = ir.components.find((candidate) => candidate.id === component);
2457
+ if (!node) {
2458
+ throw new CodexDispatchError(`component '${component}' was not found in profile '${ir.profile}'`);
2459
+ }
2460
+ return node;
2461
+ }
2462
+ function classifyDispatchContract(ir, component) {
2463
+ const node = selectedComponent(ir, component);
2464
+ assertDispatchableComponentIdentity(node);
2465
+ const matches = [
2466
+ ...matchesSetupContractShape(node) ? ["setup"] : [],
2467
+ ...matchesAskContractShape(node) ? ["ask"] : [],
2468
+ ...matchesEnrichContractShape(node) ? ["enrich"] : []
2469
+ ];
2470
+ if (matches.length === 1) return matches[0];
2471
+ if (matches.length === 0) {
2472
+ const reasons = [
2473
+ setupContractMismatchReason(node),
2474
+ askContractMismatchReason(node),
2475
+ enrichContractMismatchReason(node)
2476
+ ].filter((reason) => reason !== null);
2477
+ throw new CodexDispatchError(
2478
+ `component '${node.id}' wall-hit: no supported codex:local execution contract matches its complete IR shape` + (reasons.length > 0 ? ` (${reasons.join(" | ")})` : "")
2479
+ );
2480
+ }
2481
+ throw new CodexDispatchError(
2482
+ `component '${node.id}' wall-hit: ambiguous codex:local execution contracts (${matches.join(", ")})`
2483
+ );
2484
+ }
2485
+ function supportsSetupAggregate(ir) {
2486
+ for (const node of ir.components) assertDispatchableComponentIdentity(node);
2487
+ return ir.components.length > 0 && ir.components.every(matchesSetupContractShape);
2488
+ }
2489
+
2490
+ // src/manifest.ts
2491
+ var SESSION_LIFECYCLE_OPERATIONS = [
2492
+ "start",
2493
+ "resume",
2494
+ "read",
2495
+ "turn",
2496
+ "steer",
2497
+ "interrupt",
2498
+ "fork"
2499
+ ];
2500
+ function buildAskAgentManifest(prepared) {
2501
+ const toolAgents = /* @__PURE__ */ new Map();
2502
+ for (const step of prepared.steps) {
2503
+ for (const tool of step.enabledTools) {
2504
+ const agents = toolAgents.get(tool) ?? [];
2505
+ if (!agents.includes(step.role)) agents.push(step.role);
2506
+ toolAgents.set(tool, agents);
2507
+ }
2508
+ }
2509
+ const dashboard = prepared.executionKind === "generate_dashboard";
2510
+ return {
2511
+ id: prepared.node.id,
2512
+ verb: prepared.node.verb,
2513
+ component_type: prepared.node.type,
2514
+ realization_kind: prepared.node.realization_kind,
2515
+ trigger: prepared.node.trigger.kind,
2516
+ outcome: prepared.node.effect.outcome.kind,
2517
+ steps: prepared.steps.map((step) => ({
2518
+ name: step.name,
2519
+ tier: step.tier,
2520
+ model: step.model,
2521
+ consumes: [...step.consumes],
2522
+ produces: step.produces,
2523
+ agent_role: step.role,
2524
+ conditional: step.conditional,
2525
+ when: step.when,
2526
+ tools: [...step.enabledTools]
2527
+ })),
2528
+ capabilities: prepared.capabilities,
2529
+ tools: [...toolAgents].map(([name, agents]) => ({
2530
+ name,
2531
+ source: `mcp:${prepared.mcp.name}`,
2532
+ agents
2533
+ })),
2534
+ guardrails: {
2535
+ read_only_execution: { enforcement: "per_agent_mcp_only_read_only_sandbox", locked: true },
2536
+ ...dashboard ? {
2537
+ artifact_write: {
2538
+ enforcement: "consumer_persisted_render_envelope",
2539
+ locked: true,
2540
+ scope: "."
2541
+ },
2542
+ render_contract: {
2543
+ enforcement: "validated_ir_declared_render_envelope",
2544
+ on_failure: "degrade"
2545
+ }
2546
+ } : {
2547
+ deterministic_gate: {
2548
+ enforcement: "child_result_envelope_and_event_attribution",
2549
+ locked: true
2550
+ },
2551
+ row_limit: { threshold: 1e3 },
2552
+ statement_timeout: { threshold: 30 }
2553
+ },
2554
+ ordered_delegation: {
2555
+ enforcement: "named_child_threads_in_ir_order",
2556
+ flattening: "forbidden"
2557
+ },
2558
+ ...dashboard ? {} : {
2559
+ conditional_repair: {
2560
+ guard: prepared.steps[2].when,
2561
+ max_attempts: prepared.maxRepairAttempts,
2562
+ exhaustion: "loud_fail"
2563
+ }
2564
+ },
2565
+ isolated_codex_config: {
2566
+ parent_tools: "multi_agent_only",
2567
+ child_tools: "per_step_exact_mcp_allowlist",
2568
+ approval_policy: "never",
2569
+ sandbox: "read-only",
2570
+ api_key_environment: "removed"
2571
+ }
2572
+ },
2573
+ ...dashboard ? {
2574
+ artifact_output: {
2575
+ kind: "render_envelope",
2576
+ persistence: "consumer",
2577
+ block_types: prepared.node.effect.render_blocks.map(
2578
+ (block) => typeof block === "object" && block !== null && "type" in block ? String(block.type) : "unknown"
2579
+ )
2580
+ }
2581
+ } : {}
2582
+ };
2583
+ }
2584
+ function buildAskManifest(prepared) {
2585
+ return {
2586
+ manifest_version: "0.1",
2587
+ compat: {
2588
+ min_ir_version: SUPPORTED_IR_VERSION,
2589
+ max_ir_version: SUPPORTED_IR_VERSION
2590
+ },
2591
+ profile: prepared.profile,
2592
+ target: TARGET,
2593
+ session: {
2594
+ persistence: "codex_thread_history",
2595
+ lifecycle_operations: [...SESSION_LIFECYCLE_OPERATIONS],
2596
+ artifact_reference: prepared.executionKind === "generate_dashboard" ? "allowlisted_mcp_tool_result_or_render_envelope" : "allowlisted_mcp_tool_result",
2597
+ isolation: "dedicated_persistent_codex_home",
2598
+ authentication: "externally_provisioned"
2599
+ },
2600
+ agents: [buildAskAgentManifest(prepared)]
2601
+ };
2602
+ }
2603
+ function describeAskTarget(prepared) {
2604
+ return {
2605
+ target: TARGET,
2606
+ phase: prepared.executionKind === "generate_dashboard" ? "setup-ask-and-dashboard-parity" : "setup-and-ask-parity",
2607
+ execution_modes: ["persistent_session"],
2608
+ session_persistence: "codex_thread_history",
2609
+ lifecycle_operations: [...SESSION_LIFECYCLE_OPERATIONS],
2610
+ supported_components: [prepared.componentId],
2611
+ tiers: [...new Set(prepared.steps.map((step) => step.tier))],
2612
+ capabilities: prepared.capabilities.map((entry) => entry.capability),
2613
+ tools: [...new Set(prepared.steps.flatMap((step) => step.enabledTools))],
2614
+ guardrails: prepared.executionKind === "generate_dashboard" ? [
2615
+ "read_only_execution",
2616
+ "artifact_write",
2617
+ "render_contract",
2618
+ "ordered_delegation",
2619
+ "isolated_codex_config"
2620
+ ] : [
2621
+ "read_only_execution",
2622
+ "deterministic_gate",
2623
+ "row_limit",
2624
+ "statement_timeout",
2625
+ "ordered_delegation",
2626
+ "conditional_repair",
2627
+ "isolated_codex_config"
2628
+ ]
2629
+ };
2630
+ }
2631
+ function buildAgentManifest(prepared) {
2632
+ return {
2633
+ id: prepared.node.id,
2634
+ verb: prepared.node.verb,
2635
+ component_type: prepared.node.type,
2636
+ realization_kind: prepared.node.realization_kind,
2637
+ trigger: prepared.node.trigger.kind,
2638
+ outcome: prepared.node.effect.outcome.kind,
2639
+ steps: prepared.steps.map((step) => ({
2640
+ name: step.name,
2641
+ tier: step.tier,
2642
+ model: step.model,
2643
+ consumes: step.consumes,
2644
+ produces: step.produces
2645
+ })),
2646
+ capabilities: prepared.capabilities,
2647
+ tools: prepared.enabledTools.map((name) => ({
2648
+ name,
2649
+ source: `mcp:${prepared.mcp.name}`
2650
+ })),
2651
+ guardrails: {
2652
+ setup_execution: {
2653
+ enforcement: "mcp_only_read_only_sandbox",
2654
+ locked: true,
2655
+ scope: "."
2656
+ },
2657
+ isolated_codex_config: {
2658
+ ignore_user_config: true,
2659
+ ephemeral: true,
2660
+ approval_policy: "never",
2661
+ sandbox: "read-only",
2662
+ api_key_environment: "removed"
2663
+ }
2664
+ }
2665
+ };
2666
+ }
2667
+ function buildManifest(prepared) {
2668
+ const first = prepared[0];
2669
+ if (!first) {
2670
+ throw new Error("cannot build a manifest without prepared components");
2671
+ }
2672
+ return {
2673
+ manifest_version: "0.1",
2674
+ compat: {
2675
+ min_ir_version: SUPPORTED_IR_VERSION,
2676
+ max_ir_version: SUPPORTED_IR_VERSION
2677
+ },
2678
+ profile: first.profile,
2679
+ target: TARGET,
2680
+ session: {
2681
+ persistence: "codex_thread_history",
2682
+ lifecycle_operations: [...SESSION_LIFECYCLE_OPERATIONS],
2683
+ artifact_reference: "allowlisted_mcp_tool_result",
2684
+ isolation: "dedicated_persistent_codex_home",
2685
+ authentication: "externally_provisioned"
2686
+ },
2687
+ agents: prepared.map(buildAgentManifest)
2688
+ };
2689
+ }
2690
+ function describeTarget(prepared) {
2691
+ return {
2692
+ target: TARGET,
2693
+ phase: "setup-only",
2694
+ execution_modes: ["one_shot", "persistent_session"],
2695
+ session_persistence: "codex_thread_history",
2696
+ lifecycle_operations: [...SESSION_LIFECYCLE_OPERATIONS],
2697
+ supported_components: prepared.map((component) => component.componentId),
2698
+ tiers: [...new Set(prepared.flatMap((component) => component.steps.map((step) => step.tier)))],
2699
+ capabilities: [
2700
+ ...new Set(prepared.flatMap((component) => component.capabilities.map((entry) => entry.capability)))
2701
+ ],
2702
+ tools: [...new Set(prepared.flatMap((component) => component.enabledTools))],
2703
+ guardrails: ["setup_execution", "isolated_codex_config"]
2704
+ };
2705
+ }
2706
+ function buildEnrichAgentManifest(prepared) {
2707
+ return {
2708
+ id: prepared.node.id,
2709
+ verb: prepared.node.verb,
2710
+ component_type: prepared.node.type,
2711
+ realization_kind: prepared.node.realization_kind,
2712
+ trigger: prepared.node.trigger.kind,
2713
+ outcome: prepared.node.effect.outcome.kind,
2714
+ steps: prepared.steps.map((step) => ({
2715
+ name: step.name,
2716
+ tier: step.tier,
2717
+ model: step.model,
2718
+ consumes: step.consumes,
2719
+ produces: step.produces
2720
+ })),
2721
+ capabilities: prepared.capabilities,
2722
+ tools: prepared.enabledTools.map((name) => ({
2723
+ name,
2724
+ source: `mcp:${prepared.mcp.name}`
2725
+ })),
2726
+ guardrails: {
2727
+ read_only_execution: {
2728
+ enforcement: "mcp_only_read_only_sandbox",
2729
+ locked: true
2730
+ },
2731
+ isolated_codex_config: {
2732
+ ignore_user_config: true,
2733
+ ephemeral: true,
2734
+ approval_policy: "never",
2735
+ sandbox: "read-only",
2736
+ api_key_environment: "removed"
2737
+ }
2738
+ }
2739
+ };
2740
+ }
2741
+ function buildEnrichManifest(prepared) {
2742
+ return {
2743
+ manifest_version: "0.1",
2744
+ compat: {
2745
+ min_ir_version: SUPPORTED_IR_VERSION,
2746
+ max_ir_version: SUPPORTED_IR_VERSION
2747
+ },
2748
+ profile: prepared.profile,
2749
+ target: TARGET,
2750
+ session: {
2751
+ persistence: "codex_thread_history",
2752
+ lifecycle_operations: [...SESSION_LIFECYCLE_OPERATIONS],
2753
+ artifact_reference: "allowlisted_mcp_tool_result",
2754
+ isolation: "dedicated_persistent_codex_home",
2755
+ authentication: "externally_provisioned"
2756
+ },
2757
+ agents: [buildEnrichAgentManifest(prepared)]
2758
+ };
2759
+ }
2760
+ function describeEnrichTarget(prepared) {
2761
+ return {
2762
+ target: TARGET,
2763
+ phase: "enrich-parity",
2764
+ execution_modes: ["one_shot", "persistent_session"],
2765
+ session_persistence: "codex_thread_history",
2766
+ lifecycle_operations: [...SESSION_LIFECYCLE_OPERATIONS],
2767
+ supported_components: [prepared.componentId],
2768
+ tiers: [...new Set(prepared.steps.map((step) => step.tier))],
2769
+ capabilities: prepared.capabilities.map((entry) => entry.capability),
2770
+ tools: [...prepared.enabledTools],
2771
+ guardrails: ["read_only_execution", "isolated_codex_config"]
2772
+ };
2773
+ }
2774
+
2775
+ // src/model_catalog.ts
2776
+ import { resolve as resolve2 } from "path";
2777
+ var MODEL_CATALOG_VERSION = 1;
2778
+ var PAGE_LIMIT = 100;
2779
+ var MAX_PAGES = 100;
2780
+ function isRecord5(value) {
2781
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2782
+ }
2783
+ function unavailable(code, retryable) {
2784
+ return { version: MODEL_CATALOG_VERSION, status: "unavailable", provider: "codex", code, retryable };
2785
+ }
2786
+ function classify(error) {
2787
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
2788
+ if (message.includes("timed out")) return unavailable("timeout", true);
2789
+ if (/(not authenticated|unauthenticated|authentication|login required|sign in)/.test(message)) {
2790
+ return unavailable("not_authenticated", false);
2791
+ }
2792
+ if (/(enoent|failed to start|not found|transport is not available|disconnected)/.test(message)) {
2793
+ return unavailable("runtime_unavailable", true);
2794
+ }
2795
+ return unavailable("protocol_error", false);
2796
+ }
2797
+ function text(record2, field, required = false) {
2798
+ const value = record2[field];
2799
+ if (value === void 0 && !required) return void 0;
2800
+ if (typeof value !== "string") throw new Error("malformed model catalog response");
2801
+ return value;
2802
+ }
2803
+ function mapModel(raw) {
2804
+ if (!isRecord5(raw)) throw new Error("malformed model catalog response");
2805
+ if (raw["hidden"] === true) return null;
2806
+ const model = text(raw, "model", true);
2807
+ const displayName = text(raw, "displayName", true);
2808
+ const description = text(raw, "description");
2809
+ const isDefault = raw["isDefault"];
2810
+ if (isDefault !== void 0 && typeof isDefault !== "boolean") {
2811
+ throw new Error("malformed model catalog response");
2812
+ }
2813
+ const effortsRaw = raw["supportedReasoningEfforts"];
2814
+ let reasoningEfforts;
2815
+ if (effortsRaw !== void 0) {
2816
+ if (!Array.isArray(effortsRaw)) throw new Error("malformed model catalog response");
2817
+ reasoningEfforts = effortsRaw.map((effort) => {
2818
+ if (!isRecord5(effort)) throw new Error("malformed model catalog response");
2819
+ const value = text(effort, "reasoningEffort", true);
2820
+ const effortDescription = text(effort, "description");
2821
+ return {
2822
+ value,
2823
+ // The app-server protocol exposes an effort value, not a separate label.
2824
+ displayName: value,
2825
+ ...effortDescription === void 0 ? {} : { description: effortDescription }
2826
+ };
2827
+ });
2828
+ }
2829
+ return {
2830
+ model,
2831
+ displayName,
2832
+ ...description === void 0 ? {} : { description },
2833
+ ...isDefault === void 0 ? {} : { isDefault },
2834
+ ...reasoningEfforts === void 0 ? {} : { reasoningEfforts }
2835
+ };
2836
+ }
2837
+ function pageResponse(value) {
2838
+ if (!isRecord5(value) || !Array.isArray(value["data"])) {
2839
+ throw new Error("malformed model catalog response");
2840
+ }
2841
+ const nextCursor = value["nextCursor"];
2842
+ if (nextCursor !== null && nextCursor !== void 0 && typeof nextCursor !== "string") {
2843
+ throw new Error("malformed model catalog response");
2844
+ }
2845
+ return { data: value["data"], nextCursor: nextCursor ?? null };
2846
+ }
2847
+ async function discoverCodexModels(options = {}) {
2848
+ const timeoutMs = options.timeoutMs ?? 1e4;
2849
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return unavailable("protocol_error", false);
2850
+ let transport;
2851
+ let timeout;
2852
+ try {
2853
+ const transportOptions = {
2854
+ cwd: resolve2(options.cwd ?? process.cwd()),
2855
+ timeoutMs,
2856
+ ...options.codexHome ? { codexHome: resolve2(options.codexHome) } : {},
2857
+ ...options.codexBin ? { codexBin: resolve2(options.codexBin) } : {},
2858
+ ...options.env ? { env: options.env } : {}
2859
+ };
2860
+ const deadline = new Promise((_, reject) => {
2861
+ timeout = setTimeout(() => {
2862
+ void transport?.close();
2863
+ reject(new Error("model catalog timed out"));
2864
+ }, timeoutMs);
2865
+ });
2866
+ const list = (async () => {
2867
+ transport = await CodexAppServerTransport.startCatalog(transportOptions);
2868
+ const models = [];
2869
+ let cursor = null;
2870
+ for (let page = 0; page < MAX_PAGES; page += 1) {
2871
+ const response = pageResponse(await transport.request("model/list", {
2872
+ cursor,
2873
+ limit: PAGE_LIMIT,
2874
+ includeHidden: false
2875
+ }));
2876
+ for (const raw of response.data) {
2877
+ const model = mapModel(raw);
2878
+ if (model !== null) models.push(model);
2879
+ }
2880
+ if (response.nextCursor === null) {
2881
+ return { version: MODEL_CATALOG_VERSION, status: "ready", provider: "codex", models };
2882
+ }
2883
+ cursor = response.nextCursor;
2884
+ }
2885
+ throw new Error("model catalog pagination limit exceeded");
2886
+ })();
2887
+ return await Promise.race([list, deadline]);
2888
+ } catch (error) {
2889
+ return classify(error);
2890
+ } finally {
2891
+ if (timeout !== void 0) clearTimeout(timeout);
2892
+ await transport?.close();
2893
+ }
2894
+ }
2895
+
2896
+ // src/session.ts
2897
+ var FORBIDDEN_ITEM_TYPES = /* @__PURE__ */ new Set([
2898
+ "commandExecution",
2899
+ "fileChange",
2900
+ "webSearch",
2901
+ "imageGeneration",
2902
+ "collabAgentToolCall",
2903
+ "subAgentActivity",
2904
+ "dynamicToolCall",
2905
+ "imageView",
2906
+ "sleep",
2907
+ "enteredReviewMode",
2908
+ "exitedReviewMode"
2909
+ ]);
2910
+ var PASSIVE_ITEM_TYPES = /* @__PURE__ */ new Set([
2911
+ "userMessage",
2912
+ "agentMessage",
2913
+ "reasoning",
2914
+ "plan",
2915
+ "compacted",
2916
+ "contextCompaction"
2917
+ ]);
2918
+ var IGNORED_NOTIFICATIONS2 = /* @__PURE__ */ new Set([
2919
+ "skills/changed",
2920
+ "thread/name/updated",
2921
+ "thread/goal/updated",
2922
+ "thread/goal/cleared",
2923
+ "thread/settings/updated",
2924
+ "thread/status/changed",
2925
+ "thread/tokenUsage/updated",
2926
+ "thread/compacted",
2927
+ "turn/diff/updated",
2928
+ "turn/plan/updated",
2929
+ "item/agentMessage/delta",
2930
+ "item/plan/delta",
2931
+ "item/mcpToolCall/progress",
2932
+ "item/reasoning/summaryTextDelta",
2933
+ "item/reasoning/summaryPartAdded",
2934
+ "item/reasoning/textDelta",
2935
+ "mcpServer/startupStatus/updated",
2936
+ "account/updated",
2937
+ "account/rateLimits/updated",
2938
+ "app/list/updated",
2939
+ "remoteControl/status/changed",
2940
+ "fs/changed",
2941
+ "model/rerouted",
2942
+ "model/verification",
2943
+ "model/safetyBuffering/updated",
2944
+ "turn/moderationMetadata",
2945
+ "warning",
2946
+ "guardianWarning",
2947
+ "deprecationNotice"
2948
+ ]);
2949
+ function isRecord6(value) {
2950
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2951
+ }
2952
+ function requiredRecord(value, context) {
2953
+ if (!isRecord6(value)) throw new CodexDispatchError(`${context} requires an object`);
2954
+ return value;
2955
+ }
2956
+ function requiredString(record2, key, context) {
2957
+ const value = record2[key];
2958
+ if (typeof value !== "string" || value.length === 0) {
2959
+ throw new CodexDispatchError(`${context} requires string ${key}`);
2960
+ }
2961
+ return value;
2962
+ }
2963
+ function sessionReference2(thread) {
2964
+ return {
2965
+ version: SESSION_REFERENCE_VERSION,
2966
+ target: "codex:local",
2967
+ threadId: requiredString(thread, "id", "thread"),
2968
+ forkedFromThreadId: typeof thread["forkedFromId"] === "string" ? thread["forkedFromId"] : null
2969
+ };
2970
+ }
2971
+ function turnStatus2(value) {
2972
+ switch (value) {
2973
+ case "inProgress":
2974
+ return "in_progress";
2975
+ case "completed":
2976
+ case "interrupted":
2977
+ case "failed":
2978
+ return value;
2979
+ default:
2980
+ throw new CodexDispatchError("turn requires a recognized status");
2981
+ }
2982
+ }
2983
+ function turnReference2(threadId, turn) {
2984
+ return {
2985
+ threadId,
2986
+ turnId: requiredString(turn, "id", "turn"),
2987
+ status: turnStatus2(turn["status"])
2988
+ };
2989
+ }
2990
+ function validateReference2(reference) {
2991
+ if (reference.version !== SESSION_REFERENCE_VERSION || reference.target !== "codex:local" || reference.threadId.length === 0) {
2992
+ throw new CodexDispatchError("invalid codex session reference");
2993
+ }
2994
+ }
2995
+ var CodexSessionRuntime = class _CodexSessionRuntime {
2996
+ constructor(prepared, options) {
2997
+ this.prepared = prepared;
2998
+ this.options = options;
2999
+ }
3000
+ prepared;
3001
+ options;
3002
+ transport;
3003
+ session = null;
3004
+ activeTurns = /* @__PURE__ */ new Map();
3005
+ waiters = /* @__PURE__ */ new Map();
3006
+ stepNameByTurn = /* @__PURE__ */ new Map();
3007
+ disconnected = false;
3008
+ /**
3009
+ * The model bound to this persistent thread for its whole lifetime. `thread/start` takes a
3010
+ * single `model` with no per-turn override, so unlike Setup's one-shot-process-per-step
3011
+ * transport, every step dispatched through one session must resolve to the same model — see
3012
+ * `enrich_prepare.ts`'s single-tier-per-component requirement, which is what makes this true by
3013
+ * construction rather than by convention.
3014
+ */
3015
+ get model() {
3016
+ return this.prepared.steps[0].model;
3017
+ }
3018
+ static async connect(prepared, options) {
3019
+ if (prepared.steps.length === 0) {
3020
+ throw new CodexDispatchError("cannot connect a session runtime without at least one prepared step");
3021
+ }
3022
+ const sessionModel = prepared.steps[0].model;
3023
+ for (const step of prepared.steps) {
3024
+ if (step.model !== sessionModel) {
3025
+ throw new CodexDispatchError(
3026
+ `this transport's persistent session is bound to one model per thread; step '${step.name}' requires a different model than the session's first step`
3027
+ );
3028
+ }
3029
+ }
3030
+ const runtime = new _CodexSessionRuntime(prepared, options);
3031
+ runtime.transport = await CodexAppServerTransport.start(
3032
+ prepared,
3033
+ options,
3034
+ (method, params) => runtime.onNotification(method, params),
3035
+ (error) => runtime.onDisconnect(error)
3036
+ );
3037
+ return runtime;
3038
+ }
3039
+ async start() {
3040
+ this.ensureConnected();
3041
+ if (this.session !== null) {
3042
+ throw new CodexDispatchError("a session is already loaded; use a new runtime to start another");
3043
+ }
3044
+ const result = requiredRecord(
3045
+ await this.transport.request("thread/start", {
3046
+ model: this.model,
3047
+ cwd: this.options.cwd,
3048
+ approvalPolicy: "never",
3049
+ sandbox: "read-only",
3050
+ config: buildIsolationConfig(this.prepared),
3051
+ ephemeral: false,
3052
+ historyMode: "legacy",
3053
+ environments: [],
3054
+ runtimeWorkspaceRoots: [],
3055
+ selectedCapabilityRoots: [],
3056
+ dynamicTools: [],
3057
+ experimentalRawEvents: false
3058
+ }),
3059
+ "thread/start response"
3060
+ );
3061
+ const reference = sessionReference2(requiredRecord(result["thread"], "thread/start thread"));
3062
+ this.session = reference;
3063
+ this.emit({ t: "session_started", session: reference });
3064
+ return reference;
3065
+ }
3066
+ async resume(reference) {
3067
+ validateReference2(reference);
3068
+ this.ensureConnected();
3069
+ this.requireNoActiveTurns("resume");
3070
+ if (this.session !== null && this.session.threadId !== reference.threadId) {
3071
+ throw new CodexDispatchError(
3072
+ "a different session is already loaded; use a new runtime to resume another"
3073
+ );
3074
+ }
3075
+ const result = requiredRecord(
3076
+ await this.transport.request("thread/resume", {
3077
+ threadId: reference.threadId,
3078
+ model: this.model,
3079
+ cwd: this.options.cwd,
3080
+ approvalPolicy: "never",
3081
+ sandbox: "read-only",
3082
+ config: buildIsolationConfig(this.prepared),
3083
+ runtimeWorkspaceRoots: []
3084
+ }),
3085
+ "thread/resume response"
3086
+ );
3087
+ const resumed = sessionReference2(requiredRecord(result["thread"], "thread/resume thread"));
3088
+ if (resumed.threadId !== reference.threadId) {
3089
+ throw new CodexDispatchError("thread/resume returned a different thread id");
3090
+ }
3091
+ this.session = resumed;
3092
+ this.emit({ t: "session_resumed", session: resumed });
3093
+ return resumed;
3094
+ }
3095
+ async read(reference) {
3096
+ validateReference2(reference);
3097
+ this.ensureConnected();
3098
+ const result = requiredRecord(
3099
+ await this.transport.request("thread/read", { threadId: reference.threadId, includeTurns: true }),
3100
+ "thread/read response"
3101
+ );
3102
+ const thread = requiredRecord(result["thread"], "thread/read thread");
3103
+ const readReference = sessionReference2(thread);
3104
+ if (readReference.threadId !== reference.threadId) {
3105
+ throw new CodexDispatchError("thread/read returned a different thread id");
3106
+ }
3107
+ const turns = Array.isArray(thread["turns"]) ? thread["turns"].map((turn) => this.projectHistoryTurn(reference.threadId, turn)) : [];
3108
+ return { session: readReference, turns };
3109
+ }
3110
+ /**
3111
+ * `step`/`inputs` default to this component's first (and, for every existing single-step
3112
+ * fixture, only) step with no marshalled inputs — so every pre-existing caller that never named
3113
+ * a step keeps building the exact same prompt as before. A multi-step caller (the n-step Enrich
3114
+ * executor) passes the step actually being dispatched this turn, plus that step's marshalled
3115
+ * `consumes` values, and this records which step owns the resulting turn id so the
3116
+ * `step_start`/`step_finish` events this turn emits are attributed correctly rather than always
3117
+ * naming the component's first step.
3118
+ */
3119
+ async turn(reference, input, step = this.prepared.steps[0], inputs = {}) {
3120
+ this.requireCurrent(reference);
3121
+ if (input.length === 0) throw new CodexDispatchError("turn input must not be empty");
3122
+ const result = requiredRecord(
3123
+ await this.transport.request("turn/start", {
3124
+ threadId: reference.threadId,
3125
+ input: [
3126
+ { type: "text", text: buildPrompt(this.prepared, step, input, inputs), text_elements: [] }
3127
+ ],
3128
+ approvalPolicy: "never",
3129
+ environments: [],
3130
+ runtimeWorkspaceRoots: []
3131
+ }),
3132
+ "turn/start response"
3133
+ );
3134
+ const turn = turnReference2(reference.threadId, requiredRecord(result["turn"], "turn/start turn"));
3135
+ if (turn.status !== "in_progress") {
3136
+ throw new CodexDispatchError("turn/start did not return an in-progress turn");
3137
+ }
3138
+ this.ensureActiveTurn(turn.turnId);
3139
+ this.stepNameByTurn.set(turn.turnId, step.name);
3140
+ return turn;
3141
+ }
3142
+ async steer(reference, turnId, input) {
3143
+ this.requireCurrent(reference);
3144
+ const result = requiredRecord(
3145
+ await this.transport.request("turn/steer", {
3146
+ threadId: reference.threadId,
3147
+ expectedTurnId: turnId,
3148
+ input: [{ type: "text", text: input, text_elements: [] }]
3149
+ }),
3150
+ "turn/steer response"
3151
+ );
3152
+ if (requiredString(result, "turnId", "turn/steer response") !== turnId) {
3153
+ throw new CodexDispatchError("turn/steer returned a different turn id");
3154
+ }
3155
+ return { threadId: reference.threadId, turnId, status: "in_progress" };
3156
+ }
3157
+ async interrupt(reference, turnId) {
3158
+ this.requireCurrent(reference);
3159
+ await this.transport.request("turn/interrupt", { threadId: reference.threadId, turnId });
3160
+ }
3161
+ async fork(reference, lastTurnId) {
3162
+ validateReference2(reference);
3163
+ this.ensureConnected();
3164
+ this.requireNoActiveTurns("fork");
3165
+ const result = requiredRecord(
3166
+ await this.transport.request("thread/fork", {
3167
+ threadId: reference.threadId,
3168
+ ...lastTurnId === void 0 ? {} : { lastTurnId },
3169
+ model: this.model,
3170
+ cwd: this.options.cwd,
3171
+ approvalPolicy: "never",
3172
+ sandbox: "read-only",
3173
+ config: buildIsolationConfig(this.prepared),
3174
+ ephemeral: false,
3175
+ runtimeWorkspaceRoots: []
3176
+ }),
3177
+ "thread/fork response"
3178
+ );
3179
+ const forked = sessionReference2(requiredRecord(result["thread"], "thread/fork thread"));
3180
+ if (forked.threadId === reference.threadId || forked.forkedFromThreadId !== reference.threadId) {
3181
+ throw new CodexDispatchError("thread/fork returned an invalid lineage");
3182
+ }
3183
+ this.emit({ t: "session_forked", session: forked });
3184
+ return forked;
3185
+ }
3186
+ waitForTurn(turn, timeoutMs = this.options.timeoutMs ?? 12e4) {
3187
+ if (turn.status !== "in_progress") return Promise.resolve(turn);
3188
+ if (this.disconnected || !this.activeTurns.has(turn.turnId)) {
3189
+ return Promise.reject(new CodexDispatchError("turn is no longer active; resume required"));
3190
+ }
3191
+ return new Promise((resolveWaiter, rejectWaiter) => {
3192
+ const timer = setTimeout(() => {
3193
+ this.removeWaiter(turn.turnId, waiter);
3194
+ const error = new CodexDispatchError(`turn '${turn.turnId}' timed out`);
3195
+ void (async () => {
3196
+ try {
3197
+ await this.interrupt(
3198
+ { version: SESSION_REFERENCE_VERSION, target: "codex:local", threadId: turn.threadId, forkedFromThreadId: null },
3199
+ turn.turnId
3200
+ );
3201
+ } catch {
3202
+ }
3203
+ this.onDisconnect(error, "turn_timeout");
3204
+ await this.transport.close();
3205
+ rejectWaiter(error);
3206
+ })();
3207
+ }, timeoutMs);
3208
+ const waiter = { resolve: resolveWaiter, reject: rejectWaiter, timer };
3209
+ const list = this.waiters.get(turn.turnId) ?? [];
3210
+ list.push(waiter);
3211
+ this.waiters.set(turn.turnId, list);
3212
+ });
3213
+ }
3214
+ async restartAndResume(reference) {
3215
+ if (!this.disconnected && this.activeTurns.size > 0) {
3216
+ throw new CodexDispatchError("cannot restart while a turn is active; interrupt it first");
3217
+ }
3218
+ await this.transport.close();
3219
+ const transport = await CodexAppServerTransport.start(
3220
+ this.prepared,
3221
+ this.options,
3222
+ (method, params) => this.onNotification(method, params),
3223
+ (error) => this.onDisconnect(error)
3224
+ );
3225
+ this.transport = transport;
3226
+ this.disconnected = false;
3227
+ try {
3228
+ return await this.resume(reference);
3229
+ } catch (error) {
3230
+ this.disconnected = true;
3231
+ await this.transport.close();
3232
+ throw error;
3233
+ }
3234
+ }
3235
+ async close() {
3236
+ this.disconnected = true;
3237
+ const error = new CodexDispatchError("session runtime closed during an active turn");
3238
+ for (const [turnId] of this.activeTurns) {
3239
+ this.settleWaiters(
3240
+ { threadId: this.session?.threadId ?? "unknown", turnId, status: "failed" },
3241
+ error
3242
+ );
3243
+ }
3244
+ this.activeTurns.clear();
3245
+ this.stepNameByTurn.clear();
3246
+ await this.transport.close();
3247
+ }
3248
+ onNotification(method, paramsValue) {
3249
+ const params = requiredRecord(paramsValue, `${method} notification`);
3250
+ if (method === "thread/started" || IGNORED_NOTIFICATIONS2.has(method)) return;
3251
+ if (method === "error") {
3252
+ const threadId = requiredString(params, "threadId", method);
3253
+ this.requireNotificationThread(threadId);
3254
+ const turnId = requiredString(params, "turnId", method);
3255
+ requiredRecord(params["error"], "error notification error");
3256
+ const active = this.activeTurns.get(turnId);
3257
+ if (!active?.started) {
3258
+ throw new CodexDispatchError("app-server error notification has no active turn");
3259
+ }
3260
+ if (params["willRetry"] === true) return;
3261
+ if (params["willRetry"] !== false) {
3262
+ throw new CodexDispatchError("app-server error notification requires willRetry");
3263
+ }
3264
+ throw new CodexDispatchError("app-server reported a terminal turn error");
3265
+ }
3266
+ if (method === "turn/started") {
3267
+ const threadId = requiredString(params, "threadId", method);
3268
+ this.requireNotificationThread(threadId);
3269
+ const turn = turnReference2(threadId, requiredRecord(params["turn"], `${method} turn`));
3270
+ const active = this.ensureActiveTurn(turn.turnId);
3271
+ if (active.started) throw new CodexDispatchError("duplicate turn start notification");
3272
+ active.started = true;
3273
+ this.emit({ t: "turn_started", turn });
3274
+ const stepName = this.stepNameByTurn.get(turn.turnId) ?? this.prepared.steps[0].name;
3275
+ this.emit({ threadId, turnId: turn.turnId, t: "step_start", id: stepName, name: stepName });
3276
+ return;
3277
+ }
3278
+ if (method === "item/started" || method === "item/completed") {
3279
+ this.onItem(method, params);
3280
+ return;
3281
+ }
3282
+ if (method === "turn/completed") {
3283
+ this.onTurnCompleted(params);
3284
+ return;
3285
+ }
3286
+ throw new CodexDispatchError(`unsupported app-server notification '${method}'`);
3287
+ }
3288
+ onItem(method, params) {
3289
+ const threadId = requiredString(params, "threadId", method);
3290
+ this.requireNotificationThread(threadId);
3291
+ const turnId = requiredString(params, "turnId", method);
3292
+ const item = requiredRecord(params["item"], `${method} item`);
3293
+ const type = requiredString(item, "type", `${method} item`);
3294
+ if (FORBIDDEN_ITEM_TYPES.has(type)) {
3295
+ throw new CodexDispatchError(`isolation violation: app-server emitted forbidden '${type}'`);
3296
+ }
3297
+ const active = this.ensureActiveTurn(turnId);
3298
+ if (!active.started) throw new CodexDispatchError("item emitted before turn started");
3299
+ if (type === "mcpToolCall") {
3300
+ const itemId = requiredString(item, "id", type);
3301
+ const server = requiredString(item, "server", type);
3302
+ const tool = requiredString(item, "tool", type);
3303
+ if (server !== this.prepared.mcp.name || !this.prepared.enabledTools.includes(tool)) {
3304
+ throw new CodexDispatchError(`isolation violation: non-allowlisted MCP tool '${server}.${tool}'`);
3305
+ }
3306
+ if (method === "item/started") {
3307
+ if (item["status"] !== "inProgress") {
3308
+ throw new CodexDispatchError("MCP item start requires in-progress status");
3309
+ }
3310
+ if (active.pendingTools.has(itemId)) throw new CodexDispatchError("duplicate MCP item start");
3311
+ active.pendingTools.add(itemId);
3312
+ this.emit({ threadId, turnId, t: "tool_call", id: itemId, name: `${server}.${tool}` });
3313
+ return;
3314
+ }
3315
+ if (!active.pendingTools.delete(itemId)) throw new CodexDispatchError("MCP item completed without start");
3316
+ const status = requiredString(item, "status", type);
3317
+ if (status !== "completed" && status !== "failed") {
3318
+ throw new CodexDispatchError("MCP item completed with an invalid status");
3319
+ }
3320
+ const ok = status === "completed" && (item["error"] === null || item["error"] === void 0);
3321
+ if (ok) active.successfulTools += 1;
3322
+ const reference = {
3323
+ version: SESSION_REFERENCE_VERSION,
3324
+ kind: "mcp_tool_result",
3325
+ threadId,
3326
+ turnId,
3327
+ itemId,
3328
+ server,
3329
+ tool,
3330
+ ok
3331
+ };
3332
+ this.emit({ t: "artifact", reference });
3333
+ this.emit({ threadId, turnId, t: "tool_result", id: itemId, ok, ...ok ? {} : { error: "allowlisted MCP tool failed" } });
3334
+ return;
3335
+ }
3336
+ if (!PASSIVE_ITEM_TYPES.has(type)) {
3337
+ throw new CodexDispatchError(`unsupported app-server item type '${type}'`);
3338
+ }
3339
+ if (method === "item/completed" && type === "agentMessage") {
3340
+ const text2 = requiredString(item, "text", type);
3341
+ active.hasAnswer = true;
3342
+ this.emit({ threadId, turnId, t: "answer", text: text2 });
3343
+ }
3344
+ }
3345
+ onTurnCompleted(params) {
3346
+ const threadId = requiredString(params, "threadId", "turn/completed");
3347
+ this.requireNotificationThread(threadId);
3348
+ const turn = turnReference2(threadId, requiredRecord(params["turn"], "turn/completed turn"));
3349
+ const active = this.activeTurns.get(turn.turnId);
3350
+ if (!active) throw new CodexDispatchError("turn completed without starting");
3351
+ if (!active.started) throw new CodexDispatchError("turn completed before start notification");
3352
+ if (active.pendingTools.size > 0) throw new CodexDispatchError("turn completed with pending MCP tools");
3353
+ if (turn.status === "completed" && (active.successfulTools === 0 || !active.hasAnswer)) {
3354
+ throw new CodexDispatchError("completed turn lacks a successful allowlisted tool or answer");
3355
+ }
3356
+ this.activeTurns.delete(turn.turnId);
3357
+ const ok = turn.status === "completed";
3358
+ const stepName = this.stepNameByTurn.get(turn.turnId) ?? this.prepared.steps[0].name;
3359
+ this.stepNameByTurn.delete(turn.turnId);
3360
+ this.emit({ threadId, turnId: turn.turnId, t: "step_finish", id: stepName, ok });
3361
+ this.emit({ t: "turn_completed", turn });
3362
+ const error = turn.status === "failed" ? new CodexDispatchError(`turn '${turn.turnId}' failed`) : null;
3363
+ this.settleWaiters(turn, error);
3364
+ }
3365
+ projectHistoryTurn(threadId, value) {
3366
+ const turn = requiredRecord(value, "history turn");
3367
+ const reference = turnReference2(threadId, turn);
3368
+ const items = [];
3369
+ if (Array.isArray(turn["items"])) {
3370
+ for (const itemValue of turn["items"]) {
3371
+ const item = requiredRecord(itemValue, "history item");
3372
+ const type = requiredString(item, "type", "history item");
3373
+ if (type === "agentMessage") {
3374
+ items.push({ type: "assistant", itemId: requiredString(item, "id", type) });
3375
+ } else if (type === "userMessage") {
3376
+ items.push({ type: "user", itemId: requiredString(item, "id", type) });
3377
+ } else if (type === "mcpToolCall") {
3378
+ const server = requiredString(item, "server", type);
3379
+ const tool = requiredString(item, "tool", type);
3380
+ if (server !== this.prepared.mcp.name || !this.prepared.enabledTools.includes(tool)) {
3381
+ throw new CodexDispatchError("history contains a non-allowlisted MCP tool");
3382
+ }
3383
+ const status = requiredString(item, "status", type);
3384
+ if (status !== "completed" && status !== "failed") {
3385
+ throw new CodexDispatchError("history MCP item has an invalid status");
3386
+ }
3387
+ items.push({
3388
+ type: "artifact",
3389
+ reference: {
3390
+ version: SESSION_REFERENCE_VERSION,
3391
+ kind: "mcp_tool_result",
3392
+ threadId,
3393
+ turnId: reference.turnId,
3394
+ itemId: requiredString(item, "id", type),
3395
+ server,
3396
+ tool,
3397
+ ok: status === "completed" && (item["error"] === null || item["error"] === void 0)
3398
+ }
3399
+ });
3400
+ } else if (FORBIDDEN_ITEM_TYPES.has(type)) {
3401
+ throw new CodexDispatchError(`history contains forbidden '${type}' item`);
3402
+ } else if (!PASSIVE_ITEM_TYPES.has(type)) {
3403
+ throw new CodexDispatchError(`history contains unsupported '${type}' item`);
3404
+ }
3405
+ }
3406
+ }
3407
+ return { id: reference.turnId, status: reference.status, items };
3408
+ }
3409
+ ensureActiveTurn(turnId) {
3410
+ let active = this.activeTurns.get(turnId);
3411
+ if (!active) {
3412
+ active = { started: false, pendingTools: /* @__PURE__ */ new Set(), successfulTools: 0, hasAnswer: false };
3413
+ this.activeTurns.set(turnId, active);
3414
+ }
3415
+ return active;
3416
+ }
3417
+ requireCurrent(reference) {
3418
+ validateReference2(reference);
3419
+ this.ensureConnected();
3420
+ if (this.session?.threadId !== reference.threadId) {
3421
+ throw new CodexDispatchError("session reference is not loaded; resume it first");
3422
+ }
3423
+ }
3424
+ requireNotificationThread(threadId) {
3425
+ if (this.session?.threadId !== threadId) {
3426
+ throw new CodexDispatchError("app-server notification belongs to a different thread");
3427
+ }
3428
+ }
3429
+ requireNoActiveTurns(operation) {
3430
+ if (this.activeTurns.size > 0) {
3431
+ throw new CodexDispatchError(
3432
+ `cannot ${operation} while a turn is active; interrupt it first`
3433
+ );
3434
+ }
3435
+ }
3436
+ ensureConnected() {
3437
+ if (this.disconnected) throw new CodexDispatchError("app-server transport disconnected; resume required");
3438
+ }
3439
+ onDisconnect(protocolError, reasonOverride) {
3440
+ if (this.disconnected) return;
3441
+ this.disconnected = true;
3442
+ if (protocolError && reasonOverride === void 0) {
3443
+ this.emit({
3444
+ t: "session_failed",
3445
+ threadId: this.session?.threadId ?? null,
3446
+ reason: "protocol_violation"
3447
+ });
3448
+ } else {
3449
+ const reason = reasonOverride ?? (this.activeTurns.size > 0 ? "app_server_crash" : "transport_disconnect");
3450
+ this.emit({ t: "session_recoverable", threadId: this.session?.threadId ?? null, reason });
3451
+ }
3452
+ for (const [turnId] of this.activeTurns) {
3453
+ this.settleWaiters(
3454
+ { threadId: this.session?.threadId ?? "unknown", turnId, status: "failed" },
3455
+ protocolError ?? new CodexDispatchError("app-server disconnected during an active turn")
3456
+ );
3457
+ }
3458
+ this.activeTurns.clear();
3459
+ this.stepNameByTurn.clear();
3460
+ }
3461
+ settleWaiters(turn, error) {
3462
+ const waiters = this.waiters.get(turn.turnId) ?? [];
3463
+ this.waiters.delete(turn.turnId);
3464
+ for (const waiter of waiters) {
3465
+ clearTimeout(waiter.timer);
3466
+ if (error) waiter.reject(error);
3467
+ else waiter.resolve(turn);
3468
+ }
3469
+ }
3470
+ removeWaiter(turnId, waiter) {
3471
+ const remaining = (this.waiters.get(turnId) ?? []).filter((candidate) => candidate !== waiter);
3472
+ if (remaining.length === 0) this.waiters.delete(turnId);
3473
+ else this.waiters.set(turnId, remaining);
3474
+ }
3475
+ emit(event) {
3476
+ this.options.onEvent?.(event);
3477
+ }
3478
+ };
3479
+
3480
+ // src/enrich_run.ts
3481
+ async function runEnrich(prepared, request, options) {
3482
+ if (request.trim().length === 0) throw new CodexDispatchError("enrichment request must not be empty");
3483
+ const events = [];
3484
+ let currentAnswer = null;
3485
+ const onEvent = (event) => {
3486
+ events.push(event);
3487
+ if (event.t === "answer") currentAnswer = event.text;
3488
+ options.onEvent?.(event);
3489
+ };
3490
+ const runtime = await CodexSessionRuntime.connect(prepared, { ...options, onEvent });
3491
+ try {
3492
+ const session = await runtime.start();
3493
+ const slots = {};
3494
+ const outcomes = /* @__PURE__ */ new Map();
3495
+ const steps = [];
3496
+ let lastFinalText = null;
3497
+ let lastValue;
3498
+ for (const step of prepared.steps) {
3499
+ if (!shouldRunStep(step.when, outcomes)) {
3500
+ outcomes.set(step.name, { ran: false });
3501
+ steps.push({ name: step.name, ran: false, ok: false });
3502
+ continue;
3503
+ }
3504
+ const inputs = Object.fromEntries(step.consumes.map((name) => [name, slots[name]]));
3505
+ currentAnswer = null;
3506
+ const turn = await runtime.turn(session, request, step, inputs);
3507
+ const completed = await runtime.waitForTurn(turn, options.timeoutMs ?? 12e4);
3508
+ if (completed.status !== "completed" || currentAnswer === null) {
3509
+ throw new CodexDispatchError(`enrichment step '${step.name}' did not complete with a terminal answer`);
3510
+ }
3511
+ const finalText = currentAnswer;
3512
+ const hasGuardedConsumer = prepared.steps.some((candidate) => candidate.when?.target === step.name);
3513
+ let record2;
3514
+ try {
3515
+ record2 = parseStepTerminal(finalText, step.produces);
3516
+ } catch (error) {
3517
+ if (hasGuardedConsumer && error instanceof CodexDispatchError) {
3518
+ outcomes.set(step.name, { ran: true, ok: false });
3519
+ steps.push({ name: step.name, ran: true, ok: false });
3520
+ lastFinalText = finalText;
3521
+ continue;
3522
+ }
3523
+ throw error;
3524
+ }
3525
+ const value = record2[step.produces];
3526
+ slots[step.produces] = value;
3527
+ outcomes.set(step.name, { ran: true, ok: true, value });
3528
+ steps.push({ name: step.name, ran: true, ok: true, value });
3529
+ lastFinalText = finalText;
3530
+ lastValue = record2;
3531
+ }
3532
+ if (lastFinalText === null) {
3533
+ throw new CodexDispatchError("enrichment dispatch completed without running any step");
3534
+ }
3535
+ return {
3536
+ target: prepared.target,
3537
+ component: prepared.componentId,
3538
+ finalText: lastFinalText,
3539
+ value: lastValue,
3540
+ events,
3541
+ steps
3542
+ };
3543
+ } finally {
3544
+ await runtime.close();
3545
+ }
3546
+ }
3547
+
3548
+ // src/run.ts
3549
+ import { spawn as spawn2 } from "child_process";
3550
+ import { createInterface as createInterface2 } from "readline";
3551
+
3552
+ // src/events.ts
3553
+ function isRecord7(value) {
3554
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3555
+ }
3556
+ function itemOf(event) {
3557
+ return isRecord7(event["item"]) ? event["item"] : null;
3558
+ }
3559
+ function itemType(item) {
3560
+ return typeof item["type"] === "string" ? item["type"] : "";
3561
+ }
3562
+ function toolIdentity(item) {
3563
+ const server = typeof item["server"] === "string" ? item["server"] : "";
3564
+ const tool = typeof item["tool"] === "string" ? item["tool"] : "";
3565
+ if (server.length === 0 || tool.length === 0) {
3566
+ throw new CodexDispatchError("mcp_tool_call requires string server and tool fields");
3567
+ }
3568
+ return { server, tool, name: `${server}.${tool}` };
3569
+ }
3570
+ var FORBIDDEN_ITEM_TYPES2 = /* @__PURE__ */ new Set([
3571
+ "command_execution",
3572
+ "file_change",
3573
+ "web_search",
3574
+ "image_generation",
3575
+ "collab_agent_tool_call"
3576
+ ]);
3577
+ var CodexJsonlMapper = class {
3578
+ constructor(stepId, expectedMcpServer, enabledTools) {
3579
+ this.stepId = stepId;
3580
+ this.expectedMcpServer = expectedMcpServer;
3581
+ this.enabledTools = new Set(enabledTools);
3582
+ }
3583
+ stepId;
3584
+ expectedMcpServer;
3585
+ started = false;
3586
+ finished = false;
3587
+ threadStarted = false;
3588
+ finalText = null;
3589
+ failureDetail = null;
3590
+ toolFailureDetail = null;
3591
+ pendingTools = /* @__PURE__ */ new Map();
3592
+ successfulToolCount = 0;
3593
+ enabledTools;
3594
+ nextLine(line) {
3595
+ let parsed;
3596
+ try {
3597
+ parsed = JSON.parse(line);
3598
+ } catch (error) {
3599
+ throw new CodexDispatchError(`codex stdout contained non-JSONL data: ${String(error)}`);
3600
+ }
3601
+ if (!isRecord7(parsed) || typeof parsed["type"] !== "string") {
3602
+ throw new CodexDispatchError("codex JSONL event requires a string type");
3603
+ }
3604
+ const type = parsed["type"];
3605
+ if (this.finished) {
3606
+ throw new CodexDispatchError(`codex emitted '${type}' after the terminal turn event`);
3607
+ }
3608
+ if (type === "thread.started") {
3609
+ if (this.threadStarted || this.started) {
3610
+ throw new CodexDispatchError("codex emitted duplicate or out-of-order thread.started");
3611
+ }
3612
+ this.threadStarted = true;
3613
+ return [];
3614
+ }
3615
+ if (type === "turn.started") {
3616
+ if (!this.threadStarted) {
3617
+ throw new CodexDispatchError("codex emitted turn.started before thread.started");
3618
+ }
3619
+ if (this.started) throw new CodexDispatchError("codex emitted duplicate turn.started");
3620
+ this.started = true;
3621
+ return [{ t: "step_start", id: this.stepId, name: this.stepId }];
3622
+ }
3623
+ if (type === "item.started" || type === "item.completed") {
3624
+ if (!this.started) {
3625
+ throw new CodexDispatchError(`codex emitted ${type} before turn.started`);
3626
+ }
3627
+ return this.onItem(type, parsed);
3628
+ }
3629
+ if (type === "turn.failed" || type === "error") {
3630
+ return this.finish(false, type === "turn.failed" ? "codex turn failed" : "codex runtime error");
3631
+ }
3632
+ if (type === "turn.completed") {
3633
+ return this.finish(true);
3634
+ }
3635
+ return [];
3636
+ }
3637
+ result() {
3638
+ if (!this.threadStarted) throw new CodexDispatchError("codex JSONL ended without thread.started");
3639
+ if (!this.finished) throw new CodexDispatchError("codex JSONL ended without turn.completed");
3640
+ if (this.failureDetail !== null) {
3641
+ throw new CodexDispatchError(`codex turn failed: ${this.failureDetail}`);
3642
+ }
3643
+ if (this.successfulToolCount === 0) {
3644
+ if (this.toolFailureDetail !== null) {
3645
+ throw new CodexDispatchError(`required MCP tool failed: ${this.toolFailureDetail}`);
3646
+ }
3647
+ throw new CodexDispatchError("codex turn completed without a successful allowlisted MCP tool call");
3648
+ }
3649
+ if (this.finalText === null) throw new CodexDispatchError("codex JSONL ended without an agent message");
3650
+ return {
3651
+ finalText: this.finalText,
3652
+ threadStarted: this.threadStarted,
3653
+ turnCompleted: this.finished
3654
+ };
3655
+ }
3656
+ onItem(eventType, event) {
3657
+ const item = itemOf(event);
3658
+ if (!item) throw new CodexDispatchError(`${eventType} requires an item object`);
3659
+ const type = itemType(item);
3660
+ if (FORBIDDEN_ITEM_TYPES2.has(type)) {
3661
+ throw new CodexDispatchError(
3662
+ `isolation violation: codex emitted forbidden '${type}' item`
3663
+ );
3664
+ }
3665
+ if (type === "mcp_tool_call") {
3666
+ const id = typeof item["id"] === "string" ? item["id"] : "";
3667
+ if (id.length === 0) throw new CodexDispatchError("mcp_tool_call requires an id");
3668
+ const identity = toolIdentity(item);
3669
+ if (identity.server !== this.expectedMcpServer || !this.enabledTools.has(identity.tool)) {
3670
+ throw new CodexDispatchError(
3671
+ `isolation violation: codex emitted non-allowlisted MCP tool '${identity.name}'`
3672
+ );
3673
+ }
3674
+ if (eventType === "item.started") {
3675
+ if (this.pendingTools.has(id)) {
3676
+ throw new CodexDispatchError(`mcp_tool_call '${id}' started more than once`);
3677
+ }
3678
+ this.pendingTools.set(id, identity.name);
3679
+ return [
3680
+ {
3681
+ t: "tool_call",
3682
+ id,
3683
+ name: identity.name
3684
+ }
3685
+ ];
3686
+ }
3687
+ const name = this.pendingTools.get(id);
3688
+ if (name === void 0) {
3689
+ throw new CodexDispatchError(`mcp_tool_call '${id}' completed without starting`);
3690
+ }
3691
+ if (name !== identity.name) {
3692
+ throw new CodexDispatchError(
3693
+ `mcp_tool_call '${id}' completed as '${identity.name}' after starting as '${name}'`
3694
+ );
3695
+ }
3696
+ this.pendingTools.delete(id);
3697
+ const status = item["status"];
3698
+ if (status !== "completed" && status !== "failed") {
3699
+ throw new CodexDispatchError(
3700
+ `completed mcp_tool_call '${id}' requires completed or failed status`
3701
+ );
3702
+ }
3703
+ const failed = status === "failed" || item["error"] !== void 0 && item["error"] !== null;
3704
+ if (failed) this.toolFailureDetail = name;
3705
+ else this.successfulToolCount += 1;
3706
+ return [
3707
+ {
3708
+ t: "tool_result",
3709
+ id,
3710
+ ok: !failed,
3711
+ ...failed ? { error: "allowlisted MCP tool failed" } : {}
3712
+ }
3713
+ ];
3714
+ }
3715
+ if (eventType === "item.completed" && type === "agent_message") {
3716
+ const text2 = item["text"];
3717
+ if (typeof text2 !== "string") {
3718
+ throw new CodexDispatchError("completed agent_message requires text");
3719
+ }
3720
+ this.finalText = text2;
3721
+ return [{ t: "answer", text: text2 }];
3722
+ }
3723
+ return [];
3724
+ }
3725
+ finish(ok, detail) {
3726
+ if (!this.started) {
3727
+ throw new CodexDispatchError("codex turn finished before turn.started");
3728
+ }
3729
+ if (this.finished) throw new CodexDispatchError("codex emitted duplicate terminal turn event");
3730
+ if (this.pendingTools.size > 0) {
3731
+ throw new CodexDispatchError(
3732
+ `codex turn finished with pending MCP tool calls: ${[...this.pendingTools.keys()].join(", ")}`
3733
+ );
3734
+ }
3735
+ if (ok && this.successfulToolCount === 0) {
3736
+ if (this.toolFailureDetail !== null) {
3737
+ throw new CodexDispatchError(`required MCP tool failed: ${this.toolFailureDetail}`);
3738
+ }
3739
+ throw new CodexDispatchError("codex turn completed without a successful allowlisted MCP tool call");
3740
+ }
3741
+ if (ok && this.finalText === null) {
3742
+ throw new CodexDispatchError("codex JSONL ended without an agent message");
3743
+ }
3744
+ this.finished = true;
3745
+ if (!ok) this.failureDetail = detail ?? "unknown failure";
3746
+ return [
3747
+ {
3748
+ t: "step_finish",
3749
+ id: this.stepId,
3750
+ ok,
3751
+ ...detail !== void 0 ? { detail } : {}
3752
+ }
3753
+ ];
3754
+ }
3755
+ };
3756
+
3757
+ // src/run.ts
3758
+ async function runOneStep(prepared, step, inputs, options, events) {
3759
+ const mapper = new CodexJsonlMapper(step.name, prepared.mcp.name, prepared.enabledTools);
3760
+ const args = buildCodexArgs(prepared, step, {
3761
+ cwd: options.cwd,
3762
+ ...options.codexArgsPrefix ? { codexArgsPrefix: options.codexArgsPrefix } : {}
3763
+ });
3764
+ let child;
3765
+ try {
3766
+ child = spawn2(options.codexBin ?? "codex", args, {
3767
+ cwd: options.cwd,
3768
+ env: sanitizeCodexEnvironment(options.env),
3769
+ stdio: ["pipe", "pipe", "pipe"],
3770
+ detached: process.platform !== "win32"
3771
+ });
3772
+ } catch (error) {
3773
+ throw new CodexDispatchError(`failed to start codex: ${String(error)}`);
3774
+ }
3775
+ if (child.stdin === null || child.stdout === null || child.stderr === null) {
3776
+ child.kill("SIGTERM");
3777
+ throw new CodexDispatchError("failed to start codex with piped stdio");
3778
+ }
3779
+ const childStdin = child.stdin;
3780
+ const childStdout = child.stdout;
3781
+ const childStderr = child.stderr;
3782
+ const exitPromise = new Promise(
3783
+ (resolve4, reject) => {
3784
+ child.once(
3785
+ "error",
3786
+ (error) => reject(new CodexDispatchError(`failed to start codex: ${error.message}`))
3787
+ );
3788
+ child.once("close", (code, signal) => resolve4({ code, signal }));
3789
+ }
3790
+ );
3791
+ childStderr.resume();
3792
+ let terminalError = null;
3793
+ let terminationRequested = false;
3794
+ let killTimer;
3795
+ const terminationGraceMs = options.terminationGraceMs ?? 1e3;
3796
+ const signalProcessTree = (signal) => {
3797
+ if (child.pid === void 0) return;
3798
+ if (process.platform !== "win32") {
3799
+ try {
3800
+ process.kill(-child.pid, signal);
3801
+ return;
3802
+ } catch (error) {
3803
+ if (error.code === "ESRCH") return;
3804
+ }
3805
+ }
3806
+ child.kill(signal);
3807
+ };
3808
+ const terminateProcessTree = () => {
3809
+ if (terminationRequested) return;
3810
+ terminationRequested = true;
3811
+ signalProcessTree("SIGTERM");
3812
+ killTimer = setTimeout(() => signalProcessTree("SIGKILL"), terminationGraceMs);
3813
+ };
3814
+ const lines = createInterface2({ input: childStdout });
3815
+ lines.on("line", (line) => {
3816
+ if (line.trim().length === 0 || terminalError) return;
3817
+ try {
3818
+ for (const event of mapper.nextLine(line)) {
3819
+ events.push(event);
3820
+ options.onEvent?.(event);
3821
+ }
3822
+ } catch (error) {
3823
+ terminalError = error instanceof Error ? error : new Error(String(error));
3824
+ terminateProcessTree();
3825
+ }
3826
+ });
3827
+ const prompt = buildPrompt(prepared, step, options.request, inputs, { producedValue: "string" });
3828
+ childStdin.end(prompt);
3829
+ let aborted = false;
3830
+ const abort = () => {
3831
+ aborted = true;
3832
+ terminateProcessTree();
3833
+ };
3834
+ options.signal?.addEventListener("abort", abort, { once: true });
3835
+ const timeoutMs = options.timeoutMs ?? 12e4;
3836
+ const timer = setTimeout(abort, timeoutMs);
3837
+ const exit = await exitPromise.finally(() => {
3838
+ clearTimeout(timer);
3839
+ options.signal?.removeEventListener("abort", abort);
3840
+ lines.close();
3841
+ if (terminationRequested) signalProcessTree("SIGKILL");
3842
+ if (killTimer !== void 0) clearTimeout(killTimer);
3843
+ });
3844
+ if (terminalError) throw terminalError;
3845
+ if (aborted) {
3846
+ const reason = options.signal?.aborted ? "cancelled" : `timed out after ${timeoutMs}ms`;
3847
+ throw new CodexDispatchError(`codex dispatch ${reason}`);
3848
+ }
3849
+ if (exit.code !== 0) {
3850
+ throw new CodexDispatchError(
3851
+ `codex exited with ${exit.code ?? exit.signal ?? "unknown"}`
3852
+ );
3853
+ }
3854
+ return mapper.result().finalText;
3855
+ }
3856
+ async function runSetup(prepared, options) {
3857
+ if (options.signal?.aborted) {
3858
+ throw new CodexDispatchError("codex dispatch cancelled before start");
3859
+ }
3860
+ const events = [];
3861
+ const slots = {};
3862
+ const outcomes = /* @__PURE__ */ new Map();
3863
+ const steps = [];
3864
+ let lastFinalText = null;
3865
+ for (const step of prepared.steps) {
3866
+ if (!shouldRunStep(step.when, outcomes)) {
3867
+ outcomes.set(step.name, { ran: false });
3868
+ steps.push({ name: step.name, ran: false, ok: false });
3869
+ continue;
3870
+ }
3871
+ const inputs = Object.fromEntries(step.consumes.map((name) => [name, slots[name]]));
3872
+ const finalText = await runOneStep(prepared, step, inputs, options, events);
3873
+ const hasGuardedConsumer = prepared.steps.some((candidate) => candidate.when?.target === step.name);
3874
+ let record2;
3875
+ try {
3876
+ record2 = parseStepTerminal(finalText, step.produces);
3877
+ } catch (error) {
3878
+ if (hasGuardedConsumer && error instanceof CodexDispatchError) {
3879
+ outcomes.set(step.name, { ran: true, ok: false });
3880
+ steps.push({ name: step.name, ran: true, ok: false });
3881
+ lastFinalText = finalText;
3882
+ continue;
3883
+ }
3884
+ throw error;
3885
+ }
3886
+ const value = record2[step.produces];
3887
+ slots[step.produces] = value;
3888
+ outcomes.set(step.name, { ran: true, ok: true, value });
3889
+ steps.push({ name: step.name, ran: true, ok: true, value });
3890
+ lastFinalText = finalText;
3891
+ }
3892
+ if (lastFinalText === null) {
3893
+ throw new CodexDispatchError("codex dispatch completed without running any step");
3894
+ }
3895
+ return {
3896
+ target: prepared.target,
3897
+ component: prepared.componentId,
3898
+ finalText: lastFinalText,
3899
+ events,
3900
+ steps
3901
+ };
3902
+ }
3903
+
3904
+ // src/cli.ts
3905
+ var USAGE = "usage: warble-codex-local <dispatch|manifest|describe> <ir.json> [request] --component <id> --server-command <absolute-path> [options]\n warble-codex-local list-models [--project <dir>] [--codex-home <dir>] [--codex-bin <path>] [--timeout <ms>]";
3906
+ function fail(message) {
3907
+ process.stderr.write(`error: ${message}
3908
+ `);
3909
+ process.exit(1);
3910
+ }
3911
+ function valuesList(value) {
3912
+ if (value === void 0) return [];
3913
+ return Array.isArray(value) ? value : [value];
3914
+ }
3915
+ async function main() {
3916
+ const { values, positionals } = parseArgs({
3917
+ allowPositionals: true,
3918
+ options: {
3919
+ component: { type: "string" },
3920
+ model: { type: "string" },
3921
+ project: { type: "string" },
3922
+ out: { type: "string" },
3923
+ timeout: { type: "string" },
3924
+ "codex-bin": { type: "string" },
3925
+ server: { type: "string" },
3926
+ "server-command": { type: "string" },
3927
+ "server-arg": { type: "string", multiple: true },
3928
+ "source-tool": { type: "string", multiple: true },
3929
+ "context-tool": { type: "string", multiple: true },
3930
+ "inspect-tool": { type: "string", multiple: true },
3931
+ "query-tool": { type: "string", multiple: true },
3932
+ "semantic-tool": { type: "string", multiple: true },
3933
+ "raw-material-tool": { type: "string", multiple: true },
3934
+ "orchestrator-model": { type: "string" },
3935
+ "cheap-model": { type: "string" },
3936
+ "strong-model": { type: "string" },
3937
+ "codex-home": { type: "string" },
3938
+ "stream-json": { type: "boolean" }
3939
+ }
3940
+ });
3941
+ const [subcommand, irPathArg, request] = positionals;
3942
+ if (subcommand === "list-models") {
3943
+ if (irPathArg !== void 0 || request !== void 0) fail("list-models does not take an <ir.json> or request");
3944
+ const timeout = values.timeout === void 0 ? void 0 : Number(values.timeout);
3945
+ if (timeout !== void 0 && (!Number.isFinite(timeout) || timeout <= 0)) fail("--timeout must be a positive number");
3946
+ const catalog = await discoverCodexModels({
3947
+ ...values.project ? { cwd: values.project } : {},
3948
+ ...values["codex-home"] ? { codexHome: values["codex-home"] } : {},
3949
+ ...values["codex-bin"] ? { codexBin: values["codex-bin"] } : {},
3950
+ ...timeout !== void 0 ? { timeoutMs: timeout } : {}
3951
+ });
3952
+ process.stdout.write(`${JSON.stringify(catalog)}
3953
+ `);
3954
+ return;
3955
+ }
3956
+ if (!["dispatch", "manifest", "describe"].includes(subcommand ?? "")) fail(USAGE);
3957
+ if (!irPathArg) fail("missing <ir.json>");
3958
+ if (!values["server-command"]) fail("missing --server-command");
3959
+ const raw = readFileSync(resolve3(irPathArg), "utf8");
3960
+ const ir = parseIr(raw);
3961
+ const model = values.model ?? "gpt-5.4";
3962
+ if (!values.component && subcommand !== "dispatch" && supportsSetupAggregate(ir)) {
3963
+ const mcp2 = {
3964
+ name: values.server ?? "setup",
3965
+ command: resolve3(values["server-command"]),
3966
+ args: valuesList(values["server-arg"]),
3967
+ toolsByCapability: {
3968
+ source_connect: valuesList(values["source-tool"]),
3969
+ context_build: valuesList(values["context-tool"])
3970
+ }
3971
+ };
3972
+ const prepared2 = prepareAllSetup(raw, { model, mcp: mcp2 });
3973
+ const output = subcommand === "manifest" ? buildManifest(prepared2) : describeTarget(prepared2);
3974
+ const text2 = `${JSON.stringify(output, null, 2)}
3975
+ `;
3976
+ if (values.out) writeFileSync2(resolve3(values.out), text2);
3977
+ else process.stdout.write(text2);
3978
+ return;
3979
+ }
3980
+ const component = values.component;
3981
+ if (!component) fail(`${subcommand} requires --component for the selected component execution contract`);
3982
+ const contract = classifyDispatchContract(ir, component);
3983
+ if (contract === "enrich") {
3984
+ const enrichMcp = {
3985
+ name: values.server ?? "enrich",
3986
+ command: resolve3(values["server-command"]),
3987
+ args: valuesList(values["server-arg"]),
3988
+ toolsByCapability: {
3989
+ semantic_introspection: valuesList(values["semantic-tool"]),
3990
+ raw_material_read: valuesList(values["raw-material-tool"])
3991
+ }
3992
+ };
3993
+ const preparedEnrich = prepareEnrich({ ir: raw, component, model, mcp: enrichMcp });
3994
+ if (subcommand === "manifest" || subcommand === "describe") {
3995
+ const output = subcommand === "manifest" ? buildEnrichManifest(preparedEnrich) : describeEnrichTarget(preparedEnrich);
3996
+ const text2 = `${JSON.stringify(output, null, 2)}
3997
+ `;
3998
+ if (values.out) writeFileSync2(resolve3(values.out), text2);
3999
+ else process.stdout.write(text2);
4000
+ return;
4001
+ }
4002
+ if (!request) fail("dispatch requires a request");
4003
+ if (!values["codex-home"]) fail("selected component requires --codex-home");
4004
+ const result2 = await runEnrich(preparedEnrich, request, {
4005
+ codexHome: resolve3(values["codex-home"]),
4006
+ cwd: resolve3(values.project ?? "."),
4007
+ externalAuthentication: "provisioned",
4008
+ ...values["codex-bin"] ? { codexBin: resolve3(values["codex-bin"]) } : {},
4009
+ ...values.timeout ? { timeoutMs: Number(values.timeout) } : {},
4010
+ ...values["stream-json"] ? { onEvent: (event) => process.stdout.write(`${JSON.stringify(event)}
4011
+ `) } : {}
4012
+ });
4013
+ if (!values["stream-json"]) process.stdout.write(`${result2.finalText}
4014
+ `);
4015
+ return;
4016
+ }
4017
+ if (contract === "ask") {
4018
+ for (const option of ["orchestrator-model", "cheap-model", "strong-model"]) {
4019
+ if (!values[option]) fail(`selected component requires --${option}`);
4020
+ }
4021
+ const askMcp = {
4022
+ name: values.server ?? "wren",
4023
+ command: resolve3(values["server-command"]),
4024
+ args: valuesList(values["server-arg"]),
4025
+ toolsByStep: {
4026
+ resolve_intent: valuesList(values["inspect-tool"]),
4027
+ generate_sql: valuesList(values["query-tool"]),
4028
+ repair_sql: valuesList(values["query-tool"]),
4029
+ plan_dashboard: valuesList(values["inspect-tool"]),
4030
+ compose_layout: valuesList(values["query-tool"])
4031
+ }
4032
+ };
4033
+ const preparedAsk = prepareAsk({
4034
+ ir: raw,
4035
+ component,
4036
+ models: {
4037
+ orchestrator: values["orchestrator-model"],
4038
+ cheap: values["cheap-model"],
4039
+ strong: values["strong-model"]
4040
+ },
4041
+ mcp: askMcp
4042
+ });
4043
+ if (subcommand === "manifest" || subcommand === "describe") {
4044
+ const output = subcommand === "manifest" ? buildAskManifest(preparedAsk) : describeAskTarget(preparedAsk);
4045
+ const text2 = `${JSON.stringify(output, null, 2)}
4046
+ `;
4047
+ if (values.out) writeFileSync2(resolve3(values.out), text2);
4048
+ else process.stdout.write(text2);
4049
+ return;
4050
+ }
4051
+ if (!request) fail("dispatch requires a request");
4052
+ if (!values["codex-home"]) fail("selected component requires --codex-home");
4053
+ const runtime = await CodexAskRuntime.connect(preparedAsk, {
4054
+ codexHome: resolve3(values["codex-home"]),
4055
+ cwd: resolve3(values.project ?? "."),
4056
+ externalAuthentication: "provisioned",
4057
+ ...values["codex-bin"] ? { codexBin: resolve3(values["codex-bin"]) } : {},
4058
+ ...values.timeout ? { turnTimeoutMs: Number(values.timeout) } : {},
4059
+ ...values["stream-json"] ? { onAskEvent: (event) => process.stdout.write(`${JSON.stringify(event)}
4060
+ `) } : {}
4061
+ });
4062
+ try {
4063
+ const session = await runtime.start();
4064
+ const result2 = await runtime.run(session, request);
4065
+ if (values["stream-json"]) {
4066
+ process.stdout.write(`${JSON.stringify({ t: "answer", text: result2.finalText })}
4067
+ `);
4068
+ } else {
4069
+ process.stdout.write(`${result2.finalText}
4070
+ `);
4071
+ }
4072
+ } finally {
4073
+ await runtime.close();
4074
+ }
4075
+ return;
4076
+ }
4077
+ const mcp = {
4078
+ name: values.server ?? "setup",
4079
+ command: resolve3(values["server-command"]),
4080
+ args: valuesList(values["server-arg"]),
4081
+ toolsByCapability: {
4082
+ source_connect: valuesList(values["source-tool"]),
4083
+ context_build: valuesList(values["context-tool"])
4084
+ }
4085
+ };
4086
+ const prepared = prepareSetup({ ir: raw, component, model, mcp });
4087
+ if (subcommand === "manifest" || subcommand === "describe") {
4088
+ const output = subcommand === "manifest" ? buildManifest([prepared]) : describeTarget([prepared]);
4089
+ const text2 = `${JSON.stringify(output, null, 2)}
4090
+ `;
4091
+ if (values.out) writeFileSync2(resolve3(values.out), text2);
4092
+ else process.stdout.write(text2);
4093
+ return;
4094
+ }
4095
+ if (!request) fail("dispatch requires a request");
4096
+ const result = await runSetup(prepared, {
4097
+ cwd: resolve3(values.project ?? "."),
4098
+ request,
4099
+ ...values["codex-bin"] ? { codexBin: resolve3(values["codex-bin"]) } : {},
4100
+ ...values.timeout ? { timeoutMs: Number(values.timeout) } : {},
4101
+ ...values["stream-json"] ? {
4102
+ onEvent: (event) => process.stdout.write(`${JSON.stringify(event)}
4103
+ `)
4104
+ } : {}
4105
+ });
4106
+ if (!values["stream-json"]) process.stdout.write(`${result.finalText}
4107
+ `);
4108
+ }
4109
+ main().catch((error) => {
4110
+ if (error instanceof CodexDispatchError) fail(error.message);
4111
+ fail(error instanceof Error ? error.stack ?? error.message : String(error));
4112
+ });
4113
+ //# sourceMappingURL=cli.js.map