pi-subagents 0.58.0 → 0.60.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 (116) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +93 -5
  8. package/docs/workflows.md +94 -3
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +48 -243
  12. package/skills/council-mode/references/pass-contracts.md +150 -0
  13. package/skills/pi-subagents/SKILL.md +87 -37
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  15. package/skills/pi-subagents/references/execution-controls.md +52 -7
  16. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  18. package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
  19. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  20. package/src/agents/agent-management.ts +159 -42
  21. package/src/agents/agent-serializer.ts +4 -2
  22. package/src/agents/agents.ts +67 -26
  23. package/src/agents/runtime-agent-registry.ts +9 -16
  24. package/src/api/background-work.ts +5 -1
  25. package/src/api/delegation.ts +0 -7
  26. package/src/api/preflight.ts +6 -9
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +5 -3
  29. package/src/extension/index.ts +51 -23
  30. package/src/extension/public-execution.ts +16 -2
  31. package/src/extension/schemas.ts +36 -10
  32. package/src/extension/tool-description.ts +24 -5
  33. package/src/intercom/result-intercom.ts +2 -0
  34. package/src/profiles/profiles.ts +5 -6
  35. package/src/runs/background/active-async-capacity.ts +2 -2
  36. package/src/runs/background/async-execution.ts +52 -37
  37. package/src/runs/background/async-job-tracker.ts +17 -12
  38. package/src/runs/background/async-resume.ts +23 -25
  39. package/src/runs/background/async-status-snapshot.ts +23 -261
  40. package/src/runs/background/async-status.ts +110 -8
  41. package/src/runs/background/chain-append.ts +6 -3
  42. package/src/runs/background/chain-root-attachment.ts +60 -8
  43. package/src/runs/background/control-channel.ts +3 -2
  44. package/src/runs/background/fleet-view.ts +21 -11
  45. package/src/runs/background/notify.ts +158 -6
  46. package/src/runs/background/result-files.ts +2 -1
  47. package/src/runs/background/result-watcher.ts +2 -0
  48. package/src/runs/background/resume-guidance.ts +1 -1
  49. package/src/runs/background/retained-children.ts +1 -1
  50. package/src/runs/background/run-status.ts +30 -9
  51. package/src/runs/background/scheduled-runs.ts +86 -7
  52. package/src/runs/background/stale-run-reconciler.ts +10 -4
  53. package/src/runs/background/steering.ts +4 -14
  54. package/src/runs/background/subagent-runner.ts +351 -358
  55. package/src/runs/background/subagent-wait.ts +68 -12
  56. package/src/runs/background/terminal-run-index.ts +1 -1
  57. package/src/runs/background/wait-completions.ts +25 -1
  58. package/src/runs/background/wait-config.ts +23 -9
  59. package/src/runs/background/wait-tool.ts +9 -2
  60. package/src/runs/foreground/async-steering-action.ts +2 -2
  61. package/src/runs/foreground/execution.ts +125 -122
  62. package/src/runs/foreground/foreground-control.ts +3 -0
  63. package/src/runs/foreground/foreground-history.ts +1 -0
  64. package/src/runs/foreground/subagent-executor.ts +576 -240
  65. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  66. package/src/runs/shared/abort-recovery.ts +119 -0
  67. package/src/runs/shared/async-status-projection.ts +597 -0
  68. package/src/runs/shared/background-process-options.ts +9 -0
  69. package/src/runs/shared/child-identity.ts +19 -4
  70. package/src/runs/shared/child-launch-plan.ts +151 -0
  71. package/src/runs/shared/completion-evidence.ts +89 -0
  72. package/src/runs/shared/completion-guard.ts +1 -1
  73. package/src/runs/shared/dynamic-fanout.ts +2 -2
  74. package/src/runs/shared/host-step-status.ts +230 -0
  75. package/src/runs/shared/lane-metadata.ts +105 -0
  76. package/src/runs/shared/mcp-config-sources.ts +42 -6
  77. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  78. package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
  79. package/src/runs/shared/model-fallback.ts +9 -2
  80. package/src/runs/shared/mutation-evidence.ts +52 -3
  81. package/src/runs/shared/nested-events.ts +6 -2
  82. package/src/runs/shared/nested-render.ts +7 -3
  83. package/src/runs/shared/parallel-handoff.ts +419 -7
  84. package/src/runs/shared/parallel-utils.ts +7 -0
  85. package/src/runs/shared/pi-args.ts +67 -3
  86. package/src/runs/shared/single-output.ts +72 -22
  87. package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
  88. package/src/runs/shared/workflow-graph.ts +15 -0
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +5 -2
  94. package/src/shared/launch-contract.ts +1 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +232 -30
  97. package/src/shared/utils.ts +35 -55
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/tui/fleet-status.ts +134 -75
  104. package/src/tui/fleet.ts +11 -5
  105. package/src/tui/render-helpers.ts +31 -0
  106. package/src/tui/render.ts +895 -121
  107. package/src/watchdog/change-signature.ts +40 -1
  108. package/src/watchdog/turn-delta.ts +1 -1
  109. package/src/workflows/chat-progress.ts +6 -3
  110. package/src/workflows/host-command.ts +235 -0
  111. package/src/workflows/scripted-workflow.ts +503 -38
  112. package/src/workflows/workflow-child-summary.ts +9 -5
  113. package/src/workflows/workflow-preflight.ts +270 -0
  114. package/src/workflows/workflow-receipt.ts +43 -4
  115. package/src/workflows/workflow-settlement.ts +246 -0
  116. package/src/runs/shared/turn-budget.ts +0 -98
