intentdna 1.8.6 → 1.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -0
  4. package/dist/cli/commands/run-lifecycle.d.ts +73 -0
  5. package/dist/cli/commands/run-lifecycle.js +240 -0
  6. package/dist/cli/commands/run.d.ts +22 -40
  7. package/dist/cli/commands/run.js +674 -392
  8. package/dist/cli/index.js +87 -2
  9. package/dist/compiler/workflow.js +3 -2
  10. package/dist/hooks/cli.d.ts +1 -2
  11. package/dist/hooks/cli.js +119 -80
  12. package/dist/hooks/enforce.d.ts +2 -0
  13. package/dist/hooks/enforce.js +56 -27
  14. package/dist/hooks/enforcement-boundary.d.ts +13 -0
  15. package/dist/hooks/enforcement-boundary.js +33 -0
  16. package/dist/hooks/index.d.ts +3 -2
  17. package/dist/hooks/index.js +3 -2
  18. package/dist/hooks/protocol.d.ts +12 -4
  19. package/dist/hooks/protocol.js +20 -14
  20. package/dist/hooks/schema.d.ts +2 -1
  21. package/dist/hooks/schema.js +6 -2
  22. package/dist/hooks/state-manager.d.ts +5 -5
  23. package/dist/hooks/state-manager.js +26 -24
  24. package/dist/hooks/state.d.ts +19 -3
  25. package/dist/hooks/state.js +327 -80
  26. package/dist/mcp/index.js +0 -0
  27. package/dist/runtime/diagnosis-contract-verifier.d.ts +11 -0
  28. package/dist/runtime/diagnosis-contract-verifier.js +417 -0
  29. package/dist/runtime/execution-provider.d.ts +40 -0
  30. package/dist/runtime/execution-provider.js +138 -0
  31. package/dist/runtime/handoff-resolver.d.ts +61 -0
  32. package/dist/runtime/handoff-resolver.js +167 -0
  33. package/dist/runtime/index.d.ts +24 -0
  34. package/dist/runtime/index.js +13 -0
  35. package/dist/runtime/process-tree.d.ts +47 -0
  36. package/dist/runtime/process-tree.js +402 -0
  37. package/dist/runtime/providers/claude.d.ts +9 -0
  38. package/dist/runtime/providers/claude.js +64 -0
  39. package/dist/runtime/providers/codex.d.ts +8 -0
  40. package/dist/runtime/providers/codex.js +72 -0
  41. package/dist/runtime/result-store.d.ts +32 -0
  42. package/dist/runtime/result-store.js +130 -0
  43. package/dist/runtime/run-contracts.d.ts +290 -0
  44. package/dist/runtime/run-contracts.js +58 -0
  45. package/dist/runtime/run-controller.d.ts +149 -0
  46. package/dist/runtime/run-controller.js +1108 -0
  47. package/dist/runtime/run-store.d.ts +96 -0
  48. package/dist/runtime/run-store.js +725 -0
  49. package/dist/runtime/worker-executor.d.ts +19 -0
  50. package/dist/runtime/worker-executor.js +194 -0
  51. package/dist/runtime/workflow-plan-adapter.d.ts +26 -0
  52. package/dist/runtime/workflow-plan-adapter.js +416 -0
  53. package/dist/runtime/workflow-runner.d.ts +15 -3
  54. package/dist/runtime/workflow-runner.js +13 -1
  55. package/dist/runtime/workspace-isolation.d.ts +103 -0
  56. package/dist/runtime/workspace-isolation.js +373 -0
  57. package/dist/schema/types.d.ts +1 -0
  58. package/dist/schema/validate.js +64 -6
  59. package/dist/schema/yaml-parser.js +7 -2
  60. package/package.json +1 -1
