@kal-elsam/kairo-runtime 0.6.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.
Files changed (43) hide show
  1. package/README.md +36 -0
  2. package/global-template/components/catalog.json +4 -1
  3. package/global-template/components/orchestrator/extensions/pi/kairo-minion.js +604 -0
  4. package/package.json +1 -1
  5. package/scripts/cockpit-smoke.mjs +1 -1
  6. package/src/cli.js +69 -1
  7. package/src/global/adapters/pi.js +1 -1
  8. package/src/global/ink/cockpit-controller.js +10 -0
  9. package/src/global/ink/cockpit-focus.js +18 -3
  10. package/src/global/ink/cockpit-models.js +8 -1
  11. package/src/global/ink/cockpit-reviews.js +62 -0
  12. package/src/global/ink/cockpit-runs.js +11 -2
  13. package/src/global/ink/cockpit-views.js +19 -2
  14. package/src/global/ink/orchestrator-app.js +23 -3
  15. package/src/global/ink/orchestrator-state.js +2 -0
  16. package/src/global/ink/use-orchestrator-data.js +30 -0
  17. package/src/global/paths.js +1 -0
  18. package/src/global/runtime/execution-adapters/codex.js +2 -1
  19. package/src/global/runtime/execution-adapters/create-execution-adapter.js +1 -0
  20. package/src/global/runtime/execution-adapters/index.js +1 -0
  21. package/src/global/runtime/execution-adapters/pi.js +14 -3
  22. package/src/global/runtime/orchestration/index.js +23 -0
  23. package/src/global/runtime/orchestration/orch-receipts.js +234 -0
  24. package/src/global/runtime/orchestration/orch-types.js +173 -0
  25. package/src/global/runtime/orchestration/orch-validate.js +63 -0
  26. package/src/global/runtime/review/index.js +37 -0
  27. package/src/global/runtime/review/review-cli.js +150 -0
  28. package/src/global/runtime/review/review-codex.js +132 -0
  29. package/src/global/runtime/review/review-exec.js +136 -0
  30. package/src/global/runtime/review/review-fs.js +52 -0
  31. package/src/global/runtime/review/review-git.js +212 -0
  32. package/src/global/runtime/review/review-patch.js +122 -0
  33. package/src/global/runtime/review/review-pi.js +168 -0
  34. package/src/global/runtime/review/review-receipts.js +128 -0
  35. package/src/global/runtime/review/review-runner.js +119 -0
  36. package/src/global/runtime/review/review-types.js +108 -0
  37. package/src/global/runtime/review/review-validate.js +280 -0
  38. package/src/global/runtime/run-cli.js +1 -0
  39. package/src/global/runtime/run-manager.js +59 -4
  40. package/src/global/runtime/run-strategy.js +71 -0
  41. package/src/global/runtime/run-supervisor.js +22 -2
  42. package/src/global/runtime/run-types.js +5 -1
  43. package/src/global/runtime/write-atomic-json.js +24 -20
