@kal-elsam/kairo-runtime 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ export const PI_CODING_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
7
7
 
8
8
  const CUSTOM_DIR_MESSAGE =
9
9
  `Pi config writes are unsupported when ${PI_CODING_AGENT_DIR_ENV} points away from `
10
- + `~/${PI_DEFAULT_ROOT_DIR} (out of scope for Kairo 0.6.0). `
10
+ + `~/${PI_DEFAULT_ROOT_DIR} (out of scope for Kairo managed config). `
11
11
  + `Unset ${PI_CODING_AGENT_DIR_ENV} to manage the default directory; runtime launches remain available.`;
12
12
 
13
13
  /** True when PI_CODING_AGENT_DIR is set to a non-default absolute/relative path. */
@@ -30,12 +30,15 @@ export function buildPiPermissionsArgs(permissions = []) {
30
30
  }
31
31
 
32
32
  throw new Error(
33
- `Pi permissions only support "read-only" in Kairo 0.6.0 (got: ${permissions.join(", ")}). `
33
+ `Pi permissions only support "read-only" (got: ${permissions.join(", ")}). `
34
34
  + "Other aliases are rejected and are never translated to --approve."
35
35
  );
36
36
  }
37
37
 
38
- export function buildPiLaunch({ task, cwd, model, permissions = [], env = process.env } = {}) {
38
+ export function buildPiLaunch({
39
+ task, cwd, model, permissions = [], env = process.env,
40
+ strategy = "direct", extensionPath = null
41
+ } = {}) {
39
42
  const args = [
40
43
  "--mode",
41
44
  "json",
@@ -47,6 +50,13 @@ export function buildPiLaunch({ task, cwd, model, permissions = [], env = proces
47
50
  args.push("--model", model);
48
51
  }
49
52
 
53
+ if (strategy === "orchestrated") {
54
+ if (typeof extensionPath !== "string" || !extensionPath) {
55
+ throw new Error("Orchestrated Pi requires a managed extension path.");
56
+ }
57
+ args.push("--no-extensions", "--extension", extensionPath);
58
+ }
59
+
50
60
  args.push(task);
51
61
 
52
62
  return {
@@ -0,0 +1,23 @@
1
+ import { join } from "node:path";
2
+ import { harnessHomePaths } from "../../paths.js";
3
+
4
+ export {
5
+ RUN_STRATEGIES, DAG_NODE_STATES, DAG_TERMINAL_STATES, ORCH_LIMITS, ORCH_ERROR_CODES,
6
+ OrchContractError, createTaskId, isTerminalDagState, normalizeRunStrategy,
7
+ createOrchLineage, createBudgetUsage, createDagNode, createMinionBrief,
8
+ createMinionResult, digestAllowlisted
9
+ } from "./orch-types.js";
10
+ export { assertOrchReceiptSecretFree, FORBIDDEN_KEYS, walkForbiddenKeys } from "./orch-validate.js";
11
+ export {
12
+ orchPaths, buildOrchReceipt, saveOrchReceipt, loadOrchReceipt,
13
+ createOrchState, saveOrchState, loadOrchState, terminalizeOrchNodes,
14
+ updateOrchState, applyMinionDagUpdate, finalizeOrchState, reconcileOrchState
15
+ } from "./orch-receipts.js";
16
+
17
+ export const KAIRO_MINION_RELATIVE_ASSET =
18
+ "components/orchestrator/extensions/pi/kairo-minion.js";
19
+
20
+ /** Materialized extension path under ~/.harness (never Pi global auto-discover). */
21
+ export function resolveKairoMinionExtensionPath(homeDir) {
22
+ return join(harnessHomePaths(homeDir).root, KAIRO_MINION_RELATIVE_ASSET);
23
+ }
@@ -0,0 +1,234 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { runPaths } from "../../paths.js";
5
+ import { writeAtomicJson } from "../write-atomic-json.js";
6
+ import {
7
+ DAG_NODE_STATES, ORCH_ERROR_CODES, OrchContractError, RUN_STRATEGIES,
8
+ createBudgetUsage, createDagNode, createMinionResult, createOrchLineage,
9
+ digestAllowlisted, isTerminalDagState, normalizeRunStrategy
10
+ } from "./orch-types.js";
11
+ import { assertOrchReceiptSecretFree, walkForbiddenKeys } from "./orch-validate.js";
12
+
13
+ export function orchPaths(homeDir, rootRunId) {
14
+ if (typeof rootRunId !== "string" || !rootRunId) {
15
+ throw new OrchContractError("rootRunId is required for orchestration paths.", {
16
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
17
+ });
18
+ }
19
+ const { runDir } = runPaths(homeDir, rootRunId);
20
+ const orchDir = join(runDir, "orchestration");
21
+ return {
22
+ runDir, orchDir,
23
+ receiptPath: join(orchDir, "receipt.json"),
24
+ statePath: join(orchDir, "state.json")
25
+ };
26
+ }
27
+
28
+ function normalizeNodes(nodes = []) {
29
+ return nodes.map((node) => {
30
+ const { objective, ...rest } = node;
31
+ return createDagNode({
32
+ ...rest,
33
+ budget: createBudgetUsage(rest.budget ?? {}),
34
+ objectiveDigest: rest.objectiveDigest
35
+ ?? (objective ? digestAllowlisted({ objective }) : null)
36
+ });
37
+ });
38
+ }
39
+
40
+ export function buildOrchReceipt({
41
+ rootRunId, strategy = RUN_STRATEGIES.ORCHESTRATED, lineage = null,
42
+ nodes = [], results = [], cliVersion = null, createdAt = null, recovered = false
43
+ } = {}) {
44
+ const normalizedStrategy = normalizeRunStrategy(strategy);
45
+ const normalizedLineage = createOrchLineage(lineage ?? { rootRunId, parentRunId: null, depth: 0 });
46
+ if (normalizedLineage.rootRunId !== rootRunId) {
47
+ throw new OrchContractError("lineage.rootRunId must match receipt rootRunId.", {
48
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
49
+ });
50
+ }
51
+ return assertOrchReceiptSecretFree({
52
+ version: 1, strategy: normalizedStrategy, rootRunId, lineage: normalizedLineage,
53
+ nodes: normalizeNodes(nodes),
54
+ results: results.map((entry) => createMinionResult(entry)),
55
+ createdAt: createdAt ?? new Date().toISOString(),
56
+ cliVersion,
57
+ recovered: Boolean(recovered)
58
+ });
59
+ }
60
+
61
+ export async function saveOrchReceipt(receipt, { homeDir } = {}) {
62
+ const sanitized = assertOrchReceiptSecretFree(receipt);
63
+ const { orchDir, receiptPath } = orchPaths(homeDir, sanitized.rootRunId);
64
+ await mkdir(orchDir, { recursive: true });
65
+ try {
66
+ await writeAtomicJson(receiptPath, sanitized, { createExclusive: true });
67
+ } catch (error) {
68
+ if (error?.code === "EEXIST") {
69
+ throw new OrchContractError(`Orchestration receipt already exists: ${sanitized.rootRunId}`, {
70
+ code: ORCH_ERROR_CODES.RECEIPT_EXISTS,
71
+ details: { rootRunId: sanitized.rootRunId, path: receiptPath }
72
+ });
73
+ }
74
+ throw error;
75
+ }
76
+ return { path: receiptPath, receipt: sanitized };
77
+ }
78
+
79
+ export async function loadOrchReceipt(rootRunId, { homeDir } = {}) {
80
+ const { receiptPath } = orchPaths(homeDir, rootRunId);
81
+ if (!existsSync(receiptPath)) {
82
+ throw new OrchContractError(`Orchestration receipt not found: ${rootRunId}`, {
83
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE, details: { rootRunId }
84
+ });
85
+ }
86
+ return assertOrchReceiptSecretFree(JSON.parse(await readFile(receiptPath, "utf8")));
87
+ }
88
+
89
+ export function createOrchState({
90
+ rootRunId, strategy = RUN_STRATEGIES.ORCHESTRATED, lineage = null,
91
+ nodes = [], results = [], cliVersion = null, updatedAt = null
92
+ } = {}) {
93
+ const normalizedStrategy = normalizeRunStrategy(strategy);
94
+ if (normalizedStrategy !== RUN_STRATEGIES.ORCHESTRATED) {
95
+ throw new OrchContractError("Orchestration state requires strategy orchestrated.", {
96
+ code: ORCH_ERROR_CODES.INVALID_STRATEGY
97
+ });
98
+ }
99
+ const normalizedLineage = createOrchLineage(lineage ?? { rootRunId, parentRunId: null, depth: 0 });
100
+ if (normalizedLineage.rootRunId !== rootRunId) {
101
+ throw new OrchContractError("lineage.rootRunId must match state rootRunId.", {
102
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
103
+ });
104
+ }
105
+ const state = {
106
+ version: 1, strategy: normalizedStrategy, rootRunId, lineage: normalizedLineage,
107
+ nodes: normalizeNodes(nodes),
108
+ results: (results ?? []).map((entry) => createMinionResult(entry)),
109
+ cliVersion: cliVersion ?? null,
110
+ updatedAt: updatedAt ?? new Date().toISOString()
111
+ };
112
+ walkForbiddenKeys(state);
113
+ return state;
114
+ }
115
+
116
+ export async function saveOrchState(state, { homeDir } = {}) {
117
+ const sanitized = createOrchState(state);
118
+ const { orchDir, statePath } = orchPaths(homeDir, sanitized.rootRunId);
119
+ await mkdir(orchDir, { recursive: true });
120
+ await writeAtomicJson(statePath, sanitized);
121
+ return { path: statePath, state: sanitized };
122
+ }
123
+
124
+ export async function loadOrchState(rootRunId, { homeDir } = {}) {
125
+ const { statePath } = orchPaths(homeDir, rootRunId);
126
+ if (!existsSync(statePath)) {
127
+ throw new OrchContractError(`Orchestration state not found: ${rootRunId}`, {
128
+ code: ORCH_ERROR_CODES.INVALID_NODE, details: { rootRunId }
129
+ });
130
+ }
131
+ try {
132
+ return createOrchState(JSON.parse(await readFile(statePath, "utf8")));
133
+ } catch {
134
+ throw new OrchContractError(`Corrupt orchestration state: ${rootRunId}`, {
135
+ code: ORCH_ERROR_CODES.INVALID_NODE, details: { rootRunId }
136
+ });
137
+ }
138
+ }
139
+
140
+ export function terminalizeOrchNodes(nodes, { recovered = false } = {}) {
141
+ return (nodes ?? []).map((node) => {
142
+ if (isTerminalDagState(node.state)) return createDagNode(node);
143
+ return createDagNode({
144
+ ...node,
145
+ state: recovered ? DAG_NODE_STATES.CANCELLED : DAG_NODE_STATES.COMPLETED,
146
+ error: recovered ? (node.error ?? { code: "interrupted" }) : node.error
147
+ });
148
+ });
149
+ }
150
+
151
+ const orchWriteLocks = new Map();
152
+
153
+ function withOrchWriteLock(rootRunId, work) {
154
+ const previous = orchWriteLocks.get(rootRunId) ?? Promise.resolve();
155
+ const next = previous.then(work);
156
+ orchWriteLocks.set(rootRunId, next.catch(() => {}));
157
+ return next;
158
+ }
159
+
160
+ /** Serialized load → mutate → save for concurrent minion DAG updates. */
161
+ export async function updateOrchState(rootRunId, mutator, { homeDir } = {}) {
162
+ return withOrchWriteLock(rootRunId, async () => {
163
+ const current = await loadOrchState(rootRunId, { homeDir });
164
+ return saveOrchState(await mutator(current), { homeDir });
165
+ });
166
+ }
167
+
168
+ /** Upsert one depth-1 node by taskId; append/replace MinionResult on completed. */
169
+ export async function applyMinionDagUpdate(rootRunId, {
170
+ homeDir, taskId, parentTaskId, attempt = 0, state,
171
+ objectiveDigest = null, result = null, error = null
172
+ } = {}) {
173
+ return updateOrchState(rootRunId, (current) => {
174
+ const rootTaskId = current.lineage?.taskId;
175
+ if (!rootTaskId || taskId === rootTaskId || parentTaskId !== rootTaskId) {
176
+ throw new OrchContractError("Minion taskId/parentTaskId must honor supervisor rootTaskId.", {
177
+ code: ORCH_ERROR_CODES.INVALID_NODE, details: { taskId, parentTaskId, rootTaskId }
178
+ });
179
+ }
180
+ const node = createDagNode({
181
+ taskId, parentTaskId, depth: 1, state, attempt,
182
+ objectiveDigest: objectiveDigest ?? null, error: error ?? null
183
+ });
184
+ const nodes = [...current.nodes];
185
+ const idx = nodes.findIndex((entry) => entry.taskId === taskId);
186
+ if (idx >= 0) {
187
+ nodes[idx] = createDagNode({
188
+ ...nodes[idx], ...node,
189
+ objectiveDigest: node.objectiveDigest ?? nodes[idx].objectiveDigest
190
+ });
191
+ } else {
192
+ nodes.push(node);
193
+ }
194
+ let results = current.results;
195
+ if (state === DAG_NODE_STATES.COMPLETED && result) {
196
+ const sealed = createMinionResult(result);
197
+ results = [...results.filter((entry) => entry.taskId !== taskId), sealed];
198
+ }
199
+ return { ...current, nodes, results, updatedAt: new Date().toISOString() };
200
+ }, { homeDir });
201
+ }
202
+
203
+ export async function finalizeOrchState(rootRunId, { homeDir, recovered = false } = {}) {
204
+ return withOrchWriteLock(rootRunId, async () => {
205
+ const { receiptPath } = orchPaths(homeDir, rootRunId);
206
+ if (existsSync(receiptPath)) {
207
+ return { path: receiptPath, receipt: await loadOrchReceipt(rootRunId, { homeDir }), idempotent: true };
208
+ }
209
+ const state = await loadOrchState(rootRunId, { homeDir });
210
+ const nodes = terminalizeOrchNodes(state.nodes, { recovered });
211
+ await saveOrchState({ ...state, nodes, updatedAt: new Date().toISOString() }, { homeDir });
212
+ try {
213
+ const saved = await saveOrchReceipt(buildOrchReceipt({
214
+ rootRunId: state.rootRunId, strategy: state.strategy, lineage: state.lineage,
215
+ nodes, results: state.results, cliVersion: state.cliVersion, recovered
216
+ }), { homeDir });
217
+ return { ...saved, idempotent: false };
218
+ } catch (error) {
219
+ if (error?.code === ORCH_ERROR_CODES.RECEIPT_EXISTS) {
220
+ return { path: receiptPath, receipt: await loadOrchReceipt(rootRunId, { homeDir }), idempotent: true };
221
+ }
222
+ throw error;
223
+ }
224
+ });
225
+ }
226
+
227
+ export async function reconcileOrchState(rootRunId, { homeDir } = {}) {
228
+ const { statePath, receiptPath } = orchPaths(homeDir, rootRunId);
229
+ if (existsSync(receiptPath)) {
230
+ return { rootRunId, path: receiptPath, receipt: await loadOrchReceipt(rootRunId, { homeDir }), idempotent: true };
231
+ }
232
+ if (!existsSync(statePath)) return null;
233
+ return finalizeOrchState(rootRunId, { homeDir, recovered: true });
234
+ }
@@ -0,0 +1,173 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ export const RUN_STRATEGIES = Object.freeze({ DIRECT: "direct", ORCHESTRATED: "orchestrated" });
3
+ export const DAG_NODE_STATES = Object.freeze({
4
+ PENDING: "pending", READY: "ready", RUNNING: "running", COMPACTING: "compacting",
5
+ COMPLETED: "completed", FAILED: "failed", CANCELLED: "cancelled", BLOCKED: "blocked"
6
+ });
7
+ export const DAG_TERMINAL_STATES = new Set([
8
+ DAG_NODE_STATES.COMPLETED, DAG_NODE_STATES.FAILED, DAG_NODE_STATES.CANCELLED
9
+ ]);
10
+ export const ORCH_LIMITS = Object.freeze({
11
+ MAX_DEPTH: 1, DEFAULT_CONCURRENCY: 2, MAX_ATTEMPTS: 2, COMPACT_RATIO: 0.7, STOP_RATIO: 0.9
12
+ });
13
+ export const ORCH_ERROR_CODES = Object.freeze({
14
+ INVALID_STRATEGY: "invalid_strategy", INVALID_DEPTH: "invalid_depth",
15
+ INVALID_LINEAGE: "invalid_lineage", INVALID_HANDOFF: "invalid_handoff",
16
+ INVALID_NODE: "invalid_node", LIMIT_EXCEEDED: "limit_exceeded",
17
+ FORBIDDEN_FIELD: "forbidden_field", RECEIPT_EXISTS: "receipt_exists"
18
+ });
19
+ export class OrchContractError extends Error {
20
+ constructor(message, { code, details = null } = {}) {
21
+ super(message);
22
+ this.name = "OrchContractError";
23
+ this.code = code;
24
+ this.details = details;
25
+ }
26
+ }
27
+ export function createTaskId() {
28
+ return `task_${randomBytes(8).toString("hex")}`;
29
+ }
30
+ export function isTerminalDagState(state) {
31
+ return DAG_TERMINAL_STATES.has(state);
32
+ }
33
+
34
+ function assertDepth(depth) {
35
+ const d = Number(depth);
36
+ if (!Number.isInteger(d) || d < 0) {
37
+ throw new OrchContractError(`Invalid depth "${depth}".`, {
38
+ code: ORCH_ERROR_CODES.INVALID_DEPTH, details: { depth }
39
+ });
40
+ }
41
+ if (d > ORCH_LIMITS.MAX_DEPTH) {
42
+ throw new OrchContractError(`Orchestration depth ${d} exceeds max ${ORCH_LIMITS.MAX_DEPTH}.`, {
43
+ code: ORCH_ERROR_CODES.INVALID_DEPTH, details: { depth: d }
44
+ });
45
+ }
46
+ return d;
47
+ }
48
+
49
+ function assertParentForDepth(depth, parentId, { rootMsg, minionMsg, code }) {
50
+ if (depth === 0 && parentId != null) {
51
+ throw new OrchContractError(rootMsg, { code });
52
+ }
53
+ if (depth > 0 && !parentId) {
54
+ throw new OrchContractError(minionMsg, { code });
55
+ }
56
+ }
57
+
58
+ export function normalizeRunStrategy(value = RUN_STRATEGIES.DIRECT) {
59
+ const strategy = String(value ?? RUN_STRATEGIES.DIRECT).trim().toLowerCase();
60
+ if (!Object.values(RUN_STRATEGIES).includes(strategy)) {
61
+ throw new OrchContractError(`Invalid run strategy "${value}". Use direct or orchestrated.`, {
62
+ code: ORCH_ERROR_CODES.INVALID_STRATEGY, details: { value }
63
+ });
64
+ }
65
+ return strategy;
66
+ }
67
+
68
+ /** Lineage: max depth 1 (root=0, minion=1). */
69
+ export function createOrchLineage({ rootRunId, parentRunId = null, taskId = null, depth = 0 } = {}) {
70
+ if (typeof rootRunId !== "string" || !rootRunId) {
71
+ throw new OrchContractError("rootRunId is required.", { code: ORCH_ERROR_CODES.INVALID_LINEAGE });
72
+ }
73
+ const d = assertDepth(depth);
74
+ assertParentForDepth(d, parentRunId, {
75
+ rootMsg: "Root nodes must not set parentRunId.",
76
+ minionMsg: "Minion nodes require parentRunId.",
77
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
78
+ });
79
+ return { rootRunId, parentRunId: parentRunId ?? null, taskId: taskId ?? createTaskId(), depth: d };
80
+ }
81
+
82
+ export function createBudgetUsage({
83
+ contextTokens = 0, contextLimit = 0,
84
+ compactRatio = ORCH_LIMITS.COMPACT_RATIO, stopRatio = ORCH_LIMITS.STOP_RATIO
85
+ } = {}) {
86
+ const tokens = Math.max(0, Number(contextTokens) || 0);
87
+ const limit = Math.max(0, Number(contextLimit) || 0);
88
+ const ratio = limit > 0 ? tokens / limit : 0;
89
+ return {
90
+ contextTokens: tokens, contextLimit: limit, ratio,
91
+ shouldCompact: limit > 0 && ratio >= compactRatio && ratio < stopRatio,
92
+ shouldStop: limit > 0 && ratio >= stopRatio
93
+ };
94
+ }
95
+
96
+ export function createDagNode({
97
+ taskId = null, runId = null, parentTaskId = null, depth = 0,
98
+ state = DAG_NODE_STATES.PENDING, dependsOn = [], attempt = 0,
99
+ objectiveDigest = null, budget = null, resultDigest = null, error = null
100
+ } = {}) {
101
+ if (!Object.values(DAG_NODE_STATES).includes(state)) {
102
+ throw new OrchContractError(`Invalid DAG node state "${state}".`, {
103
+ code: ORCH_ERROR_CODES.INVALID_NODE, details: { state }
104
+ });
105
+ }
106
+ const d = assertDepth(depth);
107
+ assertParentForDepth(d, parentTaskId, {
108
+ rootMsg: "Root nodes must not set parentTaskId.",
109
+ minionMsg: "Minion nodes require parentTaskId.",
110
+ code: ORCH_ERROR_CODES.INVALID_NODE
111
+ });
112
+ const attempts = Number(attempt) || 0;
113
+ if (attempts < 0 || attempts > ORCH_LIMITS.MAX_ATTEMPTS) {
114
+ throw new OrchContractError(`Attempt ${attempts} outside 0..${ORCH_LIMITS.MAX_ATTEMPTS}.`, {
115
+ code: ORCH_ERROR_CODES.LIMIT_EXCEEDED, details: { attempt: attempts }
116
+ });
117
+ }
118
+ const deps = [...new Set((dependsOn ?? []).map((id) => {
119
+ if (typeof id !== "string" || !id) {
120
+ throw new OrchContractError("Dependency taskId is required.", { code: ORCH_ERROR_CODES.INVALID_NODE });
121
+ }
122
+ return id;
123
+ }))];
124
+ return {
125
+ taskId: taskId ?? createTaskId(), runId: runId ?? null, parentTaskId: parentTaskId ?? null,
126
+ depth: d, state, dependsOn: deps, attempt: attempts,
127
+ objectiveDigest, budget: budget ?? null, resultDigest, error: error ?? null
128
+ };
129
+ }
130
+
131
+ export function createMinionBrief({
132
+ objective, constraints = [], admittedPaths = [], exitCriteria = [],
133
+ parentTaskId = null, taskId = null
134
+ } = {}) {
135
+ if (typeof objective !== "string" || !objective.trim()) {
136
+ throw new OrchContractError("Minion brief requires a non-empty objective.", {
137
+ code: ORCH_ERROR_CODES.INVALID_HANDOFF
138
+ });
139
+ }
140
+ return {
141
+ taskId: taskId ?? createTaskId(), parentTaskId: parentTaskId ?? null,
142
+ objective: objective.trim(), constraints: (constraints ?? []).map(String),
143
+ admittedPaths: (admittedPaths ?? []).map(String), exitCriteria: (exitCriteria ?? []).map(String)
144
+ };
145
+ }
146
+
147
+ export function createMinionResult({
148
+ taskId = null, summary, decisions = [], files = [], risks = [],
149
+ evidence = [], usage = null, compact = false
150
+ } = {}) {
151
+ if (typeof taskId !== "string" || !taskId) {
152
+ throw new OrchContractError("Minion result requires taskId.", { code: ORCH_ERROR_CODES.INVALID_HANDOFF });
153
+ }
154
+ if (typeof summary !== "string" || !summary.trim()) {
155
+ throw new OrchContractError("Minion result requires a non-empty summary.", {
156
+ code: ORCH_ERROR_CODES.INVALID_HANDOFF
157
+ });
158
+ }
159
+ return {
160
+ taskId, summary: summary.trim(),
161
+ decisions: (decisions ?? []).map(String), files: (files ?? []).map(String),
162
+ risks: (risks ?? []).map(String), evidence: (evidence ?? []).map(String),
163
+ usage: {
164
+ inputTokens: usage?.inputTokens ?? null, outputTokens: usage?.outputTokens ?? null,
165
+ totalTokens: usage?.totalTokens ?? null, cost: usage?.cost ?? null
166
+ },
167
+ compact: Boolean(compact)
168
+ };
169
+ }
170
+
171
+ export function digestAllowlisted(value) {
172
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
173
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ ORCH_ERROR_CODES, OrchContractError, createBudgetUsage, createDagNode,
3
+ createMinionResult, createOrchLineage, normalizeRunStrategy
4
+ } from "./orch-types.js";
5
+ export const FORBIDDEN_KEYS = new Set([
6
+ "prompt", "diff", "transcript", "raw", "rawOutput", "stdout", "stderr", "output",
7
+ "message", "messages", "content", "secret", "secrets", "token", "apiKey",
8
+ "conversation", "history", "toolArgs", "arguments", "objective"
9
+ ]);
10
+ export function walkForbiddenKeys(value, path = "") {
11
+ if (!value || typeof value !== "object") return;
12
+ if (Array.isArray(value)) {
13
+ value.forEach((item, i) => walkForbiddenKeys(item, `${path}[${i}]`));
14
+ return;
15
+ }
16
+ for (const [key, child] of Object.entries(value)) {
17
+ if (FORBIDDEN_KEYS.has(key)) {
18
+ throw new OrchContractError(`Forbidden field "${path}${key}" in orchestration receipt.`, {
19
+ code: ORCH_ERROR_CODES.FORBIDDEN_FIELD, details: { key, path }
20
+ });
21
+ }
22
+ walkForbiddenKeys(child, `${path}${key}.`);
23
+ }
24
+ }
25
+ export function assertOrchReceiptSecretFree(receipt) {
26
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
27
+ throw new OrchContractError("Invalid orchestration receipt: expected object.", {
28
+ code: ORCH_ERROR_CODES.FORBIDDEN_FIELD
29
+ });
30
+ }
31
+ walkForbiddenKeys(receipt);
32
+ if (receipt.version !== 1) {
33
+ throw new OrchContractError("Orchestration receipt version must be 1.", {
34
+ code: ORCH_ERROR_CODES.FORBIDDEN_FIELD
35
+ });
36
+ }
37
+ normalizeRunStrategy(receipt.strategy);
38
+ if (typeof receipt.rootRunId !== "string" || !receipt.rootRunId) {
39
+ throw new OrchContractError("rootRunId is required on receipt.", {
40
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
41
+ });
42
+ }
43
+ createOrchLineage(receipt.lineage);
44
+ if (receipt.lineage.rootRunId !== receipt.rootRunId) {
45
+ throw new OrchContractError("lineage.rootRunId must match receipt.rootRunId.", {
46
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
47
+ });
48
+ }
49
+ if (!Array.isArray(receipt.nodes) || !Array.isArray(receipt.results)) {
50
+ throw new OrchContractError("Receipt requires nodes[] and results[].", {
51
+ code: ORCH_ERROR_CODES.INVALID_NODE
52
+ });
53
+ }
54
+ for (const node of receipt.nodes) {
55
+ createDagNode(node);
56
+ if (node.budget) createBudgetUsage(node.budget);
57
+ }
58
+ for (const result of receipt.results) createMinionResult(result);
59
+ if (typeof receipt.createdAt !== "string" || !receipt.createdAt) {
60
+ throw new OrchContractError("createdAt is required.", { code: ORCH_ERROR_CODES.FORBIDDEN_FIELD });
61
+ }
62
+ return receipt;
63
+ }
@@ -45,6 +45,7 @@ export async function runGlobalRun(options, packageManifest, { startRunImpl = st
45
45
  captureTranscript,
46
46
  cliVersion: packageManifest.version,
47
47
  profile: profileResolved,
48
+ strategy: options.strategy ?? "direct",
48
49
  follow: options.follow,
49
50
  timeoutMs: options.timeoutMs,
50
51
  wait: options.wait !== false
@@ -5,6 +5,7 @@ import {
5
5
  appendRunEvent,
6
6
  appendRunStartedEvent,
7
7
  createRunRecord,
8
+ listRunRecords,
8
9
  readRunState,
9
10
  reconcileActiveRuns,
10
11
  writeRunState
@@ -22,6 +23,20 @@ import {
22
23
  readSupervisorLockForRun,
23
24
  supervisePreparedRun
24
25
  } from "./run-supervisor.js";
26
+ import {
27
+ assertManagedMinionExtension,
28
+ assertOrchestratedAgent,
29
+ createRootRunLineage,
30
+ normalizeRunStrategy,
31
+ RUN_STRATEGIES
32
+ } from "./run-strategy.js";
33
+ import {
34
+ DAG_NODE_STATES,
35
+ createDagNode,
36
+ createOrchState,
37
+ reconcileOrchState,
38
+ saveOrchState
39
+ } from "./orchestration/index.js";
25
40
 
26
41
  const activeProcesses = new Map();
27
42
  const cancelledRuns = new Set();
@@ -55,6 +70,19 @@ export async function recoverRuns(homeDir) {
55
70
  exceptRunIds: listActiveRunIds(),
56
71
  isRunAliveImpl: isRunSupervisedAlive
57
72
  });
73
+ for (const run of await listRunRecords(homeDir)) {
74
+ if (normalizeRunStrategy(run.strategy ?? RUN_STRATEGIES.DIRECT) !== RUN_STRATEGIES.ORCHESTRATED) {
75
+ continue;
76
+ }
77
+ if (await isRunSupervisedAlive(homeDir, run)) {
78
+ continue;
79
+ }
80
+ try {
81
+ await reconcileOrchState(run.runId, { homeDir });
82
+ } catch {
83
+ // Fail closed per root: never invent receipt evidence from corrupt state.
84
+ }
85
+ }
58
86
  return interrupted;
59
87
  }
60
88
 
@@ -67,8 +95,10 @@ async function prepareRun({
67
95
  permissions = [],
68
96
  captureTranscript = false,
69
97
  cliVersion,
70
- profile = null
98
+ profile = null,
99
+ strategy = "direct"
71
100
  }) {
101
+ const normalizedStrategy = assertOrchestratedAgent(agentId, strategy);
72
102
  const adapter = resolveExecutionAdapter(agentId);
73
103
  const availability = adapter.availability({ cwd });
74
104
 
@@ -83,7 +113,12 @@ async function prepareRun({
83
113
  );
84
114
  }
85
115
 
116
+ if (normalizedStrategy === RUN_STRATEGIES.ORCHESTRATED) {
117
+ await assertManagedMinionExtension(homeDir);
118
+ }
119
+
86
120
  const runId = createRunId();
121
+ const lineage = createRootRunLineage(runId);
87
122
  const metadata = createRunMetadata({
88
123
  runId,
89
124
  agentId,
@@ -94,7 +129,9 @@ async function prepareRun({
94
129
  permissions,
95
130
  captureTranscript,
96
131
  cliVersion,
97
- profileSources: profile?.sources ?? null
132
+ profileSources: profile?.sources ?? null,
133
+ strategy: normalizedStrategy,
134
+ lineage
98
135
  });
99
136
 
100
137
  await createRunRecord(homeDir, metadata);
@@ -107,9 +144,25 @@ async function prepareRun({
107
144
  permissions,
108
145
  captureTranscript,
109
146
  cliVersion,
110
- profile: profile?.profile ?? null
147
+ profile: profile?.profile ?? null,
148
+ strategy: normalizedStrategy
111
149
  });
112
150
 
151
+ if (normalizedStrategy === RUN_STRATEGIES.ORCHESTRATED) {
152
+ await saveOrchState(createOrchState({
153
+ rootRunId: runId,
154
+ strategy: normalizedStrategy,
155
+ lineage,
156
+ nodes: [createDagNode({
157
+ taskId: lineage.taskId,
158
+ runId,
159
+ depth: 0,
160
+ state: DAG_NODE_STATES.RUNNING
161
+ })],
162
+ cliVersion
163
+ }), { homeDir });
164
+ }
165
+
113
166
  return { runId, metadata };
114
167
  }
115
168
 
@@ -123,6 +176,7 @@ export async function startRun({
123
176
  captureTranscript = false,
124
177
  cliVersion,
125
178
  profile = null,
179
+ strategy = "direct",
126
180
  follow = false,
127
181
  timeoutMs = null,
128
182
  wait = true,
@@ -138,7 +192,8 @@ export async function startRun({
138
192
  permissions,
139
193
  captureTranscript,
140
194
  cliVersion,
141
- profile
195
+ profile,
196
+ strategy: normalizeRunStrategy(strategy)
142
197
  });
143
198
 
144
199
  if (!wait) {