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
@@ -1,8 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
+ import { previewDisplayText } from "../../shared/display-text.ts";
4
5
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState } from "../../shared/types.ts";
6
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNodeV1, type HostStepState, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TimeoutRecoveryProjection, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorkflowPreflightV1, type WorkflowGraphSnapshot } from "../../shared/types.ts";
6
7
  import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
7
8
  import { readStatus } from "../../shared/utils.ts";
8
9
  import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
@@ -17,16 +18,27 @@ import { readRecentTerminalRunIndex, TERMINAL_RUN_INDEX_DIR } from "./terminal-r
17
18
  import { canScanAsyncRunPrefix } from "./run-id-query.ts";
18
19
  import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
19
20
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
21
+ import { assertWorkflowGraphHostSteps, hostStepReportName, hostStepVerdictLabel, validHostStepNodes } from "../shared/host-step-status.ts";
22
+ import { projectAsyncWorkflowRows } from "../shared/async-status-projection.ts";
23
+ import { validateAsyncStatusLaneMetadata } from "../shared/lane-metadata.ts";
24
+ import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
25
+ import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
26
+ import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
20
27
 
21
28
  interface AsyncRunStepSummary {
22
29
  index: number;
23
30
  childId?: string;
24
31
  agent: string;
32
+ /** Human-readable display name for the child session, when derived at launch. */
33
+ sessionName?: string;
25
34
  context?: ContextMode;
26
35
  label?: string;
27
36
  description?: string;
28
37
  phase?: string;
29
38
  workflowKey?: string;
39
+ lane?: AsyncJobStep["lane"];
40
+ worktreePath?: string;
41
+ branch?: string;
30
42
  runId?: string;
31
43
  outputName?: string;
32
44
  structured?: boolean;
@@ -58,6 +70,7 @@ interface AsyncRunStepSummary {
58
70
  stopped?: boolean;
59
71
  turnBudget?: TurnBudgetState;
60
72
  turnBudgetExceeded?: boolean;
73
+ toolBudgetBlocked?: boolean;
61
74
  wrapUpRequested?: boolean;
62
75
  acceptance?: AsyncJobStep["acceptance"];
63
76
  agentContract?: AsyncJobStep["agentContract"];
@@ -65,6 +78,7 @@ interface AsyncRunStepSummary {
65
78
  review?: AsyncJobStep["review"];
66
79
  effects?: AsyncJobStep["effects"];
67
80
  processTerminal?: AsyncJobStep["processTerminal"];
81
+ timeoutRecovery?: TimeoutRecoveryProjection;
68
82
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
69
83
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
70
84
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
@@ -77,7 +91,7 @@ export interface AsyncRunSummary {
77
91
  asyncDir: string;
78
92
  toolCallId?: string;
79
93
  sessionId?: string;
80
- state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
94
+ state: "queued" | "running" | "complete" | "failed" | "partial" | "paused" | "stopped" | "rejected";
81
95
  error?: string;
82
96
  activityState?: ActivityState;
83
97
  lastActivityAt?: number;
@@ -105,6 +119,8 @@ export interface AsyncRunSummary {
105
119
  chainStepCount?: number;
106
120
  pendingAppends?: number;
107
121
  parallelGroups?: AsyncParallelGroupStatus[];
122
+ hostSteps?: HostStepNodeV1[];
123
+ workflowGraph?: AsyncStatus["workflowGraph"];
108
124
  steps: AsyncRunStepSummary[];
109
125
  sessionDir?: string;
110
126
  outputFile?: string;
@@ -122,8 +138,10 @@ export interface AsyncRunSummary {
122
138
  capabilityAudit?: SubagentCapabilityAudit;
123
139
  parentWorkflowRunId?: string;
124
140
  workflowKey?: string;
141
+ lane?: AsyncStatus["lane"];
125
142
  workflow?: Details["workflow"];
126
143
  workflowChildren?: Details["workflowChildren"];
144
+ preflight?: WorkflowPreflightV1;
127
145
  }
128
146
 
129
147
  interface AsyncRunListOptions {
@@ -232,21 +250,32 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
232
250
  }
233
251
 
234
252
  function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = [], nestedRoute?: NestedRoute): AsyncRunSummary {
253
+ validateAsyncStatusLaneMetadata(status, `Invalid async status '${path.join(asyncDir, "status.json")}'`);
235
254
  const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
236
255
  if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': workflowChildren.workflowRunId does not match.`);
256
+ assertWorkflowGraphHostSteps(status.workflowGraph, path.join(asyncDir, "status.json"), status.runId);
257
+ const hostSteps = validHostStepNodes(status.workflowGraph);
237
258
  if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
238
259
  throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
239
260
  }
240
261
  const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
241
262
  const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
242
263
  ?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
243
- const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
244
- const runFanoutBudget = runFanoutBudgetDescriptor ? getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor) : status.runFanoutBudget;
264
+ const nestedProjectionAllowed = nestedWarnings.length === 0;
265
+ // Degrade to the status-stored snapshot when the persisted budget is unavailable (e.g. removed
266
+ // by OS temp cleanup) instead of failing the whole run list; admission paths stay strict.
267
+ let runFanoutBudget: AsyncStatus["runFanoutBudget"] = status.runFanoutBudget;
268
+ try {
269
+ const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
270
+ if (runFanoutBudgetDescriptor) runFanoutBudget = getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor);
271
+ } catch (error) {
272
+ nestedWarnings.push(`Run fan-out status unavailable: ${getErrorMessage(error)}`);
273
+ }
245
274
  const steps = status.steps ?? [];
246
275
  const chainStepCount = status.chainStepCount ?? steps.length;
247
276
  const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
248
277
  let nestedChildren: NestedRunSummary[] = [];
249
- if (nestedWarnings.length === 0 && nestedRoute) {
278
+ if (nestedProjectionAllowed && nestedRoute) {
250
279
  try {
251
280
  // The route is resolved by the caller via buildNestedRouteIndex, so this
252
281
  // avoids a fresh scan of the nested-events directory per run.
@@ -258,15 +287,20 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
258
287
  const summarizedSteps = steps.map((step, index) => {
259
288
  const stepActivityState = step.activityState;
260
289
  const stepLastActivityAt = step.lastActivityAt;
290
+ const timeoutRecovery = projectTimeoutRecovery(step.timeoutRecovery);
261
291
  return {
262
292
  index,
263
293
  childId: asyncStatusChildIdentity(step, index),
264
294
  agent: step.agent,
295
+ ...(step.sessionName ? { sessionName: step.sessionName } : {}),
265
296
  ...(step.context ? { context: step.context } : {}),
266
297
  ...(step.label ? { label: step.label } : {}),
267
298
  ...(step.description ? { description: step.description } : {}),
268
299
  ...(step.phase ? { phase: step.phase } : {}),
269
300
  ...(step.workflowKey ? { workflowKey: step.workflowKey } : {}),
301
+ ...(step.lane ? { lane: step.lane } : {}),
302
+ ...(step.worktreePath ? { worktreePath: step.worktreePath } : {}),
303
+ ...(step.branch ? { branch: step.branch } : {}),
270
304
  ...(step.runId ? { runId: step.runId } : {}),
271
305
  ...(step.outputName ? { outputName: step.outputName } : {}),
272
306
  ...(step.structured ? { structured: step.structured } : {}),
@@ -301,6 +335,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
301
335
  ...(step.stopRequestedAt !== undefined ? { stopRequestedAt: step.stopRequestedAt } : {}),
302
336
  ...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
303
337
  ...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
338
+ ...(step.toolBudgetBlocked !== undefined ? { toolBudgetBlocked: step.toolBudgetBlocked } : {}),
304
339
  ...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
305
340
  ...(step.acceptance ? { acceptance: step.acceptance } : {}),
306
341
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
@@ -310,7 +345,9 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
310
345
  ...(step.execution ? { execution: step.execution } : {}),
311
346
  ...(step.review ? { review: step.review } : {}),
312
347
  ...(step.effects ? { effects: step.effects } : {}),
348
+ ...(step.watchdog ? { watchdog: step.watchdog } : {}),
313
349
  ...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
350
+ ...(timeoutRecovery ? { timeoutRecovery } : {}),
314
351
  ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
315
352
  ...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
316
353
  ...(step.children?.length ? { children: step.children } : {}),
@@ -350,6 +387,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
350
387
  ...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
351
388
  ...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
352
389
  ...(parallelGroups.length ? { parallelGroups } : {}),
390
+ ...(hostSteps.length ? { hostSteps } : {}),
391
+ ...(status.mode === "workflow" && status.workflowGraph ? { workflowGraph: status.workflowGraph } : {}),
353
392
  steps: summarizedSteps,
354
393
  ...(nestedChildren.length ? { nestedChildren } : {}),
355
394
  ...(nestedWarnings.length ? { nestedWarnings } : {}),
@@ -362,8 +401,10 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
362
401
  ...(status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {}),
363
402
  ...(status.parentWorkflowRunId ? { parentWorkflowRunId: status.parentWorkflowRunId } : {}),
364
403
  ...(status.workflowKey ? { workflowKey: status.workflowKey } : {}),
404
+ ...(status.lane ? { lane: status.lane } : {}),
365
405
  ...(status.workflow ? { workflow: status.workflow } : {}),
366
406
  ...(workflowChildren ? { workflowChildren } : {}),
407
+ ...(status.preflight ? { preflight: status.preflight } : {}),
367
408
  ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
368
409
  ...(status.outputFile ? { outputFile: status.outputFile } : {}),
369
410
  ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
@@ -383,6 +424,7 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
383
424
  case "running": return 0;
384
425
  case "queued": return 1;
385
426
  case "failed": return 2;
427
+ case "partial": return 2;
386
428
  case "stopped": return 2;
387
429
  case "paused": return 2;
388
430
  case "complete": return 3;
@@ -511,7 +553,7 @@ function formatActivityFacts(input: { activityState?: ActivityState; lastActivit
511
553
  }
512
554
 
513
555
  function formatStepLine(step: AsyncRunStepSummary): string {
514
- const display = step.label ? `${step.label} (${step.agent})` : step.agent;
556
+ const display = step.sessionName?.trim() || (step.label ? `${step.label} (${step.agent})` : step.agent);
515
557
  const context = contextModeLabel(step.context);
516
558
  const phase = step.phase ? `[${step.phase}] ` : "";
517
559
  const parts = [`${step.index + 1}. ${phase}${display}${context ? ` ${context}` : ""}`, step.status];
@@ -521,15 +563,61 @@ function formatStepLine(step: AsyncRunStepSummary): string {
521
563
  if (modelThinking) parts.push(modelThinking);
522
564
  if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
523
565
  if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
566
+ if (step.lane) parts.push(`lane ${step.lane.key}`);
567
+ if (step.worktreePath) parts.push(`worktree ${shortenPath(step.worktreePath)} · branch ${step.branch ?? "unknown"}`);
524
568
  return parts.join(" | ");
525
569
  }
526
570
 
571
+ function formatHostStepLine(row: ReturnType<typeof projectAsyncWorkflowRows>[number]): string {
572
+ if (!row.kind) return "";
573
+ const state = hostStepVerdictLabel(row.state as HostStepState, row.verdict);
574
+ const details = [
575
+ row.provider ? `provider:${row.provider}` : undefined,
576
+ row.role ? `role:${row.role}` : undefined,
577
+ row.target,
578
+ row.detail,
579
+ row.reasonCode ? `reason:${row.reasonCode}` : undefined,
580
+ row.freshness?.stale ? "stale" : row.freshness?.observedRef ? `ref:${row.freshness.observedRef}` : undefined,
581
+ row.reportPath ? `out:${hostStepReportName(row.reportPath)}` : undefined,
582
+ ].filter((value): value is string => Boolean(value));
583
+ return `host ${row.kind}: ${row.name} | ${state}${details.length ? ` | ${details.join(" | ")}` : ""}`;
584
+ }
585
+
586
+ function workflowStageStateLabel(status: WorkflowGraphSnapshot["nodes"][number]["status"]): string {
587
+ switch (status) {
588
+ case "completed":
589
+ return "complete";
590
+ case "detached":
591
+ return "paused";
592
+ default:
593
+ return status;
594
+ }
595
+ }
596
+
597
+ export function formatWorkflowStageLine(node: WorkflowGraphSnapshot["nodes"][number], index: number, total: number): string {
598
+ const state = workflowStageStateLabel(node.status);
599
+ const id = previewDisplayText(node.id, 160);
600
+ const label = node.label ? previewDisplayText(node.label, 160) : "";
601
+ const display = label && label !== id ? ` | ${label}` : "";
602
+ const agent = node.agent ? ` | ${previewDisplayText(node.agent, 80)}` : "";
603
+ const error = node.error ? ` | ${previewDisplayText(node.error, 240)}` : "";
604
+ return `stage ${index + 1}/${total}: ${id}${display}${agent} | ${state}${error}`;
605
+ }
606
+
527
607
  export function formatAsyncRunOutputPath(run: Pick<AsyncRunSummary, "asyncDir" | "outputFile">): string | undefined {
528
608
  if (!run.outputFile) return undefined;
529
609
  return path.isAbsolute(run.outputFile) ? run.outputFile : path.join(run.asyncDir, run.outputFile);
530
610
  }
531
611
 
532
- export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps">): string {
612
+ export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps"> & { workflowGraph?: WorkflowGraphSnapshot }): string {
613
+ const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
614
+ if (graphStages.length > 0) {
615
+ const currentNode = graphStages.find((node) => node.id === run.workflowGraph?.currentNodeId);
616
+ if (currentNode && currentNode.status !== "completed") return `stage ${graphStages.indexOf(currentNode) + 1}/${graphStages.length}`;
617
+ const activeNode = graphStages.find((node) => node.status === "running") ?? graphStages.find((node) => node.status !== "completed");
618
+ if (activeNode) return `stage ${graphStages.indexOf(activeNode) + 1}/${graphStages.length}`;
619
+ return `stage ${graphStages.length}/${graphStages.length}`;
620
+ }
533
621
  const stepCount = run.steps.length || 1;
534
622
  const chainStepCount = run.chainStepCount ?? stepCount;
535
623
  const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, chainStepCount);
@@ -556,7 +644,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
556
644
  const activity = formatActivityFacts(run);
557
645
  const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
558
646
  const context = contextModeLabel(run.context);
559
- return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${context ? ` ${context}` : ""} | ${stepLabel}${pending} | ${cwd}`;
647
+ const lane = run.lane ? ` | lane ${run.lane.key}` : "";
648
+ return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${context ? ` ${context}` : ""} | ${stepLabel}${pending}${lane} | ${cwd}`;
560
649
  }
561
650
 
562
651
  export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
@@ -565,10 +654,23 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
565
654
  const lines = [`${heading}: ${runs.length}`, ""];
566
655
  for (const run of runs) {
567
656
  lines.push(`- ${formatRunHeader(run)}`);
657
+ if (run.preflight) lines.push(formatWorkflowPreflightPlanSummary(run.preflight, { indent: " " }));
658
+ const preflightWarning = formatWorkflowPreflightWarningSummary(run.workflow?.preflightWarnings, { indent: " " });
659
+ if (preflightWarning) lines.push(preflightWarning);
568
660
  for (const step of run.steps) {
569
661
  lines.push(` ${formatStepLine(step)}`);
662
+ lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));
570
663
  lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
571
664
  }
665
+ const loadedWorkflowKeys = new Set(run.steps.flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
666
+ const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
667
+ for (const [index, node] of graphStages.entries()) {
668
+ if (!loadedWorkflowKeys.has(node.id)) lines.push(` ${formatWorkflowStageLine(node, index, graphStages.length)}`);
669
+ }
670
+ for (const row of projectAsyncWorkflowRows([], run.hostSteps)) {
671
+ const line = formatHostStepLine(row);
672
+ if (line) lines.push(` ${line}`);
673
+ }
572
674
  const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
573
675
  const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
574
676
  lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
@@ -3,8 +3,9 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
5
5
  import { appendJsonl } from "../../shared/artifacts.ts";
6
- import type { AsyncParallelGroupStatus, AsyncStatus, WorkflowGraphNode, WorkflowGraphSnapshot } from "../../shared/types.ts";
6
+ import type { AsyncStatus, WorkflowGraphNode, WorkflowGraphSnapshot } from "../../shared/types.ts";
7
7
  import { PROMPT_REDACTED, readStatus } from "../../shared/utils.ts";
8
+ import { deriveChildSessionName } from "../../shared/child-session-name.ts";
8
9
  import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep } from "../shared/parallel-utils.ts";
9
10
  import { isDynamicRunnerGroup, isParallelGroup } from "../shared/parallel-utils.ts";
10
11
 
@@ -160,8 +161,10 @@ export function statusStepDescription(task: string | undefined): string | undefi
160
161
 
161
162
  function statusStepForTask(task: RunnerSubagentStep): StatusStep {
162
163
  const description = statusStepDescription(task.task);
164
+ const sessionName = task.sessionName ?? deriveChildSessionName({ agent: task.agent, task: task.task, label: task.label });
163
165
  return {
164
166
  agent: task.agent,
167
+ ...(sessionName ? { sessionName } : {}),
165
168
  ...(description ? { description } : {}),
166
169
  ...(task.context ? { context: task.context } : {}),
167
170
  phase: task.phase,
@@ -180,7 +183,7 @@ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
180
183
  };
181
184
  }
182
185
 
183
- function statusStepsForRunnerStep(step: RunnerStep, stepIndex: number): StatusStep[] {
186
+ function statusStepsForRunnerStep(step: RunnerStep): StatusStep[] {
184
187
  if (isParallelGroup(step)) return step.parallel.map(statusStepForTask);
185
188
  if (isDynamicRunnerGroup(step)) {
186
189
  return [{
@@ -297,7 +300,7 @@ export function appendRunnerStepsToStatus(input: {
297
300
  for (const step of input.steps) {
298
301
  const stepIndex = input.status.chainStepCount ?? input.status.steps?.length ?? 0;
299
302
  const flatIndex = input.status.steps?.length ?? 0;
300
- const statusSteps = statusStepsForRunnerStep(step, stepIndex);
303
+ const statusSteps = statusStepsForRunnerStep(step);
301
304
  input.status.steps ??= [];
302
305
  input.status.steps.push(...statusSteps);
303
306
  if (isParallelGroup(step)) {
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
- import { resultFilePath } from "./result-files.ts";
3
- import type { AcceptanceLedger, ArtifactPaths, AsyncStatus, CostSummary, ModelAttempt } from "../../shared/types.ts";
2
+ import * as path from "node:path";
3
+ import { resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
4
+ import type { AcceptanceLedger, ArtifactPaths, AsyncStatus, CostSummary, EffectsProjection, ExecutionProjection, ModelAttempt, Usage } from "../../shared/types.ts";
4
5
  import { readStatus } from "../../shared/utils.ts";
5
6
 
6
7
  export interface ImportedAsyncRoot {
@@ -12,6 +13,8 @@ export interface ImportedAsyncRoot {
12
13
 
13
14
  export interface ImportedAsyncRootResult {
14
15
  agent: string;
16
+ /** Human-readable display name for the child session, when derived at launch. */
17
+ sessionName?: string;
15
18
  output: string;
16
19
  success: boolean;
17
20
  exitCode: number;
@@ -23,6 +26,7 @@ export interface ImportedAsyncRootResult {
23
26
  modelAttempts?: ModelAttempt[];
24
27
  contextOverflow?: boolean;
25
28
  totalCost?: CostSummary;
29
+ usage?: Usage;
26
30
  structuredOutput?: unknown;
27
31
  structuredOutputPath?: string;
28
32
  structuredOutputSchemaPath?: string;
@@ -33,6 +37,8 @@ export interface ImportedAsyncRootResult {
33
37
  transcriptError?: string;
34
38
  timedOut?: boolean;
35
39
  stopped?: boolean;
40
+ execution?: ExecutionProjection;
41
+ effects?: EffectsProjection;
36
42
  }
37
43
 
38
44
  interface AsyncResultFile {
@@ -44,11 +50,14 @@ interface AsyncResultFile {
44
50
  stopped?: boolean;
45
51
  results?: Array<{
46
52
  agent?: string;
53
+ sessionName?: string;
47
54
  output?: string;
48
55
  error?: string;
49
56
  success?: boolean;
50
57
  timedOut?: boolean;
51
58
  stopped?: boolean;
59
+ execution?: ExecutionProjection;
60
+ effects?: EffectsProjection;
52
61
  sessionFile?: string;
53
62
  intercomTarget?: string;
54
63
  model?: string;
@@ -56,6 +65,7 @@ interface AsyncResultFile {
56
65
  modelAttempts?: ModelAttempt[];
57
66
  contextOverflow?: boolean;
58
67
  totalCost?: CostSummary;
68
+ usage?: Usage;
59
69
  structuredOutput?: unknown;
60
70
  structuredOutputPath?: string;
61
71
  structuredOutputSchemaPath?: string;
@@ -67,8 +77,25 @@ interface AsyncResultFile {
67
77
  }>;
68
78
  }
69
79
 
70
- const TERMINAL_STATES = new Set(["complete", "failed", "paused", "stopped"]);
71
- const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "paused", "stopped"]);
80
+ const TERMINAL_STATES = new Set(["complete", "failed", "partial", "paused", "stopped"]);
81
+ const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "partial", "paused", "stopped"]);
82
+
83
+ function usageFromAttempts(attempts: ModelAttempt[] | undefined): Usage | undefined {
84
+ if (!attempts?.length) return undefined;
85
+ const usage: Usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
86
+ for (const attempt of attempts) {
87
+ if (!attempt.usage) continue;
88
+ usage.input += attempt.usage.input;
89
+ usage.output += attempt.usage.output;
90
+ usage.cacheRead += attempt.usage.cacheRead;
91
+ usage.cacheWrite += attempt.usage.cacheWrite;
92
+ usage.cost += attempt.usage.cost;
93
+ usage.turns += attempt.usage.turns;
94
+ }
95
+ return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0
96
+ ? usage
97
+ : undefined;
98
+ }
72
99
 
73
100
  function readResultFile(resultPath: string): AsyncResultFile | undefined {
74
101
  try {
@@ -81,6 +108,13 @@ function readResultFile(resultPath: string): AsyncResultFile | undefined {
81
108
  }
82
109
  }
83
110
 
111
+ function readImportedResultFile(root: ImportedAsyncRoot, status: AsyncStatus | null): AsyncResultFile | undefined {
112
+ const direct = readResultFile(root.resultPath);
113
+ if (direct || !status?.sessionId) return direct;
114
+ const indexedPath = resultPayloadPathForSessionRun(path.dirname(root.resultPath), status.sessionId, root.runId);
115
+ return indexedPath ? readResultFile(indexedPath) : undefined;
116
+ }
117
+
84
118
  function selectedStatusStep(status: AsyncStatus | null, index: number): NonNullable<AsyncStatus["steps"]>[number] | undefined {
85
119
  return status?.steps?.[index];
86
120
  }
@@ -92,12 +126,12 @@ function isTerminalStatus(status: AsyncStatus | null, index: number): boolean {
92
126
  return TERMINAL_STATES.has(status.state);
93
127
  }
94
128
 
95
- function resultState(result: AsyncResultFile | undefined, child: NonNullable<AsyncResultFile["results"]>[number] | undefined): "complete" | "failed" | "paused" | "stopped" | undefined {
129
+ function resultState(result: AsyncResultFile | undefined, child: NonNullable<AsyncResultFile["results"]>[number] | undefined): "complete" | "failed" | "partial" | "paused" | "stopped" | undefined {
96
130
  if (!result) return undefined;
97
131
  if (child?.stopped === true) return "stopped";
98
132
  if (child?.success === true) return "complete";
99
- if (child?.success === false) return result.state === "stopped" ? "stopped" : result.state === "paused" ? "paused" : "failed";
100
- if (result.state === "complete" || result.state === "failed" || result.state === "paused" || result.state === "stopped") return result.state;
133
+ if (child?.success === false) return result.state === "stopped" ? "stopped" : result.state === "paused" ? "paused" : result.state === "partial" ? "partial" : "failed";
134
+ if (result.state === "complete" || result.state === "failed" || result.state === "partial" || result.state === "paused" || result.state === "stopped") return result.state;
101
135
  if (result.success === true) return "complete";
102
136
  if (result.success === false) return "failed";
103
137
  return undefined;
@@ -108,6 +142,11 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
108
142
  const timedOut = step?.timedOut === true || status.timedOut === true;
109
143
  const stopped = step?.stopped === true || status.stopped === true || status.state === "stopped";
110
144
  const message = step?.error ?? status.error ?? (stopped ? "Subagent stopped by user." : `Attached async root ${root.runId} ended without a result file at ${root.resultPath}.`);
145
+ const partialStatus = status.state === "partial" || step?.status === "partial";
146
+ const execution = step?.execution ?? (partialStatus
147
+ ? { status: "partial" as const, success: false, exitCode: 1, error: message }
148
+ : undefined);
149
+ const usage = usageFromAttempts(step?.modelAttempts);
111
150
  return {
112
151
  agent,
113
152
  output: message,
@@ -116,22 +155,27 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
116
155
  error: message,
117
156
  ...(timedOut ? { timedOut: true } : {}),
118
157
  ...(stopped ? { stopped: true } : {}),
158
+ ...(step?.sessionName ? { sessionName: step.sessionName } : {}),
119
159
  ...(step?.sessionFile ?? status.sessionFile ? { sessionFile: step?.sessionFile ?? status.sessionFile } : {}),
120
160
  ...(step?.model ? { model: step.model } : {}),
121
161
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
122
162
  ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
123
163
  ...(step?.contextOverflow ? { contextOverflow: true } : {}),
124
164
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
165
+ ...(usage ? { usage } : {}),
125
166
  ...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
126
167
  ...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
127
168
  ...(step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: step.structuredOutputSchemaPath } : {}),
128
169
  ...(step?.acceptance ? { acceptance: step.acceptance } : {}),
170
+ ...(execution ? { execution } : {}),
171
+ ...(step?.effects ? { effects: step.effects } : {}),
129
172
  ...(step?.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
130
173
  };
131
174
  }
132
175
 
133
176
  function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null, message: string): ImportedAsyncRootResult {
134
177
  const step = selectedStatusStep(status, root.index);
178
+ const usage = usageFromAttempts(step?.modelAttempts);
135
179
  return {
136
180
  agent: step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent",
137
181
  output: message,
@@ -139,12 +183,14 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
139
183
  exitCode: 1,
140
184
  error: message,
141
185
  timedOut: true,
186
+ ...(step?.sessionName ? { sessionName: step.sessionName } : {}),
142
187
  ...(step?.sessionFile ?? status?.sessionFile ? { sessionFile: step?.sessionFile ?? status?.sessionFile } : {}),
143
188
  ...(step?.model ? { model: step.model } : {}),
144
189
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
145
190
  ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
146
191
  ...(step?.contextOverflow ? { contextOverflow: true } : {}),
147
192
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
193
+ ...(usage ? { usage } : {}),
148
194
  ...(step?.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
149
195
  };
150
196
  }
@@ -159,6 +205,8 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
159
205
  const stopped = child?.stopped === true || step?.stopped === true || result.stopped === true || status?.stopped === true || state === "stopped";
160
206
  const success = state === "complete" && !timedOut && !stopped;
161
207
  const error = child?.error ?? (success ? undefined : stopped ? "Subagent stopped by user." : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
208
+ const execution = state ? { status: state === "complete" ? "completed" as const : state, success, exitCode: success ? 0 : 1, ...(error ? { error } : {}) } : undefined;
209
+ const usage = child?.usage ?? usageFromAttempts(step?.modelAttempts);
162
210
  return {
163
211
  agent,
164
212
  output: success ? output : (output || error || ""),
@@ -167,6 +215,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
167
215
  ...(error ? { error } : {}),
168
216
  ...(timedOut ? { timedOut: true } : {}),
169
217
  ...(stopped ? { stopped: true } : {}),
218
+ ...(child?.sessionName ?? step?.sessionName ? { sessionName: child?.sessionName ?? step?.sessionName } : {}),
170
219
  ...(child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile ? { sessionFile: child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile } : {}),
171
220
  ...(child?.intercomTarget ? { intercomTarget: child.intercomTarget } : {}),
172
221
  ...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
@@ -174,6 +223,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
174
223
  ...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
175
224
  ...(child?.contextOverflow || step?.contextOverflow ? { contextOverflow: true } : {}),
176
225
  ...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
226
+ ...(usage ? { usage } : {}),
177
227
  ...(child?.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
178
228
  ...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),
179
229
  ...(child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath } : {}),
@@ -182,6 +232,8 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
182
232
  ...(child?.outputSaveError ? { outputSaveError: child.outputSaveError } : {}),
183
233
  ...(child?.transcriptPath ?? step?.transcriptPath ? { transcriptPath: child?.transcriptPath ?? step?.transcriptPath } : {}),
184
234
  ...(child?.transcriptError ? { transcriptError: child.transcriptError } : {}),
235
+ ...(execution ? { execution } : {}),
236
+ ...(child?.effects ?? step?.effects ? { effects: child?.effects ?? step?.effects } : {}),
185
237
  };
186
238
  }
187
239
 
@@ -196,7 +248,7 @@ export async function waitForImportedAsyncRoot(
196
248
  for (;;) {
197
249
  const status = readStatus(root.asyncDir);
198
250
  if (options.shouldAbort?.()) return outputFromTimeout(root, status, options.timeoutMessage ?? "Subagent timed out.");
199
- const result = readResultFile(root.resultPath);
251
+ const result = readImportedResultFile(root, status);
200
252
  if (result) return buildImportedResult(root, status, result);
201
253
  if (isTerminalStatus(status, root.index)) {
202
254
  terminalSince ??= now();
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Cross-OS control channel for async subagent runs.
3
3
  *
4
- * Background runs are detached OS processes. The original control path delivered
5
- * an interrupt with `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot
4
+ * Background runs use child processes. Unix detaches them from the parent process.
5
+ * The original control path delivered an interrupt with
6
+ * `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot
6
7
  * deliver those signals cross-process via `process.kill` and throws `ENOSYS`,
7
8
  * which left async runs uninterruptible (no stop, no live steer) on Windows.
8
9
  *