@@ -72,6 +72,14 @@ function uniqueStrings(values: Array<string | undefined>): string[] {
72
72
  return result;
73
73
  }
74
74
 
75
+ function fleetChildDisplayName(child: { agent?: string; sessionName?: string }, fallback = "subagent"): string {
76
+ return child.sessionName?.trim() || child.agent || fallback;
77
+ }
78
+
79
+ function fleetStepDisplayName(step: Pick<AsyncJobStep, "agent" | "sessionName" | "label">): string {
80
+ return step.sessionName?.trim() || (step.label ? `${step.label} (${step.agent})` : step.agent);
81
+ }
82
+
75
83
  function resolveMaybeRelative(asyncDir: string, filePath: string | undefined): string | undefined {
76
84
  if (!filePath) return undefined;
77
85
  return path.resolve(asyncDir, filePath);
@@ -283,7 +291,8 @@ function formatActivityFacts(input: {
283
291
  }
284
292
 
285
293
  function foregroundModeName(control: ForegroundControl): string {
286
- if (control.mode === "single" && control.currentAgent) return control.currentAgent;
294
+ const currentDisplayName = control.sessionName?.trim() || control.currentAgent;
295
+ if (control.mode === "single" && currentDisplayName) return currentDisplayName;
287
296
  return control.mode;
288
297
  }
289
298
 
@@ -302,7 +311,8 @@ function formatForegroundFleetLines(controls: ForegroundControl[]): string[] {
302
311
  toolCount: control.toolCount,
303
312
  ...(control.tokens !== undefined ? { tokens: { total: control.tokens } } : {}),
304
313
  });
305
- const current = control.currentAgent ? ` | ${control.currentAgent}${control.currentIndex !== undefined ? ` #${control.currentIndex}` : ""}` : "";
314
+ const currentDisplayName = control.sessionName?.trim() || control.currentAgent;
315
+ const current = currentDisplayName ? ` | ${currentDisplayName}${control.currentIndex !== undefined ? ` #${control.currentIndex}` : ""}` : "";
306
316
  lines.push(`- ${control.runId} | running | ${foregroundModeName(control)}${current}${activity ? ` | ${activity}` : ""}`);
307
317
  lines.push(` status: subagent({ action: "status", id: "${control.runId}" })`);
308
318
  lines.push(" transcript: live in the expanded foreground result; persisted session transcript appears after completion when sessions are enabled.");
@@ -317,7 +327,7 @@ function formatDetachedForegroundFleetLines(runs: ForegroundRun[]): string[] {
317
327
  const ordered = [...runs].sort((left, right) => right.updatedAt - left.updatedAt);
318
328
  for (const run of ordered) {
319
329
  const detachedChildren = run.children.filter((child) => child.status === "detached");
320
- const childSummary = detachedChildren.map((child) => `${child.agent} #${child.index}`).join(", ");
330
+ const childSummary = detachedChildren.map((child) => `${fleetChildDisplayName(child)} #${child.index}`).join(", ");
321
331
  lines.push(`- ${run.runId} | detached | ${run.mode}${childSummary ? ` | ${childSummary}` : ""}`);
322
332
  lines.push(` status: subagent({ action: "status", id: "${run.runId}" })`);
323
333
  lines.push(` recovery: reply to the supervisor request first, then wait with subagent_wait({ id: "${run.runId}" }); do not resume or launch a replacement while any child remains detached.`);
@@ -338,7 +348,7 @@ function formatAsyncFleetLines(runs: AsyncRunSummary[]): string[] {
338
348
  lines.push(` status: subagent({ action: "status", id: "${run.id}" })`);
339
349
  lines.push(` transcript: subagent({ action: "status", id: "${run.id}", view: "transcript" })`);
340
350
  for (const step of run.steps) {
341
- const display = step.label ? `${step.label} (${step.agent})` : step.agent;
351
+ const display = fleetStepDisplayName(step);
342
352
  const stepContext = contextModeLabel(step.context);
343
353
  const phase = step.phase ? `[${step.phase}] ` : "";
344
354
  const stepActivity = formatActivityFacts(step);
@@ -435,7 +445,7 @@ function selectTranscriptStep(status: AsyncStatus, options: TranscriptOptions):
435
445
  }
436
446
  const step = selectedIndex !== undefined ? steps[selectedIndex] : undefined;
437
447
  const hint = options.index === undefined && steps.length > 1
438
- ? `Tip: pass index to inspect a specific child transcript (${steps.map((candidate, index) => `${index}=${candidate.agent}`).join(", ")}).`
448
+ ? `Tip: pass index to inspect a specific child transcript (${steps.map((candidate, index) => `${index}=${fleetChildDisplayName(candidate)}`).join(", ")}).`
439
449
  : undefined;
440
450
  return { index: selectedIndex, step, hint };
441
451
  }
@@ -445,7 +455,7 @@ function stepStateLine(mode: SubagentRunMode, index: number | undefined, step: A
445
455
  const modelThinking = formatModelThinking(step.model, step.thinking);
446
456
  const context = contextModeLabel(step.context);
447
457
  const parts = [
448
- `${mode === "parallel" ? "Agent" : "Step"}: ${index} (${step.agent})${context ? ` ${context}` : ""}`,
458
+ `${mode === "parallel" ? "Agent" : "Step"}: ${index} (${fleetChildDisplayName(step)})${context ? ` ${context}` : ""}`,
449
459
  step.status,
450
460
  formatActivityFacts(step),
451
461
  modelThinking,
@@ -553,7 +563,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
553
563
  `Nested run: ${run.id}`,
554
564
  `State: ${run.state}`,
555
565
  run.mode ? `Mode: ${run.mode}` : undefined,
556
- run.agent ? `Agent: ${run.agent}` : run.agents?.length ? `Agents: ${run.agents.join(", ")}` : undefined,
566
+ run.sessionName?.trim() ? `Agent: ${run.sessionName.trim()}` : run.agent ? `Agent: ${run.agent}` : run.agents?.length ? `Agents: ${run.agents.join(", ")}` : undefined,
557
567
  ].filter((line): line is string => Boolean(line));
558
568
  appendKnownArtifacts(lines, { outputPaths: [], sessionFile: run.sessionFile });
559
569
  if (!run.sessionFile) {
@@ -579,14 +589,14 @@ export function formatAsyncResultTranscript(data: {
579
589
  sessionFile?: string;
580
590
  agent?: string;
581
591
  exitCode?: number | null;
582
- results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null }>;
592
+ results?: Array<{ agent?: string; sessionName?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null }>;
583
593
  }, resultPath: string, options: TranscriptOptions = {}): string {
584
594
  const lineLimit = transcriptLineLimit(options.lines);
585
595
  const runId = data.runId ?? data.id ?? path.basename(resultPath, ".json");
586
596
  const children = Array.isArray(data.results)
587
597
  ? data.results
588
598
  : data.agent
589
- ? [{ agent: data.agent, output: data.output, summary: data.summary, sessionFile: data.sessionFile, state: data.state, success: data.success, exitCode: data.exitCode }]
599
+ ? [{ agent: data.agent, sessionName: undefined, output: data.output, summary: data.summary, sessionFile: data.sessionFile, state: data.state, success: data.success, exitCode: data.exitCode }]
590
600
  : [];
591
601
  let index = options.index;
592
602
  if (index !== undefined && !Number.isInteger(index)) throw new Error("Transcript index must be an integer.");
@@ -601,8 +611,8 @@ export function formatAsyncResultTranscript(data: {
601
611
  const lines = [
602
612
  `Run: ${runId}`,
603
613
  `State: ${data.state ?? (data.success ? "complete" : "failed")}`,
604
- index !== undefined && child ? `Child: ${index} (${child.agent ?? "subagent"})` : undefined,
605
- index === undefined && children.length > 1 ? `Tip: pass index to inspect a specific child transcript (${children.map((candidate, childIndex) => `${childIndex}=${candidate.agent ?? "subagent"}`).join(", ")}).` : undefined,
614
+ index !== undefined && child ? `Child: ${index} (${fleetChildDisplayName(child)})` : undefined,
615
+ index === undefined && children.length > 1 ? `Tip: pass index to inspect a specific child transcript (${children.map((candidate, childIndex) => `${childIndex}=${fleetChildDisplayName(candidate)}`).join(", ")}).` : undefined,
606
616
  ].filter((line): line is string => Boolean(line));
607
617
  appendKnownArtifacts(lines, { outputPaths: [], sessionFile, resultPath });
608
618
  appendTranscriptBody(lines, "Result transcript tail", transcriptLines.filter((line) => line.trim()), output.split(/\r?\n/).length > lineLimit);
@@ -15,9 +15,22 @@ import {
15
15
  resolveCompletionBatchConfig,
16
16
  } from "./completion-batcher.ts";
17
17
  import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ParallelHandoffReference, type ScheduleOrigin, type SubagentState } from "../../shared/types.ts";
18
+ import { safeTerminalText } from "../../shared/display-text.ts";
19
+ import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
18
20
  import { isUnexplainedProcessSignal } from "../shared/process-signal.ts";
19
21
  import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
20
22
 
23
+ export interface SubagentNotifyChildOutput {
24
+ workflowKey?: string;
25
+ runId?: string;
26
+ agent?: string;
27
+ status: string;
28
+ savedOutputPath?: string;
29
+ preview: string;
30
+ previewTruncated?: boolean;
31
+ previewUnavailableReason?: string;
32
+ }
33
+
21
34
  export interface SubagentNotifyDetails {
22
35
  agent: string;
23
36
  status: "completed" | "failed" | "paused" | "stopped";
@@ -27,6 +40,7 @@ export interface SubagentNotifyDetails {
27
40
  durationMs?: number;
28
41
  workflowRunId?: string;
29
42
  childRuns?: Array<{ runId: string; workflowKey?: string; agent?: string; status?: string }>;
43
+ childOutputs?: SubagentNotifyChildOutput[];
30
44
  reconciledFromDetachedChild?: string;
31
45
  sessionLabel?: string;
32
46
  sessionValue?: string;
@@ -57,7 +71,14 @@ export interface CompletionNotification {
57
71
  workflowKey?: string;
58
72
  agent?: string;
59
73
  status?: string;
74
+ state?: string;
60
75
  success?: boolean;
76
+ output?: string;
77
+ structuredOutput?: unknown;
78
+ outputState?: "present" | "absent" | "unknown";
79
+ outputReference?: string | { path?: string };
80
+ artifactPaths?: { outputPath?: string };
81
+ detached?: boolean;
61
82
  exitCode?: number | null;
62
83
  processSignal?: string | null;
63
84
  interrupted?: boolean;
@@ -100,6 +121,115 @@ export interface CompletionNotifier {
100
121
  dispose(): void;
101
122
  }
102
123
 
124
+ const CHILD_OUTPUT_PREVIEW_MAX_BYTES = 4 * 1024;
125
+ const CHILD_OUTPUT_PREVIEW_COUNT = 8;
126
+ const PREVIEW_TRUNCATION_MARKER = "...[preview truncated]";
127
+ type CompletionChild = NonNullable<CompletionNotification["results"]>[number];
128
+
129
+ function truncateUtf8Head(value: string, maxBytes: number): { text: string; truncated: boolean } {
130
+ const bytes = Buffer.from(value, "utf8");
131
+ if (bytes.length <= maxBytes) return { text: value, truncated: false };
132
+ const markerBytes = Buffer.byteLength(PREVIEW_TRUNCATION_MARKER, "utf8");
133
+ const contentBytes = Math.max(0, maxBytes - markerBytes);
134
+ let end = contentBytes;
135
+ while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
136
+ return {
137
+ text: `${bytes.subarray(0, end).toString("utf8")}${PREVIEW_TRUNCATION_MARKER}`,
138
+ truncated: true,
139
+ };
140
+ }
141
+
142
+ function boundedSafeText(value: string, maxBytes = 1_024): string {
143
+ return truncateUtf8Head(safeTerminalText(value).replace(/\n/g, "\\n"), maxBytes).text;
144
+ }
145
+
146
+ function outputPathFromReference(value: CompletionChild["outputReference"]): string | undefined {
147
+ if (typeof value === "string" && value.trim()) return value.trim();
148
+ if (value && typeof value === "object" && typeof value.path === "string" && value.path.trim()) return value.path.trim();
149
+ return undefined;
150
+ }
151
+
152
+ function childSavedOutputPath(child: CompletionChild): string | undefined {
153
+ return outputPathFromReference(child.outputReference);
154
+ }
155
+
156
+ function childStatus(child: CompletionChild, workflowState?: string): string {
157
+ const knownStatus = child.status === "complete"
158
+ ? "completed"
159
+ : child.status === "completed" || child.status === "failed" || child.status === "paused" || child.status === "stopped" || child.status === "detached"
160
+ ? child.status
161
+ : undefined;
162
+ if (knownStatus) return knownStatus;
163
+ return resolveSubagentResultStatus({
164
+ success: child.success,
165
+ state: child.state ?? (child.success === undefined ? workflowState : undefined),
166
+ interrupted: child.interrupted,
167
+ detached: child.detached,
168
+ processSignal: child.processSignal,
169
+ timedOut: child.timedOut,
170
+ stopped: child.stopped,
171
+ turnBudgetExceeded: child.turnBudgetExceeded,
172
+ exitCode: typeof child.exitCode === "number" ? child.exitCode : undefined,
173
+ });
174
+ }
175
+
176
+ function structuredOutputText(value: unknown): string | undefined {
177
+ if (value === undefined) return undefined;
178
+ try {
179
+ const serialized = JSON.stringify(value, null, 2);
180
+ return typeof serialized === "string" ? serialized : undefined;
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ }
185
+
186
+ function childInlinePreview(child: CompletionChild): { preview?: string; truncated?: boolean; unavailableReason?: string } {
187
+ const output = typeof child.output === "string" ? child.output : "";
188
+ const outputReference = childSavedOutputPath(child);
189
+ const referenceOnly = Boolean(outputReference && output.trim().startsWith("Output saved to:"));
190
+ const raw = child.outputState === "absent" || referenceOnly ? structuredOutputText(child.structuredOutput) : output || structuredOutputText(child.structuredOutput);
191
+ if (!raw?.trim()) {
192
+ return { unavailableReason: referenceOnly ? "saved output is file-only" : "no safe inline output" };
193
+ }
194
+ const safe = safeTerminalText(raw);
195
+ if (!safe.trim()) return { unavailableReason: "no safe inline output" };
196
+ const bounded = truncateUtf8Head(safe, CHILD_OUTPUT_PREVIEW_MAX_BYTES);
197
+ return { preview: bounded.text, ...(bounded.truncated ? { truncated: true } : {}) };
198
+ }
199
+
200
+ function formatChildOutputBlock(children: SubagentNotifyChildOutput[] | undefined): string | undefined {
201
+ if (!children?.length) return undefined;
202
+ const lines = ["Child outputs:"];
203
+ for (let index = 0; index < children.length; index++) {
204
+ const child = children[index]!;
205
+ const key = child.workflowKey ? boundedSafeText(child.workflowKey) : "unavailable";
206
+ const runId = child.runId ? boundedSafeText(child.runId) : "unavailable";
207
+ const status = boundedSafeText(child.status) || "unavailable";
208
+ lines.push(`- key=${key} run=${runId} status=${status}`);
209
+ lines.push(` Saved output: ${child.savedOutputPath ? boundedSafeText(child.savedOutputPath) : "unavailable"}`);
210
+ if (index >= CHILD_OUTPUT_PREVIEW_COUNT) {
211
+ lines.push(" Preview: unavailable (notice preview budget exceeded)");
212
+ continue;
213
+ }
214
+ if (!child.preview) {
215
+ lines.push(` Preview: unavailable (${child.previewUnavailableReason ?? "no safe inline output"})`);
216
+ continue;
217
+ }
218
+ lines.push(" Preview:");
219
+ for (const line of child.preview.split("\n")) lines.push(` | ${line}`);
220
+ }
221
+ if (children.length > CHILD_OUTPUT_PREVIEW_COUNT) {
222
+ lines.push(`- ${children.length - CHILD_OUTPUT_PREVIEW_COUNT} additional child preview(s) omitted by notice budget; child run metadata is retained below.`);
223
+ }
224
+ return lines.join("\n");
225
+ }
226
+
227
+ function formatResultPreview(details: SubagentNotifyDetails): string {
228
+ const summary = details.resultPreview.trim() ? details.resultPreview : "(no output)";
229
+ const childOutputs = formatChildOutputBlock(details.childOutputs);
230
+ return childOutputs ? `${summary}\n\n${childOutputs}` : summary;
231
+ }
232
+
103
233
  function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
104
234
  if (!details.sessionValue) return undefined;
105
235
  return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;
@@ -131,7 +261,7 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
131
261
  "",
132
262
  scheduleLine,
133
263
  scheduleLine ? "" : undefined,
134
- details.resultPreview.trim() ? details.resultPreview : "(no output)",
264
+ formatResultPreview(details),
135
265
  details.handoffPath ? "" : undefined,
136
266
  details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,
137
267
  correlationLines.length && !details.handoffPath ? "" : undefined,
@@ -218,7 +348,7 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
218
348
  if (!detail) continue;
219
349
  const sessionLine = formatSessionLine(detail);
220
350
  blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ""}${detail.scheduleOrigin ? ` — scheduled run from ${detail.scheduleOrigin.name ?? detail.scheduleOrigin.id} (schedule ${detail.scheduleOrigin.id})` : ""}`);
221
- blocks.push(detail.resultPreview.trim() ? detail.resultPreview : "(no output)");
351
+ blocks.push(formatResultPreview(detail));
222
352
  if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);
223
353
  blocks.push(...formatCorrelationLines(detail));
224
354
  if (sessionLine) blocks.push(sessionLine);
@@ -275,6 +405,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
275
405
  const paused = !stopped && !result.success && (
276
406
  result.exitCode === 0
277
407
  || result.state === "paused"
408
+ || result.interrupted === true
278
409
  || summary.startsWith("Paused after interrupt.")
279
410
  );
280
411
  const status = stopped ? "stopped" : paused ? "paused" : result.success ? "completed" : "failed";
@@ -290,14 +421,34 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
290
421
  const rawRunId = typeof result.runId === "string" ? result.runId : typeof result.id === "string" ? result.id : undefined;
291
422
  const workflowRunId = (result.mode === "workflow" || agent === "workflow") && rawRunId ? rawRunId : undefined;
292
423
  const childRuns = result.results?.flatMap((child) => {
293
- if (typeof child.runId !== "string" || !child.runId.trim()) return [];
424
+ const runId = typeof child.runId === "string" && child.runId.trim() ? child.runId.trim() : undefined;
425
+ const workflowKey = typeof child.workflowKey === "string" && child.workflowKey.trim() ? child.workflowKey.trim() : undefined;
426
+ if (!runId && (!workflowRunId || !workflowKey)) return [];
294
427
  return [{
295
- runId: child.runId,
296
- ...(typeof child.workflowKey === "string" ? { workflowKey: child.workflowKey } : {}),
428
+ runId: runId ?? "unavailable",
429
+ ...(workflowKey ? { workflowKey } : {}),
297
430
  ...(typeof child.agent === "string" ? { agent: child.agent } : {}),
298
- ...(typeof child.status === "string" ? { status: child.status } : {}),
431
+ status: childStatus(child, workflowRunId ? result.state : undefined),
299
432
  }];
300
433
  }) ?? [];
434
+ const childOutputs = workflowRunId && result.results?.length
435
+ ? result.results.map((child) => {
436
+ const inline = childInlinePreview(child);
437
+ const workflowKey = typeof child.workflowKey === "string" && child.workflowKey.trim() ? child.workflowKey.trim() : undefined;
438
+ const runId = typeof child.runId === "string" && child.runId.trim() ? child.runId.trim() : undefined;
439
+ const savedOutputPath = childSavedOutputPath(child);
440
+ return {
441
+ ...(workflowKey ? { workflowKey } : {}),
442
+ ...(runId ? { runId } : {}),
443
+ ...(typeof child.agent === "string" ? { agent: child.agent } : {}),
444
+ status: childStatus(child, result.state),
445
+ ...(savedOutputPath ? { savedOutputPath } : {}),
446
+ ...(inline.preview ? { preview: inline.preview } : { preview: "" }),
447
+ ...(inline.truncated ? { previewTruncated: true } : {}),
448
+ ...(inline.unavailableReason ? { previewUnavailableReason: inline.unavailableReason } : {}),
449
+ };
450
+ })
451
+ : undefined;
301
452
  const reconciledFromDetachedChild = typeof result.reconciledFromDetachedChild === "string" ? result.reconciledFromDetachedChild : undefined;
302
453
  const session =
303
454
  result.shareUrl
@@ -322,6 +473,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
322
473
  ...(handoffPath ? { handoffPath } : {}),
323
474
  ...(workflowRunId ? { workflowRunId } : {}),
324
475
  ...(childRuns.length ? { childRuns } : {}),
476
+ ...(childOutputs?.length ? { childOutputs } : {}),
325
477
  ...(reconciledFromDetachedChild ? { reconciledFromDetachedChild } : {}),
326
478
  ...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
327
479
  };
@@ -343,7 +343,8 @@ export function resultPayloadPathForMissionObserverRun(resultsDir: string, runId
343
343
  if (!entry || entry.runId !== runId) return undefined;
344
344
  return resultPayloadLocationFromIndex(resultsDir, entry)?.path;
345
345
  } catch (error) {
346
- if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
346
+ const code = (error as NodeJS.ErrnoException).code;
347
+ if (code !== "ENOENT" && code !== "ENOTDIR" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
347
348
  return undefined;
348
349
  }
349
350
  }
@@ -67,6 +67,7 @@ type ResultWatcherDeps = {
67
67
 
68
68
  type ResultFileChild = {
69
69
  agent?: string;
70
+ sessionName?: string;
70
71
  output?: string;
71
72
  structuredOutput?: unknown;
72
73
  outputState?: SubagentOutputState;
@@ -482,6 +483,7 @@ export function createResultWatcher(
482
483
  : undefined;
483
484
  return {
484
485
  agent: result.agent ?? data.agent ?? `step-${index + 1}`,
486
+ ...(result.sessionName ? { sessionName: result.sessionName } : {}),
485
487
  status: resolveSubagentResultStatus({
486
488
  success: result.success,
487
489
  state: childState,
@@ -39,7 +39,7 @@ export function formatResumeFirstFailedRunDetail(run: AsyncRunSummary): string |
39
39
  }
40
40
 
41
41
  export function formatResumeFirstFailedRunsNote(runs: AsyncRunSummary[]): string {
42
- const failedRuns = runs.filter((run) => run.state === "failed");
42
+ const failedRuns = runs.filter((run) => run.state === "failed" || run.state === "partial");
43
43
  const detachGuidance = failedRuns
44
44
  .map(formatIntercomDetachGuidance)
45
45
  .filter((guidance): guidance is string => Boolean(guidance));
@@ -27,7 +27,7 @@ export interface RetainedChild {
27
27
  }
28
28
 
29
29
  function isRetainedChildState(state: AsyncRunSummary["state"]): state is RetainedChildState {
30
- return state === "complete" || state === "failed" || state === "paused" || state === "stopped";
30
+ return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped";
31
31
  }
32
32
 
33
33
  function isTerminalStepStatus(status: AsyncRunSummary["steps"][number]["status"]): boolean {
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import { safeTerminalText } from "../../shared/display-text.ts";
5
- import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
5
+ import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, formatWorkflowStageLine, listAsyncRuns } from "./async-status.ts";
6
6
  import { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
7
7
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
8
8
  import { formatModelThinking } from "../../shared/formatters.ts";
@@ -23,8 +23,11 @@ import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegis
23
23
  import { readMissionBinding } from "../../missions/lifecycle.ts";
24
24
  import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts";
25
25
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
26
+ import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
26
27
  import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
28
+ import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
27
29
  import { getExternalJobProvider } from "../../api/external-job-provider.ts";
30
+ import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
28
31
 
29
32
  interface RunStatusParams {
30
33
  action?: string;
@@ -64,13 +67,14 @@ function formatCapacityOwner(inspect: ActiveAsyncCapacityInspection): string[] {
64
67
  }
65
68
 
66
69
  function formatWorkflowDebug(status: AsyncStatus): string[] {
67
- if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey) return [];
70
+ if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey && !status.lane) return [];
68
71
  const lines = [
69
72
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
70
73
  status.mode === "workflow" ? `Workflow children: ${(status.steps ?? []).length}` : undefined,
74
+ status.lane ? `Lane: ${status.lane.key}${status.lane.mode ? ` (${status.lane.mode})` : ""}` : undefined,
71
75
  ].filter((line): line is string => line !== undefined);
72
76
  for (const [index, step] of (status.steps ?? []).entries()) {
73
- lines.push(` ${index + 1}. key ${step.workflowKey ?? "n/a"} · ${step.agent} · ${step.status} · async ${step.async === undefined ? "unknown" : step.async ? "yes" : "no"}${step.runId ? ` · run ${step.runId}` : ""}`);
77
+ lines.push(` ${index + 1}. key ${step.workflowKey ?? "n/a"} · ${runStatusStepDisplayName(step)} · ${step.status} · async ${step.async === undefined ? "unknown" : step.async ? "yes" : "no"}${step.runId ? ` · run ${step.runId}` : ""}${step.lane ? ` · lane ${step.lane.key}` : ""}${step.worktreePath ? ` · worktree ${step.worktreePath} · branch ${step.branch ?? "unknown"}` : ""}`);
74
78
  }
75
79
  return lines;
76
80
  }
@@ -88,6 +92,7 @@ function formatRunLifecycleDebug(input: { status: AsyncStatus; asyncDir: string;
88
92
  `Mode: ${status.mode}`,
89
93
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}` : undefined,
90
94
  status.workflowKey ? `Workflow key: ${status.workflowKey}` : undefined,
95
+ status.lane ? `Lane: ${status.lane.key}${status.lane.mode ? ` (${status.lane.mode})` : ""}` : undefined,
91
96
  `Status process terminal: ${formatProcessTerminal(overlayProcessTerminal)}`,
92
97
  `Sidecar process terminal: ${formatProcessTerminal(sidecarProcessTerminal)}`,
93
98
  ...formatCapacityOwner(capacity),
@@ -152,7 +157,12 @@ function stepLineLabel(status: AsyncStatus, index: number): string {
152
157
  return `Step ${index + 1}`;
153
158
  }
154
159
 
160
+ function runStatusStepDisplayName(step: { agent: string; sessionName?: string; label?: string }): string {
161
+ return step.sessionName?.trim() || (step.label ? `${step.label} (${step.agent})` : step.agent);
162
+ }
163
+
155
164
  function nestedRunDisplayName(run: NestedRunSummary): string {
165
+ if (run.sessionName?.trim()) return run.sessionName.trim();
156
166
  if (run.agent) return run.agent;
157
167
  if (run.agents?.length) return run.agents.join(", ");
158
168
  return run.id;
@@ -197,7 +207,7 @@ function formatRememberedForegroundStatus(run: ForegroundResumeRun): string {
197
207
  for (const child of run.children) {
198
208
  const output = rememberedForegroundChildOutput(child).trim().split(/\r?\n/).find((line) => line.trim());
199
209
  const parts = [
200
- `${child.index + 1}. ${child.agent} ${child.status}`,
210
+ `${child.index + 1}. ${child.sessionName?.trim() || child.agent} ${child.status}`,
201
211
  child.exitCode !== undefined ? `exit ${child.exitCode}` : undefined,
202
212
  child.detachedReason ? `detached: ${child.detachedReason}` : undefined,
203
213
  child.acceptance ? `acceptance: ${child.acceptance.status}` : undefined,
@@ -241,7 +251,7 @@ function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options:
241
251
  const lines = [
242
252
  `Run: ${run.runId}`,
243
253
  `State: ${child.status}`,
244
- `Child: ${index} (${child.agent})`,
254
+ `Child: ${index} (${child.sessionName?.trim() || child.agent})`,
245
255
  child.sessionFile ? `Session: ${child.sessionFile}` : undefined,
246
256
  child.transcriptPath ? `Transcript: ${child.transcriptPath}` : undefined,
247
257
  child.artifactPaths?.outputPath ? `Output: ${child.artifactPaths.outputPath}` : undefined,
@@ -277,7 +287,7 @@ function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): stri
277
287
  for (const [index, step] of run.steps.entries()) {
278
288
  const activity = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
279
289
  const budget = step.turnBudget ? `, turn budget: ${step.turnBudget.turnCount}/${step.turnBudget.maxTurns}+${step.turnBudget.graceTurns} (${step.turnBudget.outcome})` : "";
280
- lines.push(` ${index + 1}. ${step.agent} ${step.status}${activity ? `, ${activity}` : ""}${budget}${step.error ? `, error: ${step.error}` : ""}`);
290
+ lines.push(` ${index + 1}. ${step.sessionName?.trim() || step.agent} ${step.status}${activity ? `, ${activity}` : ""}${budget}${step.error ? `, error: ${step.error}` : ""}`);
281
291
  lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true }));
282
292
  }
283
293
  }
@@ -473,6 +483,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
473
483
  currentStep: status.currentStep,
474
484
  chainStepCount: status.chainStepCount,
475
485
  parallelGroups: status.parallelGroups,
486
+ workflowGraph: status.workflowGraph,
476
487
  steps: (status.steps ?? []).map((step, index) => ({ index, agent: step.agent, status: step.status })),
477
488
  });
478
489
  const started = new Date(status.startedAt).toISOString();
@@ -504,6 +515,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
504
515
  statusActivityText ? `Activity: ${statusActivityText}` : undefined,
505
516
  steeringText ? `Steering: ${steeringText}` : undefined,
506
517
  `Mode: ${status.mode}`,
518
+ ...(status.preflight ? [formatWorkflowPreflightPlanSummary(status.preflight)] : []),
519
+ ...(status.workflow?.preflightWarnings?.length ? [formatWorkflowPreflightWarningSummary(status.workflow.preflightWarnings)] : []),
507
520
  runFanoutBudget ? formatRunFanoutBudget(runFanoutBudget) : undefined,
508
521
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
509
522
  status.mode === "workflow" && workflowReturnPreview !== undefined ? `Return: ${workflowReturnPreview}` : undefined,
@@ -535,9 +548,10 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
535
548
  const errorText = step.error ? `, error: ${step.error}` : "";
536
549
  const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}` : "";
537
550
  const budgetText = step.turnBudget ? `, turn budget: ${step.turnBudget.turnCount}/${step.turnBudget.maxTurns}+${step.turnBudget.graceTurns} (${step.turnBudget.outcome})` : "";
538
- const display = step.label ? `${step.label} (${step.agent})` : step.agent;
551
+ const display = runStatusStepDisplayName(step);
539
552
  const phase = step.phase ? `[${step.phase}] ` : "";
540
553
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
554
+ lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));
541
555
  if (step.runner?.type === "external-cli") {
542
556
  const runner = normalizeExternalCliRunnerStatus(step.runner);
543
557
  if (runner) {
@@ -583,6 +597,11 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
583
597
  lines.push(" Steer: unavailable; external runners do not accept live messages.");
584
598
  }
585
599
  }
600
+ const loadedWorkflowKeys = new Set((status.steps ?? []).flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
601
+ const graphStages = status.mode === "workflow" ? workflowGraphStageNodes(status.workflowGraph) : [];
602
+ for (const [index, node] of graphStages.entries()) {
603
+ if (!loadedWorkflowKeys.has(node.id)) lines.push(` ${formatWorkflowStageLine(node, index, graphStages.length)}`);
604
+ }
586
605
  const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
587
606
  const unattached = nestedChildren.filter((child) => !attached.has(child.id));
588
607
  lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
@@ -608,7 +627,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
608
627
 
609
628
  const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
610
629
  if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
611
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
630
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(status.preflight ? { preflight: status.preflight } : {}), ...(status.workflow?.preflightWarnings?.length ? { preflightWarnings: status.workflow.preflightWarnings } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
612
631
  }
613
632
  }
614
633
 
@@ -622,7 +641,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
622
641
  }
623
642
  try {
624
643
  const raw = fs.readFileSync(resultPath, "utf-8");
625
- const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
644
+ const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; timeoutRecovery?: unknown; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; sessionName?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null; timeoutRecovery?: unknown }> };
626
645
  if (params.view === "transcript") {
627
646
  try {
628
647
  return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
@@ -650,6 +669,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
650
669
  const lines = [`Run: ${runId}`, data.toolCallId ? `Tool call: ${data.toolCallId}` : undefined, `State: ${status}`, `Result: ${resultPath}`].filter((line): line is string => Boolean(line));
651
670
  if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
652
671
  const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
672
+ lines.push(...formatTimeoutRecoveryLines(data.timeoutRecovery, " "));
673
+ for (const child of children) lines.push(...formatTimeoutRecoveryLines(child.timeoutRecovery, " "));
653
674
  lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
654
675
  if (data.summary) lines.push("", data.summary);
655
676
  const workflowChildren = parseWorkflowChildSummary((data as unknown as Record<string, unknown>).workflowChildren);