pi-subagents 0.64.0 → 0.65.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 (89) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +2 -2
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +43 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/process-terminal.ts +1 -1
  34. package/src/runs/background/run-child-session.ts +613 -0
  35. package/src/runs/background/run-status.ts +0 -1
  36. package/src/runs/background/runner-aliases.ts +125 -0
  37. package/src/runs/background/runner-child-sessions.ts +31 -0
  38. package/src/runs/background/scheduled-runs.ts +18 -4
  39. package/src/runs/background/subagent-runner.ts +184 -901
  40. package/src/runs/foreground/async-steering-action.ts +1 -17
  41. package/src/runs/foreground/execution.ts +189 -377
  42. package/src/runs/foreground/foreground-control.ts +4 -0
  43. package/src/runs/foreground/subagent-executor.ts +100 -57
  44. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  45. package/src/runs/shared/abort-recovery.ts +3 -3
  46. package/src/runs/shared/capability-ceiling.ts +1 -2
  47. package/src/runs/shared/child-hooks.ts +25 -0
  48. package/src/runs/shared/child-identity.ts +13 -2
  49. package/src/runs/shared/child-launch.ts +314 -0
  50. package/src/runs/shared/child-lifecycle.ts +25 -0
  51. package/src/runs/shared/child-runtime-config.ts +126 -0
  52. package/src/runs/shared/child-session.ts +342 -0
  53. package/src/runs/shared/child-tool-plan.ts +530 -0
  54. package/src/runs/shared/claude-code-adapter.ts +5 -1
  55. package/src/runs/shared/completion-guard.ts +1 -1
  56. package/src/runs/shared/external-cli-preflight.ts +16 -0
  57. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  58. package/src/runs/shared/model-exclusions.ts +82 -14
  59. package/src/runs/shared/model-fallback.ts +47 -4
  60. package/src/runs/shared/nested-events.ts +29 -45
  61. package/src/runs/shared/nested-path.ts +0 -14
  62. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  63. package/src/runs/shared/parallel-utils.ts +0 -2
  64. package/src/runs/shared/permissions.ts +0 -13
  65. package/src/runs/shared/process-signal.ts +4 -1
  66. package/src/runs/shared/run-fanout-budget.ts +0 -13
  67. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  68. package/src/runs/shared/structured-output.ts +17 -4
  69. package/src/runs/shared/subagent-control.ts +2 -0
  70. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  71. package/src/runs/shared/tool-availability.ts +18 -62
  72. package/src/runs/shared/tool-budget.ts +0 -14
  73. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  74. package/src/runs/shared/worktree.ts +117 -30
  75. package/src/shared/child-session-name.ts +1 -1
  76. package/src/shared/jsonl-writer.ts +11 -0
  77. package/src/shared/thinking-ceiling.ts +0 -6
  78. package/src/shared/types.ts +55 -28
  79. package/src/shared/utils.ts +3 -4
  80. package/src/slash/slash-commands.ts +0 -6
  81. package/src/tui/fleet.ts +0 -1
  82. package/src/tui/render.ts +233 -31
  83. package/src/watchdog/child-status.ts +0 -1
  84. package/src/watchdog/register-child.ts +12 -12
  85. package/src/workflows/scripted-workflow.ts +48 -1
  86. package/src/runs/shared/child-protocol.ts +0 -415
  87. package/src/runs/shared/pi-args.ts +0 -1059
  88. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  89. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -369,6 +369,7 @@ export interface ControlEvent {
369
369
  tokens?: number;
370
370
  toolCount?: number;
371
371
  currentTool?: string;
372
+ toolCallId?: string;
372
373
  currentToolDurationMs?: number;
373
374
  currentPath?: string;
374
375
  elapsedMs?: number;
@@ -786,6 +787,8 @@ export interface SteeringRecoveryDescriptor {
786
787
  agentContract?: AgentContract;
787
788
  agent: string;
788
789
  sessionFile?: string;
790
+ /** Git ref used to allocate managed worktrees for this run. */
791
+ baseRef?: string;
789
792
  cwd: string;
790
793
  model?: string;
791
794
  modelProvider?: string;
@@ -1159,15 +1162,6 @@ export interface AcceptanceLedger {
1159
1162
  };
1160
1163
  }
1161
1164
 
1162
- export interface ProtocolOutputLimit {
1163
- code: "protocol_output_limit";
1164
- stream: "stdout" | "stderr";
1165
- limitBytes: number;
1166
- observedBytes: number;
1167
- diagnosticPrefix: string;
1168
- diagnosticTail: string;
1169
- }
1170
-
1171
1165
  export interface LaunchResolvedChildExtensionsV1 {
1172
1166
  version: 1;
1173
1167
  /** This is parent-resolved launch intent, not child-runtime acknowledgement that extensions loaded. */
@@ -1262,7 +1256,6 @@ export interface SingleResult {
1262
1256
  * a signal to reduce input size or re-decompose the task.
1263
1257
  */
1264
1258
  contextOverflow?: boolean;
1265
- protocolError?: ProtocolOutputLimit;
1266
1259
  sessionFile?: string;
1267
1260
  skills?: string[];
1268
1261
  skillsWarning?: string;
@@ -1369,7 +1362,7 @@ export interface AgentCapabilityRow {
1369
1362
  executable: boolean;
1370
1363
  restrictionSources?: string[];
1371
1364
  aliases?: string[];
1372
- runner: { type: "pi" } | { type: "external-cli"; adapter?: string; capabilities: ExternalCliCapabilities } | { type: "external-job"; provider: string; available?: boolean; capabilities: ExternalJobRunnerStatus["capabilities"] };
1365
+ runner: { type: "pi" } | { type: "external-cli"; adapter?: string; command: string; available: boolean; unavailableReason?: string; capabilities: ExternalCliCapabilities } | { type: "external-job"; provider: string; available?: boolean; capabilities: ExternalJobRunnerStatus["capabilities"] };
1373
1366
  tools: { ambient: boolean; names: string[]; excludeTools?: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
1374
1367
  model?: { value?: string; fallbackModels?: string[]; thinking?: string | false };
1375
1368
  execution?: { defaultAsync?: boolean; timeoutMs?: number };
@@ -2107,6 +2100,18 @@ export interface ForegroundChildControl {
2107
2100
  toolCount?: number;
2108
2101
  interrupt?: () => boolean;
2109
2102
  detach?: () => boolean;
2103
+ /** Steer the live in-process child session; undefined until the session exists. */
2104
+ steer?: (input: ForegroundSteerInput) => Promise<ForegroundSteerOutcome>;
2105
+ }
2106
+
2107
+ export interface ForegroundSteerInput {
2108
+ message: string;
2109
+ mode?: "steer" | "follow_up" | "auto";
2110
+ }
2111
+
2112
+ export interface ForegroundSteerOutcome {
2113
+ state: "delivered" | "queued" | "failed";
2114
+ reason?: string;
2110
2115
  }
2111
2116
 
2112
2117
  export interface ForegroundRunControl {
@@ -2115,8 +2120,6 @@ export interface ForegroundRunControl {
2115
2120
  parentWorkflowRunId?: string;
2116
2121
  /** Stable workflow lane key for this live foreground child. */
2117
2122
  workflowKey?: string;
2118
- /** Private control root used to steer a live workflow-owned child. */
2119
- workflowSteeringDir?: string;
2120
2123
  /** Originating parent session; required for public fleet projection. */
2121
2124
  sessionId?: string;
2122
2125
  mode: SubagentRunMode;
@@ -2158,6 +2161,7 @@ export interface ForegroundRunControl {
2158
2161
  nestedChildren?: NestedRunSummary[];
2159
2162
  interrupt?: () => boolean;
2160
2163
  detach?: () => boolean;
2164
+ steer?: ForegroundChildControl["steer"];
2161
2165
  }
2162
2166
 
2163
2167
  export interface WaitSubscriptionRecord {
@@ -2318,17 +2322,25 @@ export interface SubagentChildStatusEvent {
2318
2322
  // Execution Options
2319
2323
  // ============================================================================
2320
2324
 
2325
+ /** Live controls for one in-process foreground child session. */
2326
+ export interface ForegroundChildSessionControls {
2327
+ steer: (text: string) => Promise<void>;
2328
+ followUp: (text: string) => Promise<void>;
2329
+ }
2330
+
2321
2331
  export interface RunSyncOptions {
2322
2332
  /** Exact discovery provenance for an unknown-agent error; omission uses defensive fallback discovery. */
2323
2333
  unknownAgentDiagnosticContext?: import("../agents/agents.ts").UnknownAgentDiagnosticContext;
2334
+ /** Session factory for the in-process child; defaults to the process-wide factory. */
2335
+ childSessionFactory?: import("../runs/shared/child-session.ts").ChildSessionFactory;
2336
+ /** The launching executor's own child runtime when it is itself an in-process child. */
2337
+ childRuntime?: import("../runs/shared/child-runtime-config.ts").ChildRuntimeConfig;
2338
+ /** Fires once the child session exists and can be steered. */
2339
+ onChildSession?: (controls: ForegroundChildSessionControls) => void;
2324
2340
  /** Opt-in global permission rules; missing tools remain allowed. */
2325
2341
  permissions?: import("../runs/shared/permissions.ts").PermissionConfig;
2326
2342
  /** Session id of the direct parent session for permission-system ask forwarding. */
2327
2343
  parentSessionId?: string;
2328
- /** Private prompt-runtime steering transport for workflow-owned foreground children. */
2329
- steerInboxDir?: string;
2330
- steerCapabilityPath?: string;
2331
- steerAckDir?: string;
2332
2344
  /** Resolved launch context for this child. */
2333
2345
  context?: "fresh" | "fork";
2334
2346
  cwd?: string;
@@ -2410,6 +2422,7 @@ export interface RunSyncOptions {
2410
2422
  schemaPath: string;
2411
2423
  outputPath: string;
2412
2424
  acceptanceReportPath?: string;
2425
+ acceptanceReportRequired?: boolean;
2413
2426
  };
2414
2427
  agentContract?: AgentContract;
2415
2428
  acceptance?: AcceptanceInput;
@@ -2765,8 +2778,17 @@ export function normalizeMaxSubagentDepth(value: unknown): number | undefined {
2765
2778
  return normalizeNonNegativeInteger(value);
2766
2779
  }
2767
2780
 
2768
- export function resolveCurrentMaxSubagentDepth(configMaxDepth?: number): number {
2769
- return normalizeMaxSubagentDepth(process.env.PI_SUBAGENT_MAX_DEPTH)
2781
+ /** Depth context of the executor's own child runtime, when it runs as an in-process child. */
2782
+ export interface SubagentDepthContext {
2783
+ depth: number;
2784
+ maxDepth?: number;
2785
+ }
2786
+
2787
+ /** Operator override for the top-level parent; children inherit their limit through their runtime config. */
2788
+ export const SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
2789
+
2790
+ export function resolveCurrentMaxSubagentDepth(configMaxDepth?: number, runtime?: SubagentDepthContext): number {
2791
+ return normalizeMaxSubagentDepth(runtime ? runtime.maxDepth : process.env[SUBAGENT_MAX_DEPTH_ENV])
2770
2792
  ?? normalizeMaxSubagentDepth(configMaxDepth)
2771
2793
  ?? DEFAULT_SUBAGENT_MAX_DEPTH;
2772
2794
  }
@@ -2777,19 +2799,24 @@ export function resolveChildMaxSubagentDepth(parentMaxDepth: number, agentMaxDep
2777
2799
  return normalizedAgent === undefined ? normalizedParent : Math.min(normalizedParent, normalizedAgent);
2778
2800
  }
2779
2801
 
2780
- export function checkSubagentDepth(configMaxDepth?: number): { blocked: boolean; depth: number; maxDepth: number } {
2781
- const depth = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
2782
- const maxDepth = resolveCurrentMaxSubagentDepth(configMaxDepth);
2783
- const blocked = Number.isFinite(depth) && depth >= maxDepth;
2802
+ /** Depth of the executor itself: 0 for a top-level parent, its own child depth otherwise. */
2803
+ export function resolveCurrentSubagentDepth(runtime?: SubagentDepthContext): number {
2804
+ const depth = runtime ? runtime.depth : 0;
2805
+ return Number.isFinite(depth) ? depth : 0;
2806
+ }
2807
+
2808
+ export function checkSubagentDepth(configMaxDepth?: number, runtime?: SubagentDepthContext): { blocked: boolean; depth: number; maxDepth: number } {
2809
+ const depth = resolveCurrentSubagentDepth(runtime);
2810
+ const maxDepth = resolveCurrentMaxSubagentDepth(configMaxDepth, runtime);
2811
+ const blocked = depth >= maxDepth;
2784
2812
  return { blocked, depth, maxDepth };
2785
2813
  }
2786
2814
 
2787
- export function getSubagentDepthEnv(maxDepth?: number): Record<string, string> {
2788
- const parentDepth = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
2789
- const nextDepth = Number.isFinite(parentDepth) ? parentDepth + 1 : 1;
2815
+ /** Depth context handed to a child launched by an executor at `runtime` (undefined for a top-level parent). */
2816
+ export function resolveChildDepth(maxDepth?: number, runtime?: SubagentDepthContext): Required<SubagentDepthContext> {
2790
2817
  return {
2791
- PI_SUBAGENT_DEPTH: String(nextDepth),
2792
- PI_SUBAGENT_MAX_DEPTH: String(normalizeMaxSubagentDepth(maxDepth) ?? resolveCurrentMaxSubagentDepth()),
2818
+ depth: resolveCurrentSubagentDepth(runtime) + 1,
2819
+ maxDepth: normalizeMaxSubagentDepth(maxDepth) ?? resolveCurrentMaxSubagentDepth(undefined, runtime),
2793
2820
  };
2794
2821
  }
2795
2822
 
@@ -413,10 +413,9 @@ export function compactForegroundDetails(details: Details): Details {
413
413
  *
414
414
  * The completed-compaction helpers above bail out while a child is still
415
415
  * `running`, so a long or deeply nested fan-out streams full, unbounded progress on
416
- * every tick. Pi serializes each streamed `tool_execution_update` as a single
417
- * child-stdout line, which the parent reads under `MAX_CHILD_PENDING_LINE_BYTES`;
418
- * an unbounded running snapshot can cross that cap and kill the child with
419
- * `protocol_output_limit`.
416
+ * every tick. The parent records every streamed `tool_execution_update` in its
417
+ * transcript and `events.jsonl`, so an unbounded running snapshot grows those
418
+ * artifacts and the live display state without bound.
420
419
  *
421
420
  * These bound the STREAMED snapshot only. The final returned result keeps the full
422
421
  * live progress and message transcript, and every live-display consumer already
@@ -21,7 +21,6 @@ import { formatTokens, shortenPath } from "../shared/formatters.ts";
21
21
  import { listAsyncRuns, formatAsyncRunProgressLabel, type AsyncRunSummary } from "../runs/background/async-status.ts";
22
22
  import { encodeInspectReply, handleInspectRpcArgs, INSPECT_WIDGET_KEY } from "../runs/background/inspect-rpc.ts";
23
23
  import { listScheduledRunSummaries } from "../runs/background/scheduled-runs.ts";
24
- import { SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
25
24
  import { resolveAsyncStatusChild } from "../runs/shared/child-identity.ts";
26
25
  import { readStatus } from "../shared/utils.ts";
27
26
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
@@ -1030,11 +1029,6 @@ export function registerSlashCommands(
1030
1029
  return;
1031
1030
  }
1032
1031
 
1033
- if (process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1") {
1034
- sendSlashText(pi, "Selector unavailable in child-safe fanout mode. Pass an explicit current-session top-level async run id, for example `/subagents-stop <run-id>` or `subagent({ action: \"stop\", id: \"<run-id>\" })`.");
1035
- return;
1036
- }
1037
-
1038
1032
  let targets: StopSelectorTarget[];
1039
1033
  try {
1040
1034
  targets = discoverStopTargets(ctx, state);
package/src/tui/fleet.ts CHANGED
@@ -204,7 +204,6 @@ export function collectFleetSnapshot(
204
204
  if (!control.parentWorkflowRunId
205
205
  || !workflowParentIds.has(control.parentWorkflowRunId)
206
206
  || !belongsToCurrentSession(control.sessionId, state.currentSessionId)
207
- || !control.workflowSteeringDir
208
207
  || activeChildCount === 0) continue;
209
208
  liveWorkflowForegroundControls.add(control);
210
209
  workflowForegroundChildCounts.set(control.parentWorkflowRunId, (workflowForegroundChildCounts.get(control.parentWorkflowRunId) ?? 0) + activeChildCount);
package/src/tui/render.ts CHANGED
@@ -94,6 +94,10 @@ export function liveDetailHintText(): string {
94
94
  return `Press ${liveDetailKeyText()} for live detail · ${formatShortcutLabel(FLEET_OPEN_SHORTCUT)} Fleet`;
95
95
  }
96
96
 
97
+ function workflowDetailHintText(): string {
98
+ return `Press ${liveDetailKeyText()} for details · ${formatShortcutLabel(FLEET_OPEN_SHORTCUT)} Fleet`;
99
+ }
100
+
97
101
  function foregroundSingleHintText(shortcut?: string): string {
98
102
  if (!shortcut) return liveDetailHintText();
99
103
  const label = formatShortcutLabel(shortcut);
@@ -609,15 +613,8 @@ function widgetLaneDetailLines(job: AsyncJobState, theme: Theme, projection?: Wo
609
613
  return lane ? formatLaneProjectionLines(lane, theme, " ") : [];
610
614
  }
611
615
 
612
- function workflowPreflightLines(job: AsyncJobState, expanded = false): string[] {
616
+ function workflowPreflightLines(job: AsyncJobState): string[] {
613
617
  if (job.mode !== "workflow" || !job.preflight) return [];
614
- if (!expanded) {
615
- const warning = formatWorkflowPreflightWarningSummary(job.workflow?.preflightWarnings, { indent: " ", hint: "expand for debug" });
616
- return [
617
- formatWorkflowPreflightPlanSummary(job.preflight, { indent: " " }),
618
- ...(warning ? [warning] : []),
619
- ];
620
- }
621
618
  return [
622
619
  ...formatWorkflowPreflight(job.preflight, { indent: " " }).split("\n"),
623
620
  ...(job.workflow?.preflightWarnings ? formatWorkflowPreflightWarnings(job.workflow.preflightWarnings, { indent: " " }).split("\n") : []),
@@ -1058,6 +1055,7 @@ export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
1058
1055
  return JSON.stringify({
1059
1056
  asyncDir: job.asyncDir,
1060
1057
  status: job.status,
1058
+ description: job.mode === "workflow" ? job.description : undefined,
1061
1059
  activityState: job.activityState,
1062
1060
  lastActivityAt: job.lastActivityAt,
1063
1061
  currentTool: job.currentTool,
@@ -1075,8 +1073,8 @@ export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
1075
1073
  currentNodeId: job.workflowGraph.currentNodeId,
1076
1074
  stages: projection.stages.map((node) => [node.id, node.status, node.agent, node.phase, node.label, node.flatIndex, node.outputName, node.structured, node.error]),
1077
1075
  } : undefined,
1078
- preflight: expanded ? job.preflight : job.preflight ? formatWorkflowPreflightPlanSummary(job.preflight) : undefined,
1079
- preflightWarnings: expanded ? job.workflow?.preflightWarnings : job.workflow?.preflightWarnings?.length || undefined,
1076
+ preflight: expanded ? job.preflight : job.preflight?.lanes.map((lane) => [lane.key, lane.mode, lane.decision, lane.claims, lane.expectedOutput]),
1077
+ preflightWarnings: expanded ? job.workflow?.preflightWarnings : undefined,
1080
1078
  checklist: projection.checklist ? {
1081
1079
  total: projection.checklist.total,
1082
1080
  done: projection.checklist.done,
@@ -1084,7 +1082,7 @@ export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
1084
1082
  queued: projection.checklist.queued,
1085
1083
  blocked: projection.checklist.blocked,
1086
1084
  failed: projection.checklist.failed,
1087
- phases: projection.checklist.phases.map((phase) => [phase.key, phase.state, phase.done, phase.total, phase.running, phase.queued, phase.blocked, phase.failed, phase.paused, phase.stopped, phase.items.map((item) => [item.key, item.state, item.currentTool, item.currentPath, item.durationMs, item.toolCount, item.error])]),
1085
+ phases: projection.checklist.phases.map((phase) => [phase.key, phase.state, phase.done, phase.total, phase.running, phase.queued, phase.blocked, phase.failed, phase.paused, phase.stopped, phase.items.map((item) => [item.key, item.label, item.agent, item.state, item.preflight?.key, item.preflight?.mode, item.preflight?.decision, item.preflight?.claims, item.preflight?.expectedOutput, item.currentTool, item.currentPath, item.durationMs, item.toolCount, item.error])]),
1088
1086
  } : undefined,
1089
1087
  steps: job.steps?.map((step, index) => widgetStepRenderKey(step, index, expanded)),
1090
1088
  nestedChildren: nestedRenderKey(job.nestedChildren, expanded),
@@ -1340,6 +1338,208 @@ function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | u
1340
1338
  return lines;
1341
1339
  }
1342
1340
 
1341
+ interface CompactWorkflowLaneRow {
1342
+ key: string;
1343
+ state: WorkflowChecklistState;
1344
+ agent?: string;
1345
+ mode?: string;
1346
+ decision?: string;
1347
+ claims?: string;
1348
+ expectedOutput?: string;
1349
+ label?: string;
1350
+ toolUses?: number;
1351
+ durationMs?: number;
1352
+ }
1353
+
1354
+ interface CompactWorkflowLaneCounts {
1355
+ total: number;
1356
+ done: number;
1357
+ active: number;
1358
+ queued: number;
1359
+ blocked: number;
1360
+ failed: number;
1361
+ paused: number;
1362
+ stopped: number;
1363
+ }
1364
+
1365
+ function compactWorkflowLaneState(items: readonly WorkflowChecklistItem[]): WorkflowChecklistState {
1366
+ let state: WorkflowChecklistState | undefined;
1367
+ for (const item of items) {
1368
+ if (state === undefined || workflowChecklistItemPriority(item.state) < workflowChecklistItemPriority(state)) state = item.state;
1369
+ }
1370
+ return state ?? "queued";
1371
+ }
1372
+
1373
+ function compactWorkflowLaneOwner(items: readonly WorkflowChecklistItem[]): string | undefined {
1374
+ const owners = [...new Set(items.map((item) => item.agent ?? item.role).filter((value): value is string => Boolean(value?.trim())))];
1375
+ if (owners.length === 0) return undefined;
1376
+ return boundedLaneValue(owners.length > 2 ? `${owners.slice(0, 2).join(", ")} +${owners.length - 2}` : owners.join(", "), 32);
1377
+ }
1378
+
1379
+ function compactWorkflowLaneLabel(items: readonly WorkflowChecklistItem[], key: string): string | undefined {
1380
+ const labels = [...new Set(items.map((item) => item.label).filter((value): value is string => Boolean(value?.trim() && value.trim() !== key)))];
1381
+ if (labels.length === 0) return undefined;
1382
+ return boundedLaneValue(labels.length > 1 ? `${labels[0]} +${labels.length - 1}` : labels[0], 48);
1383
+ }
1384
+
1385
+ function compactWorkflowLaneRow(key: string, items: readonly WorkflowChecklistItem[], lane = items.find((item) => item.preflight)?.preflight, fallbackState: WorkflowChecklistState = "queued"): CompactWorkflowLaneRow {
1386
+ const state = items.length ? compactWorkflowLaneState(items) : fallbackState;
1387
+ const toolCounts = items.map((item) => item.toolCount).filter((value): value is number => value !== undefined);
1388
+ const durations = items.map((item) => item.durationMs).filter((value): value is number => value !== undefined);
1389
+ const label = compactWorkflowLaneLabel(items, key);
1390
+ return {
1391
+ key: boundedLaneValue(key, 40) ?? key,
1392
+ state,
1393
+ agent: compactWorkflowLaneOwner(items),
1394
+ ...(lane?.mode ? { mode: lane.mode } : {}),
1395
+ ...(lane?.decision ? { decision: boundedLaneValue(lane.decision, 56) } : {}),
1396
+ ...(lane?.claims?.length ? { claims: boundedLaneValue(lane.claims.join(", "), 56) } : {}),
1397
+ ...(lane?.expectedOutput ? { expectedOutput: boundedLaneValue(lane.expectedOutput, 56) } : {}),
1398
+ ...(label ? { label } : {}),
1399
+ ...(toolCounts.length ? { toolUses: toolCounts.reduce((sum, value) => sum + value, 0) } : {}),
1400
+ ...(durations.length ? { durationMs: Math.max(...durations) } : {}),
1401
+ };
1402
+ }
1403
+
1404
+ function compactWorkflowFallbackState(job: AsyncJobState): WorkflowChecklistState {
1405
+ if (job.activityState === "needs_attention" || job.timedOut || job.toolBudgetBlocked || job.turnBudgetExceeded) return "blocked";
1406
+ switch (job.status) {
1407
+ case "complete": return "complete";
1408
+ case "failed": return "failed";
1409
+ case "paused": return "paused";
1410
+ case "stopped": return "stopped";
1411
+ case "partial":
1412
+ case "rejected": return "blocked";
1413
+ default: return "queued";
1414
+ }
1415
+ }
1416
+
1417
+ /** Flatten the loaded checklist into one compact row per declared lane. */
1418
+ function compactWorkflowLaneRows(job: AsyncJobState, checklist: WorkflowChecklistProjection | undefined): CompactWorkflowLaneRow[] {
1419
+ const groups = new Map<string, WorkflowChecklistItem[]>();
1420
+ for (const item of checklist?.phases.flatMap((phase) => phase.items) ?? []) {
1421
+ const key = item.preflight?.key ?? item.key;
1422
+ const group = groups.get(key);
1423
+ if (group) group.push(item);
1424
+ else groups.set(key, [item]);
1425
+ }
1426
+
1427
+ const rows: CompactWorkflowLaneRow[] = [];
1428
+ const preflightKeys = new Set(job.preflight?.lanes.map((lane) => lane.key) ?? []);
1429
+ if (job.preflight?.lanes.length) {
1430
+ for (const lane of job.preflight.lanes) {
1431
+ rows.push(compactWorkflowLaneRow(lane.key, groups.get(lane.key) ?? [], lane, compactWorkflowFallbackState(job)));
1432
+ }
1433
+ }
1434
+
1435
+ for (const [key, items] of groups) {
1436
+ if (preflightKeys.has(key)) continue;
1437
+ rows.push(compactWorkflowLaneRow(key, items));
1438
+ }
1439
+ return rows;
1440
+ }
1441
+
1442
+ function compactWorkflowLaneCounts(rows: readonly CompactWorkflowLaneRow[]): CompactWorkflowLaneCounts {
1443
+ const counts: CompactWorkflowLaneCounts = { total: rows.length, done: 0, active: 0, queued: 0, blocked: 0, failed: 0, paused: 0, stopped: 0 };
1444
+ for (const row of rows) {
1445
+ switch (row.state) {
1446
+ case "complete": counts.done++; break;
1447
+ case "running": counts.active++; break;
1448
+ case "queued": counts.queued++; break;
1449
+ case "blocked": counts.blocked++; break;
1450
+ case "failed": counts.failed++; break;
1451
+ case "paused": counts.paused++; break;
1452
+ case "stopped": counts.stopped++; break;
1453
+ }
1454
+ }
1455
+ return counts;
1456
+ }
1457
+
1458
+ function compactWorkflowLaneLine(row: CompactWorkflowLaneRow, theme: Theme, indent: string, frame?: number): string {
1459
+ const owner = row.agent && row.mode ? `${row.agent}/${row.mode}` : row.agent ?? row.mode;
1460
+ const state = row.state === "complete" ? undefined : row.state === "running" ? "active" : row.state;
1461
+ const intent = [
1462
+ row.decision,
1463
+ row.claims,
1464
+ row.expectedOutput,
1465
+ !row.mode && row.label ? row.label : undefined,
1466
+ ].filter((value): value is string => Boolean(value));
1467
+ return `${indent}${workflowChecklistGlyph({ state: row.state, durationMs: row.durationMs, toolCount: row.toolUses }, theme, frame)} ${theme.bold(row.key)}${owner ? ` ${theme.fg("dim", `· ${owner}`)}` : ""}${state ? ` ${theme.fg("dim", `· ${state}`)}` : ""}${intent.length ? ` ${theme.fg("dim", `· ${intent.join(" · ")}`)}` : ""}`;
1468
+ }
1469
+
1470
+ function compactWorkflowShortId(job: AsyncJobState): string {
1471
+ return job.asyncId.slice(0, 8);
1472
+ }
1473
+
1474
+ function compactWorkflowDisplayLabel(job: AsyncJobState): string {
1475
+ return boundedLaneValue(job.description, 64) ?? compactWorkflowShortId(job);
1476
+ }
1477
+
1478
+ function compactWorkflowStats(job: AsyncJobState, rows: readonly CompactWorkflowLaneRow[], theme: Theme, projection: WorkflowWidgetProjection): string {
1479
+ const counts = compactWorkflowLaneCounts(rows);
1480
+ const total = counts.total || projection.stageProgress?.total || job.stepsTotal || job.agents?.length || 0;
1481
+ const progress = [
1482
+ total > 0 ? `${counts.done}/${total} done` : undefined,
1483
+ counts.active ? `${counts.active} active` : undefined,
1484
+ counts.queued ? `${counts.queued} queued` : undefined,
1485
+ counts.blocked ? `${counts.blocked} blocked` : undefined,
1486
+ counts.failed ? `${counts.failed} failed` : undefined,
1487
+ counts.paused ? `${counts.paused} paused` : undefined,
1488
+ counts.stopped ? `${counts.stopped} stopped` : undefined,
1489
+ ].filter((value): value is string => Boolean(value));
1490
+ const rowToolUses = rows.map((row) => row.toolUses).filter((value): value is number => value !== undefined);
1491
+ const toolUses = job.toolCount ?? (rowToolUses.length ? rowToolUses.reduce((sum, value) => sum + value, 0) : undefined);
1492
+ const rowDuration = rows.map((row) => row.durationMs).filter((value): value is number => value !== undefined);
1493
+ const durationMs = job.startedAt !== undefined && job.updatedAt !== undefined
1494
+ ? Math.max(0, job.updatedAt - job.startedAt)
1495
+ : rowDuration.length ? Math.max(...rowDuration) : undefined;
1496
+ return statJoin(theme, [
1497
+ `id: ${compactWorkflowShortId(job)}`,
1498
+ ...progress,
1499
+ toolUses !== undefined ? formatToolUseStat(toolUses) : undefined,
1500
+ durationMs !== undefined ? formatDuration(durationMs) : undefined,
1501
+ ].filter((value): value is string => Boolean(value)));
1502
+ }
1503
+
1504
+ interface CompactWorkflowBottleneck {
1505
+ text: string;
1506
+ tone: "warning" | "error";
1507
+ }
1508
+
1509
+ function compactWorkflowBottleneck(rows: readonly CompactWorkflowLaneRow[], job: AsyncJobState): CompactWorkflowBottleneck | undefined {
1510
+ let row: CompactWorkflowLaneRow | undefined;
1511
+ for (const candidate of rows) {
1512
+ if (candidate.state !== "blocked" && candidate.state !== "failed" && candidate.state !== "paused" && candidate.state !== "stopped") continue;
1513
+ if (!row || workflowChecklistItemPriority(candidate.state) < workflowChecklistItemPriority(row.state)) row = candidate;
1514
+ }
1515
+ if (row) return { text: `${row.key} · ${row.state}`, tone: row.state === "blocked" || row.state === "failed" ? "error" : "warning" };
1516
+ if (job.activityState === "needs_attention" || job.timedOut || job.toolBudgetBlocked || job.turnBudgetExceeded) return { text: `workflow · ${job.activityState === "needs_attention" ? "needs attention" : "blocked"}`, tone: job.activityState === "needs_attention" ? "warning" : "error" };
1517
+ if (job.status === "failed" || job.status === "partial" || job.status === "rejected") return { text: `workflow · ${job.status}`, tone: "error" };
1518
+ if (job.status === "paused" || job.status === "stopped") return { text: `workflow · ${job.status}`, tone: "warning" };
1519
+ return undefined;
1520
+ }
1521
+
1522
+ function compactWorkflowHeaderLine(job: AsyncJobState, theme: Theme, width?: number): string {
1523
+ const label = compactWorkflowDisplayLabel(job);
1524
+ const full = `${theme.fg("toolTitle", themeBold(theme, `async workflow: ${label}`))} ${theme.fg("dim", "─ background")}`;
1525
+ if (width === undefined || visibleWidth(full) <= width) return full;
1526
+ return `${theme.fg("toolTitle", themeBold(theme, `async workflow ${label}`))} ${theme.fg("dim", "· background")}`;
1527
+ }
1528
+
1529
+ function compactWorkflowWidgetBodyLines(job: AsyncJobState, theme: Theme, frame: number | undefined, projection: WorkflowWidgetProjection): string[] {
1530
+ const rows = compactWorkflowLaneRows(job, projection.checklist);
1531
+ const lines = [` ${compactWorkflowStats(job, rows, theme, projection)}`];
1532
+ if (rows.length) {
1533
+ for (const row of rows) lines.push(compactWorkflowLaneLine(row, theme, " ", frame));
1534
+ } else {
1535
+ lines.push(` ${theme.fg("dim", "◦ waiting for workflow lanes")}`);
1536
+ }
1537
+ const bottleneck = compactWorkflowBottleneck(rows, job);
1538
+ if (bottleneck) lines.push(` ${theme.fg(bottleneck.tone, `bottleneck · ${bottleneck.text}`)}`);
1539
+ if (job.status === "running" || rows.some((row) => row.state === "running")) lines.push(` ${theme.fg("accent", workflowDetailHintText())}`);
1540
+ return lines;
1541
+ }
1542
+
1343
1543
  function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], snapshotNow?: number): string {
1344
1544
  const facts: string[] = [];
1345
1545
  if (step.currentTool && step.currentToolStartedAt !== undefined && snapshotNow !== undefined) facts.push(`${step.currentTool} ${formatDuration(Math.max(0, snapshotNow - step.currentToolStartedAt))}`);
@@ -2154,19 +2354,13 @@ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string):
2154
2354
 
2155
2355
  function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
2156
2356
  const { steps } = projection;
2157
- const checklistPrimary = !expanded && job.mode === "workflow" && projection.checklist !== undefined;
2158
- if (checklistPrimary) {
2159
- const lines = [
2160
- ...workflowPreflightLines(job, false),
2161
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", false, frame, false),
2162
- ];
2163
- if (job.status === "running" || projection.checklist!.running > 0) lines.push(` ${theme.fg("accent", liveDetailHintText())}`);
2164
- return lines;
2357
+ if (!expanded && job.mode === "workflow") {
2358
+ return compactWorkflowWidgetBodyLines(job, theme, frame, projection);
2165
2359
  }
2166
2360
  if (!steps.length) {
2167
2361
  const lane = projectAsyncLane(job, laneStepForJob(job, steps));
2168
2362
  return [
2169
- ...workflowPreflightLines(job, expanded),
2363
+ ...(expanded ? workflowPreflightLines(job) : []),
2170
2364
  ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
2171
2365
  ...(lane ? formatLaneProjectionLines(lane, theme, " ") : []),
2172
2366
  ...hostStepWidgetLines(job, theme, " "),
@@ -2176,7 +2370,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
2176
2370
  }
2177
2371
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
2178
2372
  const lines: string[] = [
2179
- ...workflowPreflightLines(job, expanded),
2373
+ ...(expanded ? workflowPreflightLines(job) : []),
2180
2374
  ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
2181
2375
  ];
2182
2376
  const group = activeParallelWidgetGroup(job);
@@ -2210,6 +2404,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
2210
2404
  }
2211
2405
 
2212
2406
  function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
2407
+ if (!expanded && job.mode === "workflow") return [compactWorkflowHeaderLine(job, theme, width), ...compactWorkflowWidgetBodyLines(job, theme, frame, projection)].map((line) => truncLine(line, width));
2213
2408
  const stats = widgetStats(job, theme, projection, !expanded);
2214
2409
  const count = job.mode === "workflow"
2215
2410
  ? projection.checklist?.total ?? projection.stageProgress?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length
@@ -2386,11 +2581,17 @@ function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: numbe
2386
2581
  }
2387
2582
 
2388
2583
  function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string {
2389
- const checklistPrimary = job.mode === "workflow" && projection.checklist !== undefined;
2390
- const stats = widgetStats(job, theme, projection, true);
2391
- const activity = checklistPrimary ? "" : widgetActivity(job);
2584
+ const compactWorkflow = job.mode === "workflow";
2585
+ const compactRows = compactWorkflow ? compactWorkflowLaneRows(job, projection.checklist) : undefined;
2586
+ const stats = compactWorkflow ? compactWorkflowStats(job, compactRows ?? [], theme, projection) : widgetStats(job, theme, projection, true);
2587
+ if (compactWorkflow) {
2588
+ const bottleneck = compactWorkflowBottleneck(compactRows ?? [], job);
2589
+ const suffix = [stats, bottleneck ? theme.fg(bottleneck.tone, `bottleneck · ${bottleneck.text}`) : undefined].filter((value): value is string => Boolean(value)).join(` ${theme.fg("dim", "·")} `);
2590
+ return truncLine(` ${widgetStatusGlyph(job, theme, frame)} ${compactWorkflowHeaderLine(job, theme, Math.max(0, width - 4))}${suffix ? ` ${theme.fg("dim", "·")} ${suffix}` : ""}`, width);
2591
+ }
2592
+ const activity = widgetActivity(job);
2392
2593
  const status = job.status === "complete" ? "done" : job.status;
2393
- const lane = checklistPrimary ? undefined : projectAsyncLane(job, laneStepForJob(job, projection.steps));
2594
+ const lane = projectAsyncLane(job, laneStepForJob(job, projection.steps));
2394
2595
  const laneSummary = lane
2395
2596
  ? [lane.label ?? lane.role, lane.phase ? `phase:${lane.phase}` : undefined, lane.output ? `out:${lane.output}` : undefined, lane.workspace ? `workspace:${lane.workspace}` : `ref:${lane.ref}`].filter(Boolean).join(" · ")
2396
2597
  : "";
@@ -2557,12 +2758,11 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2557
2758
  let slots = MAX_WIDGET_JOBS;
2558
2759
  const appendJob = (job: AsyncJobState): void => {
2559
2760
  const projection = projectionFor(job);
2560
- const checklistPrimary = !expanded && job.mode === "workflow" && projection.checklist !== undefined;
2561
- const stats = widgetStats(job, theme, projection, !expanded);
2562
- const details = checklistPrimary
2761
+ const compactWorkflow = !expanded && job.mode === "workflow";
2762
+ const stats = compactWorkflow ? "" : widgetStats(job, theme, projection, !expanded);
2763
+ const details = compactWorkflow
2563
2764
  ? [
2564
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", false, frame, false),
2565
- ...(job.status === "running" || projection.checklist!.running > 0 ? [` ${theme.fg("accent", liveDetailHintText())}`] : []),
2765
+ ...compactWorkflowWidgetBodyLines(job, theme, frame, projection),
2566
2766
  ]
2567
2767
  : [
2568
2768
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
@@ -2571,7 +2771,9 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2571
2771
  ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
2572
2772
  ];
2573
2773
  items.push([
2574
- `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
2774
+ compactWorkflow
2775
+ ? compactWorkflowHeaderLine(job, theme, width)
2776
+ : `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
2575
2777
  ...details,
2576
2778
  ]);
2577
2779
  };
@@ -3,7 +3,6 @@ import { SUBAGENT_WATCHDOG_WARNING_TYPE, type ResolvedWatchdogConfig, type Watch
3
3
 
4
4
  export const CHILD_WATCHDOG_WARNING_LIMIT = 20;
5
5
 
6
- export const CHILD_WATCHDOG_CONFIG_ENV = "PI_SUBAGENT_WATCHDOG_CHILD_CONFIG";
7
6
  export const CHILD_WATCHDOG_STATUS_EVENT = "subagent.watchdog.status";
8
7
 
9
8
  export const CHILD_WATCHDOG_PHASES = ["idle", "reviewing", "stale", "failed"] as const;
@@ -5,11 +5,10 @@ import { createMainWatchdogReview } from "./review.ts";
5
5
  import { DEFAULT_WATCHDOG_CONFIG } from "./settings.ts";
6
6
  import { createWatchdogWarningMessage } from "./warning-format.ts";
7
7
  import {
8
- CHILD_WATCHDOG_CONFIG_ENV,
9
8
  CHILD_WATCHDOG_STATUS_EVENT,
10
- decodeChildWatchdogConfig,
11
9
  type ChildWatchdogConfig,
12
10
  type ChildWatchdogPhase,
11
+ type ChildWatchdogStatusEvent,
13
12
  } from "./child-status.ts";
14
13
  import type { ResolvedWatchdogConfig, WatchdogWarningDetails } from "./types.ts";
15
14
 
@@ -43,17 +42,18 @@ function childWarningDetails(details: WatchdogWarningDetails, config: ChildWatch
43
42
  };
44
43
  }
45
44
 
46
- function writeStatus(event: unknown): void {
47
- try {
48
- process.stdout.write(`${JSON.stringify(event)}\n`);
49
- } catch {
50
- // Child watchdog status is advisory; stdout failures are handled by the child process itself.
51
- }
52
- }
53
-
54
- export function registerChildWatchdog(pi: ExtensionAPI, rawConfig = process.env[CHILD_WATCHDOG_CONFIG_ENV]): MainWatchdogRuntime | undefined {
55
- const childConfig = decodeChildWatchdogConfig(rawConfig);
45
+ /**
46
+ * Register the child-side watchdog. Status events go to the sink the hosting
47
+ * process passed in the child runtime config; the host folds them into the
48
+ * child's event stream.
49
+ */
50
+ export function registerChildWatchdog(
51
+ pi: ExtensionAPI,
52
+ childConfig: ChildWatchdogConfig | undefined,
53
+ writeStatus: ((event: ChildWatchdogStatusEvent) => void) | undefined,
54
+ ): MainWatchdogRuntime | undefined {
56
55
  if (!childConfig) return undefined;
56
+ if (!writeStatus) throw new Error("Child watchdog status sink is missing; the host must pass ChildRuntimeConfig.watchdogStatus.");
57
57
  let currentContext: ExtensionContext | undefined;
58
58
  let diffBaseline: WatchdogDiffBaseline | undefined;
59
59
  let seq = 0;