intentdna 1.8.6 → 1.9.0-rc.1

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 (87) 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/candidate-promotion.d.ts +15 -0
  5. package/dist/cli/commands/candidate-promotion.js +197 -0
  6. package/dist/cli/commands/run-lifecycle.d.ts +73 -0
  7. package/dist/cli/commands/run-lifecycle.js +241 -0
  8. package/dist/cli/commands/run.d.ts +22 -40
  9. package/dist/cli/commands/run.js +676 -392
  10. package/dist/cli/commands/source-assets.d.ts +32 -0
  11. package/dist/cli/commands/source-assets.js +154 -0
  12. package/dist/cli/commands/templates.js +59 -37
  13. package/dist/cli/index.js +248 -7
  14. package/dist/compiler/cascade.js +1 -1
  15. package/dist/compiler/workflow.js +3 -2
  16. package/dist/hooks/cli.d.ts +7 -2
  17. package/dist/hooks/cli.js +192 -85
  18. package/dist/hooks/enforce.d.ts +2 -0
  19. package/dist/hooks/enforce.js +56 -27
  20. package/dist/hooks/enforcement-boundary.d.ts +13 -0
  21. package/dist/hooks/enforcement-boundary.js +33 -0
  22. package/dist/hooks/index.d.ts +3 -2
  23. package/dist/hooks/index.js +3 -2
  24. package/dist/hooks/protocol.d.ts +12 -4
  25. package/dist/hooks/protocol.js +20 -14
  26. package/dist/hooks/schema.d.ts +2 -1
  27. package/dist/hooks/schema.js +6 -2
  28. package/dist/hooks/state-manager.d.ts +5 -5
  29. package/dist/hooks/state-manager.js +26 -24
  30. package/dist/hooks/state.d.ts +19 -3
  31. package/dist/hooks/state.js +327 -80
  32. package/dist/runtime/evaluator.d.ts +13 -0
  33. package/dist/runtime/evaluator.js +46 -0
  34. package/dist/runtime/execution-provider.d.ts +40 -0
  35. package/dist/runtime/execution-provider.js +138 -0
  36. package/dist/runtime/handoff-resolver.d.ts +61 -0
  37. package/dist/runtime/handoff-resolver.js +167 -0
  38. package/dist/runtime/index.d.ts +28 -0
  39. package/dist/runtime/index.js +15 -0
  40. package/dist/runtime/process-tree.d.ts +47 -0
  41. package/dist/runtime/process-tree.js +402 -0
  42. package/dist/runtime/providers/claude.d.ts +9 -0
  43. package/dist/runtime/providers/claude.js +64 -0
  44. package/dist/runtime/providers/codex.d.ts +8 -0
  45. package/dist/runtime/providers/codex.js +72 -0
  46. package/dist/runtime/result-store.d.ts +32 -0
  47. package/dist/runtime/result-store.js +130 -0
  48. package/dist/runtime/run-contracts.d.ts +292 -0
  49. package/dist/runtime/run-contracts.js +68 -0
  50. package/dist/runtime/run-controller.d.ts +163 -0
  51. package/dist/runtime/run-controller.js +1249 -0
  52. package/dist/runtime/run-store.d.ts +96 -0
  53. package/dist/runtime/run-store.js +725 -0
  54. package/dist/runtime/verifier.js +101 -2
  55. package/dist/runtime/worker-executor.d.ts +19 -0
  56. package/dist/runtime/worker-executor.js +194 -0
  57. package/dist/runtime/workflow-plan-adapter.d.ts +27 -0
  58. package/dist/runtime/workflow-plan-adapter.js +483 -0
  59. package/dist/runtime/workflow-runner.d.ts +15 -3
  60. package/dist/runtime/workflow-runner.js +13 -1
  61. package/dist/runtime/workspace-isolation.d.ts +103 -0
  62. package/dist/runtime/workspace-isolation.js +373 -0
  63. package/dist/schema/types.d.ts +6 -0
  64. package/dist/schema/validate.js +93 -7
  65. package/dist/schema/yaml-parser.js +7 -2
  66. package/dist/sources/authoring-packet.d.ts +8 -0
  67. package/dist/sources/authoring-packet.js +85 -0
  68. package/dist/sources/discovery.d.ts +7 -0
  69. package/dist/sources/discovery.js +150 -0
  70. package/dist/sources/license.d.ts +5 -0
  71. package/dist/sources/license.js +94 -0
  72. package/dist/sources/manifest.d.ts +11 -0
  73. package/dist/sources/manifest.js +84 -0
  74. package/dist/sources/skill-markdown.d.ts +6 -0
  75. package/dist/sources/skill-markdown.js +174 -0
  76. package/dist/sources/types.d.ts +125 -0
  77. package/dist/sources/types.js +1 -0
  78. package/dist/templates/asset-refinery.dna.yaml +192 -0
  79. package/dist/templates/flutter-rewrite.dna.yaml +154 -28
  80. package/dist/templates/metadata.js +2 -0
  81. package/dist/templates/multi-agent-handoff-coordination.dna.yaml +102 -37
  82. package/dist/templates/persistent-executor.dna.yaml +61 -3
  83. package/dist/templates/research-improvement.dna.yaml +295 -0
  84. package/dist/templates/subagent-parallel.dna.yaml +90 -17
  85. package/dist/workflow/candidate-projection.d.ts +33 -0
  86. package/dist/workflow/candidate-projection.js +70 -0
  87. package/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
