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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -0
- package/dist/cli/commands/run-lifecycle.d.ts +73 -0
- package/dist/cli/commands/run-lifecycle.js +240 -0
- package/dist/cli/commands/run.d.ts +22 -40
- package/dist/cli/commands/run.js +674 -392
- package/dist/cli/index.js +87 -2
- package/dist/compiler/workflow.js +3 -2
- package/dist/hooks/cli.d.ts +1 -2
- package/dist/hooks/cli.js +119 -80
- package/dist/hooks/enforce.d.ts +2 -0
- package/dist/hooks/enforce.js +56 -27
- package/dist/hooks/enforcement-boundary.d.ts +13 -0
- package/dist/hooks/enforcement-boundary.js +33 -0
- package/dist/hooks/index.d.ts +3 -2
- package/dist/hooks/index.js +3 -2
- package/dist/hooks/protocol.d.ts +12 -4
- package/dist/hooks/protocol.js +20 -14
- package/dist/hooks/schema.d.ts +2 -1
- package/dist/hooks/schema.js +6 -2
- package/dist/hooks/state-manager.d.ts +5 -5
- package/dist/hooks/state-manager.js +26 -24
- package/dist/hooks/state.d.ts +19 -3
- package/dist/hooks/state.js +327 -80
- package/dist/mcp/index.js +0 -0
- package/dist/runtime/diagnosis-contract-verifier.d.ts +11 -0
- package/dist/runtime/diagnosis-contract-verifier.js +417 -0
- package/dist/runtime/execution-provider.d.ts +40 -0
- package/dist/runtime/execution-provider.js +138 -0
- package/dist/runtime/handoff-resolver.d.ts +61 -0
- package/dist/runtime/handoff-resolver.js +167 -0
- package/dist/runtime/index.d.ts +24 -0
- package/dist/runtime/index.js +13 -0
- package/dist/runtime/process-tree.d.ts +47 -0
- package/dist/runtime/process-tree.js +402 -0
- package/dist/runtime/providers/claude.d.ts +9 -0
- package/dist/runtime/providers/claude.js +64 -0
- package/dist/runtime/providers/codex.d.ts +8 -0
- package/dist/runtime/providers/codex.js +72 -0
- package/dist/runtime/result-store.d.ts +32 -0
- package/dist/runtime/result-store.js +130 -0
- package/dist/runtime/run-contracts.d.ts +290 -0
- package/dist/runtime/run-contracts.js +58 -0
- package/dist/runtime/run-controller.d.ts +149 -0
- package/dist/runtime/run-controller.js +1108 -0
- package/dist/runtime/run-store.d.ts +96 -0
- package/dist/runtime/run-store.js +725 -0
- package/dist/runtime/worker-executor.d.ts +19 -0
- package/dist/runtime/worker-executor.js +194 -0
- package/dist/runtime/workflow-plan-adapter.d.ts +26 -0
- package/dist/runtime/workflow-plan-adapter.js +416 -0
- package/dist/runtime/workflow-runner.d.ts +15 -3
- package/dist/runtime/workflow-runner.js +13 -1
- package/dist/runtime/workspace-isolation.d.ts +103 -0
- package/dist/runtime/workspace-isolation.js +373 -0
- package/dist/schema/types.d.ts +1 -0
- package/dist/schema/validate.js +64 -6
- package/dist/schema/yaml-parser.js +7 -2
- package/package.json +1 -1
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,481 +1,763 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Intent DNA
|
|
2
|
+
* Intent DNA - canonical dna run command.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The CLI compiles one workflow into Controller definitions, persists one
|
|
5
|
+
* durable run ledger, and launches one disposable provider session per Step
|
|
6
|
+
* attempt. Cross-Step inputs come only from committed results.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { join, resolve } from "node:path";
|
|
11
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
-
import { loadDNA, compileFromFiles, expandDNAInputFiles } from "../../compiler/index.js";
|
|
8
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
9
|
+
import { isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
13
10
|
import { cascadeDNA } from "../../compiler/cascade.js";
|
|
11
|
+
import { compileFromFiles, expandDNAInputFiles, loadDNA, } from "../../compiler/index.js";
|
|
14
12
|
import { compileWorkflow } from "../../compiler/workflow.js";
|
|
15
|
-
import { compileAllRolesToAgentMD, writeAgentMDFiles, toKebabCase, } from "../../runtime/agent-md.js";
|
|
16
|
-
import { createCompiledIR, writeCompiledIR } from "../../runtime/plugin-adapter.js";
|
|
17
13
|
import { trustedEvidenceCaptureAttribution } from "../../governance/index.js";
|
|
18
14
|
import { DNAStateManager } from "../../hooks/state-manager.js";
|
|
19
15
|
import { appendEvidenceCaptureEvent, EVIDENCE_CAPTURE_SCHEMA_VERSION, } from "../../hooks/state.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
import { compileAllRolesToAgentMD, toKebabCase, writeAgentMDFiles, } from "../../runtime/agent-md.js";
|
|
17
|
+
import { HandoffResolver } from "../../runtime/handoff-resolver.js";
|
|
18
|
+
import { createCompiledIR, writeCompiledIR } from "../../runtime/plugin-adapter.js";
|
|
19
|
+
import { ImmutableResultStore } from "../../runtime/result-store.js";
|
|
20
|
+
import { RunController, } from "../../runtime/run-controller.js";
|
|
21
|
+
import { DurableRunStore, RunStoreError, } from "../../runtime/run-store.js";
|
|
22
|
+
import { createClaudeExecutionProvider } from "../../runtime/providers/claude.js";
|
|
23
|
+
import { createCodexExecutionProvider } from "../../runtime/providers/codex.js";
|
|
24
|
+
import { runCheckpointVerifier, runCompletionVerifier, } from "../../runtime/verifier.js";
|
|
25
|
+
import { adaptWorkflowPlan, } from "../../runtime/workflow-plan-adapter.js";
|
|
26
|
+
import { executeWorkerAttempt, } from "../../runtime/worker-executor.js";
|
|
27
|
+
import { allocateAttemptWorkspace, applyWorkspaceLifecycleDecision, planAttemptWorkspace, } from "../../runtime/workspace-isolation.js";
|
|
28
|
+
import { runLifecycleCancel, runLifecycleInspect, runLifecycleResume, runLifecycleStart, runLifecycleStatus, } from "./run-lifecycle.js";
|
|
29
|
+
class RunUsageError extends Error {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "RunUsageError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ── Compatibility-only pure helpers ───────────────────────
|
|
26
36
|
export function substituteTemplate(template, vars) {
|
|
27
|
-
return template.replace(/\{\{(\w+)\}\}/g, (match, key) =>
|
|
28
|
-
return key in vars ? vars[key] : match;
|
|
29
|
-
});
|
|
37
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => (key in vars ? vars[key] : match));
|
|
30
38
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Evaluate a run_if condition against the current round.
|
|
33
|
-
* Returns true if the step should execute.
|
|
34
|
-
*/
|
|
35
39
|
export function evaluateRunIf(condition, round) {
|
|
36
40
|
if (condition === null)
|
|
37
41
|
return true;
|
|
38
|
-
// "round == N"
|
|
39
42
|
const eqMatch = condition.match(/^round\s*==\s*(\d+)$/);
|
|
40
43
|
if (eqMatch)
|
|
41
44
|
return round === parseInt(eqMatch[1], 10);
|
|
42
|
-
// "round > N"
|
|
43
45
|
const gtMatch = condition.match(/^round\s*>\s*(\d+)$/);
|
|
44
46
|
if (gtMatch)
|
|
45
47
|
return round > parseInt(gtMatch[1], 10);
|
|
46
|
-
// "round >= N"
|
|
47
48
|
const geMatch = condition.match(/^round\s*>=\s*(\d+)$/);
|
|
48
49
|
if (geMatch)
|
|
49
50
|
return round >= parseInt(geMatch[1], 10);
|
|
50
|
-
// "retry" = round > 1
|
|
51
51
|
if (condition === "retry")
|
|
52
52
|
return round > 1;
|
|
53
|
-
// "always" or unknown → run
|
|
54
53
|
return true;
|
|
55
54
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!step.completion)
|
|
62
|
-
return true;
|
|
63
|
-
for (const completion of step.completion) {
|
|
64
|
-
const result = await runCompletionVerifier(completion, opts.ir, {
|
|
65
|
-
projectDir: opts.projectDir,
|
|
66
|
-
variables: vars,
|
|
67
|
-
});
|
|
68
|
-
if (!result.passed) {
|
|
69
|
-
logVerifierFailure("Completion failed", step.id, result);
|
|
70
|
-
return false;
|
|
55
|
+
export function checkRetryNeeded(stepResults, transitions) {
|
|
56
|
+
for (const transition of transitions) {
|
|
57
|
+
if (transition.condition === "fail"
|
|
58
|
+
&& stepResults.get(transition.from)?.status === "fail") {
|
|
59
|
+
return true;
|
|
71
60
|
}
|
|
72
61
|
}
|
|
73
|
-
return
|
|
62
|
+
return false;
|
|
74
63
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
64
|
+
// ── Validation and durable metadata ───────────────────────
|
|
65
|
+
function requireText(value, label) {
|
|
66
|
+
const trimmed = value?.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
throw new RunUsageError(`${label} is required`);
|
|
69
|
+
return trimmed;
|
|
70
|
+
}
|
|
71
|
+
function requirePositiveNumber(value, label) {
|
|
72
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
73
|
+
throw new RunUsageError(`${label} must be a positive number`);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
function requirePositiveInteger(value, label) {
|
|
78
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
79
|
+
throw new RunUsageError(`${label} must be a positive integer`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function requireNonNegativeInteger(value, label) {
|
|
84
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
85
|
+
throw new RunUsageError(`${label} must be a non-negative integer`);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function asRunId(value) {
|
|
90
|
+
return requireText(value, "run_id");
|
|
91
|
+
}
|
|
92
|
+
function isRecord(value) {
|
|
93
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
94
|
+
}
|
|
95
|
+
function readStringRecord(value, label) {
|
|
96
|
+
if (!isRecord(value))
|
|
97
|
+
throw new Error(`${label} must be an object`);
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
100
|
+
if (typeof entry !== "string") {
|
|
101
|
+
throw new Error(`${label}.${key} must be a string`);
|
|
102
|
+
}
|
|
103
|
+
result[key] = entry;
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
function readCanonicalMetadata(value) {
|
|
108
|
+
if (!isRecord(value) || value.schema_version !== "intentdna.canonical_run.v1") {
|
|
109
|
+
throw new Error("run metadata is not intentdna.canonical_run.v1");
|
|
110
|
+
}
|
|
111
|
+
const dnaFiles = value.dna_files;
|
|
112
|
+
const context = value.context;
|
|
113
|
+
const provider = value.provider;
|
|
114
|
+
const timeoutMs = value.timeout_ms;
|
|
115
|
+
const workflowAsset = value.workflow_asset;
|
|
116
|
+
if (typeof value.project_directory !== "string"
|
|
117
|
+
|| !Array.isArray(dnaFiles)
|
|
118
|
+
|| !dnaFiles.every((entry) => typeof entry === "string")
|
|
119
|
+
|| typeof value.workflow_name !== "string"
|
|
120
|
+
|| typeof value.task_id !== "string"
|
|
121
|
+
|| (context !== null && typeof context !== "string")
|
|
122
|
+
|| typeof value.agents_directory !== "string"
|
|
123
|
+
|| !isRecord(provider)
|
|
124
|
+
|| !["claude", "codex"].includes(String(provider.kind))
|
|
125
|
+
|| (provider.executable !== null && typeof provider.executable !== "string")
|
|
126
|
+
|| typeof provider.max_budget_usd !== "number"
|
|
127
|
+
|| typeof provider.permission_mode !== "string"
|
|
128
|
+
|| typeof value.max_concurrency !== "number"
|
|
129
|
+
|| (timeoutMs !== null && typeof timeoutMs !== "number")
|
|
130
|
+
|| typeof value.cancellation_grace_ms !== "number"
|
|
131
|
+
|| (workflowAsset !== null && typeof workflowAsset !== "string")) {
|
|
132
|
+
throw new Error("run metadata is malformed");
|
|
133
|
+
}
|
|
134
|
+
const metadata = {
|
|
135
|
+
schema_version: "intentdna.canonical_run.v1",
|
|
136
|
+
project_directory: value.project_directory,
|
|
137
|
+
dna_files: dnaFiles,
|
|
138
|
+
workflow_name: value.workflow_name,
|
|
139
|
+
task_id: value.task_id,
|
|
140
|
+
context,
|
|
141
|
+
variables: readStringRecord(value.variables, "run metadata variables"),
|
|
142
|
+
agents_directory: value.agents_directory,
|
|
143
|
+
provider: {
|
|
144
|
+
kind: provider.kind,
|
|
145
|
+
executable: provider.executable === null
|
|
146
|
+
? null
|
|
147
|
+
: requireText(provider.executable, "run metadata provider.executable"),
|
|
148
|
+
max_budget_usd: requirePositiveNumber(provider.max_budget_usd, "run metadata provider.max_budget_usd"),
|
|
149
|
+
permission_mode: requireText(provider.permission_mode, "run metadata provider.permission_mode"),
|
|
150
|
+
},
|
|
151
|
+
max_concurrency: requirePositiveInteger(value.max_concurrency, "run metadata max_concurrency"),
|
|
152
|
+
timeout_ms: timeoutMs === null
|
|
153
|
+
? null
|
|
154
|
+
: requireNonNegativeInteger(timeoutMs, "run metadata timeout_ms"),
|
|
155
|
+
cancellation_grace_ms: requireNonNegativeInteger(value.cancellation_grace_ms, "run metadata cancellation_grace_ms"),
|
|
156
|
+
workflow_asset: workflowAsset,
|
|
157
|
+
};
|
|
158
|
+
requireText(metadata.project_directory, "run metadata project_directory");
|
|
159
|
+
requireText(metadata.workflow_name, "run metadata workflow_name");
|
|
160
|
+
requireText(metadata.task_id, "run metadata task_id");
|
|
161
|
+
requireText(metadata.agents_directory, "run metadata agents_directory");
|
|
162
|
+
if (metadata.dna_files.length === 0) {
|
|
163
|
+
throw new Error("run metadata dna_files must not be empty");
|
|
164
|
+
}
|
|
165
|
+
return metadata;
|
|
166
|
+
}
|
|
167
|
+
function stableJson(value) {
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
170
|
+
}
|
|
171
|
+
if (value !== null && typeof value === "object") {
|
|
172
|
+
const entries = Object.entries(value)
|
|
173
|
+
.filter(([, entry]) => entry !== undefined)
|
|
174
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
175
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`).join(",")}}`;
|
|
176
|
+
}
|
|
177
|
+
return JSON.stringify(value) ?? "null";
|
|
178
|
+
}
|
|
179
|
+
function planDigest(plan, roles) {
|
|
180
|
+
const stablePlan = { ...plan, compiled_at: null };
|
|
181
|
+
return createHash("sha256")
|
|
182
|
+
.update(stableJson({ plan: stablePlan, roles }), "utf-8")
|
|
183
|
+
.digest("hex");
|
|
184
|
+
}
|
|
185
|
+
function runtimeRoot(projectDirectory) {
|
|
186
|
+
return join(projectDirectory, ".dna", "runtime");
|
|
187
|
+
}
|
|
188
|
+
// ── Compilation and provider construction ─────────────────
|
|
189
|
+
async function compileRunAssets(metadata) {
|
|
190
|
+
const dnas = [];
|
|
191
|
+
for (const file of metadata.dna_files)
|
|
192
|
+
dnas.push(await loadDNA(file));
|
|
193
|
+
const cascaded = cascadeDNA(dnas);
|
|
194
|
+
const ir = await compileFromFiles([...metadata.dna_files], {
|
|
195
|
+
context: metadata.context ?? undefined,
|
|
196
|
+
});
|
|
197
|
+
const definition = cascaded.workflows[metadata.workflow_name];
|
|
198
|
+
if (!definition) {
|
|
199
|
+
throw new Error(`workflow '${metadata.workflow_name}' not found; available: `
|
|
200
|
+
+ `${Object.keys(cascaded.workflows).join(", ") || "(none)"}`);
|
|
201
|
+
}
|
|
202
|
+
const result = compileWorkflow(definition, {
|
|
203
|
+
workflow_key: metadata.workflow_name,
|
|
204
|
+
});
|
|
205
|
+
if (!result.ok || !result.plan) {
|
|
206
|
+
throw new Error("workflow compilation failed:\n"
|
|
207
|
+
+ result.errors.map((error) => ` ${error.path}: ${error.message}`).join("\n"));
|
|
208
|
+
}
|
|
209
|
+
const plan = result.plan;
|
|
210
|
+
const attribution = trustedEvidenceCaptureAttribution(ir, {
|
|
211
|
+
workflowId: metadata.workflow_name,
|
|
212
|
+
});
|
|
213
|
+
const workflowAsset = attribution.workflowAsset === "unknown"
|
|
214
|
+
? null
|
|
215
|
+
: attribution.workflowAsset;
|
|
216
|
+
if (metadata.workflow_asset !== null
|
|
217
|
+
&& workflowAsset !== metadata.workflow_asset) {
|
|
218
|
+
throw new Error(`workflow asset changed from '${metadata.workflow_asset}' to '${workflowAsset ?? "unknown"}'`);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
plan,
|
|
222
|
+
roles: cascaded.roles,
|
|
223
|
+
ir,
|
|
224
|
+
plan_digest: planDigest(plan, cascaded.roles),
|
|
225
|
+
workflow_asset: workflowAsset,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function compileCanonicalRun(metadata) {
|
|
229
|
+
const assets = await compileRunAssets(metadata);
|
|
230
|
+
const adapterOptions = {
|
|
231
|
+
roles: assets.roles,
|
|
232
|
+
variables: metadata.variables,
|
|
233
|
+
working_directory: metadata.project_directory,
|
|
234
|
+
timeout_ms: metadata.timeout_ms,
|
|
235
|
+
cancellation_grace_ms: metadata.cancellation_grace_ms,
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
...assets,
|
|
239
|
+
definitions: adaptWorkflowPlan(assets.plan, adapterOptions),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function syncRuntimeArtifacts(compiled, metadata) {
|
|
243
|
+
const agents = compileAllRolesToAgentMD(compiled.roles, compiled.ir);
|
|
244
|
+
await writeAgentMDFiles(agents, metadata.agents_directory);
|
|
245
|
+
await writeCompiledIR(metadata.project_directory, createCompiledIR(compiled.ir, compiled.roles));
|
|
246
|
+
}
|
|
247
|
+
function projectPathInWorkspace(metadata, workspaceDirectory, projectPath) {
|
|
248
|
+
const projectRelative = relative(metadata.project_directory, projectPath);
|
|
249
|
+
const outsideProject = (projectRelative === ".."
|
|
250
|
+
|| projectRelative.startsWith(`..${sep}`)
|
|
251
|
+
|| isAbsolute(projectRelative));
|
|
252
|
+
return outsideProject
|
|
253
|
+
? projectPath
|
|
254
|
+
: resolve(workspaceDirectory, projectRelative);
|
|
255
|
+
}
|
|
256
|
+
async function syncAttemptRuntimeArtifacts(compiled, metadata, workspace) {
|
|
257
|
+
if (workspace.isolation !== "worktree")
|
|
258
|
+
return;
|
|
259
|
+
const agentsDirectory = projectPathInWorkspace(metadata, workspace.working_directory, metadata.agents_directory);
|
|
260
|
+
if (agentsDirectory !== metadata.agents_directory) {
|
|
261
|
+
const agents = compileAllRolesToAgentMD(compiled.roles, compiled.ir);
|
|
262
|
+
await writeAgentMDFiles(agents, agentsDirectory);
|
|
263
|
+
}
|
|
264
|
+
await writeCompiledIR(workspace.working_directory, createCompiledIR(compiled.ir, compiled.roles));
|
|
265
|
+
}
|
|
266
|
+
function createProvider(metadata) {
|
|
267
|
+
if (metadata.provider.kind === "codex") {
|
|
268
|
+
return createCodexExecutionProvider({
|
|
269
|
+
executable: metadata.provider.executable ?? undefined,
|
|
83
270
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
271
|
+
}
|
|
272
|
+
return createClaudeExecutionProvider({
|
|
273
|
+
executable: metadata.provider.executable ?? undefined,
|
|
274
|
+
agentName: (packet) => `dna-${toKebabCase(packet.role)}`,
|
|
275
|
+
extraArgs: [
|
|
276
|
+
"--max-budget-usd",
|
|
277
|
+
String(metadata.provider.max_budget_usd),
|
|
278
|
+
"--permission-mode",
|
|
279
|
+
metadata.provider.permission_mode,
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function preservationReason(packet) {
|
|
284
|
+
return packet.workspace.isolation === "worktree"
|
|
285
|
+
? "integration_pending"
|
|
286
|
+
: "controller_policy";
|
|
287
|
+
}
|
|
288
|
+
function preserveWorkspace(workspace, reason, detail) {
|
|
289
|
+
const result = applyWorkspaceLifecycleDecision(workspace, {
|
|
290
|
+
action: "preserve",
|
|
291
|
+
reason,
|
|
292
|
+
detail,
|
|
293
|
+
});
|
|
294
|
+
if (result.status === "preserved") {
|
|
295
|
+
log(`Preserved worktree ${result.working_directory}: `
|
|
296
|
+
+ `${result.reason}${result.detail ? ` (${result.detail})` : ""}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function recordAttemptProcess(runStore, packet, processId) {
|
|
300
|
+
if (!Number.isSafeInteger(processId) || processId <= 0) {
|
|
301
|
+
throw new Error(`provider returned invalid process id ${processId}`);
|
|
302
|
+
}
|
|
303
|
+
while (true) {
|
|
304
|
+
const snapshot = await runStore.requireRun(packet.run_id);
|
|
305
|
+
const attempt = snapshot.attempts.find((entry) => entry.attempt_id === packet.attempt_id);
|
|
306
|
+
if (!attempt
|
|
307
|
+
|| attempt.step_id !== packet.step_id
|
|
308
|
+
|| attempt.worker_session_id !== packet.worker_session_id) {
|
|
309
|
+
throw new Error(`attempt ${packet.attempt_id} disappeared before process registration`);
|
|
310
|
+
}
|
|
311
|
+
const recordedProcessId = attempt.process_id ?? null;
|
|
312
|
+
if (recordedProcessId === processId || attempt.phase === "terminal")
|
|
313
|
+
return;
|
|
314
|
+
if (recordedProcessId !== null) {
|
|
315
|
+
throw new Error(`attempt ${packet.attempt_id} already owns process ${recordedProcessId}`);
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
await runStore.updateRun({
|
|
319
|
+
run_id: packet.run_id,
|
|
320
|
+
expected_record_version: snapshot.record_version,
|
|
321
|
+
attempts: [{ ...attempt, process_id: processId }],
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (error instanceof RunStoreError && error.code === "version_conflict") {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
88
330
|
}
|
|
89
331
|
}
|
|
90
|
-
return true;
|
|
91
332
|
}
|
|
92
|
-
|
|
93
|
-
const
|
|
333
|
+
const reconcileLocalProcess = async (attempt) => {
|
|
334
|
+
const processId = attempt.process_id ?? null;
|
|
335
|
+
if (processId === null
|
|
336
|
+
|| !Number.isSafeInteger(processId)
|
|
337
|
+
|| processId <= 0) {
|
|
338
|
+
return "unknown";
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
process.kill(processId, 0);
|
|
342
|
+
return "live";
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
const code = error instanceof Error
|
|
346
|
+
? error.code
|
|
347
|
+
: undefined;
|
|
348
|
+
if (code === "EPERM")
|
|
349
|
+
return "live";
|
|
350
|
+
if (code === "ESRCH")
|
|
351
|
+
return "dead";
|
|
352
|
+
return "unknown";
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
function verifierFailureMessage(prefix, stepId, result) {
|
|
356
|
+
const exitCode = result.exit_code === undefined
|
|
357
|
+
? ""
|
|
358
|
+
: ` exit_code=${result.exit_code}`;
|
|
94
359
|
const target = result.target ? ` target=${result.target}` : "";
|
|
95
|
-
const message = result.message ? `
|
|
96
|
-
|
|
360
|
+
const message = result.message ? ` - ${result.message}` : "";
|
|
361
|
+
const detail = `${prefix}${target}${exitCode}${message}`;
|
|
362
|
+
log(`${detail} (step: ${stepId})`);
|
|
363
|
+
return detail;
|
|
364
|
+
}
|
|
365
|
+
async function applyStepValidation(metadata, compiled, packet, execution) {
|
|
366
|
+
if (execution.result.outcome.kind !== "success")
|
|
367
|
+
return execution;
|
|
368
|
+
const step = compiled.plan.steps.find((entry) => entry.id === packet.step_id);
|
|
369
|
+
if (!step) {
|
|
370
|
+
throw new Error(`compiled Step ${packet.step_id} disappeared before validation`);
|
|
371
|
+
}
|
|
372
|
+
const variables = {
|
|
373
|
+
...metadata.variables,
|
|
374
|
+
round: String(packet.attempt_number),
|
|
375
|
+
};
|
|
376
|
+
let failure = null;
|
|
377
|
+
for (const completion of step.completion ?? []) {
|
|
378
|
+
const result = await runCompletionVerifier(completion, compiled.ir, {
|
|
379
|
+
projectDir: packet.workspace.working_directory,
|
|
380
|
+
variables,
|
|
381
|
+
});
|
|
382
|
+
if (!result.passed) {
|
|
383
|
+
failure = verifierFailureMessage("Completion failed", step.id, result);
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!failure) {
|
|
388
|
+
for (const checkpoint of step.checkpoints ?? []) {
|
|
389
|
+
const result = await runCheckpointVerifier(checkpoint, compiled.ir, {
|
|
390
|
+
projectDir: packet.workspace.working_directory,
|
|
391
|
+
variables,
|
|
392
|
+
});
|
|
393
|
+
if (result.passed)
|
|
394
|
+
continue;
|
|
395
|
+
const detail = verifierFailureMessage(`Checkpoint failed: ${checkpoint.assert}`, step.id, result);
|
|
396
|
+
if ((checkpoint.action ?? "block") === "block") {
|
|
397
|
+
failure = detail;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!failure)
|
|
403
|
+
return execution;
|
|
404
|
+
return {
|
|
405
|
+
...execution,
|
|
406
|
+
result: {
|
|
407
|
+
...execution.result,
|
|
408
|
+
outputs: [],
|
|
409
|
+
outcome: {
|
|
410
|
+
kind: "malformed_result",
|
|
411
|
+
exit_code: 0,
|
|
412
|
+
signal: null,
|
|
413
|
+
error: failure,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
97
417
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
418
|
+
async function recordStepResultEvidence(metadata, packet, execution) {
|
|
419
|
+
const outcome = execution.result.outcome;
|
|
420
|
+
const succeeded = outcome.kind === "success";
|
|
421
|
+
const startedAt = Date.parse(execution.result.started_at);
|
|
422
|
+
const completedAt = Date.parse(execution.result.completed_at);
|
|
423
|
+
await appendEvidenceCaptureEvent(metadata.project_directory, {
|
|
102
424
|
schema_version: EVIDENCE_CAPTURE_SCHEMA_VERSION,
|
|
103
425
|
event_id: `c5:${randomUUID()}`,
|
|
104
|
-
captured_at:
|
|
105
|
-
project_root:
|
|
426
|
+
captured_at: execution.result.completed_at,
|
|
427
|
+
project_root: metadata.project_directory,
|
|
106
428
|
source: "dna_cli",
|
|
107
|
-
workflow_asset:
|
|
108
|
-
workflow_id:
|
|
109
|
-
run_id:
|
|
110
|
-
step_id:
|
|
429
|
+
workflow_asset: metadata.workflow_asset ?? "unknown",
|
|
430
|
+
workflow_id: metadata.workflow_name,
|
|
431
|
+
run_id: packet.run_id,
|
|
432
|
+
step_id: packet.step_id,
|
|
111
433
|
fact: "result",
|
|
112
|
-
summary: `dna run recorded ${
|
|
113
|
-
result,
|
|
114
|
-
|
|
434
|
+
summary: `dna run recorded ${succeeded ? "success" : "failure"} for workflow step ${packet.step_id}`,
|
|
435
|
+
result: succeeded ? "success" : "failure",
|
|
436
|
+
exit_code: "exit_code" in outcome
|
|
437
|
+
? outcome.exit_code ?? undefined
|
|
438
|
+
: undefined,
|
|
439
|
+
duration_ms: Number.isFinite(startedAt) && Number.isFinite(completedAt)
|
|
440
|
+
? Math.max(0, completedAt - startedAt)
|
|
441
|
+
: undefined,
|
|
115
442
|
});
|
|
116
443
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"--max-budget-usd",
|
|
136
|
-
String(opts.maxBudget),
|
|
137
|
-
"--permission-mode",
|
|
138
|
-
opts.permissionMode,
|
|
139
|
-
"--session-id",
|
|
140
|
-
sessionId,
|
|
141
|
-
"--no-session-persistence",
|
|
142
|
-
];
|
|
143
|
-
log(`Running step: ${step.id} (agent: ${agentName})`);
|
|
144
|
-
try {
|
|
444
|
+
function createWorkspaceAwareExecutor(metadata, compiled, runStore) {
|
|
445
|
+
return async (packet, provider, options) => {
|
|
446
|
+
const workspace = allocateAttemptWorkspace(planAttemptWorkspace({
|
|
447
|
+
isolation: packet.workspace.isolation,
|
|
448
|
+
repository_directory: metadata.project_directory,
|
|
449
|
+
run_id: packet.run_id,
|
|
450
|
+
step_id: packet.step_id,
|
|
451
|
+
attempt_id: packet.attempt_id,
|
|
452
|
+
}));
|
|
453
|
+
const workerPacket = {
|
|
454
|
+
...packet,
|
|
455
|
+
workspace: {
|
|
456
|
+
isolation: workspace.isolation,
|
|
457
|
+
working_directory: workspace.working_directory,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
const state = new DNAStateManager(workspace.working_directory, packet.worker_session_id);
|
|
461
|
+
let lifecycleRecorded = false;
|
|
145
462
|
try {
|
|
146
|
-
await
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
463
|
+
await syncAttemptRuntimeArtifacts(compiled, metadata, workspace);
|
|
464
|
+
try {
|
|
465
|
+
await state.writeWorkflowState({
|
|
466
|
+
active: true,
|
|
467
|
+
workflow_asset: metadata.workflow_asset ?? undefined,
|
|
468
|
+
workflow: metadata.workflow_name,
|
|
469
|
+
current_step: packet.step_id,
|
|
470
|
+
current_role: packet.role,
|
|
471
|
+
iteration: packet.attempt_number,
|
|
472
|
+
session_id: packet.worker_session_id,
|
|
473
|
+
worker_session_id: packet.worker_session_id,
|
|
474
|
+
run_id: packet.run_id,
|
|
475
|
+
step_id: packet.step_id,
|
|
476
|
+
attempt_id: packet.attempt_id,
|
|
477
|
+
started_at: packet.created_at,
|
|
478
|
+
inputs: { ...metadata.variables },
|
|
479
|
+
resolved_variables: {
|
|
480
|
+
...metadata.variables,
|
|
481
|
+
round: String(packet.attempt_number),
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
log(`Workflow evidence state unavailable for ${packet.step_id}; `
|
|
487
|
+
+ `continuing fail-open: ${error instanceof Error ? error.message : String(error)}`);
|
|
488
|
+
}
|
|
489
|
+
const inheritedProcessObserver = options?.onProcessStart;
|
|
490
|
+
let execution = await executeWorkerAttempt(workerPacket, provider, {
|
|
491
|
+
...options,
|
|
492
|
+
onProcessStart: async (processId) => {
|
|
493
|
+
await recordAttemptProcess(runStore, workerPacket, processId);
|
|
494
|
+
await inheritedProcessObserver?.(processId);
|
|
495
|
+
},
|
|
157
496
|
});
|
|
158
|
-
|
|
497
|
+
execution = await applyStepValidation(metadata, compiled, workerPacket, execution);
|
|
498
|
+
await recordStepResultEvidence(metadata, workerPacket, execution);
|
|
499
|
+
const reason = execution.result.outcome.kind === "cancelled"
|
|
500
|
+
? "attempt_cancelled"
|
|
501
|
+
: execution.result.outcome.kind === "success"
|
|
502
|
+
? preservationReason(workerPacket)
|
|
503
|
+
: "attempt_failed";
|
|
504
|
+
preserveWorkspace(workspace, reason, `attempt outcome: ${execution.result.outcome.kind}`);
|
|
505
|
+
lifecycleRecorded = true;
|
|
506
|
+
return execution;
|
|
159
507
|
}
|
|
160
508
|
catch (error) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const output = await spawnAsync("claude", args, prompt, opts.projectDir);
|
|
164
|
-
await writeFile(resultFile, output, "utf-8");
|
|
165
|
-
// Check for error in JSON output
|
|
166
|
-
let isError = false;
|
|
167
|
-
try {
|
|
168
|
-
const parsed = JSON.parse(output);
|
|
169
|
-
isError = parsed.is_error === true;
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
// Non-JSON output → treat as success
|
|
173
|
-
}
|
|
174
|
-
const status = isError ? "fail" : "pass";
|
|
175
|
-
if (status === "pass" && step.completion && step.completion.length > 0) {
|
|
176
|
-
const completionsPassed = await runCompletions(step, vars, opts);
|
|
177
|
-
if (!completionsPassed) {
|
|
178
|
-
const durationMs = Date.now() - start;
|
|
179
|
-
await recordStepResultEvidence(step, opts, sessionId, "fail", durationMs);
|
|
180
|
-
log(`Step ${step.id} COMPLETION_FAIL (${durationMs}ms)`);
|
|
181
|
-
return { stepId: step.id, status: "fail", durationMs, resultFile };
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (status === "pass" && step.checkpoints && step.checkpoints.length > 0) {
|
|
185
|
-
const checkpointsPassed = await runCheckpoints(step, vars, opts);
|
|
186
|
-
if (!checkpointsPassed) {
|
|
187
|
-
const durationMs = Date.now() - start;
|
|
188
|
-
await recordStepResultEvidence(step, opts, sessionId, "fail", durationMs);
|
|
189
|
-
log(`Step ${step.id} CHECKPOINT_FAIL (${durationMs}ms)`);
|
|
190
|
-
return { stepId: step.id, status: "fail", durationMs, resultFile };
|
|
509
|
+
if (!lifecycleRecorded) {
|
|
510
|
+
preserveWorkspace(workspace, "controller_policy", `attempt launch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
191
511
|
}
|
|
512
|
+
throw error;
|
|
192
513
|
}
|
|
193
|
-
|
|
194
|
-
await recordStepResultEvidence(step, opts, sessionId, status, durationMs);
|
|
195
|
-
log(`Step ${step.id} ${status.toUpperCase()} (${durationMs}ms)`);
|
|
196
|
-
return { stepId: step.id, status, durationMs, resultFile };
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
const durationMs = Date.now() - start;
|
|
200
|
-
await recordStepResultEvidence(step, opts, sessionId, "fail", durationMs);
|
|
201
|
-
log(`Step ${step.id} FAIL (process error, ${durationMs}ms)`);
|
|
202
|
-
return { stepId: step.id, status: "fail", durationMs };
|
|
203
|
-
}
|
|
204
|
-
finally {
|
|
205
|
-
if (stateWritten) {
|
|
514
|
+
finally {
|
|
206
515
|
try {
|
|
207
|
-
await
|
|
516
|
+
await state.cleanup();
|
|
208
517
|
}
|
|
209
518
|
catch (error) {
|
|
210
|
-
log(`
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Execute a parallel group of steps.
|
|
217
|
-
*/
|
|
218
|
-
export async function executeGroup(group, allSteps, round, vars, opts) {
|
|
219
|
-
const steps = group.step_ids
|
|
220
|
-
.map((id) => allSteps.find((s) => s.id === id))
|
|
221
|
-
.filter(Boolean);
|
|
222
|
-
const results = [];
|
|
223
|
-
for (const step of steps) {
|
|
224
|
-
// Evaluate run_if condition
|
|
225
|
-
if (!evaluateRunIf(step.run_if, round)) {
|
|
226
|
-
log(`Skipping step: ${step.id} (run_if: ${step.run_if}, round: ${round})`);
|
|
227
|
-
results.push({ stepId: step.id, status: "skipped", durationMs: 0 });
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
// Collect for execution
|
|
231
|
-
results.push(step); // placeholder, replaced below
|
|
232
|
-
}
|
|
233
|
-
// Filter executable steps
|
|
234
|
-
const executable = steps.filter((s) => evaluateRunIf(s.run_if, round));
|
|
235
|
-
if (executable.length === 0)
|
|
236
|
-
return results.filter((r) => r.status === "skipped");
|
|
237
|
-
// Single step: sequential
|
|
238
|
-
if (executable.length === 1) {
|
|
239
|
-
const result = await executeStep(executable[0], vars, opts, round);
|
|
240
|
-
const skipped = results.filter((r) => r.status === "skipped");
|
|
241
|
-
return [...skipped, result];
|
|
242
|
-
}
|
|
243
|
-
// Multiple steps: parallel
|
|
244
|
-
log(`Running ${executable.length} steps in parallel`);
|
|
245
|
-
const parallelResults = await Promise.all(executable.map((step) => executeStep(step, vars, opts, round)));
|
|
246
|
-
const skipped = results.filter((r) => r.status === "skipped");
|
|
247
|
-
return [...skipped, ...parallelResults];
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Check if any transition requires retry.
|
|
251
|
-
*/
|
|
252
|
-
export function checkRetryNeeded(stepResults, transitions) {
|
|
253
|
-
for (const t of transitions) {
|
|
254
|
-
if (t.condition === "fail") {
|
|
255
|
-
const result = stepResults.get(t.from);
|
|
256
|
-
if (result && result.status === "fail") {
|
|
257
|
-
return true;
|
|
519
|
+
log(`Worker Hook state cleanup failed for ${packet.worker_session_id}: `
|
|
520
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
258
521
|
}
|
|
259
522
|
}
|
|
260
|
-
}
|
|
261
|
-
return false;
|
|
523
|
+
};
|
|
262
524
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
child.on("close", (code) => {
|
|
282
|
-
if (code !== 0 && stdout === "") {
|
|
283
|
-
reject(new Error(`claude exited with code ${code}: ${stderr}`));
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
resolve(stdout);
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
child.stdin.write(stdinData);
|
|
290
|
-
child.stdin.end();
|
|
525
|
+
function buildRuntime(metadata, compiled) {
|
|
526
|
+
const runStore = new DurableRunStore({
|
|
527
|
+
root_directory: runtimeRoot(metadata.project_directory),
|
|
528
|
+
});
|
|
529
|
+
const resultStore = new ImmutableResultStore(runStore);
|
|
530
|
+
const resolver = new HandoffResolver(resultStore);
|
|
531
|
+
const provider = createProvider(metadata);
|
|
532
|
+
const controller = new RunController({
|
|
533
|
+
run_store: runStore,
|
|
534
|
+
handoff_resolver: resolver,
|
|
535
|
+
steps: compiled.definitions,
|
|
536
|
+
provider_for_step: () => provider,
|
|
537
|
+
reconcile_attempt: reconcileLocalProcess,
|
|
538
|
+
execute_attempt: createWorkspaceAwareExecutor(metadata, compiled, runStore),
|
|
539
|
+
max_concurrency: metadata.max_concurrency,
|
|
540
|
+
lease_duration_ms: 15_000,
|
|
541
|
+
lease_heartbeat_ms: 3_000,
|
|
542
|
+
scheduler_poll_ms: 50,
|
|
291
543
|
});
|
|
544
|
+
return { run_store: runStore, controller };
|
|
292
545
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
546
|
+
function buildCancellationController(runStore) {
|
|
547
|
+
return new RunController({
|
|
548
|
+
run_store: runStore,
|
|
549
|
+
steps: [],
|
|
550
|
+
provider_for_step: () => {
|
|
551
|
+
throw new Error("cancellation-only Controller cannot launch a Step");
|
|
552
|
+
},
|
|
553
|
+
reconcile_attempt: reconcileLocalProcess,
|
|
554
|
+
max_concurrency: 1,
|
|
555
|
+
lease_duration_ms: 15_000,
|
|
556
|
+
lease_heartbeat_ms: 3_000,
|
|
557
|
+
scheduler_poll_ms: 50,
|
|
558
|
+
});
|
|
297
559
|
}
|
|
298
|
-
|
|
299
|
-
export async function runRun(opts) {
|
|
300
|
-
// Validate required inputs
|
|
560
|
+
async function startMetadata(opts, projectDirectory) {
|
|
301
561
|
if (opts.dnaFiles.length === 0) {
|
|
302
|
-
|
|
303
|
-
return 2;
|
|
562
|
+
throw new RunUsageError("--dna requires at least one DNA file");
|
|
304
563
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
564
|
+
const workflowName = requireText(opts.workflowName, "--workflow");
|
|
565
|
+
const taskId = requireText(opts.taskId, "--task");
|
|
566
|
+
const expanded = await expandDNAInputFiles(opts.dnaFiles, {
|
|
567
|
+
cwd: projectDirectory,
|
|
568
|
+
});
|
|
569
|
+
if (expanded.length === 0) {
|
|
570
|
+
throw new RunUsageError("--dna did not resolve any DNA files");
|
|
571
|
+
}
|
|
572
|
+
const variables = {
|
|
573
|
+
...(opts.vars ?? {}),
|
|
574
|
+
task_id: taskId,
|
|
575
|
+
ARGUMENTS: taskId,
|
|
576
|
+
workflow: workflowName,
|
|
577
|
+
};
|
|
578
|
+
const maxBudget = requirePositiveNumber(opts.maxBudget ?? 5, "--max-budget");
|
|
579
|
+
const maxConcurrency = requirePositiveInteger(opts.maxConcurrency ?? 1, "--max-concurrency");
|
|
580
|
+
const timeoutMs = opts.timeoutMs === null || opts.timeoutMs === undefined
|
|
581
|
+
? null
|
|
582
|
+
: requireNonNegativeInteger(opts.timeoutMs, "--timeout-ms");
|
|
583
|
+
const cancellationGraceMs = requireNonNegativeInteger(opts.cancellationGraceMs ?? 5_000, "--cancellation-grace-ms");
|
|
584
|
+
const permissionMode = requireText(opts.permissionMode ?? "default", "--permission-mode");
|
|
585
|
+
return {
|
|
586
|
+
schema_version: "intentdna.canonical_run.v1",
|
|
587
|
+
project_directory: projectDirectory,
|
|
588
|
+
dna_files: expanded.map((file) => resolve(projectDirectory, file)),
|
|
589
|
+
workflow_name: workflowName,
|
|
590
|
+
task_id: taskId,
|
|
591
|
+
context: opts.context ?? null,
|
|
592
|
+
variables,
|
|
593
|
+
agents_directory: opts.agentsDir
|
|
594
|
+
? resolve(projectDirectory, opts.agentsDir)
|
|
595
|
+
: join(projectDirectory, ".claude", "agents"),
|
|
596
|
+
provider: {
|
|
597
|
+
kind: opts.provider ?? "claude",
|
|
598
|
+
executable: opts.providerExecutable === undefined
|
|
599
|
+
? null
|
|
600
|
+
: requireText(opts.providerExecutable, "--provider-executable"),
|
|
601
|
+
max_budget_usd: maxBudget,
|
|
602
|
+
permission_mode: permissionMode,
|
|
603
|
+
},
|
|
604
|
+
max_concurrency: maxConcurrency,
|
|
605
|
+
timeout_ms: timeoutMs,
|
|
606
|
+
cancellation_grace_ms: cancellationGraceMs,
|
|
607
|
+
workflow_asset: null,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
async function runtimeForExistingRun(runStore, runId, projectDirectory) {
|
|
611
|
+
const snapshot = await runStore.requireRun(runId);
|
|
612
|
+
const metadata = readCanonicalMetadata(snapshot.run.metadata);
|
|
613
|
+
if (resolve(metadata.project_directory) !== projectDirectory) {
|
|
614
|
+
throw new Error(`run ${runId} belongs to project ${metadata.project_directory}, not ${projectDirectory}`);
|
|
308
615
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
616
|
+
const compiled = await compileCanonicalRun(metadata);
|
|
617
|
+
if (compiled.plan_digest !== snapshot.run.plan_digest) {
|
|
618
|
+
throw new Error(`run ${runId} plan digest changed; resume requires the original DNA and role definitions`);
|
|
312
619
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
620
|
+
await syncRuntimeArtifacts(compiled, metadata);
|
|
621
|
+
return buildRuntime(metadata, compiled);
|
|
622
|
+
}
|
|
623
|
+
// ── CLI entry ──────────────────────────────────────────────
|
|
624
|
+
export async function runRun(opts) {
|
|
625
|
+
const action = opts.action ?? "start";
|
|
626
|
+
const projectDirectory = resolve(opts.projectDir ?? process.cwd());
|
|
627
|
+
const runStore = new DurableRunStore({
|
|
628
|
+
root_directory: runtimeRoot(projectDirectory),
|
|
629
|
+
});
|
|
317
630
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
context: opts.context,
|
|
325
|
-
});
|
|
326
|
-
// ── Step 2: Find & compile workflow ──────────────────
|
|
327
|
-
const wfDef = cascaded.workflows[opts.workflowName];
|
|
328
|
-
if (!wfDef) {
|
|
329
|
-
process.stderr.write(`Error: Workflow '${opts.workflowName}' not found. Available: ${Object.keys(cascaded.workflows).join(", ") || "(none)"}\n`);
|
|
330
|
-
return 1;
|
|
631
|
+
if (action !== "start"
|
|
632
|
+
&& action !== "status"
|
|
633
|
+
&& action !== "inspect"
|
|
634
|
+
&& action !== "resume"
|
|
635
|
+
&& action !== "cancel") {
|
|
636
|
+
throw new RunUsageError(`unsupported run action '${String(action)}'`);
|
|
331
637
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
return 1;
|
|
638
|
+
if (action === "status") {
|
|
639
|
+
return runLifecycleStatus({
|
|
640
|
+
run_store: runStore,
|
|
641
|
+
run_id: asRunId(requireText(opts.runId, "run_id")),
|
|
642
|
+
json: opts.json,
|
|
643
|
+
});
|
|
339
644
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
645
|
+
if (action === "inspect") {
|
|
646
|
+
return runLifecycleInspect({
|
|
647
|
+
run_store: runStore,
|
|
648
|
+
run_id: asRunId(requireText(opts.runId, "run_id")),
|
|
649
|
+
json: opts.json,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (action === "resume") {
|
|
653
|
+
const runId = asRunId(requireText(opts.runId, "run_id"));
|
|
654
|
+
const runtime = await runtimeForExistingRun(runStore, runId, projectDirectory);
|
|
655
|
+
return runLifecycleResume({
|
|
656
|
+
controller: runtime.controller,
|
|
657
|
+
run_id: runId,
|
|
658
|
+
json: opts.json,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (action === "cancel") {
|
|
662
|
+
const runId = asRunId(requireText(opts.runId, "run_id"));
|
|
663
|
+
return runLifecycleCancel({
|
|
664
|
+
controller: buildCancellationController(runStore),
|
|
665
|
+
run_id: runId,
|
|
666
|
+
reason: opts.reason,
|
|
667
|
+
json: opts.json,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
const initialMetadata = await startMetadata(opts, projectDirectory);
|
|
350
671
|
if (opts.dryRun) {
|
|
351
|
-
|
|
672
|
+
const firstPreview = await compileRunAssets(initialMetadata);
|
|
673
|
+
const previewMetadata = {
|
|
674
|
+
...initialMetadata,
|
|
675
|
+
workflow_asset: firstPreview.workflow_asset,
|
|
676
|
+
};
|
|
677
|
+
const preview = previewMetadata.workflow_asset
|
|
678
|
+
=== initialMetadata.workflow_asset
|
|
679
|
+
? firstPreview
|
|
680
|
+
: await compileRunAssets(previewMetadata);
|
|
681
|
+
return printDryRun(preview.plan, previewMetadata, preview.plan_digest);
|
|
352
682
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
log(`Generated ${agentResults.length} agent file(s) in ${agentsDir}`);
|
|
358
|
-
const hookScripts = createCompiledIR(ir, cascaded.roles);
|
|
359
|
-
await writeCompiledIR(projectDir, hookScripts);
|
|
360
|
-
log("Synced compiled IR for dna-hook enforcement");
|
|
361
|
-
// ── Step 5: Prepare log directory ────────────────────
|
|
362
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
363
|
-
const logDir = opts.logDir
|
|
364
|
-
? resolve(projectDir, opts.logDir)
|
|
365
|
-
: join(projectDir, ".intentdna", "runs", `${opts.workflowName}-${timestamp}`);
|
|
366
|
-
await mkdir(logDir, { recursive: true });
|
|
367
|
-
const attribution = trustedEvidenceCaptureAttribution(ir, { workflowId: opts.workflowName });
|
|
368
|
-
const execOpts = {
|
|
369
|
-
agentPrefix: "dna-",
|
|
370
|
-
maxBudget,
|
|
371
|
-
permissionMode,
|
|
372
|
-
logDir,
|
|
373
|
-
projectDir,
|
|
374
|
-
workflowName: opts.workflowName,
|
|
375
|
-
workflowAsset: attribution.workflowAsset === "unknown" ? undefined : attribution.workflowAsset,
|
|
376
|
-
inputs: {
|
|
377
|
-
...(opts.vars ?? {}),
|
|
378
|
-
task_id: opts.taskId,
|
|
379
|
-
ARGUMENTS: opts.taskId,
|
|
380
|
-
workflow: opts.workflowName,
|
|
381
|
-
},
|
|
382
|
-
ir,
|
|
683
|
+
const firstCompilation = await compileCanonicalRun(initialMetadata);
|
|
684
|
+
const metadata = {
|
|
685
|
+
...initialMetadata,
|
|
686
|
+
workflow_asset: firstCompilation.workflow_asset,
|
|
383
687
|
};
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// Check if retry needed
|
|
415
|
-
if (checkRetryNeeded(stepResults, allTransitions)) {
|
|
416
|
-
if (round >= maxRetries + 1) {
|
|
417
|
-
log(`Max retries (${maxRetries}) exhausted`);
|
|
418
|
-
return printSummary(stepResults, round, logDir, 1);
|
|
419
|
-
}
|
|
420
|
-
log("Retry triggered by fail transition");
|
|
421
|
-
round++;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
// All passed
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
return printSummary(stepResults, round, logDir, 0);
|
|
688
|
+
const compiled = metadata.workflow_asset === initialMetadata.workflow_asset
|
|
689
|
+
? firstCompilation
|
|
690
|
+
: await compileCanonicalRun(metadata);
|
|
691
|
+
await syncRuntimeArtifacts(compiled, metadata);
|
|
692
|
+
const runtime = buildRuntime(metadata, compiled);
|
|
693
|
+
const now = new Date().toISOString();
|
|
694
|
+
const runId = opts.runId?.trim()
|
|
695
|
+
? asRunId(opts.runId)
|
|
696
|
+
: `run_${randomUUID()}`;
|
|
697
|
+
const run = {
|
|
698
|
+
run_id: runId,
|
|
699
|
+
workflow_id: metadata.workflow_name,
|
|
700
|
+
workflow_version: null,
|
|
701
|
+
plan_digest: compiled.plan_digest,
|
|
702
|
+
status: "active",
|
|
703
|
+
cancellation: null,
|
|
704
|
+
terminal: null,
|
|
705
|
+
metadata: metadata,
|
|
706
|
+
created_at: now,
|
|
707
|
+
updated_at: now,
|
|
708
|
+
};
|
|
709
|
+
return runLifecycleStart({
|
|
710
|
+
controller: runtime.controller,
|
|
711
|
+
run,
|
|
712
|
+
json: opts.json,
|
|
713
|
+
});
|
|
428
714
|
}
|
|
429
|
-
catch (
|
|
430
|
-
|
|
431
|
-
|
|
715
|
+
catch (error) {
|
|
716
|
+
const usage = error instanceof RunUsageError;
|
|
717
|
+
process.stderr.write(`${usage ? "Usage error" : "Error"}: `
|
|
718
|
+
+ `${error instanceof Error ? error.message : String(error)}\n`);
|
|
719
|
+
return usage ? 2 : 1;
|
|
432
720
|
}
|
|
433
721
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
process.stderr.write("\n=== DRY RUN — Execution Plan ===\n\n");
|
|
722
|
+
function printDryRun(plan, metadata, digest) {
|
|
723
|
+
process.stderr.write("\n=== DRY RUN - Canonical Controller Plan ===\n\n");
|
|
437
724
|
process.stderr.write(`Workflow: ${plan.name}\n`);
|
|
438
|
-
process.stderr.write(`
|
|
725
|
+
process.stderr.write(`Plan digest: ${digest}\n`);
|
|
726
|
+
process.stderr.write(`Provider: ${metadata.provider.kind}\n`);
|
|
727
|
+
process.stderr.write(`Max concurrency: ${metadata.max_concurrency}\n`);
|
|
728
|
+
process.stderr.write(`Durable root: ${runtimeRoot(metadata.project_directory)}\n`);
|
|
729
|
+
process.stderr.write(`Steps: ${plan.steps.length}\n\n`);
|
|
439
730
|
process.stderr.write(`Groups: ${plan.parallel_groups.length}\n`);
|
|
440
731
|
process.stderr.write(`Max retries: ${plan.retry.max_retries}\n`);
|
|
441
732
|
if (plan.retry.retry_from) {
|
|
442
733
|
process.stderr.write(`Retry from: ${plan.retry.retry_from}\n`);
|
|
443
734
|
}
|
|
444
|
-
process.stderr.write(`Variables: ${JSON.stringify(
|
|
445
|
-
process.stderr.write("\nExecution order:\n");
|
|
735
|
+
process.stderr.write(`Variables: ${JSON.stringify(metadata.variables)}\n\n`);
|
|
446
736
|
for (const group of plan.parallel_groups) {
|
|
447
|
-
|
|
448
|
-
process.stderr.write(`\n Group ${group.group_index + 1}${parallel}:\n`);
|
|
737
|
+
process.stderr.write(`Group ${group.group_index + 1} (${group.isolation}):\n`);
|
|
449
738
|
for (const stepId of group.step_ids) {
|
|
450
|
-
const step = plan.steps.find((
|
|
451
|
-
|
|
739
|
+
const step = plan.steps.find((entry) => entry.id === stepId);
|
|
740
|
+
if (!step)
|
|
741
|
+
continue;
|
|
742
|
+
const prompt = substituteTemplate(step.prompt ?? step.description, metadata.variables);
|
|
452
743
|
const runIf = step.run_if ? ` [run_if: ${step.run_if}]` : "";
|
|
453
744
|
const optional = step.optional ? " (optional)" : "";
|
|
454
|
-
process.stderr.write(`
|
|
455
|
-
|
|
745
|
+
process.stderr.write(` - ${step.id} -> agent: dna-${toKebabCase(step.role)}`
|
|
746
|
+
+ `${runIf}${optional}; independent session\n`);
|
|
747
|
+
process.stderr.write(` prompt: ${prompt.slice(0, 120)}${prompt.length > 120 ? "..." : ""}\n`);
|
|
456
748
|
}
|
|
457
749
|
}
|
|
458
750
|
if (plan.transitions.length > 0) {
|
|
459
751
|
process.stderr.write("\nTransitions:\n");
|
|
460
|
-
for (const
|
|
461
|
-
process.stderr.write(`
|
|
752
|
+
for (const transition of plan.transitions) {
|
|
753
|
+
process.stderr.write(` ${transition.from} -> ${transition.to} `
|
|
754
|
+
+ `(on ${transition.condition})\n`);
|
|
462
755
|
}
|
|
463
756
|
}
|
|
464
757
|
process.stderr.write("\n");
|
|
465
758
|
return 0;
|
|
466
759
|
}
|
|
467
|
-
function
|
|
468
|
-
|
|
469
|
-
process.stderr.write(`
|
|
470
|
-
process.stderr.write(`Log directory: ${logDir}\n\n`);
|
|
471
|
-
for (const [stepId, result] of results) {
|
|
472
|
-
const icon = result.status === "pass" ? "PASS" : result.status === "fail" ? "FAIL" : "SKIP";
|
|
473
|
-
const duration = result.durationMs > 0 ? ` (${(result.durationMs / 1000).toFixed(1)}s)` : "";
|
|
474
|
-
process.stderr.write(` [${icon}] ${stepId}${duration}\n`);
|
|
475
|
-
}
|
|
476
|
-
const passed = [...results.values()].filter((r) => r.status === "pass").length;
|
|
477
|
-
const failed = [...results.values()].filter((r) => r.status === "fail").length;
|
|
478
|
-
const skipped = [...results.values()].filter((r) => r.status === "skipped").length;
|
|
479
|
-
process.stderr.write(`\nTotal: ${passed} passed, ${failed} failed, ${skipped} skipped\n`);
|
|
480
|
-
return exitCode;
|
|
760
|
+
function log(message) {
|
|
761
|
+
const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
762
|
+
process.stderr.write(`[${timestamp}] ${message}\n`);
|
|
481
763
|
}
|