@@ -0,0 +1,280 @@
1
+ import {
2
+ REVIEW_SEVERITIES,
3
+ ReviewSnapshotError,
4
+ assertReviewPathSafe,
5
+ createFindingId
6
+ } from "./review-types.js";
7
+
8
+ export const REVIEW_VALIDATION_ERROR_CODES = Object.freeze({
9
+ INVALID_OUTPUT: "invalid_output",
10
+ INVALID_FINDING: "invalid_finding",
11
+ PATH_OUT_OF_SCOPE: "path_out_of_scope",
12
+ FORBIDDEN_FIELD: "forbidden_field",
13
+ RECEIPT_EXISTS: "receipt_exists"
14
+ });
15
+
16
+ const SEVERITY_SET = new Set(Object.values(REVIEW_SEVERITIES));
17
+ const FORBIDDEN_KEYS = new Set([
18
+ "prompt", "diff", "transcript", "raw", "rawOutput", "stdout", "stderr",
19
+ "output", "message", "messages", "content", "secret", "secrets", "token", "apiKey"
20
+ ]);
21
+
22
+ const RECEIPT_SHAPE = Object.freeze({
23
+ version: "number",
24
+ reviewId: "string",
25
+ agentId: "string",
26
+ model: "string?",
27
+ state: "string",
28
+ snapshot: {
29
+ mode: "string",
30
+ headSha: "string",
31
+ base: "string?",
32
+ commit: "string?",
33
+ fingerprint: "string",
34
+ totals: { fileCount: "number", changedLines: "number", diffBytes: "number" },
35
+ files: [{ path: "string", sourcePath: "string?", status: "string", hash: "string", changedLines: "number" }],
36
+ excluded: [{ path: "string", reason: "string" }]
37
+ },
38
+ findings: [{
39
+ id: "string",
40
+ severity: "string",
41
+ title: "string",
42
+ path: "string",
43
+ line: "number?",
44
+ problem: "string",
45
+ recommendation: "string"
46
+ }],
47
+ warnings: ["string"],
48
+ usage: {
49
+ inputTokens: "number?",
50
+ outputTokens: "number?",
51
+ totalTokens: "number?",
52
+ cost: "number?"
53
+ },
54
+ timings: { startedAt: "string?", finishedAt: "string?", durationMs: "number?" },
55
+ cliVersion: "string?",
56
+ createdAt: "string"
57
+ });
58
+
59
+ export class ReviewValidationError extends Error {
60
+ constructor(message, { code, details = null } = {}) {
61
+ super(message);
62
+ this.name = "ReviewValidationError";
63
+ this.code = code;
64
+ this.details = details;
65
+ }
66
+ }
67
+
68
+ function asObject(value, label) {
69
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
70
+ throw new ReviewValidationError(`Invalid ${label}: expected object.`, {
71
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_OUTPUT
72
+ });
73
+ }
74
+ return value;
75
+ }
76
+
77
+ function normalizeLine(line) {
78
+ if (line == null || line === "") return null;
79
+ if (!Number.isInteger(line) || line < 1) {
80
+ throw new ReviewValidationError(`Invalid finding line "${line}".`, {
81
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_FINDING,
82
+ details: { line }
83
+ });
84
+ }
85
+ return line;
86
+ }
87
+
88
+ function requireNonEmptyString(value, field) {
89
+ if (typeof value !== "string" || value.trim() === "") {
90
+ throw new ReviewValidationError(`Finding missing ${field}.`, {
91
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_FINDING,
92
+ details: { field }
93
+ });
94
+ }
95
+ return value.trim();
96
+ }
97
+
98
+ function isOptionalScalar(spec) {
99
+ return typeof spec === "string" && spec.endsWith("?");
100
+ }
101
+
102
+ function scalarType(spec) {
103
+ return isOptionalScalar(spec) ? spec.slice(0, -1) : spec;
104
+ }
105
+
106
+ function assertNoForbiddenKeys(value, path) {
107
+ if (Array.isArray(value)) {
108
+ value.forEach((entry, index) => assertNoForbiddenKeys(entry, `${path}[${index}]`));
109
+ return;
110
+ }
111
+ if (!value || typeof value !== "object") return;
112
+ for (const [key, child] of Object.entries(value)) {
113
+ if (FORBIDDEN_KEYS.has(key)) {
114
+ throw new ReviewValidationError(`Receipt must not include "${key}" at ${path}.`, {
115
+ code: REVIEW_VALIDATION_ERROR_CODES.FORBIDDEN_FIELD,
116
+ details: { key, path }
117
+ });
118
+ }
119
+ assertNoForbiddenKeys(child, `${path}.${key}`);
120
+ }
121
+ }
122
+
123
+ function assertMatchesShape(value, shape, path) {
124
+ if (shape === null) {
125
+ if (value !== null) {
126
+ throw new ReviewValidationError(`Expected null at ${path}.`, {
127
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_OUTPUT, details: { path }
128
+ });
129
+ }
130
+ return;
131
+ }
132
+
133
+ if (typeof shape === "string") {
134
+ if (value == null) {
135
+ if (isOptionalScalar(shape) || shape === "null") return;
136
+ throw new ReviewValidationError(`Missing value at ${path}.`, {
137
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_OUTPUT, details: { path }
138
+ });
139
+ }
140
+ const expected = scalarType(shape);
141
+ if (expected === "number" && typeof value === "number" && Number.isFinite(value)) return;
142
+ if (expected === "string" && typeof value === "string") return;
143
+ throw new ReviewValidationError(`Invalid type at ${path}.`, {
144
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_OUTPUT, details: { path, expected }
145
+ });
146
+ }
147
+
148
+ if (Array.isArray(shape)) {
149
+ if (!Array.isArray(value)) {
150
+ throw new ReviewValidationError(`Expected array at ${path}.`, {
151
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_OUTPUT, details: { path }
152
+ });
153
+ }
154
+ const itemShape = shape[0];
155
+ value.forEach((entry, index) => assertMatchesShape(entry, itemShape, `${path}[${index}]`));
156
+ return;
157
+ }
158
+
159
+ if (value == null) {
160
+ // Optional object fields (usage/timings) may be null.
161
+ return;
162
+ }
163
+
164
+ const body = asObject(value, path);
165
+ const allowed = new Set(Object.keys(shape));
166
+ for (const key of Object.keys(body)) {
167
+ if (!allowed.has(key)) {
168
+ throw new ReviewValidationError(`Unexpected field "${key}" at ${path}.`, {
169
+ code: REVIEW_VALIDATION_ERROR_CODES.FORBIDDEN_FIELD,
170
+ details: { key, path }
171
+ });
172
+ }
173
+ }
174
+ for (const [key, childShape] of Object.entries(shape)) {
175
+ if (!(key in body)) {
176
+ throw new ReviewValidationError(`Missing field "${key}" at ${path}.`, {
177
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_OUTPUT, details: { path, key }
178
+ });
179
+ }
180
+ assertMatchesShape(body[key], childShape, `${path}.${key}`);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Fail-closed parse of agent review JSON against a snapshot scope.
186
+ * Never accepts paths outside snapshot.files.
187
+ */
188
+ export function validateReviewOutput(raw, snapshot) {
189
+ let parsed = raw;
190
+ if (typeof raw === "string") {
191
+ try {
192
+ parsed = JSON.parse(raw);
193
+ } catch (error) {
194
+ throw new ReviewValidationError(`Broken review JSON: ${error.message}`, {
195
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_OUTPUT
196
+ });
197
+ }
198
+ }
199
+
200
+ const body = asObject(parsed, "review output");
201
+ assertNoForbiddenKeys(body, "output");
202
+ const findingsIn = Array.isArray(body.findings) ? body.findings : null;
203
+ if (!findingsIn) {
204
+ throw new ReviewValidationError("Review output missing findings array.", {
205
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_OUTPUT
206
+ });
207
+ }
208
+
209
+ const allowed = new Set((snapshot?.files ?? []).map((f) => f.path));
210
+ const findings = [];
211
+
212
+ for (const entry of findingsIn) {
213
+ const item = asObject(entry, "finding");
214
+ assertNoForbiddenKeys(item, "finding");
215
+ const severity = requireNonEmptyString(item.severity, "severity").toLowerCase();
216
+ if (!SEVERITY_SET.has(severity)) {
217
+ throw new ReviewValidationError(`Unknown severity "${item.severity}".`, {
218
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_FINDING,
219
+ details: { severity: item.severity }
220
+ });
221
+ }
222
+
223
+ let path;
224
+ try {
225
+ path = assertReviewPathSafe(requireNonEmptyString(item.path, "path"));
226
+ } catch (error) {
227
+ if (error instanceof ReviewSnapshotError) {
228
+ throw new ReviewValidationError(error.message, {
229
+ code: REVIEW_VALIDATION_ERROR_CODES.INVALID_FINDING,
230
+ details: error.details
231
+ });
232
+ }
233
+ throw error;
234
+ }
235
+
236
+ if (!allowed.has(path)) {
237
+ throw new ReviewValidationError(`Finding path "${path}" is outside the review snapshot.`, {
238
+ code: REVIEW_VALIDATION_ERROR_CODES.PATH_OUT_OF_SCOPE,
239
+ details: { path }
240
+ });
241
+ }
242
+
243
+ const title = requireNonEmptyString(item.title, "title");
244
+ const problem = requireNonEmptyString(item.problem, "problem");
245
+ const recommendation = requireNonEmptyString(item.recommendation, "recommendation");
246
+ const line = normalizeLine(item.line);
247
+ const id = createFindingId({ severity, title, path, line, problem });
248
+ findings.push({ id, severity, title, path, line, problem, recommendation });
249
+ }
250
+
251
+ const warnings = Array.isArray(body.warnings)
252
+ ? body.warnings.filter((w) => typeof w === "string" && w.trim()).map((w) => w.trim())
253
+ : [];
254
+
255
+ return {
256
+ findings,
257
+ warnings,
258
+ model: typeof body.model === "string" ? body.model : null,
259
+ usage: sanitizeUsage(body.usage)
260
+ };
261
+ }
262
+
263
+ function sanitizeUsage(usage) {
264
+ if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
265
+ assertNoForbiddenKeys(usage, "usage");
266
+ return {
267
+ inputTokens: Number.isFinite(usage.inputTokens) ? usage.inputTokens : null,
268
+ outputTokens: Number.isFinite(usage.outputTokens) ? usage.outputTokens : null,
269
+ totalTokens: Number.isFinite(usage.totalTokens) ? usage.totalTokens : null,
270
+ cost: Number.isFinite(usage.cost) ? usage.cost : null
271
+ };
272
+ }
273
+
274
+ /** Recursive forbidden-key + allowlisted shape check before persistence. */
275
+ export function assertReceiptSecretFree(receipt) {
276
+ const body = asObject(receipt, "receipt");
277
+ assertNoForbiddenKeys(body, "receipt");
278
+ assertMatchesShape(body, RECEIPT_SHAPE, "receipt");
279
+ return body;
280
+ }
@@ -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) {
@@ -0,0 +1,71 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import {
5
+ RUN_STRATEGIES,
6
+ normalizeRunStrategy,
7
+ createOrchLineage,
8
+ resolveKairoMinionExtensionPath
9
+ } from "./orchestration/index.js";
10
+
11
+ export { RUN_STRATEGIES, normalizeRunStrategy };
12
+
13
+ const ORCH_MODULE_PATH = join(dirname(fileURLToPath(import.meta.url)), "orchestration", "index.js");
14
+
15
+ export const ORCH_RUNTIME_ENV = Object.freeze({
16
+ HOME: "KAIRO_ORCH_HOME",
17
+ ROOT_RUN_ID: "KAIRO_ORCH_ROOT_RUN_ID",
18
+ ROOT_TASK_ID: "KAIRO_ORCH_ROOT_TASK_ID",
19
+ CLI_VERSION: "KAIRO_ORCH_CLI_VERSION",
20
+ MODULE: "KAIRO_ORCH_MODULE"
21
+ });
22
+
23
+ /** Reject orchestrated for non-Pi before any run I/O. */
24
+ export function assertOrchestratedAgent(agentId, strategy) {
25
+ const normalized = normalizeRunStrategy(strategy);
26
+ if (normalized === RUN_STRATEGIES.ORCHESTRATED && agentId !== "pi") {
27
+ throw new Error(
28
+ `Strategy "orchestrated" requires agent "pi" (got "${agentId}").`
29
+ );
30
+ }
31
+ return normalized;
32
+ }
33
+
34
+ /** Fail closed when managed extension is missing or not a regular file. */
35
+ export async function assertManagedMinionExtension(homeDir) {
36
+ const extensionPath = resolveKairoMinionExtensionPath(homeDir);
37
+ let info;
38
+ try {
39
+ info = await stat(extensionPath);
40
+ } catch {
41
+ throw new Error(`Managed Kairo minion extension missing: ${extensionPath}`);
42
+ }
43
+ if (!info.isFile()) {
44
+ throw new Error(`Managed Kairo minion extension is not a regular file: ${extensionPath}`);
45
+ }
46
+ return extensionPath;
47
+ }
48
+
49
+ export function createRootRunLineage(runId) {
50
+ return createOrchLineage({ rootRunId: runId, parentRunId: null, depth: 0 });
51
+ }
52
+
53
+ /** Supervisor derives extension path from homeDir only — never from CLI/handoff. */
54
+ export function resolveOrchestratedExtensionPath(homeDir, strategy) {
55
+ if (normalizeRunStrategy(strategy) !== RUN_STRATEGIES.ORCHESTRATED) return null;
56
+ return resolveKairoMinionExtensionPath(homeDir);
57
+ }
58
+
59
+ export function buildOrchestratedRuntimeEnv({
60
+ homeDir, rootRunId, rootTaskId, cliVersion = null,
61
+ strategy = RUN_STRATEGIES.DIRECT, baseEnv = process.env
62
+ } = {}) {
63
+ const env = { ...baseEnv };
64
+ if (normalizeRunStrategy(strategy) !== RUN_STRATEGIES.ORCHESTRATED) return env;
65
+ env[ORCH_RUNTIME_ENV.HOME] = homeDir;
66
+ env[ORCH_RUNTIME_ENV.ROOT_RUN_ID] = rootRunId;
67
+ env[ORCH_RUNTIME_ENV.ROOT_TASK_ID] = rootTaskId;
68
+ env[ORCH_RUNTIME_ENV.CLI_VERSION] = cliVersion == null ? "" : String(cliVersion);
69
+ env[ORCH_RUNTIME_ENV.MODULE] = ORCH_MODULE_PATH;
70
+ return env;
71
+ }
@@ -18,6 +18,12 @@ import { consumeRunHandoff } from "./run-handoff.js";
18
18
  import { isRunCancelRequested } from "./run-cancel-signal.js";
19
19
  import { readSupervisorLock, touchSupervisorLock, writeSupervisorLock } from "./run-supervisor-lock.js";
20
20
  import { shouldPersistTranscript } from "./run-redact.js";
21
+ import {
22
+ buildOrchestratedRuntimeEnv,
23
+ normalizeRunStrategy,
24
+ resolveOrchestratedExtensionPath
25
+ } from "./run-strategy.js";
26
+ import { finalizeOrchState, RUN_STRATEGIES } from "./orchestration/index.js";
21
27
 
22
28
  async function shouldPreserveCancelledState(homeDir, runId) {
23
29
  const fresh = await readRunState(homeDir, runId);
@@ -66,12 +72,16 @@ export async function supervisePreparedRun({
66
72
  }
67
73
 
68
74
  const captureTranscript = handoff.captureTranscript === true;
75
+ const strategy = normalizeRunStrategy(handoff.strategy ?? metadata.strategy ?? "direct");
76
+ const extensionPath = resolveOrchestratedExtensionPath(homeDir, strategy);
69
77
  const launch = adapter.buildLaunch({
70
78
  task: handoff.task,
71
79
  cwd: handoff.cwd,
72
80
  model: handoff.model,
73
81
  permissions: handoff.permissions ?? [],
74
- profile: handoff.profile ?? null
82
+ profile: handoff.profile ?? null,
83
+ strategy,
84
+ extensionPath
75
85
  });
76
86
 
77
87
  metadata = {
@@ -90,7 +100,14 @@ export async function supervisePreparedRun({
90
100
 
91
101
  const child = spawnImpl(launch.command, launch.args, {
92
102
  cwd: launch.cwd,
93
- env: launch.env,
103
+ env: buildOrchestratedRuntimeEnv({
104
+ homeDir,
105
+ rootRunId: runId,
106
+ rootTaskId: metadata.lineage?.taskId,
107
+ cliVersion: metadata.cliVersion,
108
+ strategy,
109
+ baseEnv: launch.env ?? process.env
110
+ }),
94
111
  stdio: ["ignore", "pipe", "pipe"]
95
112
  });
96
113
  activeProcesses?.set(runId, child);
@@ -244,6 +261,9 @@ export async function supervisePreparedRun({
244
261
  type: failed ? "run.failed" : "run.completed",
245
262
  data: { exitCode }
246
263
  }), { captureTranscript: shouldPersistTranscript(captureTranscript) });
264
+ if (!failed && strategy === RUN_STRATEGIES.ORCHESTRATED) {
265
+ await finalizeOrchState(runId, { homeDir, recovered: false });
266
+ }
247
267
  resolve(metadata);
248
268
  });
249
269
  } catch (error) {
@@ -83,7 +83,9 @@ export function createRunMetadata({
83
83
  permissions = [],
84
84
  captureTranscript = false,
85
85
  cliVersion,
86
- profileSources = null
86
+ profileSources = null,
87
+ strategy = "direct",
88
+ lineage = null
87
89
  }) {
88
90
  const { taskDigest, taskLength } = createTaskFingerprint(task);
89
91
  const now = new Date().toISOString();
@@ -100,6 +102,8 @@ export function createRunMetadata({
100
102
  captureTranscript,
101
103
  cliVersion,
102
104
  profileSources,
105
+ strategy,
106
+ lineage,
103
107
  state: RUN_STATES.PENDING,
104
108
  pid: null,
105
109
  supervisorPid: null,
@@ -1,30 +1,36 @@
1
- import { open as fsOpen, rename as fsRename, unlink as fsUnlink } from "node:fs/promises";
1
+ import {
2
+ open as fsOpen, rename as fsRename, unlink as fsUnlink, link as fsLink
3
+ } from "node:fs/promises";
2
4
  import { constants } from "node:fs";
3
5
  import { basename, dirname, join } from "node:path";
4
6
  import { randomBytes } from "node:crypto";
5
7
 
6
8
  function defaultTempPath(targetPath) {
7
- const id = randomBytes(8).toString("hex");
8
9
  return join(
9
10
  dirname(targetPath),
10
- `.${basename(targetPath)}.${process.pid}.${id}.tmp`
11
+ `.${basename(targetPath)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
11
12
  );
12
13
  }
13
14
 
15
+ async function bestEffort(fn) {
16
+ try { await fn(); } catch { /* ignore */ }
17
+ }
18
+
14
19
  /**
15
- * Atomically replace targetPath with pretty-printed JSON.
16
- * Creates a unique temp in the same directory (O_EXCL), writes + fsync,
17
- * renames over the destination, and deletes the temp on any failure.
20
+ * Atomic JSON write. Default rename-replace; createExclusive uses link (EEXIST).
21
+ * link/rename are commit points; post-commit temp cleanup is best-effort.
18
22
  */
19
23
  export async function writeAtomicJson(targetPath, value, deps = {}) {
20
24
  const open = deps.open ?? fsOpen;
21
25
  const rename = deps.rename ?? fsRename;
22
26
  const unlink = deps.unlink ?? fsUnlink;
27
+ const link = deps.link ?? fsLink;
23
28
  const createTempPath = deps.createTempPath ?? defaultTempPath;
24
-
29
+ const createExclusive = deps.createExclusive === true;
25
30
  const payload = `${JSON.stringify(value, null, 2)}\n`;
26
31
  const tempPath = createTempPath(targetPath);
27
32
  let handle;
33
+ let committed = false;
28
34
 
29
35
  try {
30
36
  handle = await open(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o644);
@@ -32,20 +38,18 @@ export async function writeAtomicJson(targetPath, value, deps = {}) {
32
38
  await handle.sync();
33
39
  await handle.close();
34
40
  handle = undefined;
35
- await rename(tempPath, targetPath);
36
- } catch (error) {
37
- if (handle) {
38
- try {
39
- await handle.close();
40
- } catch {
41
- // Best-effort close before temp cleanup.
42
- }
43
- }
44
- try {
45
- await unlink(tempPath);
46
- } catch {
47
- // Temp may not exist yet or already renamed.
41
+ if (createExclusive) {
42
+ await link(tempPath, targetPath);
43
+ committed = true;
44
+ await bestEffort(() => unlink(tempPath));
45
+ } else {
46
+ await rename(tempPath, targetPath);
47
+ committed = true;
48
48
  }
49
+ } catch (error) {
50
+ if (committed) return;
51
+ if (handle) await bestEffort(() => handle.close());
52
+ await bestEffort(() => unlink(tempPath));
49
53
  throw error;
50
54
  }
51
55
  }