pi-subagents 0.80.0 → 0.81.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 (87) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bundle/agents.js +18 -0
  3. package/bundle/background-work.js +16 -0
  4. package/bundle/capability-ceiling.js +22 -0
  5. package/bundle/child-tool-plan.js +11 -0
  6. package/bundle/chunk-26C3WLYQ.js +8388 -0
  7. package/bundle/chunk-2XYWN7QJ.js +128 -0
  8. package/bundle/chunk-2ZSNH26H.js +4164 -0
  9. package/bundle/chunk-3DC5KBVP.js +235 -0
  10. package/bundle/chunk-3IF2PGW7.js +380 -0
  11. package/bundle/chunk-4QFZZV3O.js +57 -0
  12. package/bundle/chunk-563BHAZ5.js +1365 -0
  13. package/bundle/chunk-5LFLVSTM.js +260 -0
  14. package/bundle/chunk-62OCY2S4.js +14 -0
  15. package/bundle/chunk-74RT5YQE.js +326 -0
  16. package/bundle/chunk-7F6RKT6W.js +132 -0
  17. package/bundle/chunk-7J4ZXMY7.js +18 -0
  18. package/bundle/chunk-ARRKBB2B.js +179 -0
  19. package/bundle/chunk-BJN5LUQY.js +458 -0
  20. package/bundle/chunk-BPS2AGBZ.js +5421 -0
  21. package/bundle/chunk-CEKS3TFO.js +1337 -0
  22. package/bundle/chunk-CTPAE36Y.js +94 -0
  23. package/bundle/chunk-D4EHT5SJ.js +14 -0
  24. package/bundle/chunk-EYNJ6VL6.js +526 -0
  25. package/bundle/chunk-G5DKFLLI.js +1185 -0
  26. package/bundle/chunk-KSWQVGDW.js +1027 -0
  27. package/bundle/chunk-L5ZHE4L4.js +946 -0
  28. package/bundle/chunk-LSRUTHT7.js +57 -0
  29. package/bundle/chunk-N2BN7BF5.js +159 -0
  30. package/bundle/chunk-NF7IEKBH.js +490 -0
  31. package/bundle/chunk-NIXWDZZV.js +9 -0
  32. package/bundle/chunk-NNLASTQE.js +4082 -0
  33. package/bundle/chunk-TUWLXTAM.js +935 -0
  34. package/bundle/chunk-UKG4V45O.js +228 -0
  35. package/bundle/chunk-V2JWDB5U.js +753 -0
  36. package/bundle/chunk-V3BIUPLU.js +9163 -0
  37. package/bundle/chunk-V6MR5TOQ.js +270 -0
  38. package/bundle/chunk-VJHOELQP.js +8245 -0
  39. package/bundle/chunk-WSBNENFS.js +732 -0
  40. package/bundle/chunk-XMFLCKMQ.js +16664 -0
  41. package/bundle/chunk-YFCK7H2S.js +129 -0
  42. package/bundle/chunk-YYREO77V.js +137 -0
  43. package/bundle/chunk-Z7KVHQHL.js +173 -0
  44. package/bundle/chunk-ZBC44JJ3.js +8606 -0
  45. package/bundle/chunk-ZP3HODFH.js +175 -0
  46. package/bundle/control-channel.js +11 -0
  47. package/bundle/delegation.js +14 -0
  48. package/bundle/external-job-provider.js +20 -0
  49. package/bundle/external-runs.js +21 -0
  50. package/bundle/index.js +30 -0
  51. package/bundle/inspectors.js +8 -0
  52. package/bundle/intercom-bridge.js +11 -0
  53. package/bundle/preflight.js +425 -0
  54. package/bundle/project-panes.js +28 -0
  55. package/bundle/required-child-extensions.js +6 -0
  56. package/bundle/shared-types.js +8 -0
  57. package/bundle/workflow-resources.js +7 -0
  58. package/docs/extension-api.md +27 -0
  59. package/docs/observability.md +6 -2
  60. package/package.json +18 -18
  61. package/src/agents/frontmatter.d.ts +2 -0
  62. package/src/agents/frontmatter.js +9 -2
  63. package/src/api/delegation.d.ts +20 -0
  64. package/src/extension/index.js +27 -7
  65. package/src/runs/background/run-child-session.js +18 -0
  66. package/src/runs/background/stale-run-reconciler.js +2 -0
  67. package/src/runs/foreground/execution.js +39 -2
  68. package/src/runs/shared/structured-output.d.ts +1 -0
  69. package/src/runs/shared/structured-output.js +1 -0
  70. package/src/shared/accessible-dir.js +4 -1
  71. package/src/shared/provider-diagnostics.d.ts +9 -0
  72. package/src/shared/provider-diagnostics.js +75 -0
  73. package/src/shared/types.d.ts +3 -0
  74. package/src/slash/delegation-adapters.d.ts +3 -1
  75. package/src/slash/delegation-adapters.js +7 -0
  76. package/src/slash/prompt-template-bridge.js +3 -1
  77. package/src/slash/subagents-admin.d.ts +1 -0
  78. package/src/slash/subagents-admin.js +1 -1
  79. package/src/tui/fleet-layout.d.ts +20 -0
  80. package/src/tui/fleet-layout.js +17 -0
  81. package/src/tui/fleet-viewport.d.ts +12 -0
  82. package/src/tui/fleet-viewport.js +35 -0
  83. package/src/tui/fleet.d.ts +5 -6
  84. package/src/tui/fleet.js +69 -51
  85. package/src/workflows/structured-workflow-scripts.d.ts +1 -0
  86. package/src/workflows/structured-workflow-scripts.js +5 -4
  87. package/src/workflows/workflow-resources.js +2 -2