12
- "version": "1.8.6",
12
+ "version": "1.9.0-rc.1",
13
13
  "author": {
14
14
  "name": "Samuel"
15
15
  },
@@ -25,5 +25,5 @@
25
25
  ]
26
26
  }
27
27
  ],
28
- "version": "1.8.6"
28
+ "version": "1.9.0-rc.1"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.6",
3
+ "version": "1.9.0-rc.1",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
package/README.md CHANGED
@@ -181,6 +181,7 @@ Trusted cloud ingestion may increase persistence and visibility, but it does not
181
181
 
182
182
  - [docs/README.md](docs/README.md) - living docs map and two-state documentation rule.
183
183
  - [docs/quickstart.md](docs/quickstart.md) - local developer quickstart.
184
+ - [docs/foundation-runtime.md](docs/foundation-runtime.md) - durable `dna run` Controller, lifecycle, handoff, isolation, and recovery guide.
184
185
  - [docs/product/milestone-review-protocol.md](docs/product/milestone-review-protocol.md) - lightweight P-stage review protocol.
185
186
  - [docs/product/online-product-spec.md](docs/product/online-product-spec.md) - Online IA, K5 boundary, forbidden copy, and acceptance.
186
187
  - [docs/decisions/ADR-001-product-reset.md](docs/decisions/ADR-001-product-reset.md) - product reset decision record.