@@ -0,0 +1,19 @@
1
+ import type { ProviderEvent, StepPacket } from "./run-contracts.js";
2
+ import type { ExecutionProvider, WorkerExecution } from "./execution-provider.js";
3
+ export interface WorkerExecutorOptions {
4
+ readonly cancellationSignal?: AbortSignal;
5
+ readonly cancellationReason?: string | null;
6
+ readonly maxOutputBytes?: number;
7
+ readonly maxProcessCount?: number;
8
+ readonly processLimitPollMs?: number;
9
+ readonly onProcessStart?: (processId: number) => void | Promise<void>;
10
+ readonly now?: () => Date;
11
+ }
12
+ export interface WorkerExecutionWithEvents extends WorkerExecution {
13
+ readonly events: readonly ProviderEvent[];
14
+ }
15
+ /**
16
+ * Execute exactly one standalone Step attempt with one provider process.
17
+ * Controller retry and task/run policy are intentionally outside this boundary.
18
+ */
19
+ export declare function executeWorkerAttempt(packet: StepPacket, provider: ExecutionProvider, options?: WorkerExecutorOptions): Promise<WorkerExecutionWithEvents>;
@@ -0,0 +1,194 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { runProcessTree, } from "./process-tree.js";
3
+ function eventId() {
4
+ return randomUUID();
5
+ }
6
+ function resourceLimitError(processResult) {
7
+ if (processResult.outputLimitExceeded)
8
+ return "provider output limit exceeded";
9
+ if (processResult.processLimitExceeded)
10
+ return "provider process limit exceeded";
11
+ return null;
12
+ }
13
+ function processOutcome(processResult, timeoutMs) {
14
+ if (processResult.cancelled) {
15
+ return {
16
+ kind: "cancelled",
17
+ exit_code: processResult.status,
18
+ signal: processResult.signal,
19
+ reason: processResult.cancellationReason,
20
+ };
21
+ }
22
+ if (processResult.timedOut) {
23
+ return {
24
+ kind: "timeout",
25
+ exit_code: processResult.status,
26
+ signal: processResult.signal,
27
+ timeout_ms: timeoutMs ?? 0,
28
+ };
29
+ }
30
+ if (processResult.error) {
31
+ return {
32
+ kind: "launch_error",
33
+ exit_code: null,
34
+ signal: null,
35
+ error: processResult.error.message,
36
+ };
37
+ }
38
+ const limitError = resourceLimitError(processResult);
39
+ if (limitError) {
40
+ return {
41
+ kind: "malformed_result",
42
+ exit_code: processResult.status,
43
+ signal: null,
44
+ error: limitError,
45
+ };
46
+ }
47
+ if (processResult.signal !== null) {
48
+ return {
49
+ kind: "signal",
50
+ exit_code: null,
51
+ signal: processResult.signal,
52
+ };
53
+ }
54
+ if (processResult.status !== 0) {
55
+ return {
56
+ kind: "non_zero_exit",
57
+ exit_code: processResult.status ?? -1,
58
+ signal: null,
59
+ };
60
+ }
61
+ return null;
62
+ }
63
+ function terminationEventReason(reason) {
64
+ if (reason === "cancellation")
65
+ return "cancellation";
66
+ if (reason === "timeout")
67
+ return "timeout";
68
+ return "process_limit";
69
+ }
70
+ /**
71
+ * Execute exactly one standalone Step attempt with one provider process.
72
+ * Controller retry and task/run policy are intentionally outside this boundary.
73
+ */
74
+ export async function executeWorkerAttempt(packet, provider, options = {}) {
75
+ const now = options.now ?? (() => new Date());
76
+ const events = [];
77
+ let sequence = 0;
78
+ const pushEvent = (event) => {
79
+ events.push({
80
+ ...event,
81
+ run_id: packet.run_id,
82
+ step_id: packet.step_id,
83
+ attempt_id: packet.attempt_id,
84
+ worker_session_id: packet.worker_session_id,
85
+ event_id: eventId(),
86
+ sequence: ++sequence,
87
+ occurred_at: now().toISOString(),
88
+ });
89
+ };
90
+ const launch = provider.createLaunch(packet);
91
+ const startedAt = now().toISOString();
92
+ pushEvent({ type: "launch_started", provider: provider.name });
93
+ let processStartObservation = Promise.resolve();
94
+ let processStartFailed = false;
95
+ let processStartError;
96
+ const processResult = await runProcessTree(launch.command, launch.args, {
97
+ cwd: launch.cwd,
98
+ env: launch.env,
99
+ stdin: launch.stdin ?? undefined,
100
+ timeoutMs: packet.execution.timeout_ms ?? undefined,
101
+ cancellationSignal: options.cancellationSignal,
102
+ cancellationReason: options.cancellationReason,
103
+ cancellationGraceMs: packet.execution.cancellation_grace_ms,
104
+ maxOutputBytes: options.maxOutputBytes,
105
+ maxProcessCount: options.maxProcessCount,
106
+ processLimitPollMs: options.processLimitPollMs,
107
+ cleanupOnParentExit: true,
108
+ onProcessStart: (processId) => {
109
+ pushEvent({ type: "process_started", process_id: processId });
110
+ processStartObservation = Promise.resolve(options.onProcessStart?.(processId)).catch((error) => {
111
+ processStartFailed = true;
112
+ processStartError = error;
113
+ });
114
+ },
115
+ onStdout: (text) => {
116
+ pushEvent({ type: "stdout", text });
117
+ },
118
+ onStderr: (text) => {
119
+ pushEvent({ type: "stderr", text });
120
+ },
121
+ onTerminationRequested: (reason) => {
122
+ pushEvent({
123
+ type: "termination_requested",
124
+ reason: terminationEventReason(reason),
125
+ });
126
+ },
127
+ });
128
+ await processStartObservation;
129
+ if (processStartFailed) {
130
+ throw processStartError instanceof Error
131
+ ? processStartError
132
+ : new Error(String(processStartError ?? "process start observer failed"));
133
+ }
134
+ let providerSessionId = null;
135
+ let outputs = [];
136
+ let outcome = processOutcome(processResult, packet.execution.timeout_ms);
137
+ if (outcome === null) {
138
+ try {
139
+ const parsed = provider.parseResult({
140
+ packet,
141
+ stdout: processResult.stdout,
142
+ stderr: processResult.stderr,
143
+ });
144
+ providerSessionId = parsed.provider_session_id;
145
+ outputs = parsed.outputs;
146
+ outcome = { kind: "success", exit_code: 0, signal: null };
147
+ pushEvent({
148
+ type: "session_started",
149
+ provider_session_id: providerSessionId,
150
+ });
151
+ }
152
+ catch (error) {
153
+ outcome = {
154
+ kind: "malformed_result",
155
+ exit_code: processResult.status,
156
+ signal: null,
157
+ error: error instanceof Error ? error.message : String(error),
158
+ };
159
+ }
160
+ }
161
+ if (outcome === null) {
162
+ outcome = {
163
+ kind: "launch_error",
164
+ exit_code: null,
165
+ signal: null,
166
+ error: "worker execution completed without a terminal outcome",
167
+ };
168
+ }
169
+ pushEvent({
170
+ type: "process_exited",
171
+ exit_code: processResult.status,
172
+ signal: processResult.signal,
173
+ });
174
+ const completedAt = now().toISOString();
175
+ const result = {
176
+ run_id: packet.run_id,
177
+ step_id: packet.step_id,
178
+ attempt_id: packet.attempt_id,
179
+ worker_session_id: packet.worker_session_id,
180
+ provider: provider.name,
181
+ provider_session_id: providerSessionId,
182
+ started_at: startedAt,
183
+ completed_at: completedAt,
184
+ outcome,
185
+ outputs,
186
+ stdout_reference: null,
187
+ stderr_reference: null,
188
+ };
189
+ return {
190
+ worker_session_id: packet.worker_session_id,
191
+ result,
192
+ events,
193
+ };
194
+ }
@@ -0,0 +1,26 @@
1
+ import type { RoleDef, WorkflowPlan, WorkflowStep } from "../schema/types.js";
2
+ import type { ControllerStepDefinition, MaterializeStepPacketInput } from "./run-controller.js";
3
+ import type { TerminalOutcomeKind } from "./run-contracts.js";
4
+ export type WorkflowPlanAdapterErrorCode = "invalid_plan" | "missing_role" | "invalid_handoff" | "unsupported_routing";
5
+ export declare class WorkflowPlanAdapterError extends Error {
6
+ readonly code: WorkflowPlanAdapterErrorCode;
7
+ readonly step_id: string | null;
8
+ constructor(code: WorkflowPlanAdapterErrorCode, message: string, stepId?: string | null);
9
+ }
10
+ export interface WorkflowPlanAdapterOptions {
11
+ readonly roles: Readonly<Record<string, RoleDef>>;
12
+ readonly variables?: Readonly<Record<string, string>>;
13
+ readonly working_directory: string;
14
+ readonly working_directory_for_step?: (step: WorkflowStep, input: MaterializeStepPacketInput) => string;
15
+ readonly timeout_ms?: number | null;
16
+ readonly cancellation_grace_ms?: number;
17
+ readonly retry_initial_delay_ms?: number;
18
+ readonly retry_max_delay_ms?: number | null;
19
+ readonly retryable_outcomes?: readonly TerminalOutcomeKind[];
20
+ }
21
+ /**
22
+ * Adapt one compiled workflow plan into Controller definitions. Every
23
+ * materializer creates a self-contained packet from Controller-resolved inputs;
24
+ * no provider transcript or prior-session state is accepted by this API.
25
+ */
26
+ export declare function adaptWorkflowPlan(plan: WorkflowPlan, options: WorkflowPlanAdapterOptions): readonly ControllerStepDefinition[];
@@ -0,0 +1,416 @@
1
+ const DEFAULT_RETRYABLE_OUTCOMES = [
2
+ "non_zero_exit",
3
+ "malformed_result",
4
+ "timeout",
5
+ "signal",
6
+ "launch_error",
7
+ ];
8
+ export class WorkflowPlanAdapterError extends Error {
9
+ code;
10
+ step_id;
11
+ constructor(code, message, stepId = null) {
12
+ super(message);
13
+ this.name = "WorkflowPlanAdapterError";
14
+ this.code = code;
15
+ this.step_id = stepId;
16
+ }
17
+ }
18
+ function substitute(value, variables) {
19
+ return value.replace(/\{\{(\w+)\}\}/g, (match, key) => (Object.prototype.hasOwnProperty.call(variables, key)
20
+ ? variables[key]
21
+ : match));
22
+ }
23
+ function artifactIdentity(artifact, variables, stepId) {
24
+ const identity = artifact.artifact_id
25
+ ?? artifact.name
26
+ ?? artifact.path
27
+ ?? (artifact.type === "git_commit" ? "git_commit" : undefined);
28
+ if (!identity) {
29
+ throw new WorkflowPlanAdapterError("invalid_handoff", `handoff artifact '${artifact.description}' has no stable identity`, stepId);
30
+ }
31
+ return substitute(identity, variables);
32
+ }
33
+ function handoffMode(type) {
34
+ if (type === "summary")
35
+ return "quote";
36
+ if (type === "state")
37
+ return "structured";
38
+ return "reference";
39
+ }
40
+ function outputKind(type) {
41
+ if (type === "summary")
42
+ return "text";
43
+ if (type === "state")
44
+ return "structured";
45
+ return "reference";
46
+ }
47
+ function requireNonNegativeInteger(value, field) {
48
+ if (!Number.isSafeInteger(value) || value < 0) {
49
+ throw new WorkflowPlanAdapterError("invalid_plan", `${field} must be a non-negative safe integer`);
50
+ }
51
+ }
52
+ function isolationForStep(groups, stepId) {
53
+ return groups.find((group) => group.step_ids.includes(stepId))?.isolation
54
+ ?? "none";
55
+ }
56
+ function completionText(completion, variables) {
57
+ if (completion.file_exists !== undefined) {
58
+ return `Required file exists: ${substitute(completion.file_exists, variables)}`;
59
+ }
60
+ if (completion.file_not_empty !== undefined) {
61
+ return `Required file is not empty: ${substitute(completion.file_not_empty, variables)}`;
62
+ }
63
+ if (completion.file_contains !== undefined) {
64
+ return `Required file ${substitute(completion.file_contains.path, variables)} contains /${completion.file_contains.pattern}/`;
65
+ }
66
+ return `Required command succeeds: ${substitute(completion.command_success ?? "", variables)}`;
67
+ }
68
+ function checkpointText(checkpoint, variables) {
69
+ const command = checkpoint.command
70
+ ? `; command: ${substitute(checkpoint.command, variables)}`
71
+ : "";
72
+ return `Checkpoint ${checkpoint.assert} (${checkpoint.action ?? "block"}): ${substitute(checkpoint.message, variables)}${command}`;
73
+ }
74
+ function collectConstraints(step, role, variables) {
75
+ const constraints = [];
76
+ for (const instruction of role.instructions ?? []) {
77
+ constraints.push(substitute(instruction, variables));
78
+ }
79
+ if (role.scope?.read) {
80
+ constraints.push(`Readable paths: ${role.scope.read.map((path) => substitute(path, variables)).join(", ")}`);
81
+ }
82
+ if (role.scope?.write) {
83
+ constraints.push(`Writable paths: ${role.scope.write.map((path) => substitute(path, variables)).join(", ") || "(none)"}`);
84
+ }
85
+ if (role.tool_permissions?.allow) {
86
+ constraints.push(`Allowed tools: ${role.tool_permissions.allow.join(", ")}`);
87
+ }
88
+ if (role.tool_permissions?.deny) {
89
+ constraints.push(`Denied tools: ${role.tool_permissions.deny.join(", ")}`);
90
+ }
91
+ for (const postCheck of role.post_checks ?? []) {
92
+ constraints.push(`Required role post-check: ${substitute(postCheck, variables)}`);
93
+ }
94
+ if (step.enforce?.read_only) {
95
+ constraints.push("This Step is read-only; do not modify files.");
96
+ }
97
+ if (step.enforce?.additional_write_paths?.length) {
98
+ constraints.push(`Additional writable paths: ${step.enforce.additional_write_paths
99
+ .map((path) => substitute(path, variables))
100
+ .join(", ")}`);
101
+ }
102
+ if (step.enforce?.relax_after_iteration !== undefined) {
103
+ constraints.push(`Scope relaxation is permitted only after iteration ${step.enforce.relax_after_iteration}.`);
104
+ }
105
+ for (const completion of step.completion ?? []) {
106
+ constraints.push(completionText(completion, variables));
107
+ }
108
+ for (const checkpoint of step.checkpoints ?? []) {
109
+ constraints.push(checkpointText(checkpoint, variables));
110
+ }
111
+ return constraints;
112
+ }
113
+ function createOutputContract(step, variables) {
114
+ const outputs = (step.handoff?.produces ?? []).map((artifact) => ({
115
+ name: artifactIdentity(artifact, variables, step.id),
116
+ kind: outputKind(artifact.type),
117
+ required: artifact.required !== false,
118
+ description: substitute(artifact.description, variables),
119
+ schema_ref: null,
120
+ }));
121
+ const names = new Set();
122
+ for (const output of outputs) {
123
+ if (names.has(output.name)) {
124
+ throw new WorkflowPlanAdapterError("invalid_handoff", `step '${step.id}' declares duplicate output '${output.name}'`, step.id);
125
+ }
126
+ names.add(output.name);
127
+ }
128
+ return outputs;
129
+ }
130
+ function findSourceOutput(sourceStep, outputName, variables) {
131
+ const matches = (sourceStep.handoff?.produces ?? []).filter((artifact) => artifactIdentity(artifact, variables, sourceStep.id) === outputName);
132
+ if (matches.length !== 1) {
133
+ throw new WorkflowPlanAdapterError("invalid_handoff", `step '${sourceStep.id}' must declare exactly one output named '${outputName}'`, sourceStep.id);
134
+ }
135
+ return matches[0];
136
+ }
137
+ function createHandoffs(step, stepById, variables) {
138
+ const declarations = [];
139
+ const externalInputs = [];
140
+ for (const artifact of step.handoff?.consumes ?? []) {
141
+ if (!artifact.from) {
142
+ externalInputs.push(artifact);
143
+ continue;
144
+ }
145
+ const sourceStep = stepById.get(artifact.from);
146
+ if (!sourceStep || artifact.from === step.id) {
147
+ throw new WorkflowPlanAdapterError("invalid_handoff", `step '${step.id}' declares invalid handoff source '${artifact.from}'`, step.id);
148
+ }
149
+ const inputName = artifactIdentity(artifact, variables, step.id);
150
+ const sourceArtifact = findSourceOutput(sourceStep, inputName, variables);
151
+ const mode = handoffMode(artifact.type);
152
+ if (handoffMode(sourceArtifact.type) !== mode) {
153
+ throw new WorkflowPlanAdapterError("invalid_handoff", `handoff '${inputName}' changes mode between '${sourceStep.id}' and '${step.id}'`, step.id);
154
+ }
155
+ const base = {
156
+ binding_id: `${step.id}:${inputName}`,
157
+ input_name: inputName,
158
+ required: artifact.required !== false,
159
+ description: substitute(artifact.description, variables),
160
+ source: {
161
+ step_id: sourceStep.id,
162
+ output_name: inputName,
163
+ },
164
+ };
165
+ if (mode === "quote") {
166
+ declarations.push({ ...base, mode, selection: null });
167
+ }
168
+ else if (mode === "structured") {
169
+ declarations.push({ ...base, mode, schema_ref: null });
170
+ }
171
+ else {
172
+ declarations.push({ ...base, mode });
173
+ }
174
+ }
175
+ const bindingIds = new Set();
176
+ const inputNames = new Set();
177
+ for (const declaration of declarations) {
178
+ if (bindingIds.has(declaration.binding_id)
179
+ || inputNames.has(declaration.input_name)) {
180
+ throw new WorkflowPlanAdapterError("invalid_handoff", `step '${step.id}' declares duplicate handoff '${declaration.input_name}'`, step.id);
181
+ }
182
+ bindingIds.add(declaration.binding_id);
183
+ inputNames.add(declaration.input_name);
184
+ }
185
+ return { handoffs: declarations, external_inputs: externalInputs };
186
+ }
187
+ function retryPolicy(plan, step, options) {
188
+ const initialDelay = options.retry_initial_delay_ms ?? 0;
189
+ const maxDelay = options.retry_max_delay_ms ?? null;
190
+ requireNonNegativeInteger(initialDelay, "retry_initial_delay_ms");
191
+ if (maxDelay !== null)
192
+ requireNonNegativeInteger(maxDelay, "retry_max_delay_ms");
193
+ const maxAttempts = step.max_attempts ?? plan.retry.max_retries + 1;
194
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
195
+ throw new WorkflowPlanAdapterError("invalid_plan", `step '${step.id}' has an invalid max attempt count`, step.id);
196
+ }
197
+ return {
198
+ max_attempts: maxAttempts,
199
+ backoff: plan.retry.backoff,
200
+ initial_delay_ms: initialDelay,
201
+ max_delay_ms: maxDelay,
202
+ retryable_outcomes: options.retryable_outcomes
203
+ ?? DEFAULT_RETRYABLE_OUTCOMES,
204
+ };
205
+ }
206
+ function addHandoffDependencies(step, handoffs) {
207
+ const dependencies = new Set(step.depends_on);
208
+ for (const handoff of handoffs) {
209
+ if (handoff.mode !== "none")
210
+ dependencies.add(handoff.source.step_id);
211
+ }
212
+ return [...dependencies].map((dependency) => dependency);
213
+ }
214
+ function renderBinding(binding) {
215
+ const prefix = `- ${binding.input_name} (${binding.mode}, ${binding.required ? "required" : "optional"}): ${binding.description}`;
216
+ if (binding.mode === "reference") {
217
+ return [
218
+ prefix,
219
+ ` Reference kind: ${binding.reference.kind}`,
220
+ ` Reference value: ${binding.reference.value}`,
221
+ ];
222
+ }
223
+ if (binding.mode === "quote") {
224
+ return [
225
+ prefix,
226
+ ` Exact text JSON (${binding.exact_text.length} UTF-16 code units): ${JSON.stringify(binding.exact_text)}`,
227
+ ];
228
+ }
229
+ if (binding.mode === "structured") {
230
+ return [
231
+ prefix,
232
+ ` Schema: ${binding.schema_ref ?? "(none)"}`,
233
+ ` JSON value: ${JSON.stringify(binding.value)}`,
234
+ ];
235
+ }
236
+ return [prefix, ` Reason: ${binding.reason ?? "no upstream delivery declared"}`];
237
+ }
238
+ function renderExternalInput(artifact, variables, stepId) {
239
+ const name = artifactIdentity(artifact, variables, stepId);
240
+ const lines = [
241
+ `- ${name} (external ${handoffMode(artifact.type)}, ${artifact.required === false ? "optional" : "required"}): ${substitute(artifact.description, variables)}`,
242
+ ];
243
+ if (artifact.path) {
244
+ lines.push(` Declared path: ${substitute(artifact.path, variables)}`);
245
+ }
246
+ return lines;
247
+ }
248
+ function renderOutputProtocol(outputs) {
249
+ if (outputs.length === 0) {
250
+ return ["No handoff output is declared. Return only the Step result."];
251
+ }
252
+ if (outputs.length === 1 && outputs[0].kind === "text") {
253
+ return [
254
+ `Return the exact textual output '${outputs[0].name}' as the final response.`,
255
+ `Description: ${outputs[0].description}`,
256
+ ];
257
+ }
258
+ return [
259
+ "Return one JSON object with an `outputs` array matching these declarations exactly:",
260
+ ...outputs.map((output) => (`- ${output.name}: kind=${output.kind}, required=${String(output.required)}, schema_ref=${output.schema_ref ?? "null"}; ${output.description}`)),
261
+ "Reference values use {\"name\":\"...\",\"kind\":\"reference\",\"reference\":{\"kind\":\"file|directory|artifact|uri\",\"value\":\"...\"}}.",
262
+ "Text values use {\"name\":\"...\",\"kind\":\"text\",\"text\":\"...\"}.",
263
+ "Structured values use {\"name\":\"...\",\"kind\":\"structured\",\"value\":<JSON>,\"schema_ref\":null}.",
264
+ ];
265
+ }
266
+ function standalonePrompt(plan, adapted, bindings, variables) {
267
+ const { step, role, constraints, external_inputs: externalInputs, outputs } = adapted;
268
+ const lines = [
269
+ `# ${plan.name}: ${step.id}`,
270
+ "",
271
+ "You are executing one standalone workflow Step in an independent session.",
272
+ "Use only the task, constraints, and declared inputs below. Do not rely on or request previous conversation history.",
273
+ "Complete only this Step and stop after returning its declared result.",
274
+ "",
275
+ "## Role",
276
+ `${step.role}: ${substitute(role.description, variables)}`,
277
+ "",
278
+ "## Task",
279
+ substitute(step.prompt ?? step.description, variables),
280
+ "",
281
+ "## Constraints",
282
+ ...(constraints.length > 0
283
+ ? constraints.map((constraint) => `- ${constraint}`)
284
+ : ["- No additional current-Step constraints are declared."]),
285
+ "",
286
+ "## Inputs",
287
+ ];
288
+ if (bindings.length === 0 && externalInputs.length === 0) {
289
+ lines.push("- None. This Step has no declared upstream delivery.");
290
+ }
291
+ else {
292
+ for (const binding of bindings)
293
+ lines.push(...renderBinding(binding));
294
+ for (const artifact of externalInputs) {
295
+ lines.push(...renderExternalInput(artifact, variables, step.id));
296
+ }
297
+ }
298
+ lines.push("", "## Outputs", ...renderOutputProtocol(outputs), "");
299
+ if (role.success_criteria?.length) {
300
+ lines.push("## Success Criteria", ...role.success_criteria.map((criterion) => `- ${substitute(criterion, variables)}`), "");
301
+ }
302
+ if (role.failure_modes?.length) {
303
+ lines.push("## Failure Modes To Avoid", ...role.failure_modes.map((failure) => `- ${substitute(failure, variables)}`), "");
304
+ }
305
+ return lines.join("\n");
306
+ }
307
+ function validatePlanRouting(plan) {
308
+ if (plan.transitions.length > 0) {
309
+ throw new WorkflowPlanAdapterError("unsupported_routing", "conditional workflow transitions cannot be represented by the Foundation Controller dependency contract");
310
+ }
311
+ if (plan.retry.retry_from !== null) {
312
+ throw new WorkflowPlanAdapterError("unsupported_routing", "workflow retry_from cannot be represented by per-Step Foundation retries");
313
+ }
314
+ for (const step of plan.steps) {
315
+ if (step.optional) {
316
+ throw new WorkflowPlanAdapterError("unsupported_routing", `step '${step.id}' is optional, which the Foundation Controller cannot preserve on failure`, step.id);
317
+ }
318
+ if (step.run_if !== null && plan.retry.max_retries > 0) {
319
+ throw new WorkflowPlanAdapterError("unsupported_routing", `step '${step.id}' uses run_if across retry rounds, which the Foundation Controller cannot evaluate`, step.id);
320
+ }
321
+ if (step.on_fail === "handoff"
322
+ || step.on_fail === "skip"
323
+ || step.handoff_to !== undefined
324
+ || step.max_handoffs !== undefined
325
+ || step.on_handoff_exhausted !== undefined) {
326
+ throw new WorkflowPlanAdapterError("unsupported_routing", `step '${step.id}' uses legacy failure routing not represented by the Foundation Controller`, step.id);
327
+ }
328
+ }
329
+ }
330
+ function runsInSingleRound(condition) {
331
+ if (condition === null)
332
+ return true;
333
+ const equal = condition.match(/^round\s*==\s*(\d+)$/);
334
+ if (equal)
335
+ return Number(equal[1]) === 1;
336
+ const greater = condition.match(/^round\s*>\s*(\d+)$/);
337
+ if (greater)
338
+ return 1 > Number(greater[1]);
339
+ const greaterOrEqual = condition.match(/^round\s*>=\s*(\d+)$/);
340
+ if (greaterOrEqual)
341
+ return 1 >= Number(greaterOrEqual[1]);
342
+ if (condition === "retry")
343
+ return false;
344
+ return true;
345
+ }
346
+ /**
347
+ * Adapt one compiled workflow plan into Controller definitions. Every
348
+ * materializer creates a self-contained packet from Controller-resolved inputs;
349
+ * no provider transcript or prior-session state is accepted by this API.
350
+ */
351
+ export function adaptWorkflowPlan(plan, options) {
352
+ validatePlanRouting(plan);
353
+ if (!options.working_directory) {
354
+ throw new WorkflowPlanAdapterError("invalid_plan", "working_directory must be non-empty");
355
+ }
356
+ const cancellationGrace = options.cancellation_grace_ms ?? 5_000;
357
+ requireNonNegativeInteger(cancellationGrace, "cancellation_grace_ms");
358
+ if (options.timeout_ms !== undefined && options.timeout_ms !== null) {
359
+ requireNonNegativeInteger(options.timeout_ms, "timeout_ms");
360
+ }
361
+ const variables = options.variables ?? {};
362
+ const stepById = new Map(plan.steps.map((step) => [step.id, step]));
363
+ const adapted = plan.steps.map((step) => {
364
+ const role = options.roles[step.role];
365
+ if (!role) {
366
+ throw new WorkflowPlanAdapterError("missing_role", `step '${step.id}' references missing role '${step.role}'`, step.id);
367
+ }
368
+ const { handoffs, external_inputs } = createHandoffs(step, stepById, variables);
369
+ return {
370
+ step,
371
+ role,
372
+ constraints: collectConstraints(step, role, variables),
373
+ handoffs,
374
+ external_inputs,
375
+ outputs: createOutputContract(step, variables),
376
+ isolation: isolationForStep(plan.parallel_groups, step.id),
377
+ retry: retryPolicy(plan, step, options),
378
+ };
379
+ });
380
+ return adapted.map((entry) => ({
381
+ step_id: entry.step.id,
382
+ dependencies: addHandoffDependencies(entry.step, entry.handoffs),
383
+ initially_skipped: !runsInSingleRound(entry.step.run_if),
384
+ retry_policy: entry.retry,
385
+ handoffs: entry.handoffs,
386
+ materialize_packet(input) {
387
+ const workingDirectory = options.working_directory_for_step?.(entry.step, input) ?? options.working_directory;
388
+ if (!workingDirectory) {
389
+ throw new WorkflowPlanAdapterError("invalid_plan", `step '${entry.step.id}' resolved an empty working directory`, entry.step.id);
390
+ }
391
+ return {
392
+ packet_version: 1,
393
+ run_id: input.run_id,
394
+ step_id: input.step_id,
395
+ attempt_id: input.attempt_id,
396
+ worker_session_id: input.worker_session_id,
397
+ attempt_number: input.attempt_number,
398
+ role: entry.step.role,
399
+ constraints: entry.constraints,
400
+ standalone_prompt: standalonePrompt(plan, entry, input.input_bindings, variables),
401
+ input_bindings: input.input_bindings,
402
+ output_contract: entry.outputs,
403
+ workspace: {
404
+ isolation: entry.isolation,
405
+ working_directory: workingDirectory,
406
+ },
407
+ execution: {
408
+ timeout_ms: options.timeout_ms ?? null,
409
+ cancellation_grace_ms: cancellationGrace,
410
+ retry: input.retry_policy,
411
+ },
412
+ created_at: input.created_at,
413
+ };
414
+ },
415
+ }));
416
+ }