@@ -0,0 +1,4082 @@
1
+ import {
2
+ ACTIVE_RUN_INDEX_DIR,
3
+ DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS,
4
+ HOST_STEP_MAX_COUNT,
5
+ HOST_STEP_MAX_DETAIL_CHARS,
6
+ HOST_STEP_MAX_LABEL_CHARS,
7
+ HOST_STEP_MAX_PROVIDER_CHARS,
8
+ HOST_STEP_MAX_REASON_CHARS,
9
+ HOST_STEP_MAX_REF_CHARS,
10
+ HOST_STEP_MAX_ROLE_CHARS,
11
+ HOST_STEP_MAX_TARGET_CHARS,
12
+ TERMINAL_RUN_INDEX_DIR,
13
+ activeRunMarkerAgeMs,
14
+ assertWorkflowGraphHostSteps,
15
+ attachRootChildrenToSteps,
16
+ buildNestedRouteIndex,
17
+ cleanupWorktrees,
18
+ findNestedRouteForRootId,
19
+ flatToLogicalStepIndex,
20
+ hashedSegment,
21
+ hostStepReportName,
22
+ hostStepVerdictLabel,
23
+ isActiveAsyncState,
24
+ normalizeParallelGroups,
25
+ normalizeWorktreeBaseRef,
26
+ parseHostStepNode,
27
+ projectNestedEvents,
28
+ readActiveRunIndex,
29
+ readProcessTerminal,
30
+ readRecentTerminalRunIndex,
31
+ reconcileAsyncRun,
32
+ reconcileNestedAsyncDescendants,
33
+ releaseActiveRunIndex,
34
+ resultFilePath,
35
+ resultPayloadPathForIndexedRun,
36
+ sanitizeProcessTerminal,
37
+ updateActiveRunIndex,
38
+ validHostStepNodes,
39
+ validateModelResponseAliases,
40
+ withWorkflowChildRunModel,
41
+ withWorktreeTransaction
42
+ } from "./chunk-VJHOELQP.js";
43
+ import {
44
+ validateIntercomBridgeConfig
45
+ } from "./chunk-ZP3HODFH.js";
46
+ import {
47
+ stableJsonDigest
48
+ } from "./chunk-7F6RKT6W.js";
49
+ import {
50
+ snapshotRequiredChildExtensions
51
+ } from "./chunk-CTPAE36Y.js";
52
+ import {
53
+ DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS,
54
+ assertWorkflowLaneKey,
55
+ childThinkingLevel,
56
+ childWatchdogProgressForModel,
57
+ formatDuration,
58
+ formatModelThinking,
59
+ formatTokenUsage,
60
+ formatTokens,
61
+ intersectThinkingCeilings,
62
+ isDynamicParallelStep,
63
+ isParallelStep,
64
+ normalizeExtensionBindings,
65
+ normalizeWorkflowLaneMetadata,
66
+ parseThinkingLevel,
67
+ readStatus,
68
+ shortenPath,
69
+ splitKnownThinkingSuffix,
70
+ tryLease,
71
+ validateAcceptanceInput,
72
+ validateAsyncStatusLaneMetadata,
73
+ validateToolBudgetConfig,
74
+ waitForFileSystemRetry,
75
+ writeAtomicJson
76
+ } from "./chunk-ZBC44JJ3.js";
77
+ import {
78
+ DIRS,
79
+ RUNNER_LAUNCHER_NAME_PATTERN,
80
+ TEMP_ROOT_DIR
81
+ } from "./chunk-5LFLVSTM.js";
82
+ import {
83
+ previewDisplayText,
84
+ sanitizeDisplayText,
85
+ truncateDisplayText
86
+ } from "./chunk-YFCK7H2S.js";
87
+ import {
88
+ intersectSubagentCapabilityCeilings,
89
+ normalizeCapabilityCeilingAllowedAgents,
90
+ parseSubagentCapabilityCeiling
91
+ } from "./chunk-Z7KVHQHL.js";
92
+
93
+ // src/shared/session-succession.ts
94
+ var SUCCESSION_KEY = /* @__PURE__ */ Symbol.for("pi-subagents.session-succession");
95
+ var MAX_INHERITED_SESSIONS = 8;
96
+ var MAX_RECORDED_SESSIONS = 64;
97
+ function successions() {
98
+ const runtime = globalThis;
99
+ runtime[SUCCESSION_KEY] ??= /* @__PURE__ */ new Map();
100
+ return runtime[SUCCESSION_KEY];
101
+ }
102
+ function recordSessionReplacement(sessionId, previousSessionId) {
103
+ const chain = [.../* @__PURE__ */ new Set([previousSessionId, ...successions().get(previousSessionId) ?? []])].filter((id) => id !== sessionId);
104
+ successions().delete(sessionId);
105
+ successions().set(sessionId, chain.slice(0, MAX_INHERITED_SESSIONS));
106
+ while (successions().size > MAX_RECORDED_SESSIONS) successions().delete(successions().keys().next().value);
107
+ }
108
+ function inheritedSessionIds(sessionId) {
109
+ return successions().get(sessionId) ?? [];
110
+ }
111
+
112
+ // src/shared/extension-context.ts
113
+ var MODEL_ONLY_TOOL = { exposure: "model-only" };
114
+ function isStaleExtensionContextError(error) {
115
+ return error instanceof Error && /extension ctx is stale|extension context no longer active|stale after session replacement or reload/i.test(error.message);
116
+ }
117
+ function canRenderComponents(ctx) {
118
+ return ctx.mode === "tui" || process.env.PI_SUBAGENTS_COMPONENT_UI === "1";
119
+ }
120
+ function isUnboundExtensionRuntimeError(error) {
121
+ return error instanceof Error && /extension runtime not initialized/i.test(error.message);
122
+ }
123
+ function withCachedUiContext(ctx, onStale, run) {
124
+ if (!ctx) return void 0;
125
+ try {
126
+ if (!ctx.hasUI) return void 0;
127
+ return run(ctx);
128
+ } catch (error) {
129
+ if (!isStaleExtensionContextError(error)) throw error;
130
+ onStale();
131
+ return void 0;
132
+ }
133
+ }
134
+
135
+ // src/shared/status-format.ts
136
+ function formatActivityAge(ms) {
137
+ if (ms < 1e3) return "now";
138
+ if (ms < 6e4) return `${Math.floor(ms / 1e3)}s`;
139
+ return `${Math.floor(ms / 6e4)}m`;
140
+ }
141
+ function formatActivityLabel(lastActivityAt, activityState, now = Date.now()) {
142
+ if (lastActivityAt === void 0) {
143
+ if (activityState === "needs_attention") return "needs attention";
144
+ if (activityState === "active_long_running") return "active but long-running";
145
+ return void 0;
146
+ }
147
+ const age = formatActivityAge(Math.max(0, now - lastActivityAt));
148
+ if (activityState === "needs_attention") return `no activity for ${age}`;
149
+ if (activityState === "active_long_running") {
150
+ const activityAge = age === "now" ? "now" : `${age} ago`;
151
+ return `active but long-running \xB7 last activity ${activityAge}`;
152
+ }
153
+ return age === "now" ? "active now" : `active ${age} ago`;
154
+ }
155
+ function isCompletedStepStatus(status) {
156
+ return status === "complete" || status === "completed";
157
+ }
158
+ function aggregateStepStatus(steps) {
159
+ if (steps.some((step) => step.status === "running")) return "running";
160
+ if (steps.some((step) => step.status === "failed")) return "failed";
161
+ if (steps.some((step) => step.status === "stopped")) return "stopped";
162
+ if (steps.some((step) => step.status === "paused")) return "paused";
163
+ if (steps.length > 0 && steps.every((step) => isCompletedStepStatus(step.status))) return "complete";
164
+ return "pending";
165
+ }
166
+ function formatAgentRunningLabel(count2) {
167
+ return count2 === 1 ? "1 agent running" : `${count2} agents running`;
168
+ }
169
+ function formatParallelOutcome(steps, total, options = {}) {
170
+ const running = steps.filter((step) => step.status === "running").length;
171
+ const done = steps.filter((step) => isCompletedStepStatus(step.status)).length;
172
+ const failed = steps.filter((step) => step.status === "failed").length;
173
+ const stopped = steps.filter((step) => step.status === "stopped").length;
174
+ const paused = steps.filter((step) => step.status === "paused").length;
175
+ const parts = [`${done}/${total} done`];
176
+ if (options.showRunning !== false && running > 0) parts.unshift(formatAgentRunningLabel(running));
177
+ if (failed > 0) parts.push(`${failed} failed`);
178
+ if (stopped > 0) parts.push(`${stopped} stopped`);
179
+ if (paused > 0) parts.push(`${paused} paused`);
180
+ return parts.join(" \xB7 ");
181
+ }
182
+
183
+ // src/runs/shared/nested-render.ts
184
+ function countNestedRuns(children) {
185
+ const counts = { total: 0, running: 0, paused: 0, complete: 0, failed: 0, partial: 0, rejected: 0, stopped: 0, queued: 0 };
186
+ for (const child of children ?? []) {
187
+ counts.total++;
188
+ counts[child.state]++;
189
+ const nested = countNestedRuns([...child.children ?? [], ...child.steps?.flatMap((step) => step.children ?? []) ?? []]);
190
+ counts.total += nested.total;
191
+ counts.running += nested.running;
192
+ counts.paused += nested.paused;
193
+ counts.complete += nested.complete;
194
+ counts.failed += nested.failed;
195
+ counts.partial += nested.partial;
196
+ counts.rejected += nested.rejected;
197
+ counts.stopped += nested.stopped;
198
+ counts.queued += nested.queued;
199
+ }
200
+ return counts;
201
+ }
202
+ function formatNestedAggregate(children) {
203
+ const counts = countNestedRuns(children);
204
+ if (counts.total === 0) return void 0;
205
+ const parts = [
206
+ counts.running > 0 ? `${counts.running} running` : "",
207
+ counts.paused > 0 ? `${counts.paused} paused` : "",
208
+ counts.failed > 0 ? `${counts.failed} failed` : "",
209
+ counts.partial > 0 ? `${counts.partial} partial` : "",
210
+ counts.rejected > 0 ? `${counts.rejected} rejected` : "",
211
+ counts.stopped > 0 ? `${counts.stopped} stopped` : "",
212
+ counts.complete > 0 ? `${counts.complete} complete` : "",
213
+ counts.queued > 0 ? `${counts.queued} queued` : ""
214
+ ].filter(Boolean);
215
+ return `+${counts.total} nested run${counts.total === 1 ? "" : "s"}${parts.length ? ` (${parts.join(", ")})` : ""}`;
216
+ }
217
+ function nestedRunLabel(run) {
218
+ if (run.sessionName?.trim()) return run.sessionName.trim();
219
+ if (run.agent) return run.agent;
220
+ if (run.agents?.length) return run.agents.length === 1 ? run.agents[0] : `${run.agents.slice(0, 2).join(", ")}${run.agents.length > 2 ? ` +${run.agents.length - 2}` : ""}`;
221
+ return run.id;
222
+ }
223
+ function formatNestedActivity(input) {
224
+ const facts = [];
225
+ if (input.currentTool && input.currentToolStartedAt !== void 0) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
226
+ else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
227
+ if (input.currentPath) facts.push(shortenPath(input.currentPath));
228
+ if (input.turnCount !== void 0) facts.push(`${input.turnCount} turns`);
229
+ if (input.toolCount !== void 0) facts.push(`${input.toolCount} tools`);
230
+ if (input.totalTokens) facts.push(formatTokenUsage(input.totalTokens));
231
+ const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
232
+ return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : void 0;
233
+ }
234
+ function formatNestedRunLines(children, options) {
235
+ const lines = [];
236
+ const append = (items, depth, indent) => {
237
+ if (!items?.length || lines.length >= options.maxLines) return;
238
+ if (depth > options.maxDepth) {
239
+ const aggregate = formatNestedAggregate(items);
240
+ if (aggregate && lines.length < options.maxLines) lines.push(`${indent}\u21B3 ${aggregate}`);
241
+ return;
242
+ }
243
+ for (let index = 0; index < items.length; index++) {
244
+ const child = items[index];
245
+ if (lines.length >= options.maxLines) {
246
+ const aggregate = formatNestedAggregate(items.slice(index));
247
+ if (aggregate) lines[lines.length - 1] = `${indent}\u21B3 ${aggregate}`;
248
+ return;
249
+ }
250
+ const activity = child.state === "running" ? formatNestedActivity(child) : void 0;
251
+ const error = child.error ? ` | error: ${child.error}` : "";
252
+ const modelThinking = formatModelThinking(child.model, child.thinking);
253
+ lines.push(`${indent}\u21B3 ${nestedRunLabel(child)} [${child.id}] ${child.state}${modelThinking ? ` | ${modelThinking}` : ""}${activity ? ` | ${activity}` : ""}${error}`);
254
+ if (options.commandHints && lines.length < options.maxLines) lines.push(`${indent} Status: subagent({ action: "status", id: "${child.id}" })`);
255
+ if (depth === options.maxDepth) {
256
+ const aggregate = formatNestedAggregate([...child.steps?.flatMap((step) => step.children ?? []) ?? [], ...child.children ?? []]);
257
+ if (aggregate && lines.length < options.maxLines) lines.push(`${indent} \u21B3 ${aggregate}`);
258
+ continue;
259
+ }
260
+ for (const [stepIndex, step] of (child.steps ?? []).entries()) {
261
+ if (lines.length >= options.maxLines) return;
262
+ const stepActivity = step.status === "running" ? formatNestedActivity(step) : void 0;
263
+ const stepModelThinking = formatModelThinking(step.model, step.thinking);
264
+ lines.push(`${indent} ${stepIndex + 1}. ${step.sessionName?.trim() || step.agent} ${step.status}${stepModelThinking ? ` | ${stepModelThinking}` : ""}${stepActivity ? ` | ${stepActivity}` : ""}${step.error ? ` | error: ${step.error}` : ""}`);
265
+ append(step.children, depth + 1, `${indent} `);
266
+ }
267
+ append(child.children, depth + 1, `${indent} `);
268
+ }
269
+ };
270
+ append(children, 0, options.indent);
271
+ return lines;
272
+ }
273
+ function formatNestedRunStatusLines(children, options = {}) {
274
+ return formatNestedRunLines(children, {
275
+ indent: options.indent ?? " ",
276
+ maxDepth: options.maxDepth ?? 2,
277
+ maxLines: options.maxLines ?? 40,
278
+ commandHints: options.commandHints ?? false
279
+ });
280
+ }
281
+
282
+ // src/runs/shared/run-fanout-budget.ts
283
+ import { randomUUID } from "node:crypto";
284
+ import * as fs from "node:fs";
285
+ import * as path from "node:path";
286
+ var RUN_FANOUT_ROOT = path.join(TEMP_ROOT_DIR, "run-fanout-budgets");
287
+ var RunFanoutLimitError = class extends Error {
288
+ rejection;
289
+ snapshot;
290
+ constructor(rejection) {
291
+ super(formatRunFanoutRejection(rejection));
292
+ this.name = "RunFanoutLimitError";
293
+ this.rejection = rejection;
294
+ this.snapshot = { used: rejection.used, limit: rejection.limit, remaining: rejection.remaining };
295
+ }
296
+ };
297
+ function safeRootRunId(rootRunId) {
298
+ return rootRunId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || randomUUID();
299
+ }
300
+ function parseManifest(value) {
301
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
302
+ const manifest = value;
303
+ if (manifest.version !== 1 || typeof manifest.rootRunId !== "string" || !manifest.rootRunId || !Number.isInteger(manifest.limit) || (manifest.limit ?? 0) <= 0 || typeof manifest.createdAt !== "number" || !Number.isFinite(manifest.createdAt)) return void 0;
304
+ return manifest;
305
+ }
306
+ function readManifest(directory) {
307
+ let parsed;
308
+ try {
309
+ parsed = JSON.parse(fs.readFileSync(path.join(directory, "manifest.json"), "utf-8"));
310
+ } catch (error) {
311
+ throw new Error(`Run fan-out budget manifest is unreadable at '${directory}': ${error instanceof Error ? error.message : String(error)}`);
312
+ }
313
+ const manifest = parseManifest(parsed);
314
+ if (!manifest) throw new Error(`Run fan-out budget manifest is invalid at '${directory}'.`);
315
+ return manifest;
316
+ }
317
+ function validateDirectory(directory) {
318
+ let realDirectory;
319
+ let realRoot;
320
+ try {
321
+ realDirectory = fs.realpathSync(path.resolve(directory));
322
+ realRoot = fs.realpathSync(path.resolve(RUN_FANOUT_ROOT));
323
+ } catch (error) {
324
+ throw new Error(`Run fan-out budget directory is unavailable: ${error instanceof Error ? error.message : String(error)}`);
325
+ }
326
+ if (realDirectory !== realRoot && !realDirectory.startsWith(`${realRoot}${path.sep}`)) {
327
+ throw new Error("Run fan-out budget directory resolves outside the managed budget root.");
328
+ }
329
+ return realDirectory;
330
+ }
331
+ function createRunFanoutBudget(rootRunId, limit) {
332
+ if (!Number.isInteger(limit) || limit <= 0) throw new Error("Run fan-out limit must be a positive integer.");
333
+ fs.mkdirSync(RUN_FANOUT_ROOT, { recursive: true, mode: 448 });
334
+ let directory;
335
+ do
336
+ directory = path.join(RUN_FANOUT_ROOT, `${safeRootRunId(rootRunId)}-${randomUUID()}`);
337
+ while (fs.existsSync(directory));
338
+ fs.mkdirSync(path.join(directory, "claims"), { recursive: true, mode: 448 });
339
+ const manifest = { version: 1, rootRunId, limit, createdAt: Date.now() };
340
+ fs.writeFileSync(path.join(directory, "manifest.json"), `${JSON.stringify(manifest)}
341
+ `, { mode: 384, flag: "wx" });
342
+ return { version: 1, rootRunId, directory, limit };
343
+ }
344
+ function validateRunFanoutBudgetDescriptor(value) {
345
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Run fan-out budget descriptor is missing or invalid.");
346
+ const descriptor = value;
347
+ if (descriptor.version !== 1 || typeof descriptor.rootRunId !== "string" || !descriptor.rootRunId || typeof descriptor.directory !== "string" || !descriptor.directory || !Number.isInteger(descriptor.limit) || (descriptor.limit ?? 0) <= 0 || descriptor.parentPath !== void 0 && typeof descriptor.parentPath !== "string") {
348
+ throw new Error("Run fan-out budget descriptor is invalid.");
349
+ }
350
+ const directory = validateDirectory(descriptor.directory);
351
+ const manifest = readManifest(directory);
352
+ if (manifest.rootRunId !== descriptor.rootRunId || manifest.limit !== descriptor.limit) {
353
+ throw new Error("Run fan-out budget descriptor does not match its manifest.");
354
+ }
355
+ return { version: 1, rootRunId: descriptor.rootRunId, directory, limit: descriptor.limit, ...descriptor.parentPath ? { parentPath: descriptor.parentPath } : {} };
356
+ }
357
+ function writeRunFanoutBudgetDescriptor(asyncDir, descriptor) {
358
+ const valid = validateRunFanoutBudgetDescriptor(descriptor);
359
+ fs.mkdirSync(asyncDir, { recursive: true });
360
+ fs.writeFileSync(path.join(asyncDir, "run-fanout-budget.json"), `${JSON.stringify(valid)}
361
+ `, { mode: 384 });
362
+ }
363
+ function readRunFanoutBudgetDescriptor(asyncDir) {
364
+ if (!asyncDir) return void 0;
365
+ const descriptorPath = path.join(asyncDir, "run-fanout-budget.json");
366
+ if (!fs.existsSync(descriptorPath)) return void 0;
367
+ try {
368
+ return validateRunFanoutBudgetDescriptor(JSON.parse(fs.readFileSync(descriptorPath, "utf-8")));
369
+ } catch (error) {
370
+ throw new Error(`Invalid persisted run fan-out budget '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
371
+ }
372
+ }
373
+ var ADMISSION_LOCK_STALE_MS = 6e4;
374
+ function readAdmissionLockOwner(lockPath) {
375
+ try {
376
+ const owner = JSON.parse(fs.readFileSync(path.join(lockPath, "owner.json"), "utf-8"));
377
+ if (Number.isSafeInteger(owner.pid) && (owner.pid ?? 0) > 0 && typeof owner.token === "string" && owner.token) return owner;
378
+ } catch {
379
+ }
380
+ return void 0;
381
+ }
382
+ function admissionLockIsStale(lockPath) {
383
+ const owner = readAdmissionLockOwner(lockPath);
384
+ if (owner) {
385
+ try {
386
+ process.kill(owner.pid, 0);
387
+ return false;
388
+ } catch (error) {
389
+ return error.code !== "EPERM";
390
+ }
391
+ }
392
+ try {
393
+ return Date.now() - fs.statSync(lockPath).mtimeMs > ADMISSION_LOCK_STALE_MS;
394
+ } catch (error) {
395
+ if (error.code === "ENOENT") return false;
396
+ throw error;
397
+ }
398
+ }
399
+ function withAdmissionLock(directory, operation) {
400
+ const lockPath = path.join(directory, "admission.lock");
401
+ const owner = { pid: process.pid, token: randomUUID() };
402
+ for (let attempt = 0; ; attempt++) {
403
+ try {
404
+ fs.mkdirSync(lockPath, { mode: 448 });
405
+ try {
406
+ fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify(owner), { mode: 384 });
407
+ } catch (error) {
408
+ fs.rmSync(lockPath, { recursive: true, force: true });
409
+ throw error;
410
+ }
411
+ break;
412
+ } catch (error) {
413
+ if (error.code !== "EEXIST") throw error;
414
+ if (admissionLockIsStale(lockPath)) {
415
+ const stalePath = path.join(directory, `admission.stale-${randomUUID()}`);
416
+ try {
417
+ fs.renameSync(lockPath, stalePath);
418
+ fs.rmSync(stalePath, { recursive: true, force: true });
419
+ continue;
420
+ } catch (reclaimError) {
421
+ if (reclaimError.code !== "ENOENT") throw reclaimError;
422
+ }
423
+ }
424
+ const delay = DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS[attempt];
425
+ if (delay === void 0) throw new Error(`Timed out acquiring run fan-out admission lock at '${directory}'.`);
426
+ waitForFileSystemRetry(delay);
427
+ }
428
+ }
429
+ try {
430
+ return operation();
431
+ } finally {
432
+ if (readAdmissionLockOwner(lockPath)?.token === owner.token) fs.rmSync(lockPath, { recursive: true, force: true });
433
+ }
434
+ }
435
+ function claimCount(directory) {
436
+ const claimsDir = path.join(directory, "claims");
437
+ let entries;
438
+ try {
439
+ entries = fs.readdirSync(claimsDir, { withFileTypes: true });
440
+ } catch (error) {
441
+ throw new Error(`Run fan-out claims directory is unreadable at '${claimsDir}': ${error instanceof Error ? error.message : String(error)}`);
442
+ }
443
+ return entries.filter((entry) => /^\d{6}\.json$/.test(entry.name)).length;
444
+ }
445
+ function getRunFanoutBudgetSnapshot(descriptor) {
446
+ const valid = validateRunFanoutBudgetDescriptor(descriptor);
447
+ const used = claimCount(valid.directory);
448
+ return { used, limit: valid.limit, remaining: Math.max(0, valid.limit - used) };
449
+ }
450
+ function qualifyRunFanoutPaths(descriptor, paths) {
451
+ const prefix = descriptor.parentPath?.trim();
452
+ return paths.map((item) => prefix ? `${prefix}/${item}` : item);
453
+ }
454
+ function commitRunFanoutBatch(descriptor, paths, commit) {
455
+ const valid = validateRunFanoutBudgetDescriptor(descriptor);
456
+ if (paths.length === 0) return commit(getRunFanoutBudgetSnapshot(valid));
457
+ const qualified = qualifyRunFanoutPaths(valid, paths);
458
+ return withAdmissionLock(valid.directory, () => {
459
+ const before = getRunFanoutBudgetSnapshot(valid);
460
+ if (qualified.length > before.remaining) {
461
+ throw new RunFanoutLimitError({ code: "RUN_FANOUT_LIMIT", path: qualified[before.remaining] ?? qualified[0], requested: qualified.length, ...before });
462
+ }
463
+ const created = [];
464
+ try {
465
+ for (const claimPath of qualified) {
466
+ for (let slot = 0; slot < valid.limit; slot++) {
467
+ const slotPath = path.join(valid.directory, "claims", `${String(slot).padStart(6, "0")}.json`);
468
+ try {
469
+ const fd = fs.openSync(slotPath, "wx", 384);
470
+ created.push(slotPath);
471
+ try {
472
+ const claim = { version: 1, claimId: randomUUID(), path: claimPath, claimedAt: Date.now() };
473
+ fs.writeFileSync(fd, `${JSON.stringify(claim)}
474
+ `, "utf-8");
475
+ } finally {
476
+ fs.closeSync(fd);
477
+ }
478
+ break;
479
+ } catch (error) {
480
+ if (error.code === "EEXIST") continue;
481
+ throw error;
482
+ }
483
+ }
484
+ }
485
+ return commit(getRunFanoutBudgetSnapshot(valid));
486
+ } catch (error) {
487
+ for (const slotPath of created) {
488
+ try {
489
+ fs.unlinkSync(slotPath);
490
+ } catch {
491
+ }
492
+ }
493
+ throw error;
494
+ }
495
+ });
496
+ }
497
+ function claimRunFanoutBatch(descriptor, paths) {
498
+ return commitRunFanoutBatch(descriptor, paths, (snapshot) => snapshot);
499
+ }
500
+ function claimRunFanoutBatchWithCommit(descriptor, paths, commit) {
501
+ return commitRunFanoutBatch(descriptor, paths, commit);
502
+ }
503
+ function formatRunFanoutBudget(snapshot) {
504
+ return `Run fan-out: ${snapshot.used}/${snapshot.limit} used, ${snapshot.remaining} remaining`;
505
+ }
506
+ function formatRunFanoutRejection(rejection) {
507
+ return `Run fan-out limit reached at ${rejection.path} (${rejection.used}/${rejection.limit} used; ${rejection.requested} requested, ${rejection.remaining} remaining). No children from this admission group were started. Start a new top-level run or raise config.maxSubagentSpawnsPerRun.`;
508
+ }
509
+
510
+ // src/runs/shared/context-mode.ts
511
+ function isContextMode(value) {
512
+ return value === "fresh" || value === "fork";
513
+ }
514
+ function summarizeContextModes(modes) {
515
+ const resolved = modes.filter(isContextMode);
516
+ if (resolved.length === 0) return void 0;
517
+ const first = resolved[0];
518
+ return resolved.every((mode) => mode === first) ? first : "mixed";
519
+ }
520
+ function contextModeLabel(mode) {
521
+ if (mode === "fork") return "[fork]";
522
+ if (mode === "fresh") return "[fresh]";
523
+ if (mode === "mixed") return "[mixed]";
524
+ return "";
525
+ }
526
+ function contextModeBadge(theme, mode) {
527
+ const label = contextModeLabel(mode);
528
+ if (!label) return "";
529
+ if (mode === "fork") return theme.fg("warning", ` ${label}`);
530
+ return theme.fg("dim", ` ${label}`);
531
+ }
532
+ function contextModePrefix(theme, mode) {
533
+ const label = contextModeLabel(mode);
534
+ if (!label) return "";
535
+ if (mode === "fork") return `${theme.fg("warning", label)} `;
536
+ return `${theme.fg("dim", label)} `;
537
+ }
538
+
539
+ // src/runs/shared/child-identity.ts
540
+ function asyncStatusChildIdentity(step, index) {
541
+ return asyncStatusChildIdentityCandidates(step, index)[0];
542
+ }
543
+ function asyncStatusChildIdentityCandidates(step, index) {
544
+ return [...new Set([step.childId, step.workflowKey, step.runId, `step:${index}`].filter((value) => typeof value === "string" && value.length > 0))];
545
+ }
546
+ function resolveAsyncStatusChild(status, childId, options = {}) {
547
+ const matches = [];
548
+ for (const [index, step] of (status.steps ?? []).entries()) {
549
+ if (asyncStatusChildIdentityCandidates(step, index).includes(childId)) {
550
+ matches.push({ index, step, id: asyncStatusChildIdentity(step, index) });
551
+ }
552
+ if (options.includeNested) {
553
+ const findNested = (children) => {
554
+ for (const nested of children ?? []) {
555
+ if (nested.id === childId) matches.push({ index, step, id: nested.id, nested });
556
+ findNested(nested.children);
557
+ }
558
+ };
559
+ findNested(step.children);
560
+ }
561
+ }
562
+ if (matches.length === 1) return { ok: true, child: matches[0] };
563
+ if (matches.length > 1) return { ok: false, code: "ambiguous", message: `Child '${childId}' is ambiguous under async run '${status.runId}'.` };
564
+ return { ok: false, code: "not_found", message: `Child '${childId}' was not found under async run '${status.runId}'.` };
565
+ }
566
+ function isStoppableAsyncStatusStep(step) {
567
+ return step.status === "pending" || step.status === "running";
568
+ }
569
+ function stopStoppableAsyncStatusChildren(status, stopChild, message) {
570
+ if (!stopChild) return;
571
+ for (const [index, step] of (status.steps ?? []).entries()) {
572
+ if (isStoppableAsyncStatusStep(step)) stopChild(asyncStatusChildIdentity(step, index), message);
573
+ }
574
+ }
575
+
576
+ // src/workflows/workflow-child-summary.ts
577
+ var KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
578
+ var TERMINAL_STATES = /* @__PURE__ */ new Set(["completed", "failed", "paused", "stopped", "rejected", "detached"]);
579
+ var MAX_REQUIRED_ID_BYTES = 4096;
580
+ var ACTIVITY_COUNTERS = ["currentToolStartedAt", "lastActivityAt", "durationMs", "toolCount", "turnCount", "tokens", "inputTokens", "outputTokens"];
581
+ function workflowChildProgress(progress) {
582
+ return {
583
+ ...workflowChildActivity(progress),
584
+ agent: bounded(progress.agent, 256) ?? "",
585
+ ...bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {},
586
+ ...bounded(progress.model, 256) ? { model: progress.model } : {},
587
+ ...bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {}
588
+ };
589
+ }
590
+ function workflowChildActivity(progress) {
591
+ const activity = {};
592
+ if (bounded(progress.currentTool, 256)) activity.currentTool = progress.currentTool;
593
+ for (const field of ACTIVITY_COUNTERS) {
594
+ const value = progress[field];
595
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) activity[field] = value;
596
+ }
597
+ return activity;
598
+ }
599
+ function parseActivity(value) {
600
+ if (value === void 0) return void 0;
601
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren child activity is invalid.");
602
+ const input = value;
603
+ for (const [key, value2] of Object.entries(input)) {
604
+ if (key === "currentTool" ? !bounded(value2, 256) : !ACTIVITY_COUNTERS.includes(key) || typeof value2 !== "number" || !Number.isFinite(value2) || value2 < 0) throw new Error("workflowChildren child activity is invalid.");
605
+ }
606
+ return { ...input };
607
+ }
608
+ function bounded(value, maxBytes) {
609
+ if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value, "utf8") > maxBytes) return void 0;
610
+ return value;
611
+ }
612
+ function requiredId(value, label) {
613
+ const id = bounded(value, MAX_REQUIRED_ID_BYTES);
614
+ if (!id) throw new Error(`${label} must be a non-empty identifier of at most ${MAX_REQUIRED_ID_BYTES} UTF-8 bytes.`);
615
+ return id;
616
+ }
617
+ function parentToolCallId(value, label) {
618
+ if (typeof value === "string" && value.trim() && Buffer.byteLength(value, "utf8") > MAX_REQUIRED_ID_BYTES) return hashedSegment(value);
619
+ return requiredId(value, label);
620
+ }
621
+ function workflowChildSummary(input) {
622
+ const rows = /* @__PURE__ */ new Map();
623
+ const runningAsyncKeys = new Set(input.children?.filter((child) => child.state === "running").map((child) => child.key));
624
+ for (const entry of input.trace ?? []) {
625
+ if (entry.operation !== "run" || !KEY_PATTERN.test(entry.key)) continue;
626
+ const previous = rows.get(entry.key);
627
+ const state = entry.state === "completed" ? "completed" : entry.state === "failed" ? "failed" : entry.state === "stopped" ? "stopped" : entry.state === "detached" ? "detached" : previous?.state ?? "running";
628
+ rows.set(entry.key, {
629
+ childId: entry.key,
630
+ state,
631
+ ...bounded(entry.runId, 256) ? { runId: bounded(entry.runId, 256) } : {},
632
+ ...previous?.agent ? { agent: previous.agent } : {},
633
+ ...previous?.sessionName ? { sessionName: previous.sessionName } : {},
634
+ ...previous?.model ? { model: previous.model } : {},
635
+ ...previous?.thinking ? { thinking: previous.thinking } : {}
636
+ });
637
+ }
638
+ for (const step of input.steps ?? []) {
639
+ const key = step.workflowKey;
640
+ if (!key || !KEY_PATTERN.test(key)) continue;
641
+ if (step.async && step.runId && step.status === "running") runningAsyncKeys.add(key);
642
+ const state = step.status === "complete" || step.status === "completed" ? "completed" : step.status === "failed" ? "failed" : step.status === "paused" ? "paused" : step.status === "stopped" ? "stopped" : step.status === "rejected" ? "rejected" : step.status === "pending" ? "pending" : "running";
643
+ const launchResolved = step.async !== void 0 || step.sessionFile !== void 0 || step.runId !== void 0 || step.model !== void 0;
644
+ rows.set(key, {
645
+ childId: key,
646
+ state,
647
+ ...bounded(step.runId, 256) ? { runId: bounded(step.runId, 256) } : {},
648
+ ...launchResolved && bounded(step.agent, 256) ? { agent: bounded(step.agent, 256) } : {},
649
+ ...bounded(step.sessionName, 256) ? { sessionName: bounded(step.sessionName, 256) } : {},
650
+ ...bounded(step.model, 256) ? { model: bounded(step.model, 256) } : {},
651
+ ...bounded(step.thinking, 32) ? { thinking: bounded(step.thinking, 32) } : {}
652
+ });
653
+ }
654
+ for (const child of input.children ?? []) {
655
+ if (!KEY_PATTERN.test(child.key)) continue;
656
+ const result = Array.isArray(child.results) ? child.results.find((value) => value && typeof value === "object") : void 0;
657
+ const state = child.state === "running" ? "running" : child.detached ? "detached" : child.stopped ? "stopped" : child.interrupted ? "paused" : child.ok ? "completed" : result?.acceptance && typeof result.acceptance === "object" && result.acceptance.status === "rejected" ? "rejected" : "failed";
658
+ rows.set(child.key, {
659
+ childId: child.key,
660
+ state,
661
+ ...bounded(child.runId, 256) ? { runId: bounded(child.runId, 256) } : {},
662
+ ...bounded(child.agent, 256) ? { agent: bounded(child.agent, 256) } : {},
663
+ ...bounded(result?.sessionName, 256) ? { sessionName: bounded(result?.sessionName, 256) } : {},
664
+ ...bounded(result?.model, 256) ? { model: bounded(result?.model, 256) } : {},
665
+ ...bounded(result?.thinking, 32) ? { thinking: bounded(result?.thinking, 32) } : {}
666
+ });
667
+ }
668
+ if (input.inventoryComplete) {
669
+ for (const [key, row] of rows) {
670
+ if (!TERMINAL_STATES.has(row.state) && !runningAsyncKeys.has(key)) rows.set(key, { ...row, state: input.workflowState === "stopped" ? "stopped" : "failed" });
671
+ }
672
+ }
673
+ for (const [key, launch] of input.launches ?? []) {
674
+ const row = rows.get(key);
675
+ if (!row || row.state !== "running") continue;
676
+ rows.set(key, {
677
+ ...row,
678
+ ...!row.runId && bounded(launch.runId, 256) ? { runId: launch.runId } : {},
679
+ ...!row.agent && bounded(launch.agent, 256) ? { agent: launch.agent } : {}
680
+ });
681
+ }
682
+ for (const [key, progress] of input.progress ?? []) {
683
+ const row = rows.get(key);
684
+ if (!row || row.state !== "running") continue;
685
+ rows.set(key, {
686
+ ...row,
687
+ ...bounded(progress.agent, 256) ? { agent: progress.agent } : {},
688
+ ...bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {},
689
+ ...bounded(progress.model, 256) ? { model: progress.model } : {},
690
+ ...bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {},
691
+ activity: workflowChildActivity(progress)
692
+ });
693
+ }
694
+ return {
695
+ version: 1,
696
+ parentToolCallId: parentToolCallId(input.parentToolCallId, "parentToolCallId"),
697
+ workflowRunId: requiredId(input.workflowRunId, "workflowRunId"),
698
+ inventoryComplete: input.inventoryComplete,
699
+ workflowState: input.workflowState,
700
+ children: [...rows.values()]
701
+ };
702
+ }
703
+ function parseWorkflowChildSummary(value) {
704
+ if (value === void 0) return void 0;
705
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren must be an object.");
706
+ const input = value;
707
+ const allowed = /* @__PURE__ */ new Set(["version", "parentToolCallId", "workflowRunId", "inventoryComplete", "workflowState", "children"]);
708
+ if (Object.keys(input).some((key) => !allowed.has(key))) throw new Error("workflowChildren has unsupported fields.");
709
+ if (input.version !== 1 || typeof input.inventoryComplete !== "boolean" || !Array.isArray(input.children)) throw new Error("workflowChildren is invalid.");
710
+ const workflowState = input.workflowState;
711
+ if (workflowState !== "queued" && workflowState !== "running" && workflowState !== "completed" && workflowState !== "failed" && workflowState !== "paused" && workflowState !== "stopped") throw new Error("workflowChildren.workflowState is invalid.");
712
+ const children = input.children.map((row) => {
713
+ if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error("workflowChildren child row is invalid.");
714
+ const child = row;
715
+ if (Object.keys(child).some((key) => !["childId", "runId", "agent", "sessionName", "model", "thinking", "state", "activity"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
716
+ if (typeof child.childId !== "string" || !KEY_PATTERN.test(child.childId)) throw new Error("workflowChildren childId is invalid.");
717
+ const state = child.state;
718
+ if (state !== "pending" && state !== "running" && state !== "completed" && state !== "failed" && state !== "paused" && state !== "stopped" && state !== "rejected" && state !== "detached") throw new Error("workflowChildren child state is invalid.");
719
+ for (const [field, maxBytes] of [["runId", 256], ["agent", 256], ["sessionName", 256], ["model", 256], ["thinking", 32]]) {
720
+ if (child[field] !== void 0 && bounded(child[field], maxBytes) === void 0) throw new Error(`workflowChildren child ${field} is invalid.`);
721
+ }
722
+ const activity = parseActivity(child.activity);
723
+ if (activity && state !== "running") throw new Error("workflowChildren child activity requires running state.");
724
+ return { childId: child.childId, state, ...activity ? { activity } : {}, ...child.runId ? { runId: child.runId } : {}, ...child.agent ? { agent: child.agent } : {}, ...child.sessionName ? { sessionName: child.sessionName } : {}, ...child.model ? { model: child.model } : {}, ...child.thinking ? { thinking: child.thinking } : {} };
725
+ });
726
+ if (new Set(children.map((child) => child.childId)).size !== children.length) throw new Error("workflowChildren has duplicate childId values.");
727
+ if (typeof input.parentToolCallId !== "string") throw new Error("workflowChildren.parentToolCallId is invalid.");
728
+ if (typeof input.workflowRunId !== "string") throw new Error("workflowChildren.workflowRunId is invalid.");
729
+ return { version: 1, parentToolCallId: parentToolCallId(input.parentToolCallId, "workflowChildren.parentToolCallId"), workflowRunId: requiredId(input.workflowRunId, "workflowChildren.workflowRunId"), inventoryComplete: input.inventoryComplete, workflowState, children };
730
+ }
731
+
732
+ // src/workflows/workflow-preflight.ts
733
+ var WORKFLOW_PREFLIGHT_VERSION = 1;
734
+ var WORKFLOW_PREFLIGHT_MAX_LANES = 64;
735
+ var WORKFLOW_PREFLIGHT_MAX_STRING_LENGTH = 256;
736
+ var WORKFLOW_PREFLIGHT_MAX_CLAIMS = 16;
737
+ var WORKFLOW_PREFLIGHT_MAX_DEPTH = 3;
738
+ var WORKFLOW_PREFLIGHT_MAX_BYTES = 16 * 1024;
739
+ var WORKFLOW_PREFLIGHT_MAX_WARNINGS = 16;
740
+ var WORKFLOW_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
741
+ var PREFLIGHT_FIELDS = /* @__PURE__ */ new Set(["version", "coverage", "lanes"]);
742
+ var LANE_FIELDS = /* @__PURE__ */ new Set(["key", "mode", "decision", "claims", "expectedOutput", "independence"]);
743
+ var PREFLIGHT_MODES = /* @__PURE__ */ new Set(["mutation", "review", "scout", "gate"]);
744
+ function isPlainObject(value) {
745
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
746
+ const prototype = Object.getPrototypeOf(value);
747
+ return prototype === Object.prototype || prototype === null;
748
+ }
749
+ function assertPlainObject(value, path7) {
750
+ if (!isPlainObject(value)) throw new Error(`${path7} must be a plain JSON object.`);
751
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new Error(`${path7} must not contain symbol keys.`);
752
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
753
+ if (!descriptor.enumerable || !("value" in descriptor)) throw new Error(`${path7}.${key} must be an enumerable data property.`);
754
+ }
755
+ }
756
+ function assertKnownFields(value, allowed, path7) {
757
+ for (const key of Object.keys(value)) {
758
+ if (!allowed.has(key)) throw new Error(`${path7} contains unsupported field '${key}'.`);
759
+ }
760
+ }
761
+ function normalizeDisplayString(value, path7) {
762
+ if (typeof value !== "string") throw new Error(`${path7} must be a string.`);
763
+ const normalized = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
764
+ if (!normalized) throw new Error(`${path7} must be a non-empty string.`);
765
+ if (normalized.length > WORKFLOW_PREFLIGHT_MAX_STRING_LENGTH) {
766
+ throw new Error(`${path7} exceeds the maximum length of ${WORKFLOW_PREFLIGHT_MAX_STRING_LENGTH}.`);
767
+ }
768
+ return normalized;
769
+ }
770
+ function normalizeLane(value, index) {
771
+ const path7 = `preflight.lanes[${index}]`;
772
+ assertPlainObject(value, path7);
773
+ assertKnownFields(value, LANE_FIELDS, path7);
774
+ const key = normalizeDisplayString(value.key, `${path7}.key`);
775
+ if (!WORKFLOW_KEY_PATTERN.test(key)) throw new Error(`${path7}.key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.`);
776
+ let mode;
777
+ if (value.mode !== void 0) {
778
+ if (typeof value.mode !== "string" || !PREFLIGHT_MODES.has(value.mode)) {
779
+ throw new Error(`${path7}.mode must be one of: mutation, review, scout, gate.`);
780
+ }
781
+ mode = value.mode;
782
+ }
783
+ let claims;
784
+ if (value.claims !== void 0) {
785
+ if (!Array.isArray(value.claims)) throw new Error(`${path7}.claims must be an array of strings.`);
786
+ if (value.claims.length > WORKFLOW_PREFLIGHT_MAX_CLAIMS) throw new Error(`${path7}.claims supports at most ${WORKFLOW_PREFLIGHT_MAX_CLAIMS} entries.`);
787
+ claims = value.claims.map((claim, claimIndex) => normalizeDisplayString(claim, `${path7}.claims[${claimIndex}]`));
788
+ for (let claimIndex = 0; claimIndex < value.claims.length; claimIndex += 1) {
789
+ if (!Object.prototype.hasOwnProperty.call(value.claims, claimIndex)) throw new Error(`${path7}.claims must not contain sparse arrays.`);
790
+ }
791
+ }
792
+ return {
793
+ key,
794
+ ...mode ? { mode } : {},
795
+ ...value.decision !== void 0 ? { decision: normalizeDisplayString(value.decision, `${path7}.decision`) } : {},
796
+ ...claims ? { claims } : {},
797
+ ...value.expectedOutput !== void 0 ? { expectedOutput: normalizeDisplayString(value.expectedOutput, `${path7}.expectedOutput`) } : {},
798
+ ...value.independence !== void 0 ? { independence: normalizeDisplayString(value.independence, `${path7}.independence`) } : {}
799
+ };
800
+ }
801
+ function normalizeWorkflowPreflight(input) {
802
+ if (input === void 0) return void 0;
803
+ assertPlainObject(input, "preflight");
804
+ assertKnownFields(input, PREFLIGHT_FIELDS, "preflight");
805
+ if (input.version !== WORKFLOW_PREFLIGHT_VERSION) throw new Error(`preflight.version must be ${WORKFLOW_PREFLIGHT_VERSION}.`);
806
+ const coverage = input.coverage === void 0 ? "partial" : input.coverage;
807
+ if (coverage !== "complete" && coverage !== "partial") throw new Error("preflight.coverage must be 'complete' or 'partial'.");
808
+ if (!Array.isArray(input.lanes)) throw new Error("preflight.lanes must be an array.");
809
+ if (input.lanes.length > WORKFLOW_PREFLIGHT_MAX_LANES) throw new Error(`preflight.lanes supports at most ${WORKFLOW_PREFLIGHT_MAX_LANES} lanes.`);
810
+ for (let index = 0; index < input.lanes.length; index += 1) {
811
+ if (!Object.prototype.hasOwnProperty.call(input.lanes, index)) throw new Error("preflight.lanes must not contain sparse arrays.");
812
+ }
813
+ const lanes = input.lanes.map(normalizeLane);
814
+ const seenKeys = /* @__PURE__ */ new Set();
815
+ for (const lane of lanes) {
816
+ if (seenKeys.has(lane.key)) throw new Error(`preflight.lanes contains duplicate key '${lane.key}'.`);
817
+ seenKeys.add(lane.key);
818
+ }
819
+ const normalized = { version: WORKFLOW_PREFLIGHT_VERSION, coverage, lanes };
820
+ const depth = 1 + (lanes.length > 0 ? 1 : 0) + (lanes.some((lane) => lane.claims !== void 0) ? 1 : 0);
821
+ if (depth > WORKFLOW_PREFLIGHT_MAX_DEPTH) throw new Error(`preflight exceeds the maximum nesting depth of ${WORKFLOW_PREFLIGHT_MAX_DEPTH}.`);
822
+ const bytes = Buffer.byteLength(JSON.stringify(normalized), "utf8");
823
+ if (bytes > WORKFLOW_PREFLIGHT_MAX_BYTES) throw new Error(`preflight canonical JSON is ${bytes} bytes; maximum is ${WORKFLOW_PREFLIGHT_MAX_BYTES}.`);
824
+ return normalized;
825
+ }
826
+ function warningLimit(messages) {
827
+ if (messages.length <= WORKFLOW_PREFLIGHT_MAX_WARNINGS) return messages;
828
+ const omitted = messages.length - WORKFLOW_PREFLIGHT_MAX_WARNINGS + 1;
829
+ return [...messages.slice(0, WORKFLOW_PREFLIGHT_MAX_WARNINGS - 1), `Preflight advisory: ${omitted} additional mismatch warning(s) omitted.`];
830
+ }
831
+ function declaredKeys(preflight) {
832
+ return new Set(preflight.lanes.map((lane) => lane.key));
833
+ }
834
+ function workflowKeyMatchesPreflightLane(key, laneKey, generatedLaneKey) {
835
+ return generatedLaneKey === laneKey || key === laneKey || key.startsWith(`${laneKey}.`);
836
+ }
837
+ function workflowPreflightLaneForRuntimeKey(preflight, key, preferredKeys = []) {
838
+ const lanes = preflight?.lanes;
839
+ if (!lanes) return void 0;
840
+ const exact = lanes.find((lane) => lane.key === key);
841
+ if (exact) return exact;
842
+ let closestRoot;
843
+ for (const lane of lanes) {
844
+ if (key.startsWith(`${lane.key}.`) && (!closestRoot || lane.key.length > closestRoot.key.length)) closestRoot = lane;
845
+ }
846
+ if (closestRoot) return closestRoot;
847
+ for (const preferredKey of preferredKeys) {
848
+ if (!preferredKey) continue;
849
+ const preferred = lanes.find((lane) => lane.key === preferredKey);
850
+ if (preferred) return preferred;
851
+ }
852
+ return void 0;
853
+ }
854
+ function declaredLaneCoversWorkflowKey(entry, laneKey) {
855
+ return workflowKeyMatchesPreflightLane(entry.key, laneKey, entry.generatedLaneKey);
856
+ }
857
+ function keyCoveredByDeclaredLane(entry, declared) {
858
+ for (const laneKey of declared) if (declaredLaneCoversWorkflowKey(entry, laneKey)) return true;
859
+ return false;
860
+ }
861
+ function launchedEntries(trace) {
862
+ const entries = [];
863
+ const seen = /* @__PURE__ */ new Set();
864
+ for (const entry of trace) {
865
+ if (entry.operation !== "run" || entry.phase === "auto-resume" || seen.has(entry.key)) continue;
866
+ seen.add(entry.key);
867
+ entries.push(entry);
868
+ }
869
+ return entries;
870
+ }
871
+ function undeclaredWarning(key) {
872
+ return `Preflight advisory: workflow key '${key}' launched without a declared lane.`;
873
+ }
874
+ function unlaunchedWarning(key) {
875
+ return `Preflight advisory: declared lane '${key}' was not launched.`;
876
+ }
877
+ function workflowPreflightWarnings(preflight, trace, options = {}) {
878
+ if (!preflight) return [];
879
+ const declared = declaredKeys(preflight);
880
+ const launched = launchedEntries(trace);
881
+ const messages = launched.filter((entry) => !keyCoveredByDeclaredLane(entry, declared)).map((entry) => undeclaredWarning(entry.key));
882
+ if (options.settled === true) {
883
+ for (const lane of preflight.lanes) if (!launched.some((entry) => declaredLaneCoversWorkflowKey(entry, lane.key))) messages.push(unlaunchedWarning(lane.key));
884
+ }
885
+ return warningLimit(messages);
886
+ }
887
+ function annotateWorkflowPreflightTrace(trace, preflight) {
888
+ if (!preflight) return [...trace];
889
+ const declared = declaredKeys(preflight);
890
+ const warned = /* @__PURE__ */ new Set();
891
+ return trace.map((entry) => {
892
+ if (entry.operation !== "run" || entry.phase === "auto-resume" || keyCoveredByDeclaredLane(entry, declared) || warned.has(entry.key)) return entry;
893
+ warned.add(entry.key);
894
+ return { ...entry, warning: entry.warning ?? undeclaredWarning(entry.key) };
895
+ });
896
+ }
897
+ function laneCell(value) {
898
+ return value ?? "\u2014";
899
+ }
900
+ var WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH = 96;
901
+ function planLaneLabel(lane) {
902
+ const value = lane.decision?.trim() || lane.key;
903
+ if (value.length <= WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH) return value;
904
+ return `${value.slice(0, WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH - 1).trimEnd()}\u2026`;
905
+ }
906
+ function formatWorkflowPreflight(preflight, options = {}) {
907
+ if (!preflight) return "";
908
+ const indent = options.indent ?? "";
909
+ const count2 = preflight.lanes.length;
910
+ const lines = [
911
+ `${indent}Preflight: v${preflight.version} \xB7 ${preflight.coverage} \xB7 ${count2} lane${count2 === 1 ? "" : "s"}`,
912
+ `${indent} key | mode | decision | claims | expected output | independence`
913
+ ];
914
+ for (const lane of preflight.lanes) {
915
+ lines.push(`${indent} ${[
916
+ lane.key,
917
+ laneCell(lane.mode),
918
+ laneCell(lane.decision),
919
+ lane.claims?.join(", ") ?? "\u2014",
920
+ laneCell(lane.expectedOutput),
921
+ laneCell(lane.independence)
922
+ ].join(" | ")}`);
923
+ }
924
+ return lines.join("\n");
925
+ }
926
+ function formatWorkflowPreflightSummary(preflight) {
927
+ if (!preflight) return "";
928
+ const keys = preflight.lanes.slice(0, 4).map((lane) => lane.key).join(", ");
929
+ const remainder = preflight.lanes.length > 4 ? `, +${preflight.lanes.length - 4}` : "";
930
+ return `preflight \xB7 ${preflight.coverage} \xB7 ${preflight.lanes.length} lane${preflight.lanes.length === 1 ? "" : "s"}${keys ? `: ${keys}${remainder}` : ""}`;
931
+ }
932
+ function formatWorkflowPreflightPlanSummary(preflight, options = {}) {
933
+ if (!preflight) return "";
934
+ const indent = options.indent ?? "";
935
+ const count2 = preflight.lanes.length;
936
+ const labels = count2 === 1 ? planLaneLabel(preflight.lanes[0]) : preflight.lanes.slice(0, 4).map((lane) => lane.key).join(", ") + (count2 > 4 ? `, +${count2 - 4}` : "");
937
+ return `${indent}Plan: ${count2} lane${count2 === 1 ? "" : "s"}${labels ? ` \xB7 ${labels}` : ""}`;
938
+ }
939
+ function formatWorkflowPreflightWarningSummary(warnings, options = {}) {
940
+ if (!warnings?.length) return "";
941
+ const indent = options.indent ?? "";
942
+ const hint = options.hint ?? "details available for debug";
943
+ const count2 = warnings.length;
944
+ return `${indent}Plan note: ${count2} preflight mismatch${count2 === 1 ? "" : "es"} \xB7 ${hint}.`;
945
+ }
946
+ function formatWorkflowPreflightWarnings(warnings, options = {}) {
947
+ if (!warnings?.length) return "";
948
+ const indent = options.indent ?? "";
949
+ return [`${indent}Preflight warnings:`, ...warnings.map((warning) => `${indent} - ${warning.replace(/\s+/g, " ").trim()}`)].join("\n");
950
+ }
951
+
952
+ // src/runs/background/async-status.ts
953
+ import * as fs3 from "node:fs";
954
+ import * as path3 from "node:path";
955
+
956
+ // src/runs/background/run-id-query.ts
957
+ var MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH = 8;
958
+ var FULL_ASYNC_RUN_ID_LENGTH = 36;
959
+ function canScanAsyncRunPrefix(id) {
960
+ return id.length >= MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH && id.length < FULL_ASYNC_RUN_ID_LENGTH;
961
+ }
962
+
963
+ // src/runs/shared/workflow-graph.ts
964
+ function workflowGraphStageNodes(graph) {
965
+ const nodes = graph?.nodes;
966
+ if (!nodes?.length) return [];
967
+ const stages = [];
968
+ const visit = (node) => {
969
+ if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
970
+ for (const child of node.children ?? []) visit(child);
971
+ return;
972
+ }
973
+ if (node.kind !== "host-step") stages.push(node);
974
+ };
975
+ for (const node of nodes) visit(node);
976
+ return stages;
977
+ }
978
+ function normalizeStatus(status) {
979
+ switch (status) {
980
+ case "complete":
981
+ case "completed":
982
+ return "completed";
983
+ case "running":
984
+ return "running";
985
+ case "failed":
986
+ return "failed";
987
+ case "paused":
988
+ return "paused";
989
+ case "detached":
990
+ return "detached";
991
+ case "rejected":
992
+ return "rejected";
993
+ case "pending":
994
+ return "pending";
995
+ default:
996
+ return void 0;
997
+ }
998
+ }
999
+ function resultStatus(result) {
1000
+ if (!result) return void 0;
1001
+ if (result.detached) return "detached";
1002
+ if (result.interrupted) return "paused";
1003
+ return result.exitCode === 0 ? "completed" : "failed";
1004
+ }
1005
+ function nodeStatus(input, flatIndex) {
1006
+ return normalizeStatus(input.stepStatuses?.[flatIndex]?.status) ?? resultStatus(input.results?.[flatIndex]) ?? (input.currentFlatIndex === flatIndex ? "running" : "pending");
1007
+ }
1008
+ function pushPhase(phases, phase, nodeId) {
1009
+ if (!phase) return;
1010
+ let group = phases.find((candidate) => candidate.title === phase);
1011
+ if (!group) {
1012
+ group = { title: phase, nodeIds: [] };
1013
+ phases.push(group);
1014
+ }
1015
+ group.nodeIds.push(nodeId);
1016
+ }
1017
+ function seqLabel(step, stepIndex) {
1018
+ return step.label?.trim() || step.agent || `Step ${stepIndex + 1}`;
1019
+ }
1020
+ function summarizeParallelStatuses(statuses) {
1021
+ if (statuses.some((status) => status === "running")) return "running";
1022
+ if (statuses.some((status) => status === "rejected")) return "rejected";
1023
+ if (statuses.some((status) => status === "failed")) return "failed";
1024
+ if (statuses.some((status) => status === "paused")) return "paused";
1025
+ if (statuses.some((status) => status === "detached")) return "detached";
1026
+ if (statuses.length > 0 && statuses.every((status) => status === "completed")) return "completed";
1027
+ if (statuses.some((status) => status === "completed")) return "running";
1028
+ return "pending";
1029
+ }
1030
+ function buildWorkflowGraphSnapshot(input) {
1031
+ const nodes = [];
1032
+ const phases = [];
1033
+ let flatIndex = 0;
1034
+ let currentNodeId;
1035
+ for (let stepIndex = 0; stepIndex < input.steps.length; stepIndex++) {
1036
+ const step = input.steps[stepIndex];
1037
+ if (isParallelStep(step)) {
1038
+ const groupId = `step-${stepIndex}`;
1039
+ const children = [];
1040
+ const childStatuses = [];
1041
+ for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
1042
+ const task = step.parallel[taskIndex];
1043
+ const status2 = nodeStatus(input, flatIndex);
1044
+ childStatuses.push(status2);
1045
+ const childId = `step-${stepIndex}-agent-${taskIndex}`;
1046
+ const child = {
1047
+ id: childId,
1048
+ kind: "agent",
1049
+ agent: task.agent,
1050
+ phase: task.phase,
1051
+ label: task.label?.trim() || task.agent || `Agent ${taskIndex + 1}`,
1052
+ status: status2,
1053
+ flatIndex,
1054
+ stepIndex,
1055
+ outputName: task.as,
1056
+ structured: Boolean(task.outputSchema),
1057
+ acceptanceStatus: input.results?.[flatIndex]?.acceptance?.status,
1058
+ error: input.stepStatuses?.[flatIndex]?.error ?? input.results?.[flatIndex]?.error
1059
+ };
1060
+ children.push(child);
1061
+ pushPhase(phases, task.phase, childId);
1062
+ if (status2 === "running" || input.currentFlatIndex === flatIndex) currentNodeId = childId;
1063
+ flatIndex++;
1064
+ }
1065
+ const groupStatus = summarizeParallelStatuses(childStatuses);
1066
+ if (input.currentStepIndex === stepIndex && !currentNodeId) currentNodeId = groupId;
1067
+ nodes.push({
1068
+ id: groupId,
1069
+ kind: "parallel-group",
1070
+ label: step.parallel.length === 1 ? "Parallel task" : `Parallel group (${step.parallel.length})`,
1071
+ status: groupStatus,
1072
+ stepIndex,
1073
+ children
1074
+ });
1075
+ continue;
1076
+ }
1077
+ if (isDynamicParallelStep(step)) {
1078
+ const groupId = `step-${stepIndex}`;
1079
+ const materialized = input.dynamicChildren?.[stepIndex] ?? [];
1080
+ const groupOverride = input.dynamicGroupStatuses?.[stepIndex];
1081
+ const children = [];
1082
+ const childStatuses = [];
1083
+ for (let taskIndex = 0; taskIndex < materialized.length; taskIndex++) {
1084
+ const task = materialized[taskIndex];
1085
+ const status2 = nodeStatus(input, task.flatIndex);
1086
+ childStatuses.push(status2);
1087
+ const childId = `step-${stepIndex}-item-${task.itemKey.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
1088
+ const child = {
1089
+ id: childId,
1090
+ kind: "agent",
1091
+ agent: task.agent,
1092
+ phase: step.parallel.phase ?? step.phase,
1093
+ label: task.label?.trim() || step.parallel.label?.trim() || `${task.agent} ${task.itemKey}`,
1094
+ status: status2,
1095
+ flatIndex: task.flatIndex,
1096
+ stepIndex,
1097
+ itemKey: task.itemKey,
1098
+ outputName: task.outputName,
1099
+ structured: task.structured,
1100
+ acceptanceStatus: input.results?.[task.flatIndex]?.acceptance?.status,
1101
+ error: input.stepStatuses?.[task.flatIndex]?.error ?? input.results?.[task.flatIndex]?.error ?? task.error
1102
+ };
1103
+ children.push(child);
1104
+ pushPhase(phases, child.phase, childId);
1105
+ if (status2 === "running" || input.currentFlatIndex === task.flatIndex) currentNodeId = childId;
1106
+ }
1107
+ const groupStatus = groupOverride?.status ?? (children.length > 0 ? summarizeParallelStatuses(childStatuses) : input.currentStepIndex === stepIndex ? "running" : "pending");
1108
+ if (input.currentStepIndex === stepIndex && !currentNodeId) currentNodeId = groupId;
1109
+ nodes.push({
1110
+ id: groupId,
1111
+ kind: "dynamic-parallel-group",
1112
+ label: step.label?.trim() || step.parallel.label?.trim() || `Dynamic fanout (${step.collect.as})`,
1113
+ status: groupStatus,
1114
+ stepIndex,
1115
+ outputName: step.collect.as,
1116
+ structured: Boolean(step.collect.outputSchema),
1117
+ acceptanceStatus: groupOverride?.acceptance?.status,
1118
+ error: groupOverride?.error,
1119
+ dynamic: {
1120
+ sourceOutput: step.expand.from.output,
1121
+ sourcePath: step.expand.from.path,
1122
+ itemName: step.expand.item ?? "item",
1123
+ maxItems: step.expand.maxItems,
1124
+ collectAs: step.collect.as
1125
+ },
1126
+ children
1127
+ });
1128
+ if (materialized.length > 0) flatIndex = Math.max(flatIndex, ...materialized.map((child) => child.flatIndex + 1));
1129
+ continue;
1130
+ }
1131
+ const seq = step;
1132
+ const status = nodeStatus(input, flatIndex);
1133
+ const id = `step-${stepIndex}`;
1134
+ nodes.push({
1135
+ id,
1136
+ kind: "step",
1137
+ agent: seq.agent,
1138
+ phase: seq.phase,
1139
+ label: seqLabel(seq, stepIndex),
1140
+ status,
1141
+ flatIndex,
1142
+ stepIndex,
1143
+ outputName: seq.as,
1144
+ structured: Boolean(seq.outputSchema),
1145
+ acceptanceStatus: input.results?.[flatIndex]?.acceptance?.status,
1146
+ error: input.stepStatuses?.[flatIndex]?.error ?? input.results?.[flatIndex]?.error
1147
+ });
1148
+ pushPhase(phases, seq.phase, id);
1149
+ if (status === "running" || input.currentFlatIndex === flatIndex || input.currentStepIndex === stepIndex) currentNodeId = id;
1150
+ flatIndex++;
1151
+ }
1152
+ return {
1153
+ runId: input.runId,
1154
+ mode: input.mode ?? "chain",
1155
+ phases,
1156
+ nodes,
1157
+ currentNodeId
1158
+ };
1159
+ }
1160
+
1161
+ // src/runs/shared/async-status-projection.ts
1162
+ var ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
1163
+ var ASYNC_STATUS_SNAPSHOT_VERSION = 1;
1164
+ var DEFAULT_MAX_RUNS = 20;
1165
+ var DEFAULT_MAX_CHILDREN_PER_NODE = 8;
1166
+ var DEFAULT_MAX_DEPTH = 3;
1167
+ var DEFAULT_MAX_STRING_LENGTH = 160;
1168
+ var DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
1169
+ var ASYNC_STATUS_SNAPSHOT_STATES = {
1170
+ queued: true,
1171
+ running: true,
1172
+ complete: true,
1173
+ failed: true,
1174
+ partial: true,
1175
+ paused: true,
1176
+ stopped: true,
1177
+ rejected: true
1178
+ };
1179
+ function isAsyncStatusSnapshotState(value) {
1180
+ return Object.hasOwn(ASYNC_STATUS_SNAPSHOT_STATES, value);
1181
+ }
1182
+ function validHostStepList(source) {
1183
+ if (source && "nodes" in source) return validHostStepNodes(source);
1184
+ const hostSteps = [];
1185
+ for (const [index, value] of (source ?? []).slice(0, HOST_STEP_MAX_COUNT).entries()) {
1186
+ try {
1187
+ hostSteps.push(parseHostStepNode(value, `hostSteps[${index}]`));
1188
+ } catch {
1189
+ }
1190
+ }
1191
+ return hostSteps;
1192
+ }
1193
+ function resolveCaps(options) {
1194
+ return {
1195
+ maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
1196
+ maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
1197
+ maxDepth: Math.max(0, Math.floor(options.maxDepth ?? DEFAULT_MAX_DEPTH)),
1198
+ maxStringLength: Math.max(0, Math.floor(options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH)),
1199
+ maxSerializedBytes: Math.max(256, Math.floor(options.maxSerializedBytes ?? DEFAULT_MAX_SERIALIZED_BYTES))
1200
+ };
1201
+ }
1202
+ function publicText(value, fallback, maxLength) {
1203
+ if (typeof value !== "string") return fallback;
1204
+ const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
1205
+ return truncateDisplayText(normalized || fallback, maxLength);
1206
+ }
1207
+ function publicOptionalText(value, maxLength) {
1208
+ if (typeof value !== "string") return void 0;
1209
+ const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
1210
+ return normalized ? truncateDisplayText(normalized, maxLength) : void 0;
1211
+ }
1212
+ function publicTime(value) {
1213
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1214
+ }
1215
+ function publicCount(value) {
1216
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1217
+ }
1218
+ function normalizeState(value) {
1219
+ if (value === "completed") return "complete";
1220
+ if (value === "pending") return "queued";
1221
+ if (isAsyncStatusSnapshotState(value)) return value;
1222
+ return "partial";
1223
+ }
1224
+ function terminalState(state) {
1225
+ return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped" || state === "rejected";
1226
+ }
1227
+ function kindForMode(mode) {
1228
+ return mode === "workflow" ? "workflow" : "subagent";
1229
+ }
1230
+ function labelForAgents(agents, fallback, maxLength) {
1231
+ if (!agents?.length) return publicText(fallback, fallback, maxLength);
1232
+ const visible = agents.slice(0, 3).map((agent) => publicText(agent, "agent", maxLength)).join(", ");
1233
+ const suffix = agents.length > 3 ? `, +${agents.length - 3} more` : "";
1234
+ return truncateDisplayText(`${visible}${suffix}`, maxLength);
1235
+ }
1236
+ function activityFor(source, ctx) {
1237
+ const currentTool = publicOptionalText(source.currentTool, ctx.caps.maxStringLength);
1238
+ const lastActivityAt = publicTime(source.lastActivityAt);
1239
+ const currentToolStartedAt = publicTime(source.currentToolStartedAt);
1240
+ const turnCount = publicCount(source.turnCount);
1241
+ const toolCount = publicCount(source.toolCount);
1242
+ const activity = {
1243
+ ...typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {},
1244
+ ...currentTool ? { currentTool } : {},
1245
+ ...lastActivityAt !== void 0 ? { lastActivityAt } : {},
1246
+ ...currentToolStartedAt !== void 0 ? { currentToolStartedAt } : {},
1247
+ ...turnCount !== void 0 ? { turnCount } : {},
1248
+ ...toolCount !== void 0 ? { toolCount } : {}
1249
+ };
1250
+ return Object.keys(activity).length ? activity : void 0;
1251
+ }
1252
+ function setStepModel(node, ctx, ...sources) {
1253
+ const { baseModel, thinkingSuffix } = splitKnownThinkingSuffix(sources.find((source) => source?.model)?.model ?? "");
1254
+ const model = publicOptionalText(baseModel, ctx.caps.maxStringLength);
1255
+ if (model) node.model = model;
1256
+ const thinking = childThinkingLevel(...sources, { thinking: thinkingSuffix.slice(1) });
1257
+ if (thinking) node.thinking = thinking;
1258
+ }
1259
+ function appendBoundedChildren(children, source, ctx) {
1260
+ const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
1261
+ children.push(...source.slice(0, remaining));
1262
+ ctx.omitted.children += Math.max(0, source.length - remaining);
1263
+ }
1264
+ function stepIdentity(step, index, ctx) {
1265
+ return {
1266
+ id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
1267
+ label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength)
1268
+ };
1269
+ }
1270
+ function projectStep(step, index, depth, ctx) {
1271
+ const state = normalizeState(step.status);
1272
+ const startedAt = publicTime(step.startedAt);
1273
+ const durationMs = "durationMs" in step ? publicTime(step.durationMs) : void 0;
1274
+ const endedAt = publicTime(step.endedAt) ?? (terminalState(state) && startedAt !== void 0 && durationMs !== void 0 ? publicTime(startedAt + durationMs) : void 0);
1275
+ const updatedAt = endedAt ?? publicTime(step.lastActivityAt) ?? startedAt;
1276
+ const activity = activityFor(step, ctx);
1277
+ const { id, label } = stepIdentity(step, index, ctx);
1278
+ const agent = publicOptionalText(step.agent, ctx.caps.maxStringLength);
1279
+ const node = {
1280
+ id,
1281
+ kind: "step",
1282
+ label,
1283
+ state,
1284
+ ...startedAt !== void 0 ? { startedAt } : {},
1285
+ ...updatedAt !== void 0 ? { updatedAt } : {},
1286
+ ...terminalState(state) && endedAt !== void 0 ? { endedAt } : {},
1287
+ ...activity ? { activity } : {}
1288
+ };
1289
+ if (agent) node.agent = agent;
1290
+ setStepModel(node, ctx, step);
1291
+ if (depth < ctx.caps.maxDepth && step.children?.length) {
1292
+ const nested = step.children.map((child, childIndex) => projectNestedRun(child, childIndex, depth + 1, ctx));
1293
+ const bounded2 = [];
1294
+ appendBoundedChildren(bounded2, nested, ctx);
1295
+ if (bounded2.length) node.children = bounded2;
1296
+ } else if (step.children?.length) {
1297
+ ctx.omitted.children += step.children.length;
1298
+ }
1299
+ return node;
1300
+ }
1301
+ function projectNestedRun(child, index, depth, ctx) {
1302
+ const state = normalizeState(child.state);
1303
+ const startedAt = publicTime(child.startedAt);
1304
+ const endedAt = publicTime(child.endedAt);
1305
+ const updatedAt = publicTime(child.lastUpdate) ?? endedAt ?? publicTime(child.lastActivityAt) ?? startedAt;
1306
+ const activity = activityFor(child, ctx);
1307
+ const node = {
1308
+ id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
1309
+ kind: kindForMode(child.mode),
1310
+ label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
1311
+ state,
1312
+ ...startedAt !== void 0 ? { startedAt } : {},
1313
+ ...updatedAt !== void 0 ? { updatedAt } : {},
1314
+ ...terminalState(state) && endedAt !== void 0 ? { endedAt } : {},
1315
+ ...activity ? { activity } : {}
1316
+ };
1317
+ if (depth < ctx.caps.maxDepth) {
1318
+ const nestedSteps = child.steps?.map((step, stepIndex) => projectStep(step, stepIndex, depth + 1, ctx)) ?? [];
1319
+ const nestedChildren = child.children?.map((nested, childIndex) => projectNestedRun(nested, childIndex, depth + 1, ctx)) ?? [];
1320
+ const bounded2 = [];
1321
+ appendBoundedChildren(bounded2, [...nestedSteps, ...nestedChildren], ctx);
1322
+ if (bounded2.length) node.children = bounded2;
1323
+ } else {
1324
+ ctx.omitted.children += (child.steps?.length ?? 0) + (child.children?.length ?? 0);
1325
+ }
1326
+ return node;
1327
+ }
1328
+ function hostStepSnapshotState(state, verdict) {
1329
+ if (state === "pending") return "queued";
1330
+ if (state === "running") return "running";
1331
+ if (state === "cancelled") return "stopped";
1332
+ if (state === "error" || verdict === "fail") return "failed";
1333
+ return verdict === void 0 || verdict === "inconclusive" ? "partial" : "complete";
1334
+ }
1335
+ function projectHostStep(hostStep, ctx) {
1336
+ const state = hostStepSnapshotState(hostStep.state, hostStep.verdict);
1337
+ const detail = publicOptionalText(hostStep.detail, ctx.caps.maxStringLength);
1338
+ const report = publicOptionalText(hostStepReportName(hostStep.reportPath), ctx.caps.maxStringLength);
1339
+ const hostMetadata = {
1340
+ kind: hostStep.monitorKind,
1341
+ state: hostStep.state,
1342
+ ...hostStep.provider ? { provider: publicText(hostStep.provider, "provider", ctx.caps.maxStringLength) } : {},
1343
+ ...hostStep.role ? { role: publicText(hostStep.role, "role", ctx.caps.maxStringLength) } : {},
1344
+ ...hostStep.verdict ? { verdict: hostStep.verdict } : {},
1345
+ ...hostStep.reasonCode ? { reasonCode: publicText(hostStep.reasonCode, "reason", ctx.caps.maxStringLength) } : {},
1346
+ ...detail ? { detail } : {},
1347
+ ...hostStep.target ? { target: publicText(hostStep.target, "target", ctx.caps.maxStringLength) } : {},
1348
+ ...hostStep.freshness?.stale !== void 0 ? { stale: hostStep.freshness.stale } : {},
1349
+ ...report ? { report } : {}
1350
+ };
1351
+ return {
1352
+ id: publicText(hostStep.id, "host-step", ctx.caps.maxStringLength),
1353
+ kind: "host-step",
1354
+ label: publicText(hostStep.label, "host step", ctx.caps.maxStringLength),
1355
+ state,
1356
+ updatedAt: hostStep.updatedAt,
1357
+ ...terminalState(state) ? { endedAt: hostStep.updatedAt } : {},
1358
+ hostStep: hostMetadata
1359
+ };
1360
+ }
1361
+ function asyncJobOrder(left, right) {
1362
+ const leftRank = left.status === "running" ? 0 : left.status === "queued" ? 1 : 2;
1363
+ const rightRank = right.status === "running" ? 0 : right.status === "queued" ? 1 : 2;
1364
+ const leftTime = left.updatedAt ?? left.startedAt ?? 0;
1365
+ const rightTime = right.updatedAt ?? right.startedAt ?? 0;
1366
+ return leftRank - rightRank || rightTime - leftTime || left.asyncId.localeCompare(right.asyncId);
1367
+ }
1368
+ function hasCyclicWorkflowParent(job, parents) {
1369
+ const seen = /* @__PURE__ */ new Set([job.asyncId]);
1370
+ let parentId = job.parentWorkflowRunId;
1371
+ while (parentId) {
1372
+ if (seen.has(parentId)) return true;
1373
+ seen.add(parentId);
1374
+ parentId = parents.get(parentId)?.parentWorkflowRunId;
1375
+ }
1376
+ return false;
1377
+ }
1378
+ function groupMaterializedChildren(jobs) {
1379
+ const parents = new Map(jobs.filter((job) => job.mode === "workflow").map((job) => [job.asyncId, job]));
1380
+ const childrenByParent = /* @__PURE__ */ new Map();
1381
+ const liveRoots = /* @__PURE__ */ new Set();
1382
+ const roots = [];
1383
+ for (const job of jobs) {
1384
+ const parent = job.parentWorkflowRunId ? parents.get(job.parentWorkflowRunId) : void 0;
1385
+ const keepLiveRoot = (job.status === "running" || job.status === "queued") && parent?.status !== "running";
1386
+ if (parent && !hasCyclicWorkflowParent(job, parents)) {
1387
+ const siblings = childrenByParent.get(parent.asyncId) ?? [];
1388
+ siblings.push(job);
1389
+ childrenByParent.set(parent.asyncId, siblings);
1390
+ if (keepLiveRoot) {
1391
+ liveRoots.add(job);
1392
+ roots.push(job);
1393
+ }
1394
+ } else {
1395
+ roots.push(job);
1396
+ }
1397
+ }
1398
+ for (const children of childrenByParent.values()) children.sort(asyncJobOrder);
1399
+ return { roots, childrenByParent, liveRoots };
1400
+ }
1401
+ function assignMaterializedChildren(steps, children) {
1402
+ const assigned = /* @__PURE__ */ new Map();
1403
+ const claimed = /* @__PURE__ */ new Set();
1404
+ for (const step of steps) {
1405
+ if (!step.runId) continue;
1406
+ const child = children.find((candidate) => !claimed.has(candidate) && candidate.asyncId === step.runId);
1407
+ if (child) {
1408
+ assigned.set(step, child);
1409
+ claimed.add(child);
1410
+ }
1411
+ }
1412
+ for (const step of steps) {
1413
+ if (assigned.has(step) || !step.workflowKey) continue;
1414
+ const child = children.find((candidate) => !claimed.has(candidate) && candidate.workflowKey === step.workflowKey);
1415
+ if (child) {
1416
+ assigned.set(step, child);
1417
+ claimed.add(child);
1418
+ }
1419
+ }
1420
+ return assigned;
1421
+ }
1422
+ function projectMaterializedStep(step, index, child, depth, ctx, childrenByParent, liveRoots) {
1423
+ const { id, label } = stepIdentity(step, index, ctx);
1424
+ const selfStep = child.mode === void 0 || child.mode === "single" ? child.steps?.[0] : void 0;
1425
+ const live = projectRun(child, ctx, depth, childrenByParent, liveRoots, selfStep !== void 0);
1426
+ const laneActivity = activityFor(step, ctx);
1427
+ const selfActivity = selfStep ? activityFor(selfStep, ctx) : void 0;
1428
+ const activity = laneActivity || selfActivity || live.activity ? { ...laneActivity, ...selfActivity, ...live.activity } : void 0;
1429
+ const node = { ...live, id, kind: "step", label };
1430
+ const agent = publicOptionalText(selfStep?.agent || step.agent, ctx.caps.maxStringLength);
1431
+ if (agent) node.agent = agent;
1432
+ if (activity) node.activity = activity;
1433
+ setStepModel(node, ctx, selfStep, step);
1434
+ return node;
1435
+ }
1436
+ function projectRun(job, ctx, depth, childrenByParent, liveRoots, omitSteps = false) {
1437
+ const state = normalizeState(job.status);
1438
+ const startedAt = publicTime(job.startedAt);
1439
+ const updatedAt = publicTime(job.updatedAt) ?? startedAt;
1440
+ const activity = activityFor(job, ctx);
1441
+ const node = {
1442
+ id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
1443
+ kind: kindForMode(job.mode),
1444
+ label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
1445
+ state,
1446
+ ...startedAt !== void 0 ? { startedAt } : {},
1447
+ ...updatedAt !== void 0 ? { updatedAt } : {},
1448
+ ...terminalState(state) && updatedAt !== void 0 ? { endedAt: updatedAt } : {},
1449
+ ...activity ? { activity } : {}
1450
+ };
1451
+ const steps = omitSteps ? [] : job.steps ?? [];
1452
+ const materialized = childrenByParent.get(job.asyncId) ?? [];
1453
+ const nestedSummaries = (job.nestedChildren ?? []).filter((child) => !materialized.some((live) => live.asyncId === child.id));
1454
+ const loadedKeys = new Set(steps.flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
1455
+ const graphStages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph).filter((graphNode) => !loadedKeys.has(graphNode.id)) : [];
1456
+ const graphSteps = graphStages.map((node2, index) => ({
1457
+ step: { agent: node2.agent ?? node2.label, status: workflowGraphStepStatus(node2.status), workflowKey: node2.id, label: node2.label },
1458
+ index: node2.flatIndex ?? index
1459
+ }));
1460
+ const lanes = [...steps, ...graphSteps.map(({ step }) => step)];
1461
+ const assigned = assignMaterializedChildren(lanes, materialized);
1462
+ const claimed = new Set(assigned.values());
1463
+ if (depth < ctx.caps.maxDepth) {
1464
+ const childDepth = depth + 1;
1465
+ const projectLane = (step, index) => {
1466
+ const child = assigned.get(step);
1467
+ if (!child) return projectStep(step, index, childDepth, ctx);
1468
+ if (liveRoots.has(child)) return void 0;
1469
+ return projectMaterializedStep(step, index, child, childDepth, ctx, childrenByParent, liveRoots);
1470
+ };
1471
+ const stepChildren = steps.map((step, index) => projectLane(step, step.index ?? index)).filter((child) => child !== void 0);
1472
+ const graphChildren = graphSteps.map(({ step, index }) => projectLane(step, index)).filter((child) => child !== void 0);
1473
+ const nestedChildren = nestedSummaries.map((child, index) => projectNestedRun(child, index, childDepth, ctx));
1474
+ const unclaimedChildren = materialized.filter((child) => !claimed.has(child) && !liveRoots.has(child)).map((child) => projectRun(child, ctx, childDepth, childrenByParent, liveRoots));
1475
+ const hostStepChildren = validHostStepList(job.hostSteps).map((hostStep) => projectHostStep(hostStep, ctx));
1476
+ const ordinaryChildren = [...stepChildren, ...graphChildren, ...nestedChildren, ...unclaimedChildren];
1477
+ const retainedHostSteps = hostStepChildren.slice(0, ctx.caps.maxChildrenPerNode);
1478
+ const retainedOrdinaryChildren = ordinaryChildren.slice(0, ctx.caps.maxChildrenPerNode - retainedHostSteps.length);
1479
+ const bounded2 = [];
1480
+ bounded2.push(...retainedOrdinaryChildren, ...retainedHostSteps);
1481
+ ctx.omitted.children += ordinaryChildren.length - retainedOrdinaryChildren.length + hostStepChildren.length - retainedHostSteps.length;
1482
+ if (bounded2.length) node.children = bounded2;
1483
+ } else {
1484
+ const rootAssignments = [...assigned.values()].filter((child) => liveRoots.has(child)).length;
1485
+ const unclaimedChildren = materialized.filter((child) => !claimed.has(child) && !liveRoots.has(child)).length;
1486
+ ctx.omitted.children += lanes.length - rootAssignments + nestedSummaries.length + unclaimedChildren + validHostStepList(job.hostSteps).length;
1487
+ }
1488
+ return node;
1489
+ }
1490
+ function snapshotBytes(snapshot) {
1491
+ return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
1492
+ }
1493
+ function enforceByteLimit(snapshot) {
1494
+ if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) return;
1495
+ snapshot.omitted.byteLimitExceeded = true;
1496
+ const runs = snapshot.runs;
1497
+ const initialOmittedRuns = snapshot.omitted.runs;
1498
+ let lower = 0;
1499
+ let upper = Math.max(0, runs.length - 1);
1500
+ while (lower < upper) {
1501
+ const retained = Math.ceil((lower + upper) / 2);
1502
+ snapshot.runs = runs.slice(0, retained);
1503
+ snapshot.omitted.runs = initialOmittedRuns + runs.length - retained;
1504
+ if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) lower = retained;
1505
+ else upper = retained - 1;
1506
+ }
1507
+ snapshot.runs = runs.slice(0, lower);
1508
+ snapshot.omitted.runs = initialOmittedRuns + runs.length - lower;
1509
+ }
1510
+ function workflowStepActivity(step) {
1511
+ if (step.currentTool) return `tool ${step.currentTool}`;
1512
+ if (step.currentPath) return step.currentPath.split(/[\\/]/).at(-1);
1513
+ if (step.activityState === "needs_attention") return "needs attention";
1514
+ if (step.activityState === "active_long_running") return "long-running";
1515
+ if (step.turnCount !== void 0) return `${step.turnCount} turns`;
1516
+ if (step.toolCount !== void 0) return `${step.toolCount} tools`;
1517
+ return void 0;
1518
+ }
1519
+ function workflowStepName(step, index) {
1520
+ const key = step.workflowKey ?? `step ${index + 1}`;
1521
+ const label = step.label && step.label !== key ? ` \xB7 ${step.label}` : "";
1522
+ const phase = step.phase ? `${step.phase}: ` : "";
1523
+ return `${phase}${key}${label} (${step.agent})`;
1524
+ }
1525
+ function hostStepRow(hostStep) {
1526
+ const freshness = hostStep.freshness ? {
1527
+ expectedRef: publicText(hostStep.freshness.expectedRef, "ref", HOST_STEP_MAX_REF_CHARS),
1528
+ ...hostStep.freshness.observedRef ? { observedRef: publicText(hostStep.freshness.observedRef, "ref", HOST_STEP_MAX_REF_CHARS) } : {},
1529
+ ...hostStep.freshness.stale !== void 0 ? { stale: hostStep.freshness.stale } : {}
1530
+ } : void 0;
1531
+ return {
1532
+ name: publicText(hostStep.label, "host step", HOST_STEP_MAX_LABEL_CHARS),
1533
+ kind: hostStep.monitorKind,
1534
+ state: hostStep.state,
1535
+ ...hostStep.role ? { role: publicText(hostStep.role, "role", HOST_STEP_MAX_ROLE_CHARS) } : {},
1536
+ ...hostStep.provider ? { provider: publicText(hostStep.provider, "provider", HOST_STEP_MAX_PROVIDER_CHARS) } : {},
1537
+ ...hostStep.verdict ? { verdict: hostStep.verdict } : {},
1538
+ ...hostStep.reasonCode ? { reasonCode: publicText(hostStep.reasonCode, "reason", HOST_STEP_MAX_REASON_CHARS) } : {},
1539
+ ...hostStep.detail ? { detail: publicText(hostStep.detail, "detail", HOST_STEP_MAX_DETAIL_CHARS) } : {},
1540
+ ...hostStep.target ? { target: publicText(hostStep.target, "target", HOST_STEP_MAX_TARGET_CHARS) } : {},
1541
+ ...freshness ? { freshness } : {},
1542
+ ...hostStep.reportPath ? { reportPath: hostStepReportName(hostStep.reportPath) } : {}
1543
+ };
1544
+ }
1545
+ function isWorkflowPreflight(value) {
1546
+ return value !== void 0 && !Array.isArray(value) && "lanes" in value;
1547
+ }
1548
+ function isWorkflowGraph(value) {
1549
+ return value !== void 0 && !Array.isArray(value) && "nodes" in value;
1550
+ }
1551
+ function workflowGraphRowState(status) {
1552
+ switch (status) {
1553
+ case "pending":
1554
+ return "planned";
1555
+ case "completed":
1556
+ return "complete";
1557
+ case "detached":
1558
+ return "paused";
1559
+ default:
1560
+ return status;
1561
+ }
1562
+ }
1563
+ function workflowGraphStepStatus(status) {
1564
+ switch (status) {
1565
+ case "completed":
1566
+ return "complete";
1567
+ case "detached":
1568
+ return "paused";
1569
+ default:
1570
+ return status;
1571
+ }
1572
+ }
1573
+ function workflowGraphRowName(node) {
1574
+ const key = publicText(node.id, "stage", HOST_STEP_MAX_LABEL_CHARS);
1575
+ const label = publicOptionalText(node.label, HOST_STEP_MAX_LABEL_CHARS);
1576
+ const phase = publicOptionalText(node.phase, HOST_STEP_MAX_LABEL_CHARS);
1577
+ const agent = publicOptionalText(node.agent, HOST_STEP_MAX_LABEL_CHARS);
1578
+ return `${phase ? `${phase}: ` : ""}${key}${label && label !== node.id ? ` \xB7 ${label}` : ""}${agent ? ` (${agent})` : ""}`;
1579
+ }
1580
+ function projectWorkflowGraphRow(node, preflight) {
1581
+ return {
1582
+ name: workflowGraphRowName(node),
1583
+ state: workflowGraphRowState(node.status),
1584
+ ...preflight ? { preflight } : {}
1585
+ };
1586
+ }
1587
+ function projectAsyncWorkflowRows(steps, hostStepsOrPreflight, preflightOverride) {
1588
+ const preflight = preflightOverride ?? (isWorkflowPreflight(hostStepsOrPreflight) ? hostStepsOrPreflight : void 0);
1589
+ const graph = isWorkflowGraph(hostStepsOrPreflight) ? hostStepsOrPreflight : void 0;
1590
+ const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) || graph ? void 0 : hostStepsOrPreflight;
1591
+ const loaded = steps ?? [];
1592
+ const preflightForKey = (key, groupKeys = []) => workflowPreflightLaneForRuntimeKey(preflight, key, groupKeys);
1593
+ if (graph) {
1594
+ const loadedIndexesByKey = /* @__PURE__ */ new Map();
1595
+ for (const [index, step] of loaded.entries()) {
1596
+ if (!step.workflowKey) continue;
1597
+ const indexes = loadedIndexesByKey.get(step.workflowKey) ?? [];
1598
+ indexes.push(index);
1599
+ loadedIndexesByKey.set(step.workflowKey, indexes);
1600
+ }
1601
+ const consumed = /* @__PURE__ */ new Set();
1602
+ const childRows = [];
1603
+ const graphStages = workflowGraphStageNodes(graph);
1604
+ const graphKeys = new Set(graphStages.map((node) => node.id));
1605
+ const graphPhaseByNodeId = /* @__PURE__ */ new Map();
1606
+ for (const phase of graph.phases) {
1607
+ for (const nodeId of phase.nodeIds) {
1608
+ if (graphKeys.has(nodeId) && !graphPhaseByNodeId.has(nodeId)) graphPhaseByNodeId.set(nodeId, phase.title);
1609
+ }
1610
+ }
1611
+ for (const node of graphStages) {
1612
+ const lane = preflightForKey(node.id, [node.phase, graphPhaseByNodeId.get(node.id)]);
1613
+ const indexes = (loadedIndexesByKey.get(node.id) ?? []).filter((index) => !consumed.has(index));
1614
+ if (indexes.length > 0) {
1615
+ for (const index of indexes) {
1616
+ consumed.add(index);
1617
+ childRows.push(projectLoadedWorkflowRow(loaded[index], index, lane));
1618
+ }
1619
+ } else {
1620
+ childRows.push(projectWorkflowGraphRow(node, lane));
1621
+ }
1622
+ }
1623
+ for (const [index, step] of loaded.entries()) {
1624
+ if (!consumed.has(index)) childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? preflightForKey(step.workflowKey, [step.phase]) : void 0));
1625
+ }
1626
+ return [...childRows, ...validHostStepList(graph).map(hostStepRow)];
1627
+ }
1628
+ return [
1629
+ ...loaded.map((step, index) => projectLoadedWorkflowRow(step, index, step.workflowKey ? preflightForKey(step.workflowKey, [step.phase]) : void 0)),
1630
+ ...validHostStepList(hostSteps).map(hostStepRow)
1631
+ ];
1632
+ }
1633
+ function projectLoadedWorkflowRow(step, index, preflight) {
1634
+ const modelThinking = formatModelThinking(step.model, step.thinking) || void 0;
1635
+ const thinking = childThinkingLevel(step);
1636
+ const activity = workflowStepActivity(step);
1637
+ return {
1638
+ name: workflowStepName(step, index),
1639
+ state: step.status,
1640
+ ...step.context ? { context: step.context } : {},
1641
+ ...modelThinking ? { modelThinking } : {},
1642
+ ...thinking ? { thinking } : {},
1643
+ ...activity ? { activity } : {},
1644
+ ...step.startedAt !== void 0 ? { startedAt: step.startedAt } : {},
1645
+ ...step.endedAt !== void 0 ? { endedAt: step.endedAt } : {},
1646
+ ...step.durationMs !== void 0 ? { durationMs: step.durationMs } : {},
1647
+ ...step.tokens?.total !== void 0 ? { tokens: step.tokens.total } : {},
1648
+ ...step.tokens?.window !== void 0 ? { window: step.tokens.window } : {},
1649
+ ...preflight ? { preflight } : {}
1650
+ };
1651
+ }
1652
+ function projectAsyncStatusSnapshot(jobs, options = {}) {
1653
+ const caps = resolveCaps(options);
1654
+ const ctx = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
1655
+ const { roots, childrenByParent, liveRoots } = groupMaterializedChildren([...jobs]);
1656
+ const sorted = roots.sort(asyncJobOrder);
1657
+ ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
1658
+ const snapshot = {
1659
+ kind: ASYNC_STATUS_SNAPSHOT_KIND,
1660
+ version: ASYNC_STATUS_SNAPSHOT_VERSION,
1661
+ generatedAt: options.generatedAt ?? Date.now(),
1662
+ caps,
1663
+ omitted: ctx.omitted,
1664
+ runs: sorted.slice(0, caps.maxRuns).map((job) => projectRun(job, ctx, 0, childrenByParent, liveRoots))
1665
+ };
1666
+ enforceByteLimit(snapshot);
1667
+ return snapshot;
1668
+ }
1669
+
1670
+ // src/runs/shared/mutation-evidence.ts
1671
+ import { execFileSync } from "node:child_process";
1672
+ import { createHash } from "node:crypto";
1673
+ import * as fs2 from "node:fs";
1674
+ import * as os from "node:os";
1675
+ import * as path2 from "node:path";
1676
+ var MAX_TRACKED_PATHS = 500;
1677
+ var MAX_HASH_BYTES = 1024 * 1024;
1678
+ var MAX_TIMEOUT_FILES = 20;
1679
+ function gitArguments(args) {
1680
+ return ["-c", "core.fsmonitor=false", ...args];
1681
+ }
1682
+ function gitOutput(cwd, args, maxBuffer = MAX_HASH_BYTES) {
1683
+ return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer, windowsHide: true });
1684
+ }
1685
+ function splitNul(output) {
1686
+ return output.split("\0").filter((part) => part.length > 0);
1687
+ }
1688
+ function hashLargeDiff(cwd, relativePath) {
1689
+ const tempDir = fs2.mkdtempSync(path2.join(os.tmpdir(), "pi-subagents-tracked-diff-"));
1690
+ const diffPath = path2.join(tempDir, "diff.patch");
1691
+ try {
1692
+ execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore", windowsHide: true });
1693
+ const hash = createHash("sha256");
1694
+ const buffer = Buffer.allocUnsafe(64 * 1024);
1695
+ const fd = fs2.openSync(diffPath, "r");
1696
+ try {
1697
+ for (; ; ) {
1698
+ const bytesRead = fs2.readSync(fd, buffer, 0, buffer.length, null);
1699
+ if (bytesRead === 0) break;
1700
+ hash.update(buffer.subarray(0, bytesRead));
1701
+ }
1702
+ } finally {
1703
+ fs2.closeSync(fd);
1704
+ }
1705
+ return hash.digest("hex");
1706
+ } finally {
1707
+ fs2.rmSync(tempDir, { recursive: true, force: true });
1708
+ }
1709
+ }
1710
+ function listChangedTrackedFiles(cwd) {
1711
+ const paths = splitNul(gitOutput(cwd, ["diff", "--no-ext-diff", "--name-only", "-z", "HEAD", "--"], MAX_HASH_BYTES));
1712
+ return { paths: paths.slice(0, MAX_TRACKED_PATHS), truncated: paths.length > MAX_TRACKED_PATHS };
1713
+ }
1714
+ function fingerprintPath(cwd, relativePath) {
1715
+ try {
1716
+ const diff = gitOutput(cwd, ["diff", "--no-ext-diff", "--binary", "HEAD", "--", relativePath], MAX_HASH_BYTES);
1717
+ return { kind: "diff", digest: createHash("sha256").update(diff).digest("hex") };
1718
+ } catch {
1719
+ return { kind: "diff", digest: hashLargeDiff(cwd, relativePath) };
1720
+ }
1721
+ }
1722
+ function sameFingerprint(left, right) {
1723
+ if (!left || left.kind !== "diff" || right.kind !== "diff") return false;
1724
+ return left.digest === right.digest;
1725
+ }
1726
+ function snapshotTrackedMutations(cwd) {
1727
+ try {
1728
+ const changed = listChangedTrackedFiles(cwd);
1729
+ const fingerprints = {};
1730
+ for (const file of changed.paths) fingerprints[file] = fingerprintPath(cwd, file);
1731
+ return { source: "tracked-files", trackedOnly: true, cwd, dirtyFiles: changed.paths, fingerprints, truncated: changed.truncated };
1732
+ } catch (error) {
1733
+ return { source: "tracked-files", trackedOnly: true, cwd, dirtyFiles: [], fingerprints: {}, unavailable: error instanceof Error ? error.message : String(error) };
1734
+ }
1735
+ }
1736
+ function collectTrackedMutationEvidence(snapshot, cwd = snapshot.cwd) {
1737
+ if (snapshot.unavailable) {
1738
+ return { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: false, unavailable: snapshot.unavailable };
1739
+ }
1740
+ try {
1741
+ const current = listChangedTrackedFiles(cwd);
1742
+ const startDirty = new Set(snapshot.dirtyFiles);
1743
+ const candidates = /* @__PURE__ */ new Set([...snapshot.dirtyFiles, ...current.paths]);
1744
+ const changedFiles = [];
1745
+ for (const file of candidates) {
1746
+ if (!startDirty.has(file)) {
1747
+ if (!snapshot.truncated) changedFiles.push(file);
1748
+ continue;
1749
+ }
1750
+ if (!sameFingerprint(snapshot.fingerprints[file], fingerprintPath(cwd, file))) changedFiles.push(file);
1751
+ }
1752
+ changedFiles.sort();
1753
+ return {
1754
+ source: "tracked-files",
1755
+ trackedOnly: true,
1756
+ changedFiles,
1757
+ attemptedMutation: changedFiles.length > 0,
1758
+ truncated: snapshot.truncated || current.truncated || void 0
1759
+ };
1760
+ } catch (error) {
1761
+ return { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: false, unavailable: error instanceof Error ? error.message : String(error) };
1762
+ }
1763
+ }
1764
+ function formatPathList(paths) {
1765
+ if (paths.length === 0) return "none";
1766
+ const shown = paths.slice(0, MAX_TIMEOUT_FILES).join(", ");
1767
+ return paths.length > MAX_TIMEOUT_FILES ? `${shown}, ... (${paths.length - MAX_TIMEOUT_FILES} more)` : shown;
1768
+ }
1769
+ function projectTimeoutRecovery(value) {
1770
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1771
+ const source = value;
1772
+ const termination = source.termination === "timed-out" || source.termination === "stopped" ? source.termination : void 0;
1773
+ if (!termination || !Array.isArray(source.changedFiles)) return void 0;
1774
+ const allChangedFiles = source.changedFiles.filter((file) => typeof file === "string");
1775
+ const changedFiles = allChangedFiles.slice(0, MAX_TIMEOUT_FILES);
1776
+ const reportStatus = source.reportStatus === "missing" || source.reportStatus === "written" || source.reportStatus === "not-requested" || source.reportStatus === "unknown" ? source.reportStatus : void 0;
1777
+ return {
1778
+ termination,
1779
+ changedFiles,
1780
+ ...source.truncated === true || allChangedFiles.length > changedFiles.length ? { truncated: true } : {},
1781
+ ...source.recoveryNeeded === true ? { recoveryNeeded: true } : {},
1782
+ ...source.reason === "timed-out-with-dirty-worktree" ? { reason: source.reason } : {},
1783
+ ...reportStatus ? { reportStatus } : {}
1784
+ };
1785
+ }
1786
+ function formatTimeoutRecoveryLines(value, indent = "") {
1787
+ const recovery = projectTimeoutRecovery(value);
1788
+ if (!recovery?.recoveryNeeded) return [];
1789
+ const changedFiles = recovery.changedFiles.length > 0 ? `${recovery.changedFiles.join(", ")}${recovery.truncated ? ", \u2026" : ""}` : "none";
1790
+ const changedFileCount = recovery.changedFiles.length > 0 ? `${recovery.changedFiles.length}${recovery.truncated ? "+" : ""}` : "0";
1791
+ return [
1792
+ `${indent}Recovery needed: review the diff and artifacts before resuming or launching dependent stages.`,
1793
+ `${indent}Recovery evidence: requested report: ${recovery.reportStatus ?? "unknown"}; changed tracked files: ${changedFiles} (${changedFileCount}); classification: ${recovery.reason ?? recovery.termination}`
1794
+ ];
1795
+ }
1796
+ function buildTimeoutRecoverySummary(input) {
1797
+ const warning = "Inspect partial changes before retrying or resuming the child.";
1798
+ const changedFiles = input.evidence.changedFiles.slice(0, MAX_TIMEOUT_FILES);
1799
+ let reportStatus = "not-requested";
1800
+ if (input.requiredOutputMissing === true) reportStatus = "missing";
1801
+ else if (input.requiredOutputMissing === false) reportStatus = "written";
1802
+ const recoveryNeeded = input.termination === "timed-out" && reportStatus === "missing" && input.evidence.changedFiles.length > 0;
1803
+ const lines = [
1804
+ "Recovery summary:",
1805
+ `- termination: ${input.termination}`,
1806
+ `- changed tracked files: ${input.evidence.unavailable ? `unavailable (${input.evidence.unavailable})` : formatPathList(input.evidence.changedFiles)}`
1807
+ ];
1808
+ if (input.requiredOutputMissing !== void 0) lines.push(`- requested report: ${reportStatus}`);
1809
+ if (recoveryNeeded) lines.push("- Recovery needed: review the diff and artifacts before resuming or launching dependent stages.");
1810
+ if (input.currentTool) lines.push(`- active tool: ${input.currentTool}${input.currentToolArgs ? ` \u2014 ${input.currentToolArgs}` : ""}`);
1811
+ if (input.currentPath) lines.push(`- active path: ${input.currentPath}`);
1812
+ if (input.sessionFile) lines.push(`- session file: ${input.sessionFile}`);
1813
+ if (input.transcriptPath) lines.push(`- transcript: ${input.transcriptPath}`);
1814
+ if (input.artifactPaths?.outputPath) lines.push(`- output artifact: ${input.artifactPaths.outputPath}`);
1815
+ if (input.artifactPaths?.metadataPath) lines.push(`- metadata artifact: ${input.artifactPaths.metadataPath}`);
1816
+ lines.push(`Warning: ${warning}`);
1817
+ return {
1818
+ termination: input.termination,
1819
+ changedFiles,
1820
+ truncated: input.evidence.changedFiles.length > changedFiles.length || input.evidence.truncated || void 0,
1821
+ ...recoveryNeeded ? { recoveryNeeded: true, reason: "timed-out-with-dirty-worktree" } : {},
1822
+ reportStatus,
1823
+ currentTool: input.currentTool,
1824
+ currentToolArgs: input.currentToolArgs,
1825
+ currentPath: input.currentPath,
1826
+ sessionFile: input.sessionFile,
1827
+ transcriptPath: input.transcriptPath,
1828
+ artifactPaths: input.artifactPaths,
1829
+ warning,
1830
+ message: lines.join("\n")
1831
+ };
1832
+ }
1833
+
1834
+ // src/workflows/workflow-checklist.ts
1835
+ var MAX_TEXT = 160;
1836
+ var TERMINAL_STATES2 = /* @__PURE__ */ new Set(["complete", "blocked", "failed", "paused", "stopped"]);
1837
+ function text(value, fallback) {
1838
+ if (typeof value !== "string") return fallback;
1839
+ const clean = sanitizeDisplayText(value).replace(/\s+/g, " ").trim();
1840
+ return clean ? clean.slice(0, MAX_TEXT) : fallback;
1841
+ }
1842
+ function keyText(value, fallback) {
1843
+ return text(value, fallback) ?? fallback;
1844
+ }
1845
+ function normalizedStatus(value) {
1846
+ return typeof value === "string" ? value.toLowerCase() : "queued";
1847
+ }
1848
+ function finite(value) {
1849
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1850
+ }
1851
+ function count(value) {
1852
+ const number = finite(value);
1853
+ return number !== void 0 && number >= 0 ? Math.round(number) : void 0;
1854
+ }
1855
+ function explicitBlocked(source) {
1856
+ const acceptance = normalizedStatus(source.acceptance?.status);
1857
+ const review = normalizedStatus(source.review?.status ?? source.acceptance?.reviewResult?.status);
1858
+ return source.toolBudgetBlocked === true || source.turnBudgetExceeded === true || source.activityState === "needs_attention" || source.timedOut === true || source.watchdog?.phase === "stale" || source.stale === true || source.verdict === "inconclusive" || acceptance === "rejected" || acceptance === "blockers" || review === "blockers" || review === "review-required";
1859
+ }
1860
+ function checklistState(source) {
1861
+ if (explicitBlocked(source)) return "blocked";
1862
+ switch (normalizedStatus(source.status)) {
1863
+ case "complete":
1864
+ case "completed":
1865
+ case "done":
1866
+ case "pass":
1867
+ case "accepted":
1868
+ return "complete";
1869
+ case "running":
1870
+ case "started":
1871
+ case "active":
1872
+ return "running";
1873
+ case "failed":
1874
+ case "error":
1875
+ case "fail":
1876
+ return "failed";
1877
+ case "blocked":
1878
+ case "rejected":
1879
+ case "partial":
1880
+ case "needs_attention":
1881
+ return "blocked";
1882
+ case "paused":
1883
+ case "detached":
1884
+ return "paused";
1885
+ case "stopped":
1886
+ case "cancelled":
1887
+ case "canceled":
1888
+ return "stopped";
1889
+ default:
1890
+ return "queued";
1891
+ }
1892
+ }
1893
+ function duration(step, now, state) {
1894
+ const explicit = finite(step.durationMs);
1895
+ const startedAt = finite(step.startedAt);
1896
+ if (explicit !== void 0) return Math.max(0, explicit);
1897
+ if (startedAt === void 0) return void 0;
1898
+ const end = state === "running" ? now : finite(step.endedAt);
1899
+ return end === void 0 ? void 0 : Math.max(0, end - startedAt);
1900
+ }
1901
+ function stepKey(step) {
1902
+ return step.key ?? step.workflowKey ?? step.runId;
1903
+ }
1904
+ function stepItem(step, index, phase, key = stepKey(step) ?? `step-${index + 1}`, label = step.label ?? step.description ?? stepKey(step) ?? step.agent ?? key, preflight) {
1905
+ const state = checklistState(step);
1906
+ const thinking = childThinkingLevel(step);
1907
+ return {
1908
+ key: keyText(key, `step-${index + 1}`),
1909
+ label: keyText(label, key),
1910
+ phase,
1911
+ state,
1912
+ ...text(step.agent) ? { agent: text(step.agent) } : {},
1913
+ ...thinking ? { thinking } : {},
1914
+ ...step.context ? { context: step.context } : {},
1915
+ ...finite(step.startedAt) !== void 0 ? { startedAt: finite(step.startedAt) } : {},
1916
+ ...duration(step, void 0, state) !== void 0 ? { durationMs: duration(step, void 0, state) } : {},
1917
+ ...text(step.currentTool) ? { currentTool: text(step.currentTool) } : {},
1918
+ ...finite(step.currentToolStartedAt) !== void 0 ? { currentToolStartedAt: finite(step.currentToolStartedAt) } : {},
1919
+ ...text(step.currentPath) ? { currentPath: text(step.currentPath) } : {},
1920
+ ...count(step.toolCount) !== void 0 ? { toolCount: count(step.toolCount) } : {},
1921
+ ...text(step.outputName) ? { outputName: text(step.outputName) } : {},
1922
+ ...text(step.error) ? { error: text(step.error) } : {},
1923
+ ...preflight ? { preflight } : {}
1924
+ };
1925
+ }
1926
+ function hostItem(host, phase, key = host.id) {
1927
+ const state = checklistState({ status: host.state, verdict: host.verdict, stale: host.freshness?.stale });
1928
+ return {
1929
+ key: keyText(key, "host-step"),
1930
+ label: keyText(host.label, "host step"),
1931
+ phase,
1932
+ state,
1933
+ kind: "host",
1934
+ monitorKind: host.monitorKind,
1935
+ ...text(host.provider) ? { provider: text(host.provider) } : {},
1936
+ ...text(host.role) ? { role: text(host.role) } : {},
1937
+ ...host.verdict ? { verdict: host.verdict } : {},
1938
+ ...text(host.target) ? { target: text(host.target) } : {},
1939
+ ...text(host.reasonCode) ? { reasonCode: text(host.reasonCode) } : {},
1940
+ ...text(host.detail) ? { error: text(host.detail) } : {},
1941
+ ...host.freshness?.stale !== void 0 ? { stale: host.freshness.stale } : {},
1942
+ ...text(host.reportPath) ? { reportPath: text(host.reportPath) } : {}
1943
+ };
1944
+ }
1945
+ function graphNodes(graph) {
1946
+ const out = [];
1947
+ const visit = (node) => {
1948
+ if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
1949
+ for (const child of node.children ?? []) visit(child);
1950
+ if (!node.children?.length) out.push(node);
1951
+ } else {
1952
+ out.push(node);
1953
+ }
1954
+ };
1955
+ for (const node of graph?.nodes ?? []) visit(node);
1956
+ return out;
1957
+ }
1958
+ function traceSources(trace) {
1959
+ const latest = /* @__PURE__ */ new Map();
1960
+ for (const entry of trace ?? []) {
1961
+ const state = normalizedStatus(entry.state);
1962
+ if (entry.operation !== void 0 && entry.operation !== "run" && entry.operation !== "host" || !entry.key || state === "delivered" || state === "missed") continue;
1963
+ const existing = latest.get(entry.key);
1964
+ latest.set(entry.key, state === "reused" && existing ? { ...existing, ...entry, state: existing.state } : { ...existing, ...entry });
1965
+ }
1966
+ return [...latest.values()];
1967
+ }
1968
+ function traceItem(entry, index, preflight) {
1969
+ const phase = keyText(preflight?.key ?? entry.generatedLaneKey ?? entry.phase, "Workflow");
1970
+ const item = stepItem({ key: entry.key, label: entry.label, phase, agent: entry.agent, status: entry.state === "started" ? "running" : entry.state, durationMs: entry.durationMs, error: entry.error }, index, phase, entry.key, entry.label ?? entry.key, preflight);
1971
+ if (entry.operation === "host") item.kind = "host";
1972
+ return item;
1973
+ }
1974
+ function phaseFor(phases, label) {
1975
+ let phase = phases.get(label);
1976
+ if (!phase) {
1977
+ phase = { key: label, label, state: "queued", items: [], total: 0, done: 0, running: 0, queued: 0, blocked: 0, failed: 0, paused: 0, stopped: 0, parallel: false };
1978
+ phases.set(label, phase);
1979
+ }
1980
+ return phase;
1981
+ }
1982
+ function add(phases, phase, item) {
1983
+ phaseFor(phases, phase).items.push(item);
1984
+ }
1985
+ function mergeNodeStep(node, step, phase, trace, preflight) {
1986
+ const state = checklistState(step);
1987
+ const nodeState = checklistState({ status: node.status, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : void 0 });
1988
+ const status = TERMINAL_STATES2.has(nodeState) && !TERMINAL_STATES2.has(state) ? nodeState : trace && !TERMINAL_STATES2.has(state) ? trace.state === "started" ? "running" : trace.state : normalizedStatus(step.status) === "pending" && normalizedStatus(node.status) !== "pending" ? node.status : step.status;
1989
+ return stepItem({ ...step, status, key: node.id, label: step.label ?? node.label, phase: step.phase ?? phase, agent: step.agent ?? node.agent, outputName: step.outputName ?? node.outputName, error: step.error ?? trace?.error ?? node.error, acceptance: step.acceptance ?? (node.acceptanceStatus ? { status: node.acceptanceStatus } : void 0), durationMs: step.durationMs ?? trace?.durationMs }, node.flatIndex ?? 0, phase, node.id, step.label ?? node.label, preflight);
1990
+ }
1991
+ function priority(item) {
1992
+ return item.state === "blocked" ? 0 : item.state === "failed" ? 1 : item.state === "running" ? 2 : item.state === "paused" ? 3 : item.state === "stopped" ? 4 : item.state === "queued" ? 5 : 6;
1993
+ }
1994
+ function applyNow(item, now) {
1995
+ return item.state === "running" && item.startedAt !== void 0 && now !== void 0 ? { ...item, durationMs: Math.max(0, now - item.startedAt) } : item;
1996
+ }
1997
+ function finalize(phase) {
1998
+ phase.total = phase.items.length;
1999
+ phase.done = phase.items.filter((item) => item.state === "complete").length;
2000
+ phase.running = phase.items.filter((item) => item.state === "running").length;
2001
+ phase.queued = phase.items.filter((item) => item.state === "queued").length;
2002
+ phase.blocked = phase.items.filter((item) => item.state === "blocked").length;
2003
+ phase.failed = phase.items.filter((item) => item.state === "failed").length;
2004
+ phase.paused = phase.items.filter((item) => item.state === "paused").length;
2005
+ phase.stopped = phase.items.filter((item) => item.state === "stopped").length;
2006
+ phase.parallel = phase.total > 1;
2007
+ phase.state = phase.blocked ? "blocked" : phase.failed ? "failed" : phase.running ? "running" : phase.paused ? "paused" : phase.stopped ? "stopped" : phase.queued ? "queued" : "complete";
2008
+ }
2009
+ function projectWorkflowChecklist(input) {
2010
+ const phases = /* @__PURE__ */ new Map();
2011
+ const steps = input.steps ?? [];
2012
+ const nodes = graphNodes(input.graph);
2013
+ const trace = traceSources(input.trace);
2014
+ const traceByKey = new Map(trace.map((entry) => [entry.key, entry]));
2015
+ const phaseByNode = /* @__PURE__ */ new Map();
2016
+ for (const phase of input.graph?.phases ?? []) {
2017
+ const title = keyText(phase.title, "Workflow");
2018
+ phaseFor(phases, title);
2019
+ for (const nodeId of phase.nodeIds) if (!phaseByNode.has(nodeId)) phaseByNode.set(nodeId, title);
2020
+ }
2021
+ const stepsByKey = /* @__PURE__ */ new Map();
2022
+ for (const [index, step] of steps.entries()) {
2023
+ const key = stepKey(step);
2024
+ if (!key) continue;
2025
+ stepsByKey.set(key, [...stepsByKey.get(key) ?? [], { step, index }]);
2026
+ }
2027
+ const usedSteps = /* @__PURE__ */ new Set();
2028
+ const hostById = new Map((input.hostSteps ?? []).map((host) => [host.id, host]));
2029
+ const graphKeys = new Set(nodes.map((node) => node.id));
2030
+ const stepKeys = /* @__PURE__ */ new Set([...stepsByKey.keys()]);
2031
+ const hostKeys = new Set(hostById.keys());
2032
+ for (const node of nodes) {
2033
+ const host = node.hostStep ?? hostById.get(node.id);
2034
+ const phase = keyText(phaseByNode.get(node.id) ?? node.phase ?? (host ? host.label : node.label), "Workflow");
2035
+ if (host) {
2036
+ add(phases, phase, hostItem(host, phase, node.id));
2037
+ hostById.delete(node.id);
2038
+ continue;
2039
+ }
2040
+ const matches = (stepsByKey.get(node.id) ?? []).filter(({ index }) => !usedSteps.has(index));
2041
+ if (matches.length) {
2042
+ for (const match of matches) {
2043
+ usedSteps.add(match.index);
2044
+ add(phases, phase, applyNow(mergeNodeStep(node, match.step, phase, traceByKey.get(stepKey(match.step) ?? node.id), workflowPreflightLaneForRuntimeKey(input.preflight, node.id, [phase])), input.now));
2045
+ }
2046
+ continue;
2047
+ }
2048
+ add(phases, phase, applyNow(mergeNodeStep(node, { key: node.id, label: node.label, phase, agent: node.agent, status: node.status, outputName: node.outputName, error: node.error, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : void 0 }, phase, traceByKey.get(node.id), workflowPreflightLaneForRuntimeKey(input.preflight, node.id, [phase])), input.now));
2049
+ }
2050
+ for (const [index, step] of steps.entries()) {
2051
+ if (usedSteps.has(index)) continue;
2052
+ const key = keyText(stepKey(step), `step-${index + 1}`);
2053
+ const traceEntry = traceByKey.get(key);
2054
+ const lane = workflowPreflightLaneForRuntimeKey(input.preflight, key, [step.phase, traceEntry?.generatedLaneKey, traceEntry?.phase]);
2055
+ const phase = keyText(lane?.key ?? step.phase ?? traceEntry?.generatedLaneKey ?? traceEntry?.phase, "Workflow");
2056
+ add(phases, phase, applyNow(stepItem(step, index, phase, key, step.label ?? step.key ?? step.agent, lane), input.now));
2057
+ }
2058
+ for (const host of hostById.values()) add(phases, keyText(host.label, "Host"), hostItem(host, keyText(host.label, "Host")));
2059
+ for (const entry of trace) {
2060
+ if (graphKeys.has(entry.key) || stepKeys.has(entry.key) || hostKeys.has(entry.key)) continue;
2061
+ const item = traceItem(entry, phases.size, workflowPreflightLaneForRuntimeKey(input.preflight, entry.key, [entry.generatedLaneKey, entry.phase]));
2062
+ add(phases, item.phase, item);
2063
+ }
2064
+ const finalized = [...phases.values()].filter((phase) => phase.items.length > 0);
2065
+ for (const phase of finalized) {
2066
+ phase.items = phase.items.map((item) => applyNow(item, input.now));
2067
+ finalize(phase);
2068
+ }
2069
+ const all = finalized.flatMap((phase) => phase.items);
2070
+ const counts = (state) => all.filter((item) => item.state === state).length;
2071
+ const bottleneck = [...all].sort((left, right) => priority(left) - priority(right))[0];
2072
+ return { phases: finalized, total: all.length, done: counts("complete"), running: counts("running"), queued: counts("queued"), blocked: counts("blocked"), failed: counts("failed"), paused: counts("paused"), stopped: counts("stopped"), ...bottleneck && bottleneck.state !== "complete" ? { bottleneck } : {} };
2073
+ }
2074
+ function stateLabel(state) {
2075
+ return state === "running" ? "active" : state;
2076
+ }
2077
+ function formatWorkflowChecklistSummary(projection) {
2078
+ if (!projection.total) return "";
2079
+ return [`${projection.done}/${projection.total} done`, projection.running ? `${projection.running} active` : void 0, projection.queued ? `${projection.queued} queued` : void 0, projection.blocked ? `${projection.blocked} blocked` : void 0, projection.failed ? `${projection.failed} failed` : void 0, projection.paused ? `${projection.paused} paused` : void 0, projection.stopped ? `${projection.stopped} stopped` : void 0].filter((value) => Boolean(value)).join(" \xB7 ");
2080
+ }
2081
+ function formatWorkflowChecklistPhase(phase) {
2082
+ const counts = [phase.total > 1 && phase.done ? `${phase.done} done` : void 0, phase.running ? `${phase.running} active` : void 0, phase.queued ? `${phase.queued} queued` : void 0, phase.blocked ? `${phase.blocked} blocked` : void 0, phase.failed ? `${phase.failed} failed` : void 0, phase.paused ? `${phase.paused} paused` : void 0, phase.stopped ? `${phase.stopped} stopped` : void 0].filter((value) => Boolean(value));
2083
+ return counts.length ? `${phase.label} ${counts.join(" \xB7 ")}` : phase.label;
2084
+ }
2085
+ function formatWorkflowChecklistBottleneck(item, options = {}) {
2086
+ if (!item) return void 0;
2087
+ const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : void 0].filter((value) => Boolean(value)).join(" \xB7 ") || item.key;
2088
+ const includeOutput = options.includeOutput ?? true;
2089
+ const includeError = options.includeError ?? true;
2090
+ const details = [item.context ? `(${item.context})` : void 0, item.currentTool ? `${item.currentTool}${item.durationMs !== void 0 ? ` ${formatDurationText(item.durationMs)}` : ""}` : void 0, !item.currentTool && item.currentPath ? item.currentPath : void 0, !item.currentTool && item.durationMs !== void 0 ? formatDurationText(item.durationMs) : void 0, item.toolCount !== void 0 ? `${item.toolCount} tools` : void 0, includeOutput && item.outputName ? `out:${item.outputName}` : void 0, includeError && item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : void 0].filter((value) => Boolean(value));
2091
+ return [identity, ...details].join(" \xB7 ");
2092
+ }
2093
+ function formatWorkflowChecklistItem(item) {
2094
+ const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : void 0].filter((value) => Boolean(value)).join(" \xB7 ") || item.key;
2095
+ const details = [item.kind && item.monitorKind ? item.monitorKind : void 0, item.provider ? `provider:${item.provider}` : void 0, item.role ? `role:${item.role}` : void 0, item.target, item.currentTool, !item.currentTool && item.currentPath ? item.currentPath : void 0, item.durationMs !== void 0 ? formatDurationText(item.durationMs) : void 0, item.toolCount !== void 0 ? `${item.toolCount} tools` : void 0, item.outputName ? `out:${item.outputName}` : void 0, item.reportPath ? `out:${item.reportPath}` : void 0, item.stale ? "stale" : void 0, item.reasonCode ? `reason:${item.reasonCode}` : void 0, item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : void 0].filter((value) => Boolean(value));
2096
+ return `${identity}${item.context ? ` (${item.context})` : ""}${item.state === "complete" ? "" : ` \xB7 ${stateLabel(item.state)}`}${details.length ? ` \xB7 ${details.join(" \xB7 ")}` : ""}`;
2097
+ }
2098
+ function formatDurationText(ms) {
2099
+ const seconds = Math.max(0, Math.round(ms / 1e3));
2100
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
2101
+ }
2102
+ function formatWorkflowChecklistText(projection, indent = "", options = {}) {
2103
+ if (!projection.total) return [];
2104
+ const lines = [`${indent}Workflow checklist: ${formatWorkflowChecklistSummary(projection)}`];
2105
+ for (const phase of projection.phases) {
2106
+ const phaseMarker = phase.state === "complete" ? "\u2713" : phase.state === "running" ? "\u283C" : phase.state === "blocked" ? "!" : phase.state === "failed" ? "\u2717" : phase.state === "paused" || phase.state === "stopped" ? "\u25A0" : "\u25E6";
2107
+ lines.push(`${indent} ${phaseMarker} ${formatWorkflowChecklistPhase(phase)}`);
2108
+ if (options.includeItems === false) continue;
2109
+ for (const item of phase.items) {
2110
+ const marker = item.state === "complete" ? "\u2713" : item.state === "running" ? "\u283C" : item.state === "blocked" ? "!" : item.state === "failed" ? "\u2717" : item.state === "paused" || item.state === "stopped" ? "\u25A0" : "\u25E6";
2111
+ lines.push(`${indent} ${marker} ${formatWorkflowChecklistItem(item)}`);
2112
+ }
2113
+ }
2114
+ const bottleneck = formatWorkflowChecklistBottleneck(projection.bottleneck, { includeError: options.includeItems === false });
2115
+ if (bottleneck) lines.push(`${indent} bottleneck \xB7 ${bottleneck}`);
2116
+ return lines;
2117
+ }
2118
+
2119
+ // src/runs/shared/run-ownership.ts
2120
+ function ownerSessionIds(sessionId) {
2121
+ return /* @__PURE__ */ new Set([sessionId, ...inheritedSessionIds(sessionId)]);
2122
+ }
2123
+ function ownsRun(currentSessionId, runSessionId) {
2124
+ return currentSessionId ? ownerSessionIds(currentSessionId).has(runSessionId) : false;
2125
+ }
2126
+
2127
+ // src/runs/background/async-status.ts
2128
+ function getErrorMessage(error) {
2129
+ return error instanceof Error ? error.message : String(error);
2130
+ }
2131
+ function isAsyncStatusIsolationError(asyncDir, error) {
2132
+ const statusPath = path3.join(asyncDir, "status.json");
2133
+ const message = getErrorMessage(error);
2134
+ return /^(?:Failed to (?:inspect|read|parse|validate) async status file|Invalid async status file) '/.test(message) || message.startsWith(`Invalid async status '${statusPath}'`) || message.startsWith(`Invalid host step '${statusPath}`) || /^(workflowChildren|Invalid workflowChildren)/.test(message);
2135
+ }
2136
+ function isolateCorruptActiveRun(asyncDir, runId, error, now) {
2137
+ const statusPath = path3.join(asyncDir, "status.json");
2138
+ const processTerminal = readProcessTerminal(asyncDir, { runId });
2139
+ let markerAge;
2140
+ try {
2141
+ markerAge = activeRunMarkerAgeMs(asyncDir, now?.());
2142
+ } catch (markerError) {
2143
+ console.error(`Failed to inspect corrupt async active-run marker for '${runId}':`, markerError);
2144
+ }
2145
+ const markerCanBeReleased = processTerminal?.state === "observed" || markerAge !== void 0 && markerAge > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS;
2146
+ let markerAction = "active marker retained because runner liveness is unknown";
2147
+ if (markerCanBeReleased) {
2148
+ try {
2149
+ releaseActiveRunIndex(asyncDir);
2150
+ markerAction = processTerminal?.state === "observed" ? "active marker released after observed process-terminal proof" : "stale active marker released";
2151
+ } catch (releaseError) {
2152
+ markerAction = `failed to release active marker: ${getErrorMessage(releaseError)}`;
2153
+ }
2154
+ }
2155
+ console.error(`[pi-subagents] Skipping corrupt active async run '${runId}' at '${statusPath}': ${getErrorMessage(error)}; ${markerAction}.`);
2156
+ }
2157
+ function isNotFoundError(error) {
2158
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2159
+ }
2160
+ function isAsyncRunDir(root, entry) {
2161
+ const entryPath = path3.join(root, entry);
2162
+ try {
2163
+ return fs3.statSync(entryPath).isDirectory();
2164
+ } catch (error) {
2165
+ if (isNotFoundError(error)) return false;
2166
+ throw new Error(`Failed to inspect async run path '${entryPath}': ${getErrorMessage(error)}`, {
2167
+ cause: error instanceof Error ? error : void 0
2168
+ });
2169
+ }
2170
+ }
2171
+ function resolveTargetedAsyncRun(asyncDirRoot, id, sessionId) {
2172
+ if (!id || id === "." || id === ".." || id === ACTIVE_RUN_INDEX_DIR || id === TERMINAL_RUN_INDEX_DIR || path3.basename(id) !== id) return { kind: "reject" };
2173
+ const asyncDir = path3.join(asyncDirRoot, id);
2174
+ let entryStat;
2175
+ try {
2176
+ entryStat = fs3.lstatSync(asyncDir);
2177
+ } catch (error) {
2178
+ if (isNotFoundError(error)) return canScanAsyncRunPrefix(id) ? { kind: "prefix" } : { kind: "reject" };
2179
+ throw new Error(`Failed to inspect async run path '${asyncDir}': ${getErrorMessage(error)}`, {
2180
+ cause: error instanceof Error ? error : void 0
2181
+ });
2182
+ }
2183
+ if (!entryStat.isDirectory() || entryStat.isSymbolicLink()) return { kind: "reject" };
2184
+ try {
2185
+ const canonicalRoot = fs3.realpathSync(asyncDirRoot);
2186
+ const canonicalDir = fs3.realpathSync(asyncDir);
2187
+ if (canonicalDir !== canonicalRoot && !canonicalDir.startsWith(`${canonicalRoot}${path3.sep}`)) return { kind: "reject" };
2188
+ } catch (error) {
2189
+ if (isNotFoundError(error)) return { kind: "reject" };
2190
+ throw new Error(`Failed to resolve async run path '${asyncDir}': ${getErrorMessage(error)}`, {
2191
+ cause: error instanceof Error ? error : void 0
2192
+ });
2193
+ }
2194
+ if (sessionId !== void 0) {
2195
+ const status = readStatus(asyncDir);
2196
+ if (!ownerSessionIds(sessionId).has(status?.sessionId)) return canScanAsyncRunPrefix(id) ? { kind: "prefix" } : { kind: "reject" };
2197
+ }
2198
+ return { kind: "exact", id };
2199
+ }
2200
+ function outputFileMtime(outputFile) {
2201
+ if (!outputFile) return void 0;
2202
+ try {
2203
+ return fs3.statSync(outputFile).mtimeMs;
2204
+ } catch (error) {
2205
+ if (isNotFoundError(error)) return void 0;
2206
+ throw new Error(`Failed to inspect async output file '${outputFile}': ${getErrorMessage(error)}`, {
2207
+ cause: error instanceof Error ? error : void 0
2208
+ });
2209
+ }
2210
+ }
2211
+ function deriveAsyncActivityState(asyncDir, status) {
2212
+ if (status.state !== "running") return { activityState: status.activityState, lastActivityAt: status.lastActivityAt };
2213
+ const outputPath = status.outputFile ? path3.isAbsolute(status.outputFile) ? status.outputFile : path3.join(asyncDir, status.outputFile) : void 0;
2214
+ const currentStep = typeof status.currentStep === "number" ? status.steps?.[status.currentStep] : void 0;
2215
+ return {
2216
+ activityState: status.activityState,
2217
+ lastActivityAt: status.lastActivityAt ?? outputFileMtime(outputPath) ?? currentStep?.lastActivityAt ?? (status.mode === "workflow" ? void 0 : currentStep?.startedAt ?? status.startedAt)
2218
+ };
2219
+ }
2220
+ function statusToSummary(asyncDir, status, nestedWarnings = [], nestedRoute) {
2221
+ const statusPath = path3.join(asyncDir, "status.json");
2222
+ validateAsyncStatusLaneMetadata(status, `Invalid async status '${statusPath}'`);
2223
+ const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
2224
+ if (workflowChildren && status.mode === "workflow") workflowChildren.children = workflowChildren.children.map((child) => withWorkflowChildRunModel(asyncDir, child));
2225
+ if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error(`Invalid async status '${statusPath}': workflowChildren.workflowRunId does not match.`);
2226
+ assertWorkflowGraphHostSteps(status.workflowGraph, statusPath, status.runId);
2227
+ const hostSteps = validHostStepNodes(status.workflowGraph);
2228
+ if (status.sessionId !== void 0 && typeof status.sessionId !== "string") {
2229
+ throw new Error(`Invalid async status '${statusPath}': sessionId must be a string.`);
2230
+ }
2231
+ if (status.outputFile !== void 0 && typeof status.outputFile !== "string") throw new Error(`Invalid async status '${statusPath}': outputFile must be a string.`);
2232
+ const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
2233
+ const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }) ?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path3.join(asyncDir, "status.json"));
2234
+ const nestedProjectionAllowed = nestedWarnings.length === 0;
2235
+ let runFanoutBudget = status.runFanoutBudget;
2236
+ try {
2237
+ const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
2238
+ if (runFanoutBudgetDescriptor) runFanoutBudget = getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor);
2239
+ } catch (error) {
2240
+ nestedWarnings.push(`Run fan-out status unavailable: ${getErrorMessage(error)}`);
2241
+ }
2242
+ const steps = status.steps ?? [];
2243
+ const chainStepCount = status.chainStepCount ?? steps.length;
2244
+ const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
2245
+ let nestedChildren = [];
2246
+ if (nestedProjectionAllowed && nestedRoute) {
2247
+ try {
2248
+ nestedChildren = projectNestedEvents(nestedRoute)?.children ?? [];
2249
+ } catch (error) {
2250
+ nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
2251
+ }
2252
+ }
2253
+ const summarizedSteps = steps.map((step, index) => {
2254
+ const stepActivityState = step.activityState;
2255
+ const stepLastActivityAt = step.lastActivityAt;
2256
+ const timeoutRecovery = projectTimeoutRecovery(step.timeoutRecovery);
2257
+ const { model: stepModel, thinking: stepThinking } = status.mode === "workflow" && step.async === true ? withWorkflowChildRunModel(asyncDir, step) : step;
2258
+ return {
2259
+ index,
2260
+ childId: asyncStatusChildIdentity(step, index),
2261
+ agent: step.agent,
2262
+ ...step.sessionName ? { sessionName: step.sessionName } : {},
2263
+ ...step.context ? { context: step.context } : {},
2264
+ ...step.label ? { label: step.label } : {},
2265
+ ...step.description ? { description: step.description } : {},
2266
+ ...step.phase ? { phase: step.phase } : {},
2267
+ ...step.workflowKey ? { workflowKey: step.workflowKey } : {},
2268
+ ...step.lane ? { lane: step.lane } : {},
2269
+ ...step.worktreePath ? { worktreePath: step.worktreePath } : {},
2270
+ ...step.branch ? { branch: step.branch } : {},
2271
+ ...step.provider ? { provider: step.provider } : {},
2272
+ ...step.naming ? { naming: step.naming } : {},
2273
+ ...step.runId ? { runId: step.runId } : {},
2274
+ ...step.outputName ? { outputName: step.outputName } : {},
2275
+ ...step.structured ? { structured: step.structured } : {},
2276
+ status: step.status,
2277
+ ...step.runner ? { runner: step.runner } : {},
2278
+ ...step.externalProcess ? { externalProcess: step.externalProcess } : {},
2279
+ ...stepActivityState ? { activityState: stepActivityState } : {},
2280
+ ...stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {},
2281
+ ...step.currentTool ? { currentTool: step.currentTool } : {},
2282
+ ...step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {},
2283
+ ...step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {},
2284
+ ...step.currentPath ? { currentPath: step.currentPath } : {},
2285
+ ...step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {},
2286
+ ...step.recentOutput ? { recentOutput: [...step.recentOutput] } : {},
2287
+ ...step.turnCount !== void 0 ? { turnCount: step.turnCount } : {},
2288
+ ...step.toolCount !== void 0 ? { toolCount: step.toolCount } : {},
2289
+ ...step.steering ? { steering: step.steering } : {},
2290
+ ...step.durationMs !== void 0 ? { durationMs: step.durationMs } : {},
2291
+ ...step.tokens ? { tokens: step.tokens } : {},
2292
+ ...step.totalCost ? { totalCost: step.totalCost } : {},
2293
+ ...step.skills ? { skills: step.skills } : {},
2294
+ ...stepModel ? { model: stepModel } : {},
2295
+ ...step.contextLimit !== void 0 ? { contextLimit: step.contextLimit } : {},
2296
+ ...stepThinking ? { thinking: stepThinking } : {},
2297
+ ...step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {},
2298
+ ...step.requestedModel ? { requestedModel: step.requestedModel } : {},
2299
+ ...step.sessionFile ? { sessionFile: step.sessionFile } : {},
2300
+ ...step.transcriptPath ? { transcriptPath: step.transcriptPath } : {},
2301
+ ...step.error ? { error: step.error } : {},
2302
+ ...step.timedOut !== void 0 ? { timedOut: step.timedOut } : {},
2303
+ ...step.stopped !== void 0 ? { stopped: step.stopped } : {},
2304
+ ...step.stopRequested !== void 0 ? { stopRequested: step.stopRequested } : {},
2305
+ ...step.stopRequestedAt !== void 0 ? { stopRequestedAt: step.stopRequestedAt } : {},
2306
+ ...step.turnBudget ? { turnBudget: step.turnBudget } : {},
2307
+ ...step.turnBudgetExceeded !== void 0 ? { turnBudgetExceeded: step.turnBudgetExceeded } : {},
2308
+ ...step.toolBudgetBlocked !== void 0 ? { toolBudgetBlocked: step.toolBudgetBlocked } : {},
2309
+ ...step.wrapUpRequested !== void 0 ? { wrapUpRequested: step.wrapUpRequested } : {},
2310
+ ...step.acceptance ? { acceptance: step.acceptance } : {},
2311
+ ...step.agentContract ? { agentContract: step.agentContract } : {},
2312
+ ...step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {},
2313
+ ...step.launchResolvedExtensions ? { launchResolvedExtensions: step.launchResolvedExtensions } : {},
2314
+ ...step.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: step.runtimeAcknowledgedExtensions } : {},
2315
+ ...step.execution ? { execution: step.execution } : {},
2316
+ ...step.review ? { review: step.review } : {},
2317
+ ...step.effects ? { effects: step.effects } : {},
2318
+ ...step.watchdog ? { watchdog: childWatchdogProgressForModel(step.watchdog) } : {},
2319
+ ...step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path3.join(asyncDir, "status.json")} step ${index}`) } : {},
2320
+ ...timeoutRecovery ? { timeoutRecovery } : {},
2321
+ ...step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {},
2322
+ ...step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {},
2323
+ ...step.children?.length ? { children: step.children } : {}
2324
+ };
2325
+ });
2326
+ attachRootChildrenToSteps(status.runId || path3.basename(asyncDir), summarizedSteps, nestedChildren);
2327
+ return {
2328
+ id: status.runId || path3.basename(asyncDir),
2329
+ asyncDir,
2330
+ ...status.toolCallId ? { toolCallId: status.toolCallId } : {},
2331
+ ...status.sessionId ? { sessionId: status.sessionId } : {},
2332
+ state: status.state,
2333
+ ...status.error ? { error: status.error } : {},
2334
+ activityState,
2335
+ lastActivityAt,
2336
+ currentTool: status.currentTool,
2337
+ currentToolStartedAt: status.currentToolStartedAt,
2338
+ currentPath: status.currentPath,
2339
+ turnCount: status.turnCount,
2340
+ toolCount: status.toolCount,
2341
+ steering: status.steering,
2342
+ mode: status.mode,
2343
+ ...summarizeContextModes(summarizedSteps.map((step) => step.context)) ? { context: summarizeContextModes(summarizedSteps.map((step) => step.context)) } : {},
2344
+ cwd: status.cwd,
2345
+ ...status.sessionRoot ? { sessionRoot: status.sessionRoot } : {},
2346
+ startedAt: status.startedAt,
2347
+ lastUpdate: status.lastUpdate,
2348
+ endedAt: status.endedAt,
2349
+ ...status.timeoutMs !== void 0 ? { timeoutMs: status.timeoutMs } : {},
2350
+ ...status.deadlineAt !== void 0 ? { deadlineAt: status.deadlineAt } : {},
2351
+ ...status.timedOut !== void 0 ? { timedOut: status.timedOut } : {},
2352
+ ...status.stopped !== void 0 ? { stopped: status.stopped } : {},
2353
+ ...status.turnBudget ? { turnBudget: status.turnBudget } : {},
2354
+ ...status.turnBudgetExceeded !== void 0 ? { turnBudgetExceeded: status.turnBudgetExceeded } : {},
2355
+ ...status.wrapUpRequested !== void 0 ? { wrapUpRequested: status.wrapUpRequested } : {},
2356
+ currentStep: status.currentStep,
2357
+ ...status.chainStepCount !== void 0 ? { chainStepCount: status.chainStepCount } : {},
2358
+ ...status.pendingAppends !== void 0 ? { pendingAppends: status.pendingAppends } : {},
2359
+ ...parallelGroups.length ? { parallelGroups } : {},
2360
+ ...hostSteps.length ? { hostSteps } : {},
2361
+ ...status.mode === "workflow" && status.workflowGraph ? { workflowGraph: status.workflowGraph } : {},
2362
+ steps: summarizedSteps,
2363
+ ...nestedChildren.length ? { nestedChildren } : {},
2364
+ ...nestedWarnings.length ? { nestedWarnings } : {},
2365
+ ...processTerminal ? { processTerminal } : {},
2366
+ ...runFanoutBudget ? { runFanoutBudget } : {},
2367
+ ...status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {},
2368
+ ...status.launchResolvedExtensions ? { launchResolvedExtensions: status.launchResolvedExtensions } : {},
2369
+ ...status.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: status.runtimeAcknowledgedExtensions } : {},
2370
+ ...status.capabilityCeiling ? { capabilityCeiling: status.capabilityCeiling } : {},
2371
+ ...status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {},
2372
+ ...status.parentWorkflowRunId ? { parentWorkflowRunId: status.parentWorkflowRunId } : {},
2373
+ ...status.workflowKey ? { workflowKey: status.workflowKey } : {},
2374
+ ...status.lane ? { lane: status.lane } : {},
2375
+ ...status.workflow ? { workflow: status.workflow } : {},
2376
+ ...workflowChildren ? { workflowChildren } : {},
2377
+ ...status.preflight ? { preflight: status.preflight } : {},
2378
+ ...status.sessionDir ? { sessionDir: status.sessionDir } : {},
2379
+ ...status.outputFile ? { outputFile: status.outputFile } : {},
2380
+ ...status.totalTokens ? { totalTokens: status.totalTokens } : {},
2381
+ ...status.totalCost ? { totalCost: status.totalCost } : {},
2382
+ ...status.usageBudget ? { usageBudget: status.usageBudget } : {},
2383
+ ...status.sessionFile ? { sessionFile: status.sessionFile } : {}
2384
+ };
2385
+ }
2386
+ function sortRuns(runs) {
2387
+ const rank = (state) => {
2388
+ switch (state) {
2389
+ case "running":
2390
+ return 0;
2391
+ case "queued":
2392
+ return 1;
2393
+ case "failed":
2394
+ return 2;
2395
+ case "partial":
2396
+ return 2;
2397
+ case "stopped":
2398
+ return 2;
2399
+ case "paused":
2400
+ return 2;
2401
+ case "complete":
2402
+ return 3;
2403
+ default:
2404
+ return 4;
2405
+ }
2406
+ };
2407
+ return [...runs].sort((a, b) => {
2408
+ const byState = rank(a.state) - rank(b.state);
2409
+ if (byState !== 0) return byState;
2410
+ const aTime = a.lastUpdate ?? a.endedAt ?? a.startedAt;
2411
+ const bTime = b.lastUpdate ?? b.endedAt ?? b.startedAt;
2412
+ return bTime - aTime;
2413
+ });
2414
+ }
2415
+ function listAsyncRuns(asyncDirRoot, options = {}, observeStatus) {
2416
+ let entries;
2417
+ const activeEntries = /* @__PURE__ */ new Set();
2418
+ const wantsActive = options.states === void 0 || options.states.some(isActiveAsyncState);
2419
+ const wantsTerminal = options.states === void 0 || options.states.some((state) => !isActiveAsyncState(state));
2420
+ const includeNested = options.includeNested !== false;
2421
+ const owners = options.sessionId === void 0 ? void 0 : ownerSessionIds(options.sessionId);
2422
+ try {
2423
+ if (options.runId !== void 0) {
2424
+ const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
2425
+ entries = resolution.kind === "exact" ? [resolution.id] : resolution.kind === "prefix" && options.exactRunId !== true ? fs3.readdirSync(asyncDirRoot).filter(
2426
+ (entry) => (entry === options.runId || entry.startsWith(options.runId)) && resolveTargetedAsyncRun(asyncDirRoot, entry, options.sessionId).kind === "exact"
2427
+ ) : [];
2428
+ } else if (options.repairScan === true) {
2429
+ entries = fs3.readdirSync(asyncDirRoot).filter((entry) => entry !== ACTIVE_RUN_INDEX_DIR && entry !== TERMINAL_RUN_INDEX_DIR && isAsyncRunDir(asyncDirRoot, entry));
2430
+ } else {
2431
+ const indexed = /* @__PURE__ */ new Set();
2432
+ if (wantsActive) {
2433
+ for (const entry of readActiveRunIndex(asyncDirRoot) ?? []) {
2434
+ if (resolveTargetedAsyncRun(asyncDirRoot, entry).kind === "exact") {
2435
+ indexed.add(entry);
2436
+ activeEntries.add(entry);
2437
+ } else {
2438
+ observeStatus?.(null);
2439
+ updateActiveRunIndex(path3.join(asyncDirRoot, entry), "failed");
2440
+ }
2441
+ }
2442
+ }
2443
+ if (wantsTerminal) {
2444
+ for (const sessionId of owners ?? [void 0]) {
2445
+ for (const entry of readRecentTerminalRunIndex(asyncDirRoot, { sessionId, ...options.entryLimit !== void 0 ? { limit: options.entryLimit } : {} })) indexed.add(entry);
2446
+ }
2447
+ }
2448
+ entries = [...indexed];
2449
+ }
2450
+ } catch (error) {
2451
+ if (isNotFoundError(error)) return [];
2452
+ observeStatus?.(null);
2453
+ throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
2454
+ cause: error instanceof Error ? error : void 0
2455
+ });
2456
+ }
2457
+ const allowedStates = options.states ? new Set(options.states) : void 0;
2458
+ const runs = [];
2459
+ let nestedRouteIndex;
2460
+ const resolveNestedRoute = (rootRunId) => {
2461
+ if (!includeNested) return void 0;
2462
+ if (activeEntries.has(rootRunId)) return findNestedRouteForRootId(rootRunId);
2463
+ if (!nestedRouteIndex) nestedRouteIndex = buildNestedRouteIndex();
2464
+ return nestedRouteIndex.get(rootRunId);
2465
+ };
2466
+ for (const entry of entries) {
2467
+ const asyncDir = path3.join(asyncDirRoot, entry);
2468
+ let status;
2469
+ try {
2470
+ if (owners) {
2471
+ const stored = readStatus(asyncDir);
2472
+ if (stored && !owners.has(stored.sessionId)) {
2473
+ observeStatus?.(stored);
2474
+ continue;
2475
+ }
2476
+ }
2477
+ const reconciliation = options.reconcile === false ? void 0 : reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now }, observeStatus);
2478
+ status = reconciliation?.status ?? readStatus(asyncDir);
2479
+ if (options.reconcile === false) observeStatus?.(status);
2480
+ } catch (error) {
2481
+ observeStatus?.(null);
2482
+ if (!activeEntries.has(entry) || !isAsyncStatusIsolationError(asyncDir, error)) throw error;
2483
+ isolateCorruptActiveRun(asyncDir, entry, error, options.now);
2484
+ continue;
2485
+ }
2486
+ if (!status) {
2487
+ observeStatus?.(null);
2488
+ if (activeEntries.has(entry)) updateActiveRunIndex(asyncDir, "failed");
2489
+ continue;
2490
+ }
2491
+ if (activeEntries.has(entry) && !isActiveAsyncState(status.state)) {
2492
+ const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
2493
+ if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS) {
2494
+ updateActiveRunIndex(asyncDir, status.state, status.toolCallId, { terminalIndexBeforeRelease: true });
2495
+ }
2496
+ }
2497
+ if (status.displayDismissedAt !== void 0) continue;
2498
+ if (allowedStates && !allowedStates.has(status.state)) continue;
2499
+ if (owners && !owners.has(status.sessionId)) continue;
2500
+ const nestedWarnings = [];
2501
+ let nestedRoute;
2502
+ if (options.reconcile !== false && includeNested) {
2503
+ try {
2504
+ nestedRoute = resolveNestedRoute(status.runId || path3.basename(asyncDir));
2505
+ if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
2506
+ } catch (error) {
2507
+ observeStatus?.(null);
2508
+ nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
2509
+ }
2510
+ }
2511
+ let summary;
2512
+ try {
2513
+ summary = statusToSummary(asyncDir, status, nestedWarnings, nestedRoute);
2514
+ } catch (error) {
2515
+ observeStatus?.(null);
2516
+ if (!activeEntries.has(entry) || !isAsyncStatusIsolationError(asyncDir, error)) throw error;
2517
+ isolateCorruptActiveRun(asyncDir, entry, error, options.now);
2518
+ continue;
2519
+ }
2520
+ runs.push(summary);
2521
+ }
2522
+ const sorted = sortRuns(runs);
2523
+ return options.limit !== void 0 ? sorted.slice(0, options.limit) : sorted;
2524
+ }
2525
+ function formatActivityFacts(input) {
2526
+ const facts = [];
2527
+ if (input.currentTool && input.currentToolStartedAt !== void 0) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
2528
+ else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
2529
+ if (input.currentPath) facts.push(shortenPath(input.currentPath));
2530
+ if (input.turnCount !== void 0) facts.push(`${input.turnCount} turns`);
2531
+ if (input.turnBudgetExceeded && input.turnBudget) facts.push(`turn budget exceeded ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
2532
+ else if (input.turnBudget?.outcome === "termination-deferred") facts.push(`turn-budget termination deferred ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
2533
+ else if (input.wrapUpRequested && input.turnBudget) facts.push(`wrap-up requested ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}`);
2534
+ else if (input.turnBudget) facts.push(`turn budget ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
2535
+ if (input.toolCount !== void 0) facts.push(`${input.toolCount} tools`);
2536
+ if (input.steering) facts.push(`steering ${input.steering.scheduled} scheduled, ${input.steering.pending} pending, ${input.steering.delivered} delivered, ${input.steering.failed} failed, ${input.steering.recovered} recovered`);
2537
+ const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
2538
+ return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : void 0;
2539
+ }
2540
+ function formatStepLine(step) {
2541
+ const display = step.sessionName?.trim() || (step.label ? `${step.label} (${step.agent})` : step.agent);
2542
+ const context = contextModeLabel(step.context);
2543
+ const phase = step.phase ? `[${step.phase}] ` : "";
2544
+ const parts = [`${step.index + 1}. ${phase}${display}${context ? ` ${context}` : ""}`, step.status];
2545
+ const activity = formatActivityFacts(step);
2546
+ if (activity) parts.push(activity);
2547
+ const modelThinking = formatModelThinking(step.model, step.thinking);
2548
+ if (modelThinking) parts.push(modelThinking);
2549
+ if (step.durationMs !== void 0) parts.push(formatDuration(step.durationMs));
2550
+ if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
2551
+ if (step.lane) parts.push(`lane ${step.lane.key}`);
2552
+ if (step.worktreePath) parts.push(`worktree ${shortenPath(step.worktreePath)} \xB7 branch ${step.branch ?? "unknown"}${step.provider ? ` \xB7 provider ${step.provider}` : ""}`);
2553
+ return parts.join(" | ");
2554
+ }
2555
+ function formatHostStepLine(row) {
2556
+ if (!row.kind) return "";
2557
+ const state = hostStepVerdictLabel(row.state, row.verdict);
2558
+ const details = [
2559
+ row.provider ? `provider:${row.provider}` : void 0,
2560
+ row.role ? `role:${row.role}` : void 0,
2561
+ row.target,
2562
+ row.detail,
2563
+ row.reasonCode ? `reason:${row.reasonCode}` : void 0,
2564
+ row.freshness?.stale ? "stale" : row.freshness?.observedRef ? `ref:${row.freshness.observedRef}` : void 0,
2565
+ row.reportPath ? `out:${hostStepReportName(row.reportPath)}` : void 0
2566
+ ].filter((value) => Boolean(value));
2567
+ return `host ${row.kind}: ${row.name} | ${state}${details.length ? ` | ${details.join(" | ")}` : ""}`;
2568
+ }
2569
+ function workflowStageStateLabel(status) {
2570
+ switch (status) {
2571
+ case "completed":
2572
+ return "complete";
2573
+ case "detached":
2574
+ return "paused";
2575
+ default:
2576
+ return status;
2577
+ }
2578
+ }
2579
+ function formatWorkflowStageLine(node, index, total) {
2580
+ const state = workflowStageStateLabel(node.status);
2581
+ const id = previewDisplayText(node.id, 160);
2582
+ const label = node.label ? previewDisplayText(node.label, 160) : "";
2583
+ const display = label && label !== id ? ` | ${label}` : "";
2584
+ const agent = node.agent ? ` | ${previewDisplayText(node.agent, 80)}` : "";
2585
+ const error = node.error ? ` | ${previewDisplayText(node.error, 240)}` : "";
2586
+ return `stage ${index + 1}/${total}: ${id}${display}${agent} | ${state}${error}`;
2587
+ }
2588
+ function formatAsyncRunOutputPath(run) {
2589
+ if (!run.outputFile) return void 0;
2590
+ return path3.isAbsolute(run.outputFile) ? run.outputFile : path3.join(run.asyncDir, run.outputFile);
2591
+ }
2592
+ function formatAsyncRunProgressLabel(run) {
2593
+ const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
2594
+ if (graphStages.length > 0) {
2595
+ const currentNode = graphStages.find((node) => node.id === run.workflowGraph?.currentNodeId);
2596
+ if (currentNode && currentNode.status !== "completed") return `stage ${graphStages.indexOf(currentNode) + 1}/${graphStages.length}`;
2597
+ const activeNode = graphStages.find((node) => node.status === "running") ?? graphStages.find((node) => node.status !== "completed");
2598
+ if (activeNode) return `stage ${graphStages.indexOf(activeNode) + 1}/${graphStages.length}`;
2599
+ return `stage ${graphStages.length}/${graphStages.length}`;
2600
+ }
2601
+ const stepCount = run.steps.length || 1;
2602
+ const chainStepCount = run.chainStepCount ?? stepCount;
2603
+ const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, chainStepCount);
2604
+ const activeGroup = run.currentStep !== void 0 ? groups.find((group) => run.currentStep >= group.start && run.currentStep < group.start + group.count) : void 0;
2605
+ if (activeGroup) {
2606
+ const groupSteps = run.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count);
2607
+ const groupLabel = formatParallelOutcome(groupSteps, activeGroup.count, { showRunning: run.state === "running" });
2608
+ if (run.mode === "parallel") return groupLabel;
2609
+ return `step ${activeGroup.stepIndex + 1}/${chainStepCount} \xB7 parallel group: ${groupLabel}`;
2610
+ }
2611
+ if (run.mode === "parallel") return formatParallelOutcome(run.steps, stepCount, { showRunning: run.state === "running" });
2612
+ if (run.mode === "chain" && run.currentStep !== void 0 && groups.length > 0) {
2613
+ const logicalStep = flatToLogicalStepIndex(run.currentStep, chainStepCount, groups);
2614
+ return `step ${logicalStep + 1}/${chainStepCount}`;
2615
+ }
2616
+ return run.currentStep !== void 0 ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
2617
+ }
2618
+ function formatRunHeader(run) {
2619
+ const stepLabel = formatAsyncRunProgressLabel(run);
2620
+ const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
2621
+ const activity = formatActivityFacts(run);
2622
+ const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
2623
+ const context = contextModeLabel(run.context);
2624
+ const lane = run.lane ? ` | lane ${run.lane.key}` : "";
2625
+ return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${context ? ` ${context}` : ""} | ${stepLabel}${pending}${lane} | ${cwd}`;
2626
+ }
2627
+ function formatAsyncRunList(runs, heading = "Active async runs") {
2628
+ if (runs.length === 0) return `No ${heading.toLowerCase()}.`;
2629
+ const lines = [`${heading}: ${runs.length}`, ""];
2630
+ for (const run of runs) {
2631
+ lines.push(`- ${formatRunHeader(run)}`);
2632
+ if (run.preflight) lines.push(formatWorkflowPreflightPlanSummary(run.preflight, { indent: " " }));
2633
+ const preflightWarning = formatWorkflowPreflightWarningSummary(run.workflow?.preflightWarnings, { indent: " " });
2634
+ if (preflightWarning) lines.push(preflightWarning);
2635
+ if (run.mode === "workflow") {
2636
+ const checklist = projectWorkflowChecklist({
2637
+ graph: run.workflowGraph,
2638
+ steps: run.steps,
2639
+ hostSteps: run.hostSteps,
2640
+ preflight: run.preflight,
2641
+ trace: run.workflow?.trace,
2642
+ now: run.lastUpdate ?? run.endedAt ?? Date.now()
2643
+ });
2644
+ lines.push(...formatWorkflowChecklistText(checklist, " ", { includeItems: false }));
2645
+ }
2646
+ for (const step of run.steps) {
2647
+ lines.push(` ${formatStepLine(step)}`);
2648
+ lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));
2649
+ lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
2650
+ }
2651
+ const loadedWorkflowKeys = new Set(run.steps.flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
2652
+ const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
2653
+ for (const [index, node] of graphStages.entries()) {
2654
+ if (!loadedWorkflowKeys.has(node.id)) lines.push(` ${formatWorkflowStageLine(node, index, graphStages.length)}`);
2655
+ }
2656
+ for (const row of projectAsyncWorkflowRows([], run.hostSteps)) {
2657
+ const line = formatHostStepLine(row);
2658
+ if (line) lines.push(` ${line}`);
2659
+ }
2660
+ const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
2661
+ const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
2662
+ lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
2663
+ if (run.runFanoutBudget) lines.push(` ${formatRunFanoutBudget(run.runFanoutBudget)}`);
2664
+ if (run.error) lines.push(` Error: ${run.error}`);
2665
+ for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
2666
+ const outputPath = formatAsyncRunOutputPath(run);
2667
+ if (outputPath) lines.push(` output: ${shortenPath(outputPath)}`);
2668
+ if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);
2669
+ lines.push("");
2670
+ }
2671
+ return lines.join("\n").trimEnd();
2672
+ }
2673
+
2674
+ // src/shared/utf8.ts
2675
+ function decodeUtf8Tail(bytes) {
2676
+ let start = 0;
2677
+ while (start < bytes.length && (bytes[start] & 192) === 128) start += 1;
2678
+ return bytes.subarray(start).toString("utf-8");
2679
+ }
2680
+ function utf8Tail(value, maxBytes) {
2681
+ const bytes = Buffer.from(value, "utf-8");
2682
+ if (bytes.length <= maxBytes) return { text: value, truncated: false };
2683
+ return { text: decodeUtf8Tail(bytes.subarray(bytes.length - maxBytes)), truncated: true };
2684
+ }
2685
+
2686
+ // src/runs/shared/worktree-lock.ts
2687
+ import { spawnSync } from "node:child_process";
2688
+ import * as fs4 from "node:fs";
2689
+ import * as path4 from "node:path";
2690
+ function withHandoffWriteLock(manifestPath, action) {
2691
+ const requested = path4.resolve(manifestPath);
2692
+ fs4.mkdirSync(path4.dirname(requested), { recursive: true });
2693
+ const absolute = path4.join(fs4.realpathSync.native(path4.dirname(requested)), path4.basename(requested));
2694
+ const release = tryLease(`${absolute}.write-lock`);
2695
+ if (!release) throw new Error(`Handoff is being updated or cleaned: ${absolute}. Retry after the current operation completes.`);
2696
+ try {
2697
+ return action();
2698
+ } finally {
2699
+ release();
2700
+ }
2701
+ }
2702
+ async function withRepositoryWorktreeLock(repo, action, options = {}) {
2703
+ const git = spawnSync("git", ["-C", repo, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf-8", windowsHide: true });
2704
+ if (git.status !== 0) throw new Error(git.error?.message || git.stderr.trim() || "Cannot resolve Git common directory.");
2705
+ const lockPath = path4.join(fs4.realpathSync(git.stdout.trim()), "pi-subagents-worktree.lock");
2706
+ const deadline = Date.now() + (options.waitMs ?? 2e3);
2707
+ while (true) {
2708
+ options.signal?.throwIfAborted();
2709
+ const release = tryLease(lockPath);
2710
+ if (release) {
2711
+ try {
2712
+ return await action();
2713
+ } finally {
2714
+ release();
2715
+ }
2716
+ }
2717
+ if (Date.now() >= deadline) throw new Error("Repository worktree maintenance is busy; retry later.");
2718
+ await new Promise((resolve5) => setTimeout(resolve5, 25));
2719
+ }
2720
+ }
2721
+
2722
+ // src/runs/shared/parallel-handoff.ts
2723
+ import * as fs5 from "node:fs";
2724
+ import * as path5 from "node:path";
2725
+ function optionalIdentity(value, label) {
2726
+ if (value === void 0) return void 0;
2727
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string.`);
2728
+ return value.trim();
2729
+ }
2730
+ function nonNegativeIndex(value, label) {
2731
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a non-negative integer.`);
2732
+ return value;
2733
+ }
2734
+ function normalizeLaneBinding(value, label) {
2735
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
2736
+ const binding = value;
2737
+ const unknown = Object.keys(binding).filter((field) => !["index", "taskIndex", "workflowKey", "runId", "lane"].includes(field));
2738
+ if (unknown.length > 0) throw new Error(`${label} has unsupported fields: ${unknown.join(", ")}.`);
2739
+ const index = nonNegativeIndex(binding.index, `${label}.index`);
2740
+ const taskIndex = nonNegativeIndex(binding.taskIndex, `${label}.taskIndex`);
2741
+ const workflowKey = optionalIdentity(binding.workflowKey, `${label}.workflowKey`);
2742
+ const runId = optionalIdentity(binding.runId, `${label}.runId`);
2743
+ const lane = normalizeWorkflowLaneMetadata(binding.lane, `${label}.lane`);
2744
+ assertWorkflowLaneKey(lane, workflowKey, `${label}.lane`);
2745
+ return {
2746
+ index,
2747
+ taskIndex,
2748
+ ...workflowKey ? { workflowKey } : {},
2749
+ ...runId ? { runId } : {},
2750
+ ...lane ? { lane } : {}
2751
+ };
2752
+ }
2753
+ function validateManifestIdentity(parsed, manifestPath) {
2754
+ if (parsed.version !== 1 || !Array.isArray(parsed.groups)) {
2755
+ throw new Error(`Invalid parallel handoff manifest: ${manifestPath}`);
2756
+ }
2757
+ if (typeof parsed.runId !== "string" || !parsed.runId.trim()) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': runId must be a non-empty string.`);
2758
+ parsed.runId = parsed.runId.trim();
2759
+ const workflowKeys = /* @__PURE__ */ new Set();
2760
+ const childRunIds = /* @__PURE__ */ new Set();
2761
+ const childIndexes = /* @__PURE__ */ new Set();
2762
+ for (const [groupIndex, group] of parsed.groups.entries()) {
2763
+ if (!group || typeof group !== "object" || !Array.isArray(group.children) || !group.cleanup || !Array.isArray(group.cleanup.tasks)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}] is malformed.`);
2764
+ if (group.laneBindings !== void 0) {
2765
+ if (!Array.isArray(group.laneBindings)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].laneBindings must be an array.`);
2766
+ const taskIndexes2 = /* @__PURE__ */ new Set();
2767
+ const cleanupIndexes = new Set(group.cleanup.tasks.map((task) => task.index));
2768
+ group.laneBindings = group.laneBindings.map((binding, bindingIndex) => {
2769
+ const normalized = normalizeLaneBinding(binding, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].laneBindings[${bindingIndex}]`);
2770
+ if (taskIndexes2.has(normalized.taskIndex)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}] has duplicate lane binding taskIndex ${normalized.taskIndex}.`);
2771
+ if (!cleanupIndexes.has(normalized.taskIndex)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].laneBindings[${bindingIndex}] has no cleanup task for taskIndex ${normalized.taskIndex}.`);
2772
+ taskIndexes2.add(normalized.taskIndex);
2773
+ return normalized;
2774
+ });
2775
+ }
2776
+ const taskIndexes = /* @__PURE__ */ new Set();
2777
+ for (const [childIndex, child] of group.children.entries()) {
2778
+ if (!child || typeof child !== "object" || !child.patch || typeof child.patch !== "object") throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}] is malformed.`);
2779
+ const childRecord = child;
2780
+ const index = nonNegativeIndex(childRecord.index, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].index`);
2781
+ const taskIndex = nonNegativeIndex(childRecord.taskIndex, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].taskIndex`);
2782
+ if (taskIndexes.has(taskIndex)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}] has duplicate child taskIndex ${taskIndex}.`);
2783
+ taskIndexes.add(taskIndex);
2784
+ if (childIndexes.has(index)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': duplicate child index ${index}.`);
2785
+ childIndexes.add(index);
2786
+ if (!group.cleanup.tasks.some((task) => task.index === taskIndex)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}] has no cleanup task for taskIndex ${taskIndex}.`);
2787
+ const workflowKey = optionalIdentity(childRecord.workflowKey, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].workflowKey`);
2788
+ const runId = optionalIdentity(childRecord.runId, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].runId`);
2789
+ const lane = normalizeWorkflowLaneMetadata(childRecord.lane, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].lane`);
2790
+ assertWorkflowLaneKey(lane, workflowKey, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].lane`);
2791
+ if (workflowKey && workflowKeys.has(workflowKey)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': duplicate workflow key '${workflowKey}'.`);
2792
+ if (workflowKey) workflowKeys.add(workflowKey);
2793
+ if (runId && childRunIds.has(runId)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': duplicate child run id '${runId}'.`);
2794
+ if (runId) childRunIds.add(runId);
2795
+ if (lane) childRecord.lane = lane;
2796
+ if (workflowKey) childRecord.workflowKey = workflowKey;
2797
+ if (runId) childRecord.runId = runId;
2798
+ childRecord.index = index;
2799
+ childRecord.taskIndex = taskIndex;
2800
+ }
2801
+ }
2802
+ return parsed;
2803
+ }
2804
+ function readParallelHandoffManifest(manifestPath) {
2805
+ if (!fs5.existsSync(manifestPath)) return void 0;
2806
+ const parsed = JSON.parse(fs5.readFileSync(manifestPath, "utf-8"));
2807
+ return validateManifestIdentity(parsed, manifestPath);
2808
+ }
2809
+ function resolveExistingPath(candidate) {
2810
+ try {
2811
+ return fs5.realpathSync(candidate);
2812
+ } catch {
2813
+ return path5.resolve(candidate);
2814
+ }
2815
+ }
2816
+ function pathInside(root, candidate) {
2817
+ const relative3 = path5.relative(root, candidate);
2818
+ return relative3 === "" || relative3 !== ".." && !relative3.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relative3);
2819
+ }
2820
+ async function protectRetainedWorktreeForResume(manifestPath, runId, childIndex, signal) {
2821
+ const original = readParallelHandoffManifest(manifestPath);
2822
+ const group = original?.groups.find((item) => item.children.some((child) => child.index === childIndex));
2823
+ if (!group) throw new Error("Retained worktree handoff disappeared before resume admission.");
2824
+ return withWorktreeTransaction(() => withRepositoryWorktreeLock(group.repoRoot, () => withHandoffWriteLock(manifestPath, () => {
2825
+ signal?.throwIfAborted();
2826
+ const cwd = resolveRetainedWorktreeCwd(manifestPath, runId, childIndex);
2827
+ if (!cwd) throw new Error("Retained worktree ownership disappeared before resume admission.");
2828
+ const manifest = readParallelHandoffManifest(manifestPath);
2829
+ const currentGroup = manifest.groups.find((item) => item.children.some((child2) => child2.index === childIndex));
2830
+ const child = currentGroup.children.find((item) => item.index === childIndex);
2831
+ const task = currentGroup.cleanup.tasks.find((item) => item.index === child.taskIndex);
2832
+ task.preserved = true;
2833
+ task.reason = "retained child resume requires managed worktree cwd";
2834
+ manifest.updatedAt = Date.now();
2835
+ writeAtomicJson(manifestPath, manifest);
2836
+ return cwd;
2837
+ }), { signal }));
2838
+ }
2839
+ function resolveRetainedWorktreeCwd(manifestPath, runId, childIndex) {
2840
+ const manifest = readParallelHandoffManifest(manifestPath);
2841
+ if (!manifest) return void 0;
2842
+ if (manifest.runId !== runId) throw new Error(`Managed worktree handoff belongs to run '${manifest.runId}', not '${runId}'.`);
2843
+ const match = manifest.groups.flatMap((group) => group.children.map((child) => ({ group, child }))).find(({ child }) => child.index === childIndex);
2844
+ if (!match) return void 0;
2845
+ const cleanup = match.group.cleanup.tasks.find((task) => task.index === match.child.taskIndex);
2846
+ if (!cleanup) throw new Error(`Async run '${runId}' child ${childIndex} has no managed worktree cleanup record.`);
2847
+ const relativeCwd = path5.relative(resolveExistingPath(match.group.repoRoot), resolveExistingPath(manifest.cwd));
2848
+ if (path5.isAbsolute(relativeCwd) || relativeCwd === ".." || relativeCwd.startsWith(`..${path5.sep}`)) throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
2849
+ const requiredCwd = path5.join(cleanup.path, relativeCwd);
2850
+ if (cleanup.worktreeRemoved || cleanup.branchRemoved) throw new Error(`Async run '${runId}' required managed worktree was removed: ${requiredCwd}`);
2851
+ let worktreeRoot = "";
2852
+ let resolvedRequiredCwd = "";
2853
+ try {
2854
+ const rootStat = fs5.lstatSync(cleanup.path);
2855
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("worktree root is not a directory");
2856
+ worktreeRoot = fs5.realpathSync(cleanup.path);
2857
+ if (!fs5.statSync(requiredCwd).isDirectory()) throw new Error("path is not a directory");
2858
+ resolvedRequiredCwd = fs5.realpathSync(requiredCwd);
2859
+ } catch (error) {
2860
+ throw new Error(`Async run '${runId}' required managed worktree cwd is missing: ${requiredCwd}`, { cause: error instanceof Error ? error : void 0 });
2861
+ }
2862
+ if (!pathInside(worktreeRoot, resolvedRequiredCwd)) throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
2863
+ return requiredCwd;
2864
+ }
2865
+ function referenceFor(manifestPath, manifest) {
2866
+ const children = manifest.groups.flatMap((group) => group.children);
2867
+ return {
2868
+ version: 1,
2869
+ path: manifestPath,
2870
+ groupCount: manifest.groups.length,
2871
+ childCount: children.length,
2872
+ changedPatches: children.filter((child) => child.patch.changed).length,
2873
+ cleanupState: manifest.groups.every((group) => group.cleanup.state === "complete") ? "complete" : "partial",
2874
+ ...Object.prototype.hasOwnProperty.call(manifest, "cleanupEligibility") ? { cleanupEligibility: trustedStoredCleanupEligibility(manifest) } : {}
2875
+ };
2876
+ }
2877
+ var COMMIT_PATTERN = /^[0-9a-f]{40}$/i;
2878
+ var MANIFEST_DIGEST_PATTERN = /^[0-9a-f]{64}$/i;
2879
+ var MAX_ATTESTATION_ACTOR_LENGTH = 128;
2880
+ var MAX_ATTESTATION_TIMESTAMP_LENGTH = 64;
2881
+ var MAX_SUPERSESSION_ID_LENGTH = 128;
2882
+ function boundedString(value, label, maxBytes) {
2883
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string.`);
2884
+ const normalized = value.trim();
2885
+ if (normalized.includes("\n") || normalized.includes("\r")) throw new Error(`${label} must not contain newlines.`);
2886
+ if (Buffer.byteLength(normalized, "utf-8") > maxBytes) throw new Error(`${label} must be at most ${maxBytes} bytes.`);
2887
+ return normalized;
2888
+ }
2889
+ function commitString(value, label) {
2890
+ const normalized = boundedString(value, label, 64).toLowerCase();
2891
+ if (!COMMIT_PATTERN.test(normalized)) throw new Error(`${label} must be a full 40-character commit SHA.`);
2892
+ return normalized;
2893
+ }
2894
+ function manifestDigestString(value, label) {
2895
+ const normalized = boundedString(value, label, 64).toLowerCase();
2896
+ if (!MANIFEST_DIGEST_PATTERN.test(normalized)) throw new Error(`${label} must be a full 64-character SHA-256 digest.`);
2897
+ return normalized;
2898
+ }
2899
+ function manifestFactsDigest(manifest) {
2900
+ return stableJsonDigest({
2901
+ version: manifest.version,
2902
+ runId: manifest.runId,
2903
+ mode: manifest.mode,
2904
+ source: manifest.source,
2905
+ cwd: manifest.cwd,
2906
+ groups: manifest.groups
2907
+ });
2908
+ }
2909
+ function attestationTimestamp(value) {
2910
+ const normalized = boundedString(value, "attestedAt", MAX_ATTESTATION_TIMESTAMP_LENGTH);
2911
+ if (!Number.isFinite(Date.parse(normalized))) throw new Error("attestedAt must be a valid timestamp.");
2912
+ return normalized;
2913
+ }
2914
+ function normalizeMergeEvidence(value) {
2915
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("merge must be an object.");
2916
+ const merge = value;
2917
+ const prNumber = merge.prNumber;
2918
+ if (typeof prNumber !== "number" || !Number.isInteger(prNumber) || prNumber < 1) throw new Error("merge.prNumber must be a positive integer.");
2919
+ const treeEquivalent = merge.treeEquivalent;
2920
+ if (treeEquivalent !== true && treeEquivalent !== false && treeEquivalent !== "unknown") throw new Error("merge.treeEquivalent must be true, false, or 'unknown'.");
2921
+ const postMergeChecks = merge.postMergeChecks;
2922
+ if (postMergeChecks !== "recorded" && postMergeChecks !== "unknown") throw new Error("merge.postMergeChecks must be 'recorded' or 'unknown'.");
2923
+ return {
2924
+ prNumber,
2925
+ reviewedHead: commitString(merge.reviewedHead, "merge.reviewedHead"),
2926
+ mergeCommit: commitString(merge.mergeCommit, "merge.mergeCommit"),
2927
+ treeEquivalent,
2928
+ postMergeChecks,
2929
+ attestedBy: boundedString(merge.attestedBy, "merge.attestedBy", MAX_ATTESTATION_ACTOR_LENGTH),
2930
+ attestedAt: attestationTimestamp(merge.attestedAt),
2931
+ ...merge.manifestDigest === void 0 ? {} : { manifestDigest: manifestDigestString(merge.manifestDigest, "merge.manifestDigest") }
2932
+ };
2933
+ }
2934
+ function normalizeSupersessionEvidence(value) {
2935
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("supersession must be an object.");
2936
+ const supersession = value;
2937
+ return {
2938
+ supersededBy: boundedString(supersession.supersededBy, "supersession.supersededBy", MAX_SUPERSESSION_ID_LENGTH),
2939
+ attestedBy: boundedString(supersession.attestedBy, "supersession.attestedBy", MAX_ATTESTATION_ACTOR_LENGTH),
2940
+ attestedAt: attestationTimestamp(supersession.attestedAt),
2941
+ ...supersession.manifestDigest === void 0 ? {} : { manifestDigest: manifestDigestString(supersession.manifestDigest, "supersession.manifestDigest") }
2942
+ };
2943
+ }
2944
+ function normalizeStoredCleanupEligibility(value) {
2945
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { state: "unknown" };
2946
+ const eligibility = value;
2947
+ if (eligibility.state === "active" || eligibility.state === "terminal-eligible" || eligibility.state === "superseded-eligible" || eligibility.state === "unknown") {
2948
+ return { state: eligibility.state };
2949
+ }
2950
+ if (eligibility.state === "terminal-blocked" && typeof eligibility.reason === "string" && eligibility.reason.trim() && !eligibility.reason.includes("\n") && !eligibility.reason.includes("\r")) {
2951
+ return { state: "terminal-blocked", reason: eligibility.reason.trim().slice(0, 256) };
2952
+ }
2953
+ return { state: "unknown" };
2954
+ }
2955
+ var STORED_CHILD_STATUSES = /* @__PURE__ */ new Set(["pending", "running", "completed", "complete", "failed", "paused", "stopped", "detached", "rejected"]);
2956
+ function isTerminalParallelHandoffChildStatus(status) {
2957
+ return status === "completed" || status === "complete" || status === "failed" || status === "paused" || status === "stopped" || status === "rejected";
2958
+ }
2959
+ function hasValidStoredLaneShape(manifest) {
2960
+ if (!manifest || typeof manifest !== "object" || manifest.version !== 1 || typeof manifest.runId !== "string" || !manifest.runId.trim() || !Array.isArray(manifest.groups) || manifest.groups.length === 0) return false;
2961
+ let hasManagedTask = false;
2962
+ const valid = manifest.groups.every((group) => {
2963
+ if (!group || typeof group !== "object" || typeof group.baseCommit !== "string" || !group.baseCommit.trim() || typeof group.repoRoot !== "string" || !group.repoRoot.trim()) return false;
2964
+ const cleanup = group.cleanup;
2965
+ if (!cleanup || typeof cleanup !== "object" || cleanup.state !== "complete" && cleanup.state !== "partial" || !Array.isArray(cleanup.tasks) || !Array.isArray(group.children)) return false;
2966
+ if (cleanup.tasks.some((task) => !task || typeof task !== "object" || !Number.isSafeInteger(task.index) || task.index < 0 || typeof task.path !== "string" || !task.path.trim() || typeof task.branch !== "string" || !task.branch.trim())) return false;
2967
+ if (cleanup.tasks.length > 0) hasManagedTask = true;
2968
+ return group.children.every((child) => Boolean(child) && typeof child === "object" && typeof child.status === "string" && STORED_CHILD_STATUSES.has(child.status));
2969
+ });
2970
+ return valid && hasManagedTask;
2971
+ }
2972
+ function trustedStoredCleanupEligibility(manifest) {
2973
+ const eligibility = normalizeStoredCleanupEligibility(manifest.cleanupEligibility);
2974
+ if (!hasValidStoredLaneShape(manifest)) return { state: "unknown" };
2975
+ if (hasActiveChildren(manifest)) return { state: "active" };
2976
+ if (eligibility.state === "active") return cleanupEligibilityForEvidence(manifest);
2977
+ if (eligibility.state === "terminal-eligible" || eligibility.state === "superseded-eligible") {
2978
+ const current = cleanupEligibilityForEvidence(manifest);
2979
+ if (current.state === eligibility.state || current.state === "terminal-blocked" && / evidence is stale$/.test(current.reason)) return current;
2980
+ return { state: "unknown" };
2981
+ }
2982
+ return eligibility;
2983
+ }
2984
+ function hasActiveChildren(manifest) {
2985
+ return Array.isArray(manifest.groups) && manifest.groups.some((group) => {
2986
+ if (!group || typeof group !== "object") return false;
2987
+ const children = Array.isArray(group.children) ? group.children : [];
2988
+ if (children.length === 0 && Array.isArray(group.cleanup?.tasks) && group.cleanup.tasks.length > 0) return true;
2989
+ return children.some((child) => !child || typeof child !== "object" || !isTerminalParallelHandoffChildStatus(child.status));
2990
+ });
2991
+ }
2992
+ function hasManagedWorktreeTasks(manifest) {
2993
+ return manifest.groups.length > 0 && manifest.groups.some((group) => Array.isArray(group.cleanup?.tasks) && group.cleanup.tasks.length > 0);
2994
+ }
2995
+ function validateManifestForLaneEvidence(manifest, laneId) {
2996
+ if (manifest.runId !== laneId) throw new Error(`Lane '${laneId}' does not match manifest run '${manifest.runId}'.`);
2997
+ if (!hasValidStoredLaneShape(manifest)) throw new Error("Lane manifest is malformed.");
2998
+ if (!manifest.groups.length || !hasManagedWorktreeTasks(manifest)) throw new Error("Lane manifest has no managed worktree tasks.");
2999
+ if (hasActiveChildren(manifest)) throw new Error("Lane has an active child owner; merge evidence must be recorded after local reconciliation.");
3000
+ for (const group of manifest.groups) {
3001
+ if (typeof group.repoRoot !== "string" || !group.repoRoot.trim()) throw new Error("Lane manifest has no valid repository root.");
3002
+ if (group.cleanup.state !== "complete" && group.cleanup.state !== "partial") throw new Error("Lane manifest has an invalid cleanup state.");
3003
+ if (!Array.isArray(group.cleanup.tasks) || group.cleanup.tasks.some((task) => typeof task.path !== "string" || !task.path.trim() || typeof task.branch !== "string" || !task.branch.trim())) {
3004
+ throw new Error("Lane manifest has an invalid cleanup task.");
3005
+ }
3006
+ if (!Array.isArray(group.children)) throw new Error("Lane manifest has invalid child records.");
3007
+ for (const child of group.children) {
3008
+ if (!STORED_CHILD_STATUSES.has(child.status)) {
3009
+ throw new Error("Lane manifest has an invalid child status.");
3010
+ }
3011
+ }
3012
+ }
3013
+ }
3014
+ function cleanupEligibilityForEvidence(manifest) {
3015
+ if (hasActiveChildren(manifest)) return { state: "active" };
3016
+ let merge;
3017
+ if (manifest.merge) {
3018
+ try {
3019
+ merge = normalizeMergeEvidence(manifest.merge);
3020
+ } catch {
3021
+ return { state: "terminal-blocked", reason: "stored merge evidence is invalid" };
3022
+ }
3023
+ }
3024
+ if (manifest.supersession) {
3025
+ try {
3026
+ const supersession = normalizeSupersessionEvidence(manifest.supersession);
3027
+ if (supersession.manifestDigest !== manifestFactsDigest(manifest)) return { state: "terminal-blocked", reason: "stored supersession evidence is stale" };
3028
+ } catch {
3029
+ return { state: "terminal-blocked", reason: "stored supersession evidence is invalid" };
3030
+ }
3031
+ return { state: "superseded-eligible" };
3032
+ }
3033
+ if (!merge) return { state: "terminal-blocked", reason: "no merge or supersession evidence recorded" };
3034
+ if (merge.manifestDigest !== manifestFactsDigest(manifest)) return { state: "terminal-blocked", reason: "stored merge evidence is stale" };
3035
+ if (merge.treeEquivalent !== true) {
3036
+ return { state: "terminal-blocked", reason: merge.treeEquivalent === "unknown" ? "merged tree equivalence was not attested" : "merged tree is not equivalent to the reviewed head" };
3037
+ }
3038
+ if (merge.postMergeChecks !== "recorded") return { state: "terminal-blocked", reason: "required post-merge checks were not recorded" };
3039
+ return { state: "terminal-eligible" };
3040
+ }
3041
+ function sameMergeEvidence(left, right) {
3042
+ return left.prNumber === right.prNumber && left.reviewedHead === right.reviewedHead && left.mergeCommit === right.mergeCommit && left.treeEquivalent === right.treeEquivalent && left.postMergeChecks === right.postMergeChecks && left.attestedBy === right.attestedBy && left.attestedAt === right.attestedAt;
3043
+ }
3044
+ function sameSupersessionEvidence(left, right) {
3045
+ return left.supersededBy === right.supersededBy && left.attestedBy === right.attestedBy && left.attestedAt === right.attestedAt;
3046
+ }
3047
+ function firstRepositoryRoot(manifest) {
3048
+ const repoRoot = Array.isArray(manifest.groups) ? manifest.groups[0]?.repoRoot : void 0;
3049
+ return typeof repoRoot === "string" && repoRoot.trim() ? repoRoot.trim() : void 0;
3050
+ }
3051
+ function formatCleanupCommand(manifestPath, manifest) {
3052
+ const repoRoot = firstRepositoryRoot(manifest);
3053
+ return repoRoot ? `subagent({ action: "worktree.cleanup", options: { repo: ${JSON.stringify(repoRoot)}, handoffPath: ${JSON.stringify(manifestPath)}, mode: "plan" } })` : void 0;
3054
+ }
3055
+ function formatStoredParallelHandoffCleanup(manifestPath, manifest) {
3056
+ let stored = manifest;
3057
+ if (!stored) {
3058
+ try {
3059
+ stored = readParallelHandoffManifest(manifestPath);
3060
+ } catch {
3061
+ stored = void 0;
3062
+ }
3063
+ }
3064
+ if (!stored) return ["Cleanup eligibility: unknown", "Reason: lane manifest is missing or invalid; removal is not safe.", `Plan command: subagent({ action: "worktree.cleanup", options: { handoffPath: ${JSON.stringify(manifestPath)}, mode: "plan" } })`].join("\n");
3065
+ const eligibility = trustedStoredCleanupEligibility(stored);
3066
+ const lines = [`Cleanup eligibility: ${eligibility.state}`];
3067
+ if (eligibility.state === "terminal-blocked") lines.push(`Reason: ${eligibility.reason}`);
3068
+ if (eligibility.state === "unknown") lines.push("Reason: stored eligibility is missing or invalid; removal is not safe.");
3069
+ if (eligibility.state === "active") lines.push("Reason: a child owner is still active; removal is not safe.");
3070
+ const command = formatCleanupCommand(manifestPath, stored);
3071
+ if (command) lines.push(`Plan command: ${command}`);
3072
+ return lines.join("\n");
3073
+ }
3074
+ function recordParallelHandoffMerge(...args) {
3075
+ return withHandoffWriteLock(args[0].manifestPath, () => recordParallelHandoffMergeUnlocked(...args));
3076
+ }
3077
+ function recordParallelHandoffMergeUnlocked(input) {
3078
+ const manifest = readParallelHandoffManifest(input.manifestPath);
3079
+ if (!manifest) throw new Error(`Parallel handoff manifest not found: ${input.manifestPath}`);
3080
+ validateManifestForLaneEvidence(manifest, boundedString(input.laneId, "laneId", MAX_SUPERSESSION_ID_LENGTH));
3081
+ if (hasActiveChildren(manifest)) throw new Error("Lane has an active child owner; merge evidence must be recorded after local reconciliation.");
3082
+ const merge = { ...normalizeMergeEvidence(input.merge), manifestDigest: manifestFactsDigest(manifest) };
3083
+ if (manifest.merge) {
3084
+ const existing = normalizeMergeEvidence(manifest.merge);
3085
+ if (existing.reviewedHead !== merge.reviewedHead) throw new Error(`Lane manifest is stale: reviewed head is already recorded as ${existing.reviewedHead}.`);
3086
+ if (!sameMergeEvidence(existing, merge)) throw new Error("Lane manifest already contains different merge evidence for this reviewed head.");
3087
+ }
3088
+ const reconciled = { ...manifest, merge };
3089
+ delete reconciled.supersession;
3090
+ const updated = { ...reconciled, cleanupEligibility: cleanupEligibilityForEvidence(reconciled), updatedAt: input.now ?? Date.now() };
3091
+ writeAtomicJson(input.manifestPath, updated);
3092
+ return { manifest: updated, reference: referenceFor(input.manifestPath, updated), text: formatStoredParallelHandoffCleanup(input.manifestPath, updated) };
3093
+ }
3094
+ function recordParallelHandoffSupersession(...args) {
3095
+ return withHandoffWriteLock(args[0].manifestPath, () => recordParallelHandoffSupersessionUnlocked(...args));
3096
+ }
3097
+ function recordParallelHandoffSupersessionUnlocked(input) {
3098
+ const manifest = readParallelHandoffManifest(input.manifestPath);
3099
+ if (!manifest) throw new Error(`Parallel handoff manifest not found: ${input.manifestPath}`);
3100
+ const laneId = boundedString(input.laneId, "laneId", MAX_SUPERSESSION_ID_LENGTH);
3101
+ validateManifestForLaneEvidence(manifest, laneId);
3102
+ if (hasActiveChildren(manifest)) throw new Error("Lane has an active child owner; supersession must be recorded after local reconciliation.");
3103
+ const supersession = { ...normalizeSupersessionEvidence(input.supersession), manifestDigest: manifestFactsDigest(manifest) };
3104
+ if (supersession.supersededBy === laneId) throw new Error("supersession.supersededBy must identify a different replacement lane.");
3105
+ if (manifest.supersession) {
3106
+ const existing = normalizeSupersessionEvidence(manifest.supersession);
3107
+ if (!sameSupersessionEvidence(existing, supersession)) throw new Error("Lane manifest already contains different supersession evidence.");
3108
+ }
3109
+ const updated = { ...manifest, supersession, cleanupEligibility: cleanupEligibilityForEvidence({ ...manifest, supersession }), updatedAt: input.now ?? Date.now() };
3110
+ writeAtomicJson(input.manifestPath, updated);
3111
+ return { manifest: updated, reference: referenceFor(input.manifestPath, updated), text: formatStoredParallelHandoffCleanup(input.manifestPath, updated) };
3112
+ }
3113
+ function safeHandoffAgentName(agent) {
3114
+ return agent.replace(/[^\w.-]/g, "_");
3115
+ }
3116
+ function missingDiff(input) {
3117
+ const patchPath = path5.join(path5.dirname(input.manifestPath), `missing-diff-step-${input.stepIndex}-task-${input.taskIndex}-${safeHandoffAgentName(input.agent)}.patch`);
3118
+ try {
3119
+ fs5.mkdirSync(path5.dirname(patchPath), { recursive: true });
3120
+ fs5.writeFileSync(patchPath, "", "utf-8");
3121
+ } catch {
3122
+ }
3123
+ return {
3124
+ index: input.taskIndex,
3125
+ agent: input.agent,
3126
+ branch: input.branch ?? "",
3127
+ diffStat: "",
3128
+ filesChanged: 0,
3129
+ insertions: 0,
3130
+ deletions: 0,
3131
+ patchPath,
3132
+ error: "diff artifact unavailable; no patch was captured"
3133
+ };
3134
+ }
3135
+ function writeParallelHandoffGroup(...args) {
3136
+ return withHandoffWriteLock(args[0].manifestPath, () => writeParallelHandoffGroupUnlocked(...args));
3137
+ }
3138
+ function writeParallelHandoffGroupUnlocked(input) {
3139
+ const now = input.now ?? Date.now();
3140
+ const existing = readParallelHandoffManifest(input.manifestPath);
3141
+ if (existing && (existing.runId !== input.runId || existing.mode !== input.mode || existing.source !== input.source)) {
3142
+ throw new Error(`Parallel handoff manifest belongs to a different run: ${input.manifestPath}`);
3143
+ }
3144
+ const laneBindings = input.laneBindings?.map((binding, index) => normalizeLaneBinding(binding, `parallel handoff lane binding ${index}`));
3145
+ if (laneBindings) {
3146
+ const taskIndexes = /* @__PURE__ */ new Set();
3147
+ for (const binding of laneBindings) {
3148
+ if (taskIndexes.has(binding.taskIndex)) throw new Error(`Parallel handoff has duplicate lane binding taskIndex ${binding.taskIndex}.`);
3149
+ if (!input.setup.worktrees.some((worktree) => worktree.index === binding.taskIndex)) throw new Error(`Parallel handoff lane binding taskIndex ${binding.taskIndex} has no managed worktree.`);
3150
+ if (binding.index !== input.flatStartIndex + binding.taskIndex) throw new Error(`Parallel handoff lane binding index ${binding.index} does not match taskIndex ${binding.taskIndex}.`);
3151
+ taskIndexes.add(binding.taskIndex);
3152
+ }
3153
+ }
3154
+ const bindingForTask = (taskIndex) => laneBindings?.find((binding) => binding.taskIndex === taskIndex);
3155
+ const group = {
3156
+ stepIndex: input.stepIndex,
3157
+ baseCommit: input.setup.baseCommit,
3158
+ repoRoot: input.setup.cwd,
3159
+ children: input.results.map((result, taskIndex) => {
3160
+ const binding = bindingForTask(taskIndex);
3161
+ const runId = optionalIdentity(result.runId ?? binding?.runId, `parallel handoff child ${taskIndex}.runId`);
3162
+ const lane = normalizeWorkflowLaneMetadata(result.lane ?? binding?.lane, `parallel handoff child ${taskIndex}.lane`);
3163
+ const workflowKey = optionalIdentity(result.workflowKey ?? binding?.workflowKey, `parallel handoff child ${taskIndex}.workflowKey`);
3164
+ assertWorkflowLaneKey(lane, workflowKey, `parallel handoff child ${taskIndex}.lane`);
3165
+ const diff = input.diffs[taskIndex] ?? missingDiff({
3166
+ manifestPath: input.manifestPath,
3167
+ stepIndex: input.stepIndex,
3168
+ taskIndex,
3169
+ agent: result.agent,
3170
+ branch: input.setup.worktrees[taskIndex]?.branch
3171
+ });
3172
+ return {
3173
+ index: input.flatStartIndex + taskIndex,
3174
+ taskIndex,
3175
+ agent: result.agent,
3176
+ ...workflowKey ? { workflowKey } : {},
3177
+ ...runId ? { runId } : {},
3178
+ ...lane ? { lane } : {},
3179
+ status: result.status,
3180
+ summary: result.summary,
3181
+ ...result.outputPath ? { outputPath: result.outputPath } : {},
3182
+ ...result.structuredOutput !== void 0 ? { structuredOutput: result.structuredOutput } : {},
3183
+ ...result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {},
3184
+ ...result.sessionPath ? { sessionPath: result.sessionPath } : {},
3185
+ patch: {
3186
+ path: diff.patchPath,
3187
+ branch: diff.branch,
3188
+ changed: diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0,
3189
+ diffStat: diff.diffStat,
3190
+ filesChanged: diff.filesChanged,
3191
+ insertions: diff.insertions,
3192
+ deletions: diff.deletions,
3193
+ ...diff.error ? { error: diff.error } : {}
3194
+ }
3195
+ };
3196
+ }),
3197
+ ...laneBindings && laneBindings.length > 0 && input.results.length === 0 ? { laneBindings } : {},
3198
+ cleanup: input.cleanup ?? {
3199
+ state: "partial",
3200
+ pruned: false,
3201
+ tasks: input.setup.worktrees.map((worktree) => ({
3202
+ index: worktree.index,
3203
+ path: worktree.path,
3204
+ branch: worktree.branch,
3205
+ ...worktree.provider ? { provider: worktree.provider } : {},
3206
+ ...worktree.naming ? { naming: worktree.naming } : {},
3207
+ ...worktree.recordedBaseDir ? { recordedBaseDir: worktree.recordedBaseDir } : {},
3208
+ worktreeRemoved: false,
3209
+ branchRemoved: false,
3210
+ preserved: true,
3211
+ reason: "cleanup pending durable handoff capture"
3212
+ }))
3213
+ }
3214
+ };
3215
+ for (const task of group.cleanup.tasks) {
3216
+ const previous = existing?.groups.flatMap((item) => item.cleanup.tasks).find((item) => item.path === task.path && item.branch === task.branch);
3217
+ if (previous?.worktreeRemoved) task.worktreeRemoved = true;
3218
+ if (previous?.branchRemoved) task.branchRemoved = true;
3219
+ if (!task.worktreeRemoved && previous?.reason?.includes("retained child resume")) {
3220
+ task.preserved = true;
3221
+ task.reason = previous.reason;
3222
+ }
3223
+ }
3224
+ const groups = existing?.groups.filter((candidate) => candidate.stepIndex !== input.stepIndex) ?? [];
3225
+ groups.push(group);
3226
+ groups.sort((left, right) => left.stepIndex - right.stepIndex);
3227
+ const manifestWithoutEligibility = {
3228
+ version: 1,
3229
+ runId: input.runId,
3230
+ mode: input.mode,
3231
+ source: input.source,
3232
+ cwd: input.cwd,
3233
+ createdAt: existing?.createdAt ?? now,
3234
+ updatedAt: now,
3235
+ groups,
3236
+ ...existing && Object.hasOwn(existing, "merge") ? { merge: existing.merge } : {},
3237
+ ...existing && Object.hasOwn(existing, "supersession") ? { supersession: existing.supersession } : {}
3238
+ };
3239
+ const carriesCleanupMetadata = existing !== void 0 && (Object.hasOwn(existing, "merge") || Object.hasOwn(existing, "supersession") || Object.hasOwn(existing, "cleanupEligibility"));
3240
+ const manifest = {
3241
+ ...manifestWithoutEligibility,
3242
+ ...carriesCleanupMetadata ? { cleanupEligibility: cleanupEligibilityForEvidence(manifestWithoutEligibility) } : {}
3243
+ };
3244
+ writeAtomicJson(input.manifestPath, manifest);
3245
+ return referenceFor(input.manifestPath, manifest);
3246
+ }
3247
+ function parallelHandoffPath(baseDir, runId) {
3248
+ return runId ? path5.join(baseDir, "handoffs", `${runId}.json`) : path5.join(baseDir, "handoff.json");
3249
+ }
3250
+ function writeWorktreeSetupHandoff(input) {
3251
+ const { progress, ...handoff } = input;
3252
+ const { setup, attempts, cleanup } = progress;
3253
+ if (attempts.length === 0 && setup.worktrees.length === 0) return void 0;
3254
+ if (!setup.cwd || !setup.baseCommit) throw new Error("Cannot publish worktree allocation evidence without repository and base commit.");
3255
+ const diagnostic = (text2) => text2.slice(0, 512);
3256
+ const commandEvidence = (command) => command && {
3257
+ pid: command.pid,
3258
+ processGroupId: command.processGroupId,
3259
+ ...command.result ? {
3260
+ status: command.result.status,
3261
+ signal: command.result.signal,
3262
+ failed: Boolean(command.result.error),
3263
+ outputIncomplete: command.result.outputIncomplete,
3264
+ processTree: command.result.processTree?.state
3265
+ } : {}
3266
+ };
3267
+ const errors = [
3268
+ `Worktree setup ${JSON.stringify({
3269
+ runId: input.runId,
3270
+ stepIndex: input.stepIndex,
3271
+ phase: progress.phase,
3272
+ unknown: Boolean(progress.unknown),
3273
+ command: commandEvidence(progress.command)
3274
+ })}`,
3275
+ ...attempts.map((attempt) => {
3276
+ const task = cleanup?.tasks.find((candidate) => candidate.index === attempt.index);
3277
+ return `Allocation attempt ${JSON.stringify({
3278
+ index: attempt.index,
3279
+ branch: attempt.branch,
3280
+ path: attempt.path ?? null,
3281
+ validated: attempt.validated,
3282
+ command: commandEvidence(attempt.command),
3283
+ hookCommand: commandEvidence(attempt.hookCommand),
3284
+ ...task ? {
3285
+ worktreeRemoved: task.worktreeRemoved,
3286
+ branchRemoved: task.branchRemoved,
3287
+ preserved: task.preserved,
3288
+ reason: task.reason && diagnostic(task.reason),
3289
+ errors: task.errors?.map(diagnostic)
3290
+ } : {}
3291
+ })}`;
3292
+ }),
3293
+ ...cleanup?.errors?.map(diagnostic) ?? [],
3294
+ ...progress.unknown ? ["Setup settlement unknown; manual reconciliation required."] : []
3295
+ ];
3296
+ return writeParallelHandoffGroup({
3297
+ ...handoff,
3298
+ setup,
3299
+ diffs: [],
3300
+ results: [],
3301
+ laneBindings: input.laneBindings?.filter((binding) => setup.worktrees.some((worktree) => worktree.index === binding.taskIndex)),
3302
+ cleanup: {
3303
+ state: cleanup?.state ?? "partial",
3304
+ pruned: cleanup?.pruned ?? false,
3305
+ errors,
3306
+ // An attempted native path is not a validated recovery task, even during rollback.
3307
+ tasks: setup.worktrees.map((worktree) => {
3308
+ const task = cleanup?.tasks.find((candidate) => candidate.index === worktree.index);
3309
+ return task ? { ...task, reason: task.reason && diagnostic(task.reason), errors: task.errors?.map(diagnostic) } : {
3310
+ index: worktree.index,
3311
+ path: worktree.path,
3312
+ branch: worktree.branch,
3313
+ provider: worktree.provider,
3314
+ naming: worktree.naming,
3315
+ recordedBaseDir: worktree.recordedBaseDir,
3316
+ worktreeRemoved: false,
3317
+ branchRemoved: false,
3318
+ preserved: true,
3319
+ reason: "setup pending durable handoff capture"
3320
+ };
3321
+ })
3322
+ }
3323
+ });
3324
+ }
3325
+ function formatParallelHandoffReference(reference) {
3326
+ return `Worktree handoff: ${reference.path} (${reference.childCount} children, ${reference.changedPatches} changed patches, cleanup ${reference.cleanupState})`;
3327
+ }
3328
+ function formatParallelHandoffError(error) {
3329
+ return `Worktree handoff unavailable: ${error instanceof Error ? error.message : String(error)}`;
3330
+ }
3331
+ function discardPreservedWorktrees(...args) {
3332
+ return withHandoffWriteLock(args[0], () => discardPreservedWorktreesUnlocked(...args));
3333
+ }
3334
+ function discardPreservedWorktreesUnlocked(manifestPath, authorization) {
3335
+ const resolvedPath = path5.resolve(manifestPath);
3336
+ const manifest = readParallelHandoffManifest(resolvedPath);
3337
+ if (!manifest) throw new Error(`Parallel handoff manifest not found: ${resolvedPath}`);
3338
+ let attempted = 0;
3339
+ for (const group of manifest.groups) {
3340
+ const pending = group.cleanup.tasks.filter((task) => task.preserved && (!task.worktreeRemoved || !task.branchRemoved));
3341
+ if (pending.length === 0) continue;
3342
+ attempted += pending.length;
3343
+ const report = cleanupWorktrees({
3344
+ cwd: group.repoRoot,
3345
+ baseCommit: group.baseCommit,
3346
+ worktrees: pending.map((task) => ({
3347
+ path: task.path,
3348
+ agentCwd: task.path,
3349
+ branch: task.branch,
3350
+ ...task.provider ? { provider: task.provider } : {},
3351
+ ...task.naming ? { naming: task.naming } : {},
3352
+ ...task.recordedBaseDir ? { recordedBaseDir: task.recordedBaseDir } : {},
3353
+ index: task.index,
3354
+ nodeModulesLinked: false,
3355
+ syntheticPaths: []
3356
+ }))
3357
+ }, { kind: "discard", authorization });
3358
+ const updates = new Map(report.tasks.map((task) => [task.index, task.worktreeRemoved && task.branchRemoved ? task : { ...task, preserved: true, reason: task.reason ?? "discard cleanup remains incomplete" }]));
3359
+ group.cleanup = {
3360
+ state: group.cleanup.tasks.every((task) => {
3361
+ const next = updates.get(task.index) ?? task;
3362
+ return next.worktreeRemoved && next.branchRemoved;
3363
+ }) && report.pruned ? "complete" : "partial",
3364
+ tasks: group.cleanup.tasks.map((task) => updates.get(task.index) ?? task),
3365
+ pruned: report.pruned,
3366
+ ...report.errors ? { errors: report.errors } : {}
3367
+ };
3368
+ }
3369
+ manifest.updatedAt = Date.now();
3370
+ writeAtomicJson(resolvedPath, manifest);
3371
+ const remaining = manifest.groups.flatMap((group) => group.cleanup.tasks.map((task) => ({ group, task }))).filter(({ task }) => task.preserved && (!task.worktreeRemoved || !task.branchRemoved));
3372
+ const lines = attempted === 0 ? [`No preserved worktrees remain in ${resolvedPath}.`] : [`Discard processed ${attempted} preserved worktree${attempted === 1 ? "" : "s"}.`, `Manifest: ${resolvedPath}`];
3373
+ if (remaining.length > 0) {
3374
+ lines.push("", "Some worktrees remain. Inspect and remove them manually if appropriate:");
3375
+ for (const { group, task } of remaining) {
3376
+ lines.push(
3377
+ ` git -C ${JSON.stringify(group.repoRoot)} status --short`,
3378
+ ` git -C ${JSON.stringify(group.repoRoot)} worktree remove --force ${JSON.stringify(task.path)}`,
3379
+ ` git -C ${JSON.stringify(group.repoRoot)} branch -D ${JSON.stringify(task.branch)}`
3380
+ );
3381
+ }
3382
+ }
3383
+ return { manifest, text: lines.join("\n") };
3384
+ }
3385
+
3386
+ // src/runs/background/async-resume.ts
3387
+ import * as fs6 from "node:fs";
3388
+ import * as path6 from "node:path";
3389
+ function getErrorMessage2(error) {
3390
+ return error instanceof Error ? error.message : String(error);
3391
+ }
3392
+ function ensureObject(value, source) {
3393
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3394
+ throw new Error(`Async result file '${source}' must contain a JSON object.`);
3395
+ }
3396
+ return value;
3397
+ }
3398
+ function validateOptionalString(value, field, source, displayField = field) {
3399
+ const fieldValue = value[field];
3400
+ if (fieldValue === void 0) return void 0;
3401
+ if (typeof fieldValue !== "string") throw new Error(`Invalid async result file '${source}': ${displayField} must be a string.`);
3402
+ return fieldValue;
3403
+ }
3404
+ function validateResultFile(value, resultPath) {
3405
+ const data = ensureObject(value, resultPath);
3406
+ const resultsValue = data.results;
3407
+ let results;
3408
+ if (resultsValue !== void 0) {
3409
+ if (!Array.isArray(resultsValue)) throw new Error(`Invalid async result file '${resultPath}': results must be an array.`);
3410
+ results = resultsValue.map((entry, index) => {
3411
+ const child = ensureObject(entry, `${resultPath} results[${index}]`);
3412
+ const agent = validateOptionalString(child, "agent", resultPath, `results[${index}].agent`);
3413
+ const sessionFile = validateOptionalString(child, "sessionFile", resultPath, `results[${index}].sessionFile`);
3414
+ const sessionName = validateOptionalString(child, "sessionName", resultPath, `results[${index}].sessionName`);
3415
+ const intercomTarget = validateOptionalString(child, "intercomTarget", resultPath, `results[${index}].intercomTarget`);
3416
+ const model = validateOptionalString(child, "model", resultPath, `results[${index}].model`);
3417
+ const thinking = validateOptionalString(child, "thinking", resultPath, `results[${index}].thinking`);
3418
+ const launchContractDigest = validateOptionalString(child, "launchContractDigest", resultPath, `results[${index}].launchContractDigest`);
3419
+ const capabilityCeiling = child.capabilityCeiling === void 0 ? void 0 : parseSubagentCapabilityCeiling(child.capabilityCeiling, `async result file '${resultPath}' results[${index}].capabilityCeiling`);
3420
+ const success2 = child.success;
3421
+ if (success2 !== void 0 && typeof success2 !== "boolean") throw new Error(`Invalid async result file '${resultPath}': results[${index}].success must be a boolean.`);
3422
+ return { agent, sessionName, sessionFile, intercomTarget, model, thinking, launchContractDigest, ...capabilityCeiling ? { capabilityCeiling } : {}, ...typeof success2 === "boolean" ? { success: success2 } : {} };
3423
+ });
3424
+ }
3425
+ const success = data.success;
3426
+ if (success !== void 0 && typeof success !== "boolean") throw new Error(`Invalid async result file '${resultPath}': success must be a boolean.`);
3427
+ return {
3428
+ id: validateOptionalString(data, "id", resultPath),
3429
+ runId: validateOptionalString(data, "runId", resultPath),
3430
+ agent: validateOptionalString(data, "agent", resultPath),
3431
+ mode: validateOptionalString(data, "mode", resultPath),
3432
+ state: validateOptionalString(data, "state", resultPath),
3433
+ cwd: validateOptionalString(data, "cwd", resultPath),
3434
+ sessionId: validateOptionalString(data, "sessionId", resultPath),
3435
+ sessionFile: validateOptionalString(data, "sessionFile", resultPath),
3436
+ model: validateOptionalString(data, "model", resultPath),
3437
+ thinking: validateOptionalString(data, "thinking", resultPath),
3438
+ launchContractDigest: validateOptionalString(data, "launchContractDigest", resultPath),
3439
+ ...data.capabilityCeiling === void 0 ? {} : { capabilityCeiling: parseSubagentCapabilityCeiling(data.capabilityCeiling, `async result file '${resultPath}' capabilityCeiling`) },
3440
+ ...data.admissionCapabilityCeiling === void 0 ? {} : { admissionCapabilityCeiling: parseSubagentCapabilityCeiling(data.admissionCapabilityCeiling, `async result file '${resultPath}' admissionCapabilityCeiling`) },
3441
+ ...typeof success === "boolean" ? { success } : {},
3442
+ ...results ? { results } : {}
3443
+ };
3444
+ }
3445
+ function readResultFile(resultPath) {
3446
+ let raw;
3447
+ try {
3448
+ raw = fs6.readFileSync(resultPath, "utf-8");
3449
+ } catch (error) {
3450
+ throw new Error(`Failed to read async result file '${resultPath}': ${getErrorMessage2(error)}`, {
3451
+ cause: error instanceof Error ? error : void 0
3452
+ });
3453
+ }
3454
+ try {
3455
+ return validateResultFile(JSON.parse(raw), resultPath);
3456
+ } catch (error) {
3457
+ if (error instanceof SyntaxError) {
3458
+ throw new Error(`Failed to parse async result file '${resultPath}': ${getErrorMessage2(error)}`, {
3459
+ cause: error
3460
+ });
3461
+ }
3462
+ throw error;
3463
+ }
3464
+ }
3465
+ function assertRunId(value, field) {
3466
+ if (value === void 0) return void 0;
3467
+ if (value.trim() === "") throw new Error(`${field} must not be empty.`);
3468
+ if (path6.isAbsolute(value) || /[\\/]/.test(value) || value.includes("..")) {
3469
+ throw new Error(`${field} must be an async run id or prefix, not a path.`);
3470
+ }
3471
+ return value;
3472
+ }
3473
+ function assertInsideRoot(root, target, label) {
3474
+ const rootPath = path6.resolve(root);
3475
+ const targetPath = path6.resolve(target);
3476
+ const relative3 = path6.relative(rootPath, targetPath);
3477
+ if (relative3 === "" || !relative3.startsWith("..") && !path6.isAbsolute(relative3)) return;
3478
+ throw new Error(`${label} must be inside ${rootPath}.`);
3479
+ }
3480
+ function prefixedRunIds(dir, prefix, suffix = "") {
3481
+ if (!fs6.existsSync(dir)) return [];
3482
+ return fs6.readdirSync(dir).filter((entry) => entry.startsWith(prefix) && (!suffix || entry.endsWith(suffix))).map((entry) => suffix ? entry.slice(0, -suffix.length) : entry).sort();
3483
+ }
3484
+ function exactResultPath(resultsDir, runId) {
3485
+ const indexed = resultPayloadPathForIndexedRun(resultsDir, runId);
3486
+ if (indexed) return indexed;
3487
+ const resultPath = resultFilePath(resultsDir, runId);
3488
+ assertInsideRoot(resultsDir, resultPath, "Async result file");
3489
+ return fs6.existsSync(resultPath) ? resultPath : null;
3490
+ }
3491
+ function findAsyncRunPrefixMatches(prefix, asyncDirRoot, resultsDir) {
3492
+ const requestedId = assertRunId(prefix, "id");
3493
+ if (!requestedId) return [];
3494
+ if (!canScanAsyncRunPrefix(requestedId)) return [];
3495
+ const asyncRoot = path6.resolve(asyncDirRoot);
3496
+ const resultRoot = path6.resolve(resultsDir);
3497
+ const matchingIds = prefixedRunIds(asyncRoot, requestedId).sort();
3498
+ return matchingIds.map((id) => {
3499
+ const asyncDir = path6.join(asyncRoot, id);
3500
+ assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
3501
+ return {
3502
+ id,
3503
+ location: {
3504
+ asyncDir: fs6.existsSync(asyncDir) ? asyncDir : null,
3505
+ resultPath: exactResultPath(resultRoot, id),
3506
+ resolvedId: id
3507
+ }
3508
+ };
3509
+ });
3510
+ }
3511
+ function resolveAsyncRunLocation(params, asyncDirRoot, resultsDir) {
3512
+ const asyncRoot = path6.resolve(asyncDirRoot);
3513
+ const resultRoot = path6.resolve(resultsDir);
3514
+ const requestedId = assertRunId(params.id, "id") ?? assertRunId(params.runId, "runId");
3515
+ if (params.dir) {
3516
+ const asyncDir = path6.resolve(params.dir);
3517
+ assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
3518
+ const resolvedId = requestedId ?? path6.basename(asyncDir);
3519
+ if (requestedId && requestedId !== path6.basename(asyncDir)) {
3520
+ throw new Error(`Async run id '${requestedId}' does not match directory '${path6.basename(asyncDir)}'.`);
3521
+ }
3522
+ return { asyncDir, resultPath: exactResultPath(resultRoot, resolvedId), resolvedId };
3523
+ }
3524
+ if (!requestedId) return { asyncDir: null, resultPath: null };
3525
+ const directAsyncDir = path6.join(asyncRoot, requestedId);
3526
+ assertInsideRoot(asyncRoot, directAsyncDir, "Async run directory");
3527
+ const directResultPath = exactResultPath(resultRoot, requestedId);
3528
+ if (fs6.existsSync(directAsyncDir) || directResultPath) {
3529
+ return {
3530
+ asyncDir: fs6.existsSync(directAsyncDir) ? directAsyncDir : null,
3531
+ resultPath: directResultPath,
3532
+ resolvedId: requestedId
3533
+ };
3534
+ }
3535
+ if (requestedId.length < MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH) {
3536
+ throw new Error(`Async run id prefix '${requestedId}' is too short. Provide at least ${MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH} characters.`);
3537
+ }
3538
+ if (!canScanAsyncRunPrefix(requestedId)) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
3539
+ const matching = findAsyncRunPrefixMatches(requestedId, asyncRoot, resultRoot);
3540
+ if (matching.length === 0) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
3541
+ if (matching.length > 1) {
3542
+ throw new Error(`Ambiguous async run id prefix '${requestedId}' matched: ${matching.map((match) => match.id).join(", ")}. Provide a longer id.`);
3543
+ }
3544
+ return matching[0].location;
3545
+ }
3546
+ function resultState(result) {
3547
+ if (result.state === "complete" || result.state === "failed" || result.state === "partial" || result.state === "paused" || result.state === "stopped" || result.state === "running" || result.state === "queued") {
3548
+ return result.state;
3549
+ }
3550
+ return result.success ? "complete" : "failed";
3551
+ }
3552
+ function validateStatusForResume(status, source) {
3553
+ if (!status) return;
3554
+ if (typeof status.runId !== "string") throw new Error(`Invalid async status '${source}': runId must be a string.`);
3555
+ assertWorkflowGraphHostSteps(status.workflowGraph, source, status.runId);
3556
+ if (status.sessionId !== void 0 && typeof status.sessionId !== "string") throw new Error(`Invalid async status '${source}': sessionId must be a string.`);
3557
+ if (status.cwd !== void 0 && typeof status.cwd !== "string") throw new Error(`Invalid async status '${source}': cwd must be a string.`);
3558
+ if (status.sessionFile !== void 0 && typeof status.sessionFile !== "string") throw new Error(`Invalid async status '${source}': sessionFile must be a string.`);
3559
+ if (status.capabilityCeiling !== void 0) status.capabilityCeiling = parseSubagentCapabilityCeiling(status.capabilityCeiling, `async status '${source}' capabilityCeiling`);
3560
+ if (status.admissionCapabilityCeiling !== void 0) status.admissionCapabilityCeiling = parseSubagentCapabilityCeiling(status.admissionCapabilityCeiling, `async status '${source}' admissionCapabilityCeiling`);
3561
+ if (status.steps !== void 0) {
3562
+ if (!Array.isArray(status.steps)) throw new Error(`Invalid async status '${source}': steps must be an array.`);
3563
+ status.steps.forEach((step, index) => {
3564
+ if (!step || typeof step !== "object" || Array.isArray(step)) throw new Error(`Invalid async status '${source}': steps[${index}] must be an object.`);
3565
+ const stepRecord = step;
3566
+ if (typeof stepRecord.agent !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].agent must be a string.`);
3567
+ if (stepRecord.sessionFile !== void 0 && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
3568
+ if (stepRecord.sessionName !== void 0 && typeof stepRecord.sessionName !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionName must be a string.`);
3569
+ if (stepRecord.model !== void 0 && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
3570
+ if (stepRecord.launchModel !== void 0 && typeof stepRecord.launchModel !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].launchModel must be a string.`);
3571
+ if (stepRecord.thinking !== void 0 && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
3572
+ if (stepRecord.thinkingCeiling !== void 0) stepRecord.thinkingCeiling = parseThinkingLevel(stepRecord.thinkingCeiling, `async status '${source}' steps[${index}].thinkingCeiling`);
3573
+ if (stepRecord.launchContractDigest !== void 0 && typeof stepRecord.launchContractDigest !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].launchContractDigest must be a string.`);
3574
+ if (stepRecord.capabilityCeiling !== void 0) stepRecord.capabilityCeiling = parseSubagentCapabilityCeiling(stepRecord.capabilityCeiling, `async status '${source}' steps[${index}].capabilityCeiling`);
3575
+ });
3576
+ }
3577
+ }
3578
+ function normalizeRecoveryAcceptance(value, descriptorPath) {
3579
+ const errors = validateAcceptanceInput(value, "recoveryDescriptor.acceptance");
3580
+ if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
3581
+ return value;
3582
+ }
3583
+ function asyncReviveRequiresRecoveryDescriptor(target) {
3584
+ if (target.recoveryDescriptor) return false;
3585
+ return !(target.mode === "workflow" && target.sessionFile && target.capabilityCeiling);
3586
+ }
3587
+ function resumeTargetMode(status, result) {
3588
+ if (status?.mode) return status.mode;
3589
+ if (result?.mode === "single" || result?.mode === "parallel" || result?.mode === "chain" || result?.mode === "workflow") return result.mode;
3590
+ return void 0;
3591
+ }
3592
+ function readAsyncRecoveryDescriptor(asyncDir) {
3593
+ if (!asyncDir) return void 0;
3594
+ const descriptorPath = path6.join(asyncDir, "recovery-descriptor.json");
3595
+ if (!fs6.existsSync(descriptorPath)) return void 0;
3596
+ let value;
3597
+ try {
3598
+ value = JSON.parse(fs6.readFileSync(descriptorPath, "utf-8"));
3599
+ } catch (error) {
3600
+ throw new Error(`Failed to parse async recovery descriptor '${descriptorPath}': ${getErrorMessage2(error)}`, { cause: error instanceof Error ? error : void 0 });
3601
+ }
3602
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
3603
+ const parsed = value;
3604
+ const allowedFields = /* @__PURE__ */ new Set([
3605
+ "modelResponseAliases",
3606
+ "version",
3607
+ "launchContractDigest",
3608
+ "sourceRunId",
3609
+ "agentContract",
3610
+ "agent",
3611
+ "sessionFile",
3612
+ "cwd",
3613
+ "model",
3614
+ "modelProvider",
3615
+ "modelOverrideFromParent",
3616
+ "modelOrigin",
3617
+ "fast",
3618
+ "thinking",
3619
+ "thinkingCeiling",
3620
+ "tools",
3621
+ "allowNestedSubagents",
3622
+ "allowedAgents",
3623
+ "extensions",
3624
+ "subagentOnlyExtensions",
3625
+ "mcpDirectTools",
3626
+ "excludeTools",
3627
+ "mutationTools",
3628
+ "systemPrompt",
3629
+ "systemPromptMode",
3630
+ "inheritProjectContext",
3631
+ "inheritGlobalContext",
3632
+ "inheritSkills",
3633
+ "skills",
3634
+ "skillPath",
3635
+ "agentFilePath",
3636
+ "memory",
3637
+ "outputPath",
3638
+ "outputMode",
3639
+ "structuredOutputSchema",
3640
+ "acceptance",
3641
+ "sessionDir",
3642
+ "artifactConfig",
3643
+ "artifactsDir",
3644
+ "maxOutput",
3645
+ "controlConfig",
3646
+ "context",
3647
+ "intercomBridge",
3648
+ "absoluteDeadlineAt",
3649
+ "initialTurnBudget",
3650
+ "initialToolBudget",
3651
+ "maxSubagentDepth",
3652
+ "share",
3653
+ "capabilityCeiling",
3654
+ "launchResolvedExtensions",
3655
+ "runFanoutBudget",
3656
+ "lane",
3657
+ "baseRef",
3658
+ "launcher",
3659
+ "extensionBindings",
3660
+ "requiredExtensions"
3661
+ ]);
3662
+ for (const field of Object.keys(parsed)) {
3663
+ if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
3664
+ }
3665
+ const requiredStrings = ["sourceRunId", "agent", "cwd", "systemPromptMode", "outputMode"];
3666
+ for (const field of requiredStrings) {
3667
+ if (typeof parsed[field] !== "string" || !parsed[field].trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
3668
+ }
3669
+ if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
3670
+ if (parsed.launcher !== void 0 && (typeof parsed.launcher !== "string" || !RUNNER_LAUNCHER_NAME_PATTERN.test(parsed.launcher))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': launcher must be a launcher name.`);
3671
+ try {
3672
+ parsed.runFanoutBudget = validateRunFanoutBudgetDescriptor(parsed.runFanoutBudget);
3673
+ } catch (error) {
3674
+ throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
3675
+ }
3676
+ validateModelResponseAliases(parsed.modelResponseAliases, `async recovery descriptor '${descriptorPath}' modelResponseAliases`);
3677
+ if (parsed.capabilityCeiling !== void 0) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
3678
+ if (parsed.thinkingCeiling !== void 0) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
3679
+ if (parsed.extensionBindings !== void 0) parsed.extensionBindings = normalizeExtensionBindings(parsed.extensionBindings).value;
3680
+ if (parsed.requiredExtensions !== void 0) {
3681
+ try {
3682
+ parsed.requiredExtensions = snapshotRequiredChildExtensions(parsed.requiredExtensions, "requiredExtensions");
3683
+ } catch (error) {
3684
+ throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
3685
+ }
3686
+ }
3687
+ if (parsed.lane !== void 0) parsed.lane = normalizeWorkflowLaneMetadata(parsed.lane, `Invalid async recovery descriptor '${descriptorPath}': lane`);
3688
+ if (parsed.agentContract !== void 0) {
3689
+ if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
3690
+ const contract = parsed.agentContract;
3691
+ if (contract.version !== 1 || Object.keys(contract).some((key) => key !== "version")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be { version: 1 }.`);
3692
+ }
3693
+ if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
3694
+ if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
3695
+ if (parsed.context !== void 0 && parsed.context !== "fresh" && parsed.context !== "fork") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': context is invalid.`);
3696
+ if (parsed.modelOverrideFromParent !== void 0 && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
3697
+ if (parsed.fast !== void 0 && typeof parsed.fast !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': fast must be a boolean.`);
3698
+ if (parsed.modelOrigin !== void 0 && parsed.modelOrigin !== "explicit" && parsed.modelOrigin !== "inherited" && parsed.modelOrigin !== "configured") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOrigin must be 'explicit', 'inherited', or 'configured'.`);
3699
+ if (parsed.modelOrigin === void 0 && parsed.model !== void 0) parsed.modelOrigin = parsed.modelOverrideFromParent ? "inherited" : "configured";
3700
+ for (const field of ["inheritProjectContext", "inheritSkills", "share"]) {
3701
+ if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
3702
+ }
3703
+ if (parsed.inheritGlobalContext === void 0) parsed.inheritGlobalContext = parsed.inheritProjectContext;
3704
+ else if (typeof parsed.inheritGlobalContext !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': inheritGlobalContext must be a boolean.`);
3705
+ if (parsed.allowNestedSubagents !== void 0 && typeof parsed.allowNestedSubagents !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': allowNestedSubagents must be a boolean.`);
3706
+ if (parsed.allowedAgents !== void 0) {
3707
+ try {
3708
+ parsed.allowedAgents = normalizeCapabilityCeilingAllowedAgents(parsed.allowedAgents);
3709
+ } catch (error) {
3710
+ throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
3711
+ }
3712
+ }
3713
+ if (!Number.isInteger(parsed.maxSubagentDepth) || parsed.maxSubagentDepth < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
3714
+ for (const field of ["tools", "excludeTools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"]) {
3715
+ const item = parsed[field];
3716
+ if (item !== void 0 && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
3717
+ }
3718
+ if (parsed.systemPrompt !== void 0 && typeof parsed.systemPrompt !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPrompt must be a string.`);
3719
+ for (const field of ["launchContractDigest", "sessionFile", "model", "modelProvider", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"]) {
3720
+ if (parsed[field] !== void 0 && (typeof parsed[field] !== "string" || !parsed[field].trim())) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
3721
+ }
3722
+ if (parsed.baseRef !== void 0) {
3723
+ if (typeof parsed.baseRef !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': baseRef must be a string.`);
3724
+ try {
3725
+ parsed.baseRef = normalizeWorktreeBaseRef(parsed.baseRef);
3726
+ } catch (error) {
3727
+ throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
3728
+ }
3729
+ }
3730
+ if (parsed.structuredOutputSchema !== void 0 && (!parsed.structuredOutputSchema || typeof parsed.structuredOutputSchema !== "object" || Array.isArray(parsed.structuredOutputSchema))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': structuredOutputSchema must be an object.`);
3731
+ if (parsed.memory !== void 0) {
3732
+ if (!parsed.memory || typeof parsed.memory !== "object" || Array.isArray(parsed.memory)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': memory must be an object.`);
3733
+ const memory = parsed.memory;
3734
+ if (memory.scope !== "project" && memory.scope !== "user" || typeof memory.path !== "string" || !memory.path.trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': memory is invalid.`);
3735
+ }
3736
+ if (parsed.absoluteDeadlineAt !== void 0 && (!Number.isFinite(parsed.absoluteDeadlineAt) || parsed.absoluteDeadlineAt <= 0)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': absoluteDeadlineAt must be a positive timestamp.`);
3737
+ if (parsed.initialTurnBudget !== void 0) {
3738
+ delete parsed.initialTurnBudget;
3739
+ }
3740
+ if (parsed.initialToolBudget !== void 0) {
3741
+ const result = validateToolBudgetConfig(parsed.initialToolBudget, "recoveryDescriptor.initialToolBudget");
3742
+ if (result.error) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error}`);
3743
+ }
3744
+ if (parsed.maxOutput !== void 0) {
3745
+ if (!parsed.maxOutput || typeof parsed.maxOutput !== "object" || Array.isArray(parsed.maxOutput)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxOutput must be an object.`);
3746
+ for (const field of ["bytes", "lines"]) {
3747
+ const item = parsed.maxOutput[field];
3748
+ if (item !== void 0 && (!Number.isInteger(item) || item < 1)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxOutput.${field} must be a positive integer.`);
3749
+ }
3750
+ }
3751
+ if (parsed.artifactConfig !== void 0) {
3752
+ if (!parsed.artifactConfig || typeof parsed.artifactConfig !== "object" || Array.isArray(parsed.artifactConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig must be an object.`);
3753
+ const artifact = parsed.artifactConfig;
3754
+ for (const field of ["enabled", "includeInput", "includeOutput", "includeJsonl", "includeMetadata"]) {
3755
+ if (typeof artifact[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.${field} must be a boolean.`);
3756
+ }
3757
+ if (artifact.includeTranscript !== void 0 && typeof artifact.includeTranscript !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.includeTranscript must be a boolean.`);
3758
+ if (!Number.isInteger(artifact.cleanupDays) || artifact.cleanupDays < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
3759
+ }
3760
+ if (parsed.intercomBridge !== void 0) {
3761
+ const bridge = validateIntercomBridgeConfig({ value: parsed.intercomBridge, label: "intercomBridge" });
3762
+ if (!bridge.ok) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${bridge.error}`);
3763
+ }
3764
+ if (parsed.controlConfig !== void 0) {
3765
+ if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
3766
+ const control = parsed.controlConfig;
3767
+ if (typeof control.enabled !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.enabled must be a boolean.`);
3768
+ for (const field of ["needsAttentionAfterMs", "activeNoticeAfterMs", "failedToolAttemptsBeforeAttention"]) {
3769
+ if (!Number.isInteger(control[field]) || control[field] < 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.${field} must be a positive integer.`);
3770
+ }
3771
+ for (const field of ["activeNoticeAfterTurns", "activeNoticeAfterTokens"]) {
3772
+ if (control[field] !== void 0 && (!Number.isInteger(control[field]) || control[field] < 1)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.${field} must be a positive integer.`);
3773
+ }
3774
+ if (!Array.isArray(control.notifyOn) || control.notifyOn.some((item) => item !== "active_long_running" && item !== "needs_attention")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyOn is invalid.`);
3775
+ if (!Array.isArray(control.notifyChannels) || control.notifyChannels.some((item) => item !== "event" && item !== "async" && item !== "intercom")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyChannels is invalid.`);
3776
+ }
3777
+ if (parsed.acceptance !== void 0) parsed.acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
3778
+ return parsed;
3779
+ }
3780
+ function validateResumeSessionFile(runId, sessionFile) {
3781
+ if (path6.extname(sessionFile) !== ".jsonl") throw new Error(`Async run '${runId}' session file must be a .jsonl file: ${sessionFile}`);
3782
+ const resolved = path6.resolve(sessionFile);
3783
+ if (!fs6.existsSync(resolved)) throw new Error(`Async run '${runId}' session file does not exist: ${sessionFile}`);
3784
+ return resolved;
3785
+ }
3786
+ function validateResumeCwd(runId, cwd) {
3787
+ if (!cwd) return void 0;
3788
+ const resolved = path6.resolve(cwd);
3789
+ try {
3790
+ if (!fs6.statSync(resolved).isDirectory()) throw new Error("path is not a directory");
3791
+ } catch (error) {
3792
+ throw new Error(`Async run '${runId}' required cwd does not exist: ${cwd}`, { cause: error instanceof Error ? error : void 0 });
3793
+ }
3794
+ return resolved;
3795
+ }
3796
+ function resolveAsyncResumeTarget(params, deps = {}, options = {}) {
3797
+ const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
3798
+ const resultsDir = deps.resultsDir ?? DIRS.results;
3799
+ const requireSessionFile = options.requireSessionFile ?? true;
3800
+ const location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
3801
+ if (!location.asyncDir && !location.resultPath) {
3802
+ throw new Error("Async run not found. Provide id or dir.");
3803
+ }
3804
+ const reconciliation = location.asyncDir ? reconcileAsyncRun(location.asyncDir, { resultsDir, kill: deps.kill, now: deps.now }) : void 0;
3805
+ const status = reconciliation?.status ?? null;
3806
+ validateStatusForResume(status, location.asyncDir ? path6.join(location.asyncDir, "status.json") : "status.json");
3807
+ const recoveryDescriptor = readAsyncRecoveryDescriptor(location.asyncDir ?? void 0);
3808
+ const result = location.resultPath ? readResultFile(location.resultPath) : void 0;
3809
+ const runId = status?.runId ?? result?.runId ?? result?.id ?? location.resolvedId ?? (location.asyncDir ? path6.basename(location.asyncDir) : "unknown");
3810
+ const mode = resumeTargetMode(status, result);
3811
+ if (options.sessionId && (status && !ownsRun(options.sessionId, status.sessionId) || result && !ownsRun(options.sessionId, result.sessionId))) {
3812
+ throw new Error(`Async run '${runId}' was not found in the active session.`);
3813
+ }
3814
+ if (recoveryDescriptor && recoveryDescriptor.sourceRunId !== runId) throw new Error(`Async run '${runId}' has a recovery descriptor for a different source run.`);
3815
+ const state = status?.state ?? (result ? resultState(result) : void 0);
3816
+ if (!state) throw new Error(`Status file not found for async run '${runId}'.`);
3817
+ if (state === "stopped") throw new Error(`Async run '${runId}' was stopped and cannot be resumed. Start a new run instead.`);
3818
+ const statusSteps = status?.steps ?? [];
3819
+ const resultSteps = result?.results ?? [];
3820
+ const stepCount = statusSteps.length || resultSteps.length || (result?.agent ? 1 : 0);
3821
+ const requestedIndex = params.index;
3822
+ if (requestedIndex !== void 0 && !Number.isInteger(requestedIndex)) throw new Error(`Async run '${runId}' index must be an integer.`);
3823
+ const terminalStepStatuses = /* @__PURE__ */ new Set(["complete", "completed", "failed", "paused"]);
3824
+ if (state === "running") {
3825
+ if (requestedIndex !== void 0) {
3826
+ if (requestedIndex < 0 || requestedIndex >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${requestedIndex} is out of range.`);
3827
+ const selectedStep = statusSteps[requestedIndex];
3828
+ if (selectedStep?.status === "running") {
3829
+ const capabilityCeiling2 = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selectedStep.capabilityCeiling);
3830
+ return {
3831
+ kind: "live",
3832
+ runId,
3833
+ asyncDir: location.asyncDir ?? void 0,
3834
+ state,
3835
+ ...mode ? { mode } : {},
3836
+ agent: selectedStep.agent,
3837
+ ...selectedStep.sessionName ?? result?.results?.[requestedIndex]?.sessionName ? { sessionName: selectedStep.sessionName ?? result?.results?.[requestedIndex]?.sessionName } : {},
3838
+ index: requestedIndex,
3839
+ cwd: status?.cwd ?? result?.cwd,
3840
+ sessionFile: selectedStep.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
3841
+ model: selectedStep.model,
3842
+ thinking: selectedStep.thinking,
3843
+ launchContractDigest: selectedStep.launchContractDigest ?? result?.results?.[requestedIndex]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
3844
+ ...selectedStep.runner ? { runner: selectedStep.runner } : {},
3845
+ ...selectedStep.externalJob ? { externalJob: selectedStep.externalJob } : {},
3846
+ ...capabilityCeiling2 ? { capabilityCeiling: capabilityCeiling2 } : {},
3847
+ ...selectedStep.thinkingCeiling ? { thinkingCeiling: selectedStep.thinkingCeiling } : {},
3848
+ ...recoveryDescriptor ? { recoveryDescriptor } : {}
3849
+ };
3850
+ }
3851
+ if (selectedStep?.status === "pending") throw new Error(`Async run '${runId}' child ${requestedIndex} is pending and has not started yet. Wait for it to run or complete before resuming.`);
3852
+ if (selectedStep && !terminalStepStatuses.has(selectedStep.status)) throw new Error(`Async run '${runId}' child ${requestedIndex} is ${selectedStep.status} and cannot be revived yet.`);
3853
+ } else {
3854
+ const running = statusSteps.map((step, index2) => ({ step, index: index2 })).filter(({ step }) => step.status === "running");
3855
+ const selected = running.length === 1 ? running[0] : void 0;
3856
+ if (!selected) {
3857
+ throw new Error(`Async run '${runId}' has ${running.length} running children. Provide index to choose one.`);
3858
+ }
3859
+ const capabilityCeiling2 = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selected.step.capabilityCeiling);
3860
+ return {
3861
+ kind: "live",
3862
+ runId,
3863
+ asyncDir: location.asyncDir ?? void 0,
3864
+ state,
3865
+ ...mode ? { mode } : {},
3866
+ agent: selected.step.agent,
3867
+ ...selected.step.sessionName ?? result?.results?.[selected.index]?.sessionName ? { sessionName: selected.step.sessionName ?? result?.results?.[selected.index]?.sessionName } : {},
3868
+ index: selected.index,
3869
+ cwd: status?.cwd ?? result?.cwd,
3870
+ sessionFile: selected.step.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
3871
+ model: selected.step.model,
3872
+ thinking: selected.step.thinking,
3873
+ launchContractDigest: selected.step.launchContractDigest ?? result?.results?.[selected.index]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
3874
+ ...selected.step.runner ? { runner: selected.step.runner } : {},
3875
+ ...selected.step.externalJob ? { externalJob: selected.step.externalJob } : {},
3876
+ ...capabilityCeiling2 ? { capabilityCeiling: capabilityCeiling2 } : {},
3877
+ ...selected.step.thinkingCeiling ? { thinkingCeiling: selected.step.thinkingCeiling } : {},
3878
+ ...recoveryDescriptor ? { recoveryDescriptor } : {}
3879
+ };
3880
+ }
3881
+ }
3882
+ if (stepCount > 1 && requestedIndex === void 0) {
3883
+ throw new Error(`Async run '${runId}' has ${stepCount} children. Provide index to choose one.`);
3884
+ }
3885
+ const index = requestedIndex ?? 0;
3886
+ if (!Number.isInteger(index)) throw new Error(`Async run '${runId}' index must be an integer.`);
3887
+ if (index < 0 || index >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${index} is out of range.`);
3888
+ const selectedStatusStep = statusSteps[index];
3889
+ if (selectedStatusStep?.status === "stopped" || selectedStatusStep?.stopped === true) {
3890
+ throw new Error(`Async run '${runId}' child ${index} was stopped and cannot be resumed. Start a new run instead.`);
3891
+ }
3892
+ const agent = selectedStatusStep?.agent ?? resultSteps[index]?.agent ?? result?.agent;
3893
+ if (!agent) throw new Error(`Could not determine child agent for async run '${runId}'.`);
3894
+ if (recoveryDescriptor && recoveryDescriptor.agent !== agent) throw new Error(`Async run '${runId}' has a recovery descriptor for '${recoveryDescriptor.agent}', not '${agent}'.`);
3895
+ const sessionFile = statusSteps[index]?.sessionFile ?? resultSteps[index]?.sessionFile ?? (stepCount === 1 ? status?.sessionFile ?? result?.sessionFile : void 0);
3896
+ if (!sessionFile && requireSessionFile) throw new Error(`Async run '${runId}' child ${index} does not have a persisted session file to resume from.`);
3897
+ const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : void 0;
3898
+ const stepModel = statusSteps[index]?.launchModel ?? statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : void 0);
3899
+ const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : void 0);
3900
+ const thinkingCeiling = statusSteps[index]?.thinkingCeiling ?? (stepCount === 1 ? recoveryDescriptor?.thinkingCeiling : void 0);
3901
+ const capabilityCeiling = mode === "workflow" ? intersectSubagentCapabilityCeilings(status?.admissionCapabilityCeiling, result?.admissionCapabilityCeiling) : intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
3902
+ const managedWorktreeCwd = location.asyncDir ? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index) : void 0;
3903
+ const resumeCwd = validateResumeCwd(runId, managedWorktreeCwd ?? status?.cwd ?? result?.cwd ?? recoveryDescriptor?.cwd);
3904
+ return {
3905
+ kind: "revive",
3906
+ runId,
3907
+ asyncDir: location.asyncDir ?? void 0,
3908
+ state,
3909
+ ...mode ? { mode } : {},
3910
+ agent,
3911
+ ...statusSteps[index]?.sessionName ?? resultSteps[index]?.sessionName ? { sessionName: statusSteps[index]?.sessionName ?? resultSteps[index]?.sessionName } : {},
3912
+ index,
3913
+ ...resumeCwd ? { cwd: resumeCwd } : {},
3914
+ ...managedWorktreeCwd ? { managedWorktree: true } : {},
3915
+ ...resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {},
3916
+ ...stepModel ? { model: stepModel } : {},
3917
+ ...stepThinking ? { thinking: stepThinking } : {},
3918
+ launchContractDigest: statusSteps[index]?.launchContractDigest ?? resultSteps[index]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
3919
+ ...statusSteps[index]?.runner ? { runner: statusSteps[index].runner } : {},
3920
+ ...statusSteps[index]?.externalJob ? { externalJob: statusSteps[index].externalJob } : {},
3921
+ ...capabilityCeiling ? { capabilityCeiling } : {},
3922
+ ...thinkingCeiling ? { thinkingCeiling } : {},
3923
+ ...recoveryDescriptor ? { recoveryDescriptor } : {}
3924
+ };
3925
+ }
3926
+ function applySteeringRecoveryAgentConfig(agentConfig, descriptor) {
3927
+ return {
3928
+ ...agentConfig,
3929
+ model: descriptor.model,
3930
+ modelProvider: descriptor.modelProvider,
3931
+ thinking: descriptor.thinking,
3932
+ maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
3933
+ tools: descriptor.tools ? [...descriptor.tools] : void 0,
3934
+ excludeTools: descriptor.excludeTools ? [...descriptor.excludeTools] : void 0,
3935
+ allowNestedSubagents: descriptor.allowNestedSubagents,
3936
+ allowedAgents: descriptor.allowedAgents === void 0 ? void 0 : [...descriptor.allowedAgents],
3937
+ extensions: descriptor.extensions ? [...descriptor.extensions] : void 0,
3938
+ subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : void 0,
3939
+ mcpDirectTools: descriptor.mcpDirectTools ? [...descriptor.mcpDirectTools] : void 0,
3940
+ mutationTools: descriptor.mutationTools ? [...descriptor.mutationTools] : void 0,
3941
+ systemPrompt: descriptor.systemPrompt ?? agentConfig.systemPrompt,
3942
+ systemPromptMode: descriptor.systemPromptMode,
3943
+ inheritProjectContext: descriptor.inheritProjectContext,
3944
+ inheritGlobalContext: descriptor.inheritGlobalContext,
3945
+ inheritSkills: descriptor.inheritSkills,
3946
+ skills: descriptor.skills ? [...descriptor.skills] : void 0,
3947
+ skillPath: descriptor.skillPath ? [...descriptor.skillPath] : void 0,
3948
+ filePath: descriptor.agentFilePath,
3949
+ memory: descriptor.memory ? { ...descriptor.memory } : void 0,
3950
+ output: descriptor.outputPath,
3951
+ toolBudget: descriptor.initialToolBudget,
3952
+ maxSubagentDepth: descriptor.maxSubagentDepth,
3953
+ // The recorded launcher wins over the current agent file, including recorded absence.
3954
+ launcher: descriptor.launcher
3955
+ };
3956
+ }
3957
+ function buildRevivedAsyncTask(target, message) {
3958
+ return [
3959
+ "You are reviving a previous subagent conversation.",
3960
+ "",
3961
+ `Original run: ${target.runId}`,
3962
+ `Original agent: ${target.agent}`,
3963
+ target.sessionFile ? `Original session file: ${target.sessionFile}` : void 0,
3964
+ "",
3965
+ "Use the stored session context as background. Answer the orchestrator's follow-up below. Do not assume the original child session is still running.",
3966
+ "",
3967
+ "Follow-up:",
3968
+ message
3969
+ ].filter((line) => line !== void 0).join("\n");
3970
+ }
3971
+
3972
+ // src/shared/agent-stream-options.ts
3973
+ function agentStreamOptions(streamFn) {
3974
+ return { streamFunction: streamFn, streamFn };
3975
+ }
3976
+
3977
+ // src/shared/opencode-session-headers.ts
3978
+ var OPENCODE_HOST = "opencode.ai";
3979
+ function matchesOpenCodeHost(baseUrl) {
3980
+ try {
3981
+ return new URL(baseUrl).hostname === OPENCODE_HOST;
3982
+ } catch {
3983
+ return false;
3984
+ }
3985
+ }
3986
+ function opencodeSessionHeaders(model, sessionId) {
3987
+ if (!sessionId) return void 0;
3988
+ if (model.provider !== "opencode" && model.provider !== "opencode-go" && !matchesOpenCodeHost(model.baseUrl)) return void 0;
3989
+ return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
3990
+ }
3991
+
3992
+ export {
3993
+ recordSessionReplacement,
3994
+ inheritedSessionIds,
3995
+ ownerSessionIds,
3996
+ ownsRun,
3997
+ MODEL_ONLY_TOOL,
3998
+ isStaleExtensionContextError,
3999
+ canRenderComponents,
4000
+ isUnboundExtensionRuntimeError,
4001
+ withCachedUiContext,
4002
+ agentStreamOptions,
4003
+ opencodeSessionHeaders,
4004
+ formatActivityLabel,
4005
+ aggregateStepStatus,
4006
+ formatAgentRunningLabel,
4007
+ formatParallelOutcome,
4008
+ formatNestedAggregate,
4009
+ formatNestedRunStatusLines,
4010
+ RunFanoutLimitError,
4011
+ createRunFanoutBudget,
4012
+ writeRunFanoutBudgetDescriptor,
4013
+ readRunFanoutBudgetDescriptor,
4014
+ getRunFanoutBudgetSnapshot,
4015
+ claimRunFanoutBatch,
4016
+ claimRunFanoutBatchWithCommit,
4017
+ formatRunFanoutBudget,
4018
+ summarizeContextModes,
4019
+ contextModeLabel,
4020
+ contextModeBadge,
4021
+ contextModePrefix,
4022
+ resolveAsyncStatusChild,
4023
+ isStoppableAsyncStatusStep,
4024
+ stopStoppableAsyncStatusChildren,
4025
+ workflowChildProgress,
4026
+ workflowChildSummary,
4027
+ parseWorkflowChildSummary,
4028
+ normalizeWorkflowPreflight,
4029
+ workflowPreflightLaneForRuntimeKey,
4030
+ workflowPreflightWarnings,
4031
+ annotateWorkflowPreflightTrace,
4032
+ formatWorkflowPreflight,
4033
+ formatWorkflowPreflightSummary,
4034
+ formatWorkflowPreflightPlanSummary,
4035
+ formatWorkflowPreflightWarningSummary,
4036
+ formatWorkflowPreflightWarnings,
4037
+ workflowGraphStageNodes,
4038
+ buildWorkflowGraphSnapshot,
4039
+ ASYNC_STATUS_SNAPSHOT_KIND,
4040
+ ASYNC_STATUS_SNAPSHOT_VERSION,
4041
+ projectAsyncWorkflowRows,
4042
+ projectAsyncStatusSnapshot,
4043
+ snapshotTrackedMutations,
4044
+ collectTrackedMutationEvidence,
4045
+ projectTimeoutRecovery,
4046
+ formatTimeoutRecoveryLines,
4047
+ buildTimeoutRecoverySummary,
4048
+ projectWorkflowChecklist,
4049
+ formatWorkflowChecklistSummary,
4050
+ formatWorkflowChecklistPhase,
4051
+ formatWorkflowChecklistBottleneck,
4052
+ formatWorkflowChecklistText,
4053
+ listAsyncRuns,
4054
+ formatWorkflowStageLine,
4055
+ formatAsyncRunOutputPath,
4056
+ formatAsyncRunProgressLabel,
4057
+ formatAsyncRunList,
4058
+ decodeUtf8Tail,
4059
+ utf8Tail,
4060
+ withHandoffWriteLock,
4061
+ withRepositoryWorktreeLock,
4062
+ readParallelHandoffManifest,
4063
+ protectRetainedWorktreeForResume,
4064
+ resolveRetainedWorktreeCwd,
4065
+ isTerminalParallelHandoffChildStatus,
4066
+ formatStoredParallelHandoffCleanup,
4067
+ recordParallelHandoffMerge,
4068
+ recordParallelHandoffSupersession,
4069
+ writeParallelHandoffGroup,
4070
+ parallelHandoffPath,
4071
+ writeWorktreeSetupHandoff,
4072
+ formatParallelHandoffReference,
4073
+ formatParallelHandoffError,
4074
+ discardPreservedWorktrees,
4075
+ findAsyncRunPrefixMatches,
4076
+ resolveAsyncRunLocation,
4077
+ asyncReviveRequiresRecoveryDescriptor,
4078
+ readAsyncRecoveryDescriptor,
4079
+ resolveAsyncResumeTarget,
4080
+ applySteeringRecoveryAgentConfig,
4081
+ buildRevivedAsyncTask
4082
+ };