@@ -0,0 +1,15 @@
1
+ export interface PromoteCandidateOptions {
2
+ projectDir: string;
3
+ candidatePath: string;
4
+ templateName: string;
5
+ }
6
+ export interface CandidatePromotionResult {
7
+ schema_version: "intentdna.candidate_promotion_result.v1";
8
+ candidate_path: string;
9
+ template_path: string;
10
+ receipt_path: string;
11
+ candidate_sha256: string;
12
+ template_sha256: string;
13
+ exact_bytes_preserved: true;
14
+ }
15
+ export declare function promoteCandidate(options: PromoteCandidateOptions): Promise<CandidatePromotionResult>;
@@ -0,0 +1,197 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, open, readFile, realpath, rm, stat, } from "node:fs/promises";
3
+ import { basename, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { formatDiagnostics, hasDiagnosticErrors } from "../../compiler/diagnostics.js";
5
+ import { loadDNAWithDiagnostics } from "../../compiler/input-resolver.js";
6
+ const CANDIDATE_SUFFIX = ".candidate.dna.yaml";
7
+ const SAFE_TEMPLATE_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
8
+ function sha256(bytes) {
9
+ return createHash("sha256").update(bytes).digest("hex");
10
+ }
11
+ function isErrnoException(error) {
12
+ return error instanceof Error && "code" in error;
13
+ }
14
+ function isStrictlyWithin(parent, child) {
15
+ const pathFromParent = relative(parent, child);
16
+ return pathFromParent !== ""
17
+ && pathFromParent !== ".."
18
+ && !pathFromParent.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)
19
+ && !isAbsolute(pathFromParent);
20
+ }
21
+ function validateTemplateName(templateName) {
22
+ if (!SAFE_TEMPLATE_NAME.test(templateName)) {
23
+ throw new Error("Invalid template name: use one safe path component containing only letters, numbers, hyphens, or underscores");
24
+ }
25
+ }
26
+ async function assertPathMissing(path, label) {
27
+ try {
28
+ await lstat(path);
29
+ }
30
+ catch (error) {
31
+ if (isErrnoException(error) && error.code === "ENOENT")
32
+ return;
33
+ throw error;
34
+ }
35
+ throw new Error(`${label} already exists; overwrite is not allowed`);
36
+ }
37
+ async function ensureSafeTemplatesDirectory(templatesDir, dnaRealPath) {
38
+ try {
39
+ const entry = await lstat(templatesDir);
40
+ if (entry.isSymbolicLink()) {
41
+ throw new Error("Templates directory must not be a symlink");
42
+ }
43
+ if (!entry.isDirectory()) {
44
+ throw new Error("Templates path is not a directory");
45
+ }
46
+ }
47
+ catch (error) {
48
+ if (!isErrnoException(error) || error.code !== "ENOENT")
49
+ throw error;
50
+ await mkdir(templatesDir);
51
+ }
52
+ const entry = await lstat(templatesDir);
53
+ if (entry.isSymbolicLink()) {
54
+ throw new Error("Templates directory must not be a symlink");
55
+ }
56
+ if (!entry.isDirectory()) {
57
+ throw new Error("Templates path is not a directory");
58
+ }
59
+ const templatesRealPath = await realpath(templatesDir);
60
+ if (!isStrictlyWithin(dnaRealPath, templatesRealPath)) {
61
+ throw new Error("Templates directory escapes the project .dna directory");
62
+ }
63
+ return templatesRealPath;
64
+ }
65
+ async function removeCreatedPath(path) {
66
+ try {
67
+ await rm(path, { force: true });
68
+ return undefined;
69
+ }
70
+ catch (error) {
71
+ return error;
72
+ }
73
+ }
74
+ export async function promoteCandidate(options) {
75
+ validateTemplateName(options.templateName);
76
+ const projectPath = resolve(options.projectDir);
77
+ const projectRealPath = await realpath(projectPath);
78
+ const dnaPath = join(projectPath, ".dna");
79
+ const authoringPath = join(dnaPath, "authoring");
80
+ const candidatePath = resolve(projectPath, options.candidatePath);
81
+ if (!isStrictlyWithin(authoringPath, candidatePath)) {
82
+ throw new Error("Candidate must be located within the project .dna/authoring directory");
83
+ }
84
+ const candidateFilename = basename(candidatePath);
85
+ if (!candidateFilename.endsWith(CANDIDATE_SUFFIX)
86
+ || candidateFilename.length === CANDIDATE_SUFFIX.length) {
87
+ throw new Error(`Candidate filename must end with ${CANDIDATE_SUFFIX}`);
88
+ }
89
+ const dnaRealPath = await realpath(dnaPath);
90
+ if (!isStrictlyWithin(projectRealPath, dnaRealPath)) {
91
+ throw new Error("Project .dna directory escapes the project root");
92
+ }
93
+ const authoringRealPath = await realpath(authoringPath);
94
+ if (!isStrictlyWithin(dnaRealPath, authoringRealPath)) {
95
+ throw new Error("Authoring directory escapes the project .dna directory");
96
+ }
97
+ const candidateRealPath = await realpath(candidatePath);
98
+ if (!isStrictlyWithin(authoringRealPath, candidateRealPath)) {
99
+ throw new Error("Candidate symlink or path escapes the project authoring directory");
100
+ }
101
+ const candidateEntry = await stat(candidateRealPath);
102
+ if (!candidateEntry.isFile()) {
103
+ throw new Error("Candidate must be a regular file in the project authoring directory");
104
+ }
105
+ const candidateBytesBeforeValidation = await readFile(candidateRealPath);
106
+ const validation = await loadDNAWithDiagnostics(candidateRealPath);
107
+ if (!validation.dna || hasDiagnosticErrors(validation.diagnostics)) {
108
+ const details = formatDiagnostics(validation.diagnostics);
109
+ throw new Error(`Candidate validation failed${details ? `:\n${details}` : ""}`);
110
+ }
111
+ const candidateRealPathAfterValidation = await realpath(candidatePath);
112
+ if (candidateRealPathAfterValidation !== candidateRealPath) {
113
+ throw new Error("Candidate path changed during validation; promotion refused");
114
+ }
115
+ const candidateBytes = await readFile(candidateRealPathAfterValidation);
116
+ if (!candidateBytes.equals(candidateBytesBeforeValidation)) {
117
+ throw new Error("Candidate bytes changed during validation; promotion refused");
118
+ }
119
+ const templatesPath = join(dnaPath, "templates");
120
+ const templatesRealPath = await ensureSafeTemplatesDirectory(templatesPath, dnaRealPath);
121
+ const templatePath = join(templatesPath, `${options.templateName}.dna.yaml`);
122
+ const templateWritePath = join(templatesRealPath, `${options.templateName}.dna.yaml`);
123
+ const candidateName = candidateFilename.slice(0, -CANDIDATE_SUFFIX.length);
124
+ const receiptPath = join(authoringPath, `${candidateName}.candidate-review.json`);
125
+ const receiptWritePath = join(authoringRealPath, `${candidateName}.candidate-review.json`);
126
+ await assertPathMissing(templateWritePath, "Promotion target");
127
+ await assertPathMissing(receiptWritePath, "Candidate review receipt");
128
+ const candidateHash = sha256(candidateBytes);
129
+ let templateCreated = false;
130
+ let receiptCreated = false;
131
+ let templateHandle;
132
+ let receiptHandle;
133
+ try {
134
+ templateHandle = await open(templateWritePath, "wx");
135
+ templateCreated = true;
136
+ await templateHandle.writeFile(candidateBytes);
137
+ await templateHandle.sync();
138
+ await templateHandle.close();
139
+ templateHandle = undefined;
140
+ const currentTemplatesRealPath = await realpath(templatesPath);
141
+ if (currentTemplatesRealPath !== templatesRealPath) {
142
+ throw new Error("Templates directory changed during promotion; promotion refused");
143
+ }
144
+ const promotedBytes = await readFile(templateWritePath);
145
+ const templateHash = sha256(promotedBytes);
146
+ if (!promotedBytes.equals(candidateBytes) || templateHash !== candidateHash) {
147
+ throw new Error("Promoted template does not preserve the validated candidate bytes exactly");
148
+ }
149
+ const receipt = {
150
+ schema_version: "intentdna.candidate_promotion_receipt.v1",
151
+ decision: "promoted",
152
+ source: "explicit_human_promotion",
153
+ mutation_performed: true,
154
+ promoted_at: new Date().toISOString(),
155
+ candidate_path: candidatePath,
156
+ template_path: templatePath,
157
+ candidate_sha256: candidateHash,
158
+ template_sha256: templateHash,
159
+ exact_bytes_preserved: true,
160
+ };
161
+ const receiptBytes = Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`, "utf8");
162
+ receiptHandle = await open(receiptWritePath, "wx");
163
+ receiptCreated = true;
164
+ await receiptHandle.writeFile(receiptBytes);
165
+ await receiptHandle.sync();
166
+ await receiptHandle.close();
167
+ receiptHandle = undefined;
168
+ return {
169
+ schema_version: "intentdna.candidate_promotion_result.v1",
170
+ candidate_path: candidatePath,
171
+ template_path: templatePath,
172
+ receipt_path: receiptPath,
173
+ candidate_sha256: candidateHash,
174
+ template_sha256: templateHash,
175
+ exact_bytes_preserved: true,
176
+ };
177
+ }
178
+ catch (error) {
179
+ await receiptHandle?.close().catch(() => undefined);
180
+ await templateHandle?.close().catch(() => undefined);
181
+ const cleanupErrors = [];
182
+ if (receiptCreated) {
183
+ const cleanupError = await removeCreatedPath(receiptWritePath);
184
+ if (cleanupError)
185
+ cleanupErrors.push(cleanupError);
186
+ }
187
+ if (templateCreated) {
188
+ const cleanupError = await removeCreatedPath(templateWritePath);
189
+ if (cleanupError)
190
+ cleanupErrors.push(cleanupError);
191
+ }
192
+ if (cleanupErrors.length > 0) {
193
+ throw new AggregateError([error, ...cleanupErrors], "Candidate promotion failed and cleanup did not complete");
194
+ }
195
+ throw error;
196
+ }
197
+ }
@@ -0,0 +1,73 @@
1
+ import type { JsonValue, RunId, TaskState } from "../../runtime/run-contracts.js";
2
+ import type { RunController } from "../../runtime/run-controller.js";
3
+ import type { DurableRunStore, RunRecord, RunStatus, RunStoreSnapshot } from "../../runtime/run-store.js";
4
+ export type RunLifecycleAction = "start" | "status" | "inspect" | "resume" | "cancel";
5
+ interface LifecycleOutputOptions {
6
+ readonly json?: boolean;
7
+ }
8
+ export interface RunLifecycleStartOptions extends LifecycleOutputOptions {
9
+ readonly controller: RunController;
10
+ readonly run: RunRecord;
11
+ }
12
+ export interface RunLifecycleStatusOptions extends LifecycleOutputOptions {
13
+ readonly run_store: DurableRunStore;
14
+ readonly run_id: RunId;
15
+ }
16
+ export interface RunLifecycleInspectOptions extends LifecycleOutputOptions {
17
+ readonly run_store: DurableRunStore;
18
+ readonly run_id: RunId;
19
+ }
20
+ export interface RunLifecycleResumeOptions extends LifecycleOutputOptions {
21
+ readonly controller: RunController;
22
+ readonly run_id: RunId;
23
+ }
24
+ export interface RunLifecycleCancelOptions extends LifecycleOutputOptions {
25
+ readonly controller: RunController;
26
+ readonly run_id: RunId;
27
+ readonly reason?: string | null;
28
+ }
29
+ export interface RunStatusProjection {
30
+ readonly schema_version: "intentdna.run_status.v1";
31
+ readonly run_id: RunId;
32
+ readonly workflow_id: string;
33
+ readonly workflow_version: string | null;
34
+ readonly status: RunStatus;
35
+ readonly record_version: number;
36
+ readonly task_counts: Readonly<Record<TaskState, number>>;
37
+ readonly attempt_count: number;
38
+ readonly result_count: number;
39
+ readonly cancellation: RunRecord["cancellation"];
40
+ readonly terminal: RunRecord["terminal"];
41
+ readonly created_at: string;
42
+ readonly updated_at: string;
43
+ }
44
+ export interface DurableHandoffInspection {
45
+ readonly event_id: string;
46
+ readonly sequence: number;
47
+ readonly run_id: RunId;
48
+ readonly step_id: string;
49
+ readonly attempt_id: string;
50
+ readonly worker_session_id: string;
51
+ readonly result_id: string | null;
52
+ readonly occurred_at: string;
53
+ readonly binding: JsonValue;
54
+ }
55
+ export interface RunInspectionProjection {
56
+ readonly schema_version: "intentdna.run_inspection.v1";
57
+ readonly record_version: number;
58
+ readonly run: RunRecord;
59
+ readonly tasks: RunStoreSnapshot["tasks"];
60
+ readonly attempts: RunStoreSnapshot["attempts"];
61
+ readonly claims: RunStoreSnapshot["claims"];
62
+ readonly handoffs: readonly DurableHandoffInspection[];
63
+ readonly events: RunStoreSnapshot["events"];
64
+ readonly results: RunStoreSnapshot["results"];
65
+ readonly ledger_created_at: string;
66
+ readonly ledger_updated_at: string;
67
+ }
68
+ export declare function runLifecycleStart(options: RunLifecycleStartOptions): Promise<number>;
69
+ export declare function runLifecycleStatus(options: RunLifecycleStatusOptions): Promise<number>;
70
+ export declare function runLifecycleInspect(options: RunLifecycleInspectOptions): Promise<number>;
71
+ export declare function runLifecycleResume(options: RunLifecycleResumeOptions): Promise<number>;
72
+ export declare function runLifecycleCancel(options: RunLifecycleCancelOptions): Promise<number>;
73
+ export {};
@@ -0,0 +1,241 @@
1
+ class LifecycleUsageError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "LifecycleUsageError";
5
+ }
6
+ }
7
+ const TASK_STATES = [
8
+ "dependency_blocked",
9
+ "ready",
10
+ "claimed",
11
+ "running",
12
+ "round_succeeded",
13
+ "succeeded",
14
+ "failed",
15
+ "skipped",
16
+ "cancelled",
17
+ ];
18
+ function requireText(value, label) {
19
+ if (!value.trim()) {
20
+ throw new LifecycleUsageError(`${label} is required`);
21
+ }
22
+ }
23
+ function requireRunId(runId) {
24
+ requireText(runId, "run_id");
25
+ }
26
+ function validateStartRun(run) {
27
+ requireRunId(run.run_id);
28
+ requireText(run.workflow_id, "workflow_id");
29
+ requireText(run.plan_digest, "plan_digest");
30
+ requireText(run.created_at, "created_at");
31
+ requireText(run.updated_at, "updated_at");
32
+ if (run.status !== "active"
33
+ || run.cancellation !== null
34
+ || run.terminal !== null) {
35
+ throw new LifecycleUsageError("start requires an active run without cancellation or terminal state");
36
+ }
37
+ }
38
+ function statusProjection(snapshot) {
39
+ const taskCounts = Object.fromEntries(TASK_STATES.map((state) => [state, 0]));
40
+ for (const task of snapshot.tasks) {
41
+ taskCounts[task.state] += 1;
42
+ }
43
+ return {
44
+ schema_version: "intentdna.run_status.v1",
45
+ run_id: snapshot.run.run_id,
46
+ workflow_id: snapshot.run.workflow_id,
47
+ workflow_version: snapshot.run.workflow_version,
48
+ status: snapshot.run.status,
49
+ record_version: snapshot.record_version,
50
+ task_counts: taskCounts,
51
+ attempt_count: snapshot.attempts.length,
52
+ result_count: snapshot.results.length,
53
+ cancellation: snapshot.run.cancellation,
54
+ terminal: snapshot.run.terminal,
55
+ created_at: snapshot.run.created_at,
56
+ updated_at: snapshot.run.updated_at,
57
+ };
58
+ }
59
+ function durableHandoffs(snapshot) {
60
+ return snapshot.events.flatMap((event) => {
61
+ if (event.type !== "handoff_resolved"
62
+ || event.step_id === null
63
+ || event.attempt_id === null
64
+ || event.worker_session_id === null) {
65
+ return [];
66
+ }
67
+ return [{
68
+ event_id: event.event_id,
69
+ sequence: event.sequence,
70
+ run_id: event.run_id,
71
+ step_id: event.step_id,
72
+ attempt_id: event.attempt_id,
73
+ worker_session_id: event.worker_session_id,
74
+ result_id: event.result_id,
75
+ occurred_at: event.occurred_at,
76
+ binding: event.payload,
77
+ }];
78
+ });
79
+ }
80
+ function inspectionProjection(snapshot) {
81
+ return {
82
+ schema_version: "intentdna.run_inspection.v1",
83
+ record_version: snapshot.record_version,
84
+ run: snapshot.run,
85
+ tasks: snapshot.tasks,
86
+ attempts: snapshot.attempts,
87
+ claims: snapshot.claims,
88
+ handoffs: durableHandoffs(snapshot),
89
+ events: snapshot.events,
90
+ results: snapshot.results,
91
+ ledger_created_at: snapshot.created_at,
92
+ ledger_updated_at: snapshot.updated_at,
93
+ };
94
+ }
95
+ function writeJson(value) {
96
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
97
+ }
98
+ function nonZeroTaskCounts(counts) {
99
+ const summary = TASK_STATES
100
+ .filter((state) => counts[state] > 0)
101
+ .map((state) => `${state}=${counts[state]}`)
102
+ .join(", ");
103
+ return summary || "none";
104
+ }
105
+ function writeHumanStatus(status, action) {
106
+ if (action)
107
+ process.stdout.write(`Action: ${action}\n`);
108
+ process.stdout.write(`Run: ${status.run_id}\n`);
109
+ process.stdout.write(`Workflow: ${status.workflow_id}\n`);
110
+ process.stdout.write(`Status: ${status.status}\n`);
111
+ process.stdout.write(`Ledger version: ${status.record_version}\n`);
112
+ process.stdout.write(`Tasks: ${nonZeroTaskCounts(status.task_counts)}\n`);
113
+ process.stdout.write(`Attempts: ${status.attempt_count}; results: ${status.result_count}\n`);
114
+ if (status.cancellation) {
115
+ process.stdout.write(`Cancellation: ${status.cancellation.requested_at}`
116
+ + `${status.cancellation.reason ? ` (${status.cancellation.reason})` : ""}\n`);
117
+ }
118
+ if (status.terminal) {
119
+ process.stdout.write(`Terminal: ${status.terminal.status} at ${status.terminal.completed_at}`
120
+ + `${status.terminal.reason ? ` (${status.terminal.reason})` : ""}\n`);
121
+ }
122
+ }
123
+ function writeInspection(snapshot, json) {
124
+ const projection = inspectionProjection(snapshot);
125
+ if (json) {
126
+ writeJson(projection);
127
+ return;
128
+ }
129
+ writeHumanStatus(statusProjection(snapshot));
130
+ process.stdout.write("\nAttempts:\n");
131
+ if (projection.attempts.length === 0) {
132
+ process.stdout.write(" (none)\n");
133
+ }
134
+ for (const attempt of projection.attempts) {
135
+ process.stdout.write(` ${attempt.attempt_id} step=${attempt.step_id}`
136
+ + ` worker=${attempt.worker_session_id} phase=${attempt.phase}`
137
+ + ` pid=${attempt.process_id ?? "-"}`
138
+ + ` outcome=${attempt.terminal_outcome?.kind ?? "none"}\n`);
139
+ }
140
+ process.stdout.write("\nResolved handoffs:\n");
141
+ if (projection.handoffs.length === 0) {
142
+ process.stdout.write(" (none)\n");
143
+ }
144
+ for (const handoff of projection.handoffs) {
145
+ process.stdout.write(` attempt=${handoff.attempt_id} worker=${handoff.worker_session_id}`
146
+ + ` binding=${JSON.stringify(handoff.binding)}\n`);
147
+ }
148
+ process.stdout.write("\nCommitted results:\n");
149
+ if (projection.results.length === 0) {
150
+ process.stdout.write(" (none)\n");
151
+ }
152
+ for (const result of projection.results) {
153
+ process.stdout.write(` ${result.result_id} attempt=${result.attempt_id}`
154
+ + ` worker=${result.worker_session_id}`
155
+ + ` outputs=${JSON.stringify(result.outputs)}\n`);
156
+ }
157
+ process.stdout.write("\nEvents:\n");
158
+ if (projection.events.length === 0) {
159
+ process.stdout.write(" (none)\n");
160
+ }
161
+ for (const event of projection.events) {
162
+ process.stdout.write(` ${event.sequence} ${event.type}`
163
+ + ` step=${event.step_id ?? "-"} attempt=${event.attempt_id ?? "-"}`
164
+ + ` worker=${event.worker_session_id ?? "-"}`
165
+ + ` payload=${JSON.stringify(event.payload)}\n`);
166
+ }
167
+ }
168
+ function writeOperation(action, snapshot, json) {
169
+ const status = statusProjection(snapshot);
170
+ if (json) {
171
+ writeJson({
172
+ schema_version: "intentdna.run_lifecycle_operation.v1",
173
+ action,
174
+ status,
175
+ });
176
+ return;
177
+ }
178
+ writeHumanStatus(status, action);
179
+ }
180
+ function operationExitCode(snapshot) {
181
+ return snapshot.run.status === "failed" ? 1 : 0;
182
+ }
183
+ async function handle(operation) {
184
+ try {
185
+ return await operation();
186
+ }
187
+ catch (error) {
188
+ const prefix = error instanceof LifecycleUsageError ? "Usage error" : "Error";
189
+ process.stderr.write(`${prefix}: ${error instanceof Error ? error.message : String(error)}\n`);
190
+ return error instanceof LifecycleUsageError ? 2 : 1;
191
+ }
192
+ }
193
+ export async function runLifecycleStart(options) {
194
+ return handle(async () => {
195
+ validateStartRun(options.run);
196
+ await options.controller.createRun({ run: options.run });
197
+ const snapshot = await options.controller.runUntilTerminal(options.run.run_id);
198
+ writeOperation("start", snapshot, options.json === true);
199
+ return operationExitCode(snapshot);
200
+ });
201
+ }
202
+ export async function runLifecycleStatus(options) {
203
+ return handle(async () => {
204
+ requireRunId(options.run_id);
205
+ const snapshot = await options.run_store.requireRun(options.run_id);
206
+ const projection = statusProjection(snapshot);
207
+ if (options.json === true)
208
+ writeJson(projection);
209
+ else
210
+ writeHumanStatus(projection);
211
+ return 0;
212
+ });
213
+ }
214
+ export async function runLifecycleInspect(options) {
215
+ return handle(async () => {
216
+ requireRunId(options.run_id);
217
+ const snapshot = await options.run_store.requireRun(options.run_id);
218
+ writeInspection(snapshot, options.json === true);
219
+ return 0;
220
+ });
221
+ }
222
+ export async function runLifecycleResume(options) {
223
+ return handle(async () => {
224
+ requireRunId(options.run_id);
225
+ const snapshot = await options.controller.runUntilTerminal(options.run_id);
226
+ writeOperation("resume", snapshot, options.json === true);
227
+ return operationExitCode(snapshot);
228
+ });
229
+ }
230
+ export async function runLifecycleCancel(options) {
231
+ return handle(async () => {
232
+ requireRunId(options.run_id);
233
+ const reason = options.reason ?? null;
234
+ if (reason !== null && !reason.trim()) {
235
+ throw new LifecycleUsageError("reason must not be empty");
236
+ }
237
+ const snapshot = await options.controller.requestCancellation(options.run_id, reason);
238
+ writeOperation("cancel", snapshot, options.json === true);
239
+ return 0;
240
+ });
241
+ }
@@ -1,16 +1,19 @@
1
1
  /**
2
- * Intent DNA dna run command
2
+ * Intent DNA - canonical dna run command.
3
3
  *
4
- * TypeScript runtime execution of workflows.
5
- * Loads DNA compiles auto-syncs agents/hooks executes steps
6
- * via `claude -p --agent` child processes.
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 type { WorkflowStep, ParallelGroup, TransitionRule } from "../../schema/types.js";
9
- import type { ConstraintIR } from "../../schema/types.js";
8
+ import type { TransitionRule } from "../../schema/types.js";
9
+ import { type RunLifecycleAction } from "./run-lifecycle.js";
10
+ export type RunProviderKind = "claude" | "codex";
10
11
  export interface RunOptions {
12
+ action?: RunLifecycleAction;
13
+ runId?: string;
11
14
  dnaFiles: string[];
12
- workflowName: string;
13
- taskId: string;
15
+ workflowName?: string;
16
+ taskId?: string;
14
17
  context?: string;
15
18
  vars?: Record<string, string>;
16
19
  dryRun?: boolean;
@@ -20,46 +23,25 @@ export interface RunOptions {
20
23
  agentsDir?: string;
21
24
  hooksDir?: string;
22
25
  projectDir?: string;
26
+ provider?: RunProviderKind;
27
+ providerExecutable?: string;
28
+ maxConcurrency?: number;
29
+ timeoutMs?: number | null;
30
+ cancellationGraceMs?: number;
31
+ reason?: string | null;
32
+ json?: boolean;
23
33
  }
34
+ /**
35
+ * Retained as a source-compatible result shape for callers that only use the
36
+ * pure legacy routing helper below. Canonical execution truth is RunStoreSnapshot.
37
+ */
24
38
  export interface StepResult {
25
39
  stepId: string;
26
40
  status: "pass" | "fail" | "skipped";
27
41
  durationMs: number;
28
42
  resultFile?: string;
29
43
  }
30
- interface StepExecOptions {
31
- agentPrefix: string;
32
- maxBudget: number;
33
- permissionMode: string;
34
- logDir: string;
35
- projectDir: string;
36
- workflowName: string;
37
- workflowAsset?: string;
38
- inputs: Record<string, string>;
39
- ir: Pick<ConstraintIR, "verifier_policy">;
40
- }
41
- /**
42
- * Replace {{var}} placeholders in a template string.
43
- * Unmatched placeholders are preserved as-is.
44
- */
45
44
  export declare function substituteTemplate(template: string, vars: Record<string, string>): string;
46
- /**
47
- * Evaluate a run_if condition against the current round.
48
- * Returns true if the step should execute.
49
- */
50
45
  export declare function evaluateRunIf(condition: string | null, round: number): boolean;
51
- /**
52
- * Execute a single workflow step via `claude -p --agent`.
53
- * Returns StepResult with pass/fail status.
54
- */
55
- export declare function executeStep(step: WorkflowStep, vars: Record<string, string>, opts: StepExecOptions, iteration?: number): Promise<StepResult>;
56
- /**
57
- * Execute a parallel group of steps.
58
- */
59
- export declare function executeGroup(group: ParallelGroup, allSteps: WorkflowStep[], round: number, vars: Record<string, string>, opts: StepExecOptions): Promise<StepResult[]>;
60
- /**
61
- * Check if any transition requires retry.
62
- */
63
46
  export declare function checkRetryNeeded(stepResults: Map<string, StepResult>, transitions: TransitionRule[]): boolean;
64
47
  export declare function runRun(opts: RunOptions): Promise<number>;
65
- export {};