pi-subagents 0.64.0 → 0.65.1

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 (106) hide show
  1. package/CHANGELOG.md +65 -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 +19 -17
  6. package/docs/configuration.md +23 -15
  7. package/docs/extension-api.md +18 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +15 -8
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +13 -3
  13. package/package.json +5 -4
  14. package/runner-server-preload.mjs +13 -0
  15. package/skills/pi-subagents/SKILL.md +2 -1
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  17. package/skills/pi-subagents/references/execution-controls.md +7 -3
  18. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  19. package/src/agents/agent-management.ts +47 -15
  20. package/src/api/capability-ceiling.ts +0 -1
  21. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  22. package/src/api/preflight.ts +7 -4
  23. package/src/extension/config.ts +4 -2
  24. package/src/extension/doctor.ts +2 -10
  25. package/src/extension/fanout-child.ts +9 -11
  26. package/src/extension/index.ts +58 -7
  27. package/src/extension/public-execution.ts +14 -0
  28. package/src/extension/rpc.ts +3 -2
  29. package/src/extension/schemas.ts +3 -2
  30. package/src/extension/tool-description.ts +13 -8
  31. package/src/integrations/pi-web-session-liveness.ts +73 -0
  32. package/src/intercom/native-supervisor-channel.ts +159 -95
  33. package/src/intercom/supervisor-ui.ts +244 -0
  34. package/src/missions/workflow-state.ts +37 -16
  35. package/src/runs/background/async-execution.ts +51 -25
  36. package/src/runs/background/async-job-tracker.ts +11 -0
  37. package/src/runs/background/async-resume.ts +14 -3
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/control-channel.ts +2 -204
  40. package/src/runs/background/notify.ts +2 -0
  41. package/src/runs/background/process-terminal.ts +1 -1
  42. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  43. package/src/runs/background/run-child-session.ts +614 -0
  44. package/src/runs/background/run-status.ts +0 -1
  45. package/src/runs/background/runner-aliases.ts +152 -0
  46. package/src/runs/background/runner-child-sessions.ts +31 -0
  47. package/src/runs/background/scheduled-runs.ts +18 -4
  48. package/src/runs/background/subagent-runner.ts +203 -901
  49. package/src/runs/foreground/async-steering-action.ts +1 -17
  50. package/src/runs/foreground/execution.ts +204 -378
  51. package/src/runs/foreground/foreground-control.ts +4 -0
  52. package/src/runs/foreground/foreground-history.ts +3 -1
  53. package/src/runs/foreground/prompt-audit.ts +9 -5
  54. package/src/runs/foreground/subagent-executor.ts +161 -91
  55. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  56. package/src/runs/shared/abort-recovery.ts +3 -3
  57. package/src/runs/shared/acceptance.ts +14 -1
  58. package/src/runs/shared/capability-ceiling.ts +1 -2
  59. package/src/runs/shared/child-hooks.ts +25 -0
  60. package/src/runs/shared/child-identity.ts +13 -2
  61. package/src/runs/shared/child-launch.ts +314 -0
  62. package/src/runs/shared/child-lifecycle.ts +25 -0
  63. package/src/runs/shared/child-runtime-config.ts +126 -0
  64. package/src/runs/shared/child-session.ts +337 -0
  65. package/src/runs/shared/child-tool-plan.ts +530 -0
  66. package/src/runs/shared/claude-code-adapter.ts +5 -1
  67. package/src/runs/shared/completion-guard.ts +1 -1
  68. package/src/runs/shared/external-cli-preflight.ts +16 -0
  69. package/src/runs/shared/llm-intent-arbiter.ts +20 -11
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  71. package/src/runs/shared/model-exclusions.ts +84 -15
  72. package/src/runs/shared/model-fallback.ts +78 -6
  73. package/src/runs/shared/nested-events.ts +32 -48
  74. package/src/runs/shared/nested-path.ts +0 -14
  75. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  76. package/src/runs/shared/parallel-utils.ts +1 -2
  77. package/src/runs/shared/permissions.ts +0 -13
  78. package/src/runs/shared/process-signal.ts +4 -1
  79. package/src/runs/shared/run-fanout-budget.ts +0 -13
  80. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  81. package/src/runs/shared/structured-output.ts +17 -4
  82. package/src/runs/shared/subagent-control.ts +2 -0
  83. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  84. package/src/runs/shared/tool-availability.ts +18 -62
  85. package/src/runs/shared/tool-budget.ts +0 -14
  86. package/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  87. package/src/runs/shared/worktree.ts +192 -40
  88. package/src/shared/child-session-name.ts +1 -1
  89. package/src/shared/jsonl-writer.ts +11 -0
  90. package/src/shared/model-response-aliases.ts +13 -0
  91. package/src/shared/thinking-ceiling.ts +0 -6
  92. package/src/shared/types.ts +63 -28
  93. package/src/shared/utils.ts +6 -4
  94. package/src/shared/watch-strategy.ts +2 -0
  95. package/src/slash/slash-commands.ts +0 -6
  96. package/src/tui/fleet-status.ts +1 -1
  97. package/src/tui/fleet.ts +0 -1
  98. package/src/tui/render.ts +254 -41
  99. package/src/watchdog/child-status.ts +0 -1
  100. package/src/watchdog/register-child.ts +12 -12
  101. package/src/workflows/scripted-workflow.ts +80 -7
  102. package/src/workflows/workflow-checklist.ts +4 -3
  103. package/src/runs/shared/child-protocol.ts +0 -415
  104. package/src/runs/shared/pi-args.ts +0 -1059
  105. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  106. package/src/shared/post-exit-stdio-guard.ts +0 -85
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),
@@ -1286,7 +1284,7 @@ function workflowChecklistItemPriority(state: WorkflowChecklistState): number {
1286
1284
  return 6;
1287
1285
  }
1288
1286
 
1289
- function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, indent: string, frame?: number): string {
1287
+ function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, indent: string, frame: number | undefined, includeError: boolean): string {
1290
1288
  const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter(Boolean).join(" · ");
1291
1289
  const context = contextModeBadge(theme, item.context);
1292
1290
  const state = item.state === "complete" ? "" : ` ${theme.fg("dim", `· ${workflowChecklistStateLabel(item.state)}`)}`;
@@ -1296,15 +1294,26 @@ function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, in
1296
1294
  !item.currentTool && item.durationMs !== undefined ? formatDuration(item.durationMs) : undefined,
1297
1295
  item.toolCount !== undefined ? `${item.toolCount} tools` : undefined,
1298
1296
  item.outputName ? `out:${item.outputName}` : undefined,
1299
- item.error ? `error:${oneLine(item.error)}` : undefined,
1297
+ includeError && item.error ? `error:${oneLine(item.error)}` : undefined,
1300
1298
  ].filter(Boolean).join(" · ");
1301
1299
  return `${indent}${workflowChecklistGlyph(item, theme, frame)} ${theme.bold(identity || item.key)}${context}${state}${details ? ` ${theme.fg("dim", `· ${details}`)}` : ""}`;
1302
1300
  }
1303
1301
 
1304
1302
  const COLLAPSED_WORKFLOW_PHASE_LIMIT = 4;
1305
1303
 
1306
- function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | undefined, theme: Theme, indent: string, expanded: boolean, frame?: number, includeSummary = true, limitPhases = !expanded, includeBottleneckOutput = expanded): string[] {
1304
+ interface WorkflowChecklistWidgetOptions {
1305
+ includeSummary?: boolean;
1306
+ limitPhases?: boolean;
1307
+ includeBottleneckOutput?: boolean;
1308
+ includeItemErrors?: boolean;
1309
+ }
1310
+
1311
+ function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | undefined, theme: Theme, indent: string, expanded: boolean, frame?: number, options: WorkflowChecklistWidgetOptions = {}): string[] {
1307
1312
  if (!checklist?.total) return [];
1313
+ const includeSummary = options.includeSummary ?? true;
1314
+ const limitPhases = options.limitPhases ?? !expanded;
1315
+ const includeBottleneckOutput = options.includeBottleneckOutput ?? expanded;
1316
+ const includeItemErrors = options.includeItemErrors ?? true;
1308
1317
  const lines = includeSummary ? [`${indent}${theme.fg("dim", `Checklist ${formatWorkflowChecklistSummary(checklist)}`)}`] : [];
1309
1318
  const phases = !limitPhases || checklist.phases.length <= COLLAPSED_WORKFLOW_PHASE_LIMIT
1310
1319
  ? checklist.phases
@@ -1328,11 +1337,11 @@ function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | u
1328
1337
  lines.push(`${indent}${glyph} ${theme.bold(formatWorkflowChecklistPhase(phase))}`);
1329
1338
  if (expanded) {
1330
1339
  for (const item of [...phase.items].sort((left, right) => workflowChecklistItemPriority(left.state) - workflowChecklistItemPriority(right.state))) {
1331
- lines.push(workflowChecklistItemLine(item, theme, `${indent} `, frame));
1340
+ lines.push(workflowChecklistItemLine(item, theme, `${indent} `, frame, includeItemErrors || item.kind === "host"));
1332
1341
  }
1333
1342
  }
1334
1343
  }
1335
- const bottleneck = formatWorkflowChecklistBottleneck(checklist.bottleneck, { includeOutput: includeBottleneckOutput });
1344
+ const bottleneck = formatWorkflowChecklistBottleneck(checklist.bottleneck, { includeOutput: includeBottleneckOutput, includeError: !expanded });
1336
1345
  if (bottleneck) {
1337
1346
  const tone = checklist.bottleneck?.state === "blocked" || checklist.bottleneck?.state === "failed" ? "error" : checklist.bottleneck?.state === "running" ? "accent" : "warning";
1338
1347
  lines.push(`${indent}${theme.fg(tone, `bottleneck · ${bottleneck}`)}`);
@@ -1340,6 +1349,208 @@ function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | u
1340
1349
  return lines;
1341
1350
  }
1342
1351
 
1352
+ interface CompactWorkflowLaneRow {
1353
+ key: string;
1354
+ state: WorkflowChecklistState;
1355
+ agent?: string;
1356
+ mode?: string;
1357
+ decision?: string;
1358
+ claims?: string;
1359
+ expectedOutput?: string;
1360
+ label?: string;
1361
+ toolUses?: number;
1362
+ durationMs?: number;
1363
+ }
1364
+
1365
+ interface CompactWorkflowLaneCounts {
1366
+ total: number;
1367
+ done: number;
1368
+ active: number;
1369
+ queued: number;
1370
+ blocked: number;
1371
+ failed: number;
1372
+ paused: number;
1373
+ stopped: number;
1374
+ }
1375
+
1376
+ function compactWorkflowLaneState(items: readonly WorkflowChecklistItem[]): WorkflowChecklistState {
1377
+ let state: WorkflowChecklistState | undefined;
1378
+ for (const item of items) {
1379
+ if (state === undefined || workflowChecklistItemPriority(item.state) < workflowChecklistItemPriority(state)) state = item.state;
1380
+ }
1381
+ return state ?? "queued";
1382
+ }
1383
+
1384
+ function compactWorkflowLaneOwner(items: readonly WorkflowChecklistItem[]): string | undefined {
1385
+ const owners = [...new Set(items.map((item) => item.agent ?? item.role).filter((value): value is string => Boolean(value?.trim())))];
1386
+ if (owners.length === 0) return undefined;
1387
+ return boundedLaneValue(owners.length > 2 ? `${owners.slice(0, 2).join(", ")} +${owners.length - 2}` : owners.join(", "), 32);
1388
+ }
1389
+
1390
+ function compactWorkflowLaneLabel(items: readonly WorkflowChecklistItem[], key: string): string | undefined {
1391
+ const labels = [...new Set(items.map((item) => item.label).filter((value): value is string => Boolean(value?.trim() && value.trim() !== key)))];
1392
+ if (labels.length === 0) return undefined;
1393
+ return boundedLaneValue(labels.length > 1 ? `${labels[0]} +${labels.length - 1}` : labels[0], 48);
1394
+ }
1395
+
1396
+ function compactWorkflowLaneRow(key: string, items: readonly WorkflowChecklistItem[], lane = items.find((item) => item.preflight)?.preflight, fallbackState: WorkflowChecklistState = "queued"): CompactWorkflowLaneRow {
1397
+ const state = items.length ? compactWorkflowLaneState(items) : fallbackState;
1398
+ const toolCounts = items.map((item) => item.toolCount).filter((value): value is number => value !== undefined);
1399
+ const durations = items.map((item) => item.durationMs).filter((value): value is number => value !== undefined);
1400
+ const label = compactWorkflowLaneLabel(items, key);
1401
+ return {
1402
+ key: boundedLaneValue(key, 40) ?? key,
1403
+ state,
1404
+ agent: compactWorkflowLaneOwner(items),
1405
+ ...(lane?.mode ? { mode: lane.mode } : {}),
1406
+ ...(lane?.decision ? { decision: boundedLaneValue(lane.decision, 56) } : {}),
1407
+ ...(lane?.claims?.length ? { claims: boundedLaneValue(lane.claims.join(", "), 56) } : {}),
1408
+ ...(lane?.expectedOutput ? { expectedOutput: boundedLaneValue(lane.expectedOutput, 56) } : {}),
1409
+ ...(label ? { label } : {}),
1410
+ ...(toolCounts.length ? { toolUses: toolCounts.reduce((sum, value) => sum + value, 0) } : {}),
1411
+ ...(durations.length ? { durationMs: Math.max(...durations) } : {}),
1412
+ };
1413
+ }
1414
+
1415
+ function compactWorkflowFallbackState(job: AsyncJobState): WorkflowChecklistState {
1416
+ if (job.activityState === "needs_attention" || job.timedOut || job.toolBudgetBlocked || job.turnBudgetExceeded) return "blocked";
1417
+ switch (job.status) {
1418
+ case "complete": return "complete";
1419
+ case "failed": return "failed";
1420
+ case "paused": return "paused";
1421
+ case "stopped": return "stopped";
1422
+ case "partial":
1423
+ case "rejected": return "blocked";
1424
+ default: return "queued";
1425
+ }
1426
+ }
1427
+
1428
+ /** Flatten the loaded checklist into one compact row per declared lane. */
1429
+ function compactWorkflowLaneRows(job: AsyncJobState, checklist: WorkflowChecklistProjection | undefined): CompactWorkflowLaneRow[] {
1430
+ const groups = new Map<string, WorkflowChecklistItem[]>();
1431
+ for (const item of checklist?.phases.flatMap((phase) => phase.items) ?? []) {
1432
+ const key = item.preflight?.key ?? item.key;
1433
+ const group = groups.get(key);
1434
+ if (group) group.push(item);
1435
+ else groups.set(key, [item]);
1436
+ }
1437
+
1438
+ const rows: CompactWorkflowLaneRow[] = [];
1439
+ const preflightKeys = new Set(job.preflight?.lanes.map((lane) => lane.key) ?? []);
1440
+ if (job.preflight?.lanes.length) {
1441
+ for (const lane of job.preflight.lanes) {
1442
+ rows.push(compactWorkflowLaneRow(lane.key, groups.get(lane.key) ?? [], lane, compactWorkflowFallbackState(job)));
1443
+ }
1444
+ }
1445
+
1446
+ for (const [key, items] of groups) {
1447
+ if (preflightKeys.has(key)) continue;
1448
+ rows.push(compactWorkflowLaneRow(key, items));
1449
+ }
1450
+ return rows;
1451
+ }
1452
+
1453
+ function compactWorkflowLaneCounts(rows: readonly CompactWorkflowLaneRow[]): CompactWorkflowLaneCounts {
1454
+ const counts: CompactWorkflowLaneCounts = { total: rows.length, done: 0, active: 0, queued: 0, blocked: 0, failed: 0, paused: 0, stopped: 0 };
1455
+ for (const row of rows) {
1456
+ switch (row.state) {
1457
+ case "complete": counts.done++; break;
1458
+ case "running": counts.active++; break;
1459
+ case "queued": counts.queued++; break;
1460
+ case "blocked": counts.blocked++; break;
1461
+ case "failed": counts.failed++; break;
1462
+ case "paused": counts.paused++; break;
1463
+ case "stopped": counts.stopped++; break;
1464
+ }
1465
+ }
1466
+ return counts;
1467
+ }
1468
+
1469
+ function compactWorkflowLaneLine(row: CompactWorkflowLaneRow, theme: Theme, indent: string, frame?: number): string {
1470
+ const owner = row.agent && row.mode ? `${row.agent}/${row.mode}` : row.agent ?? row.mode;
1471
+ const state = row.state === "complete" ? undefined : row.state === "running" ? "active" : row.state;
1472
+ const intent = [
1473
+ row.decision,
1474
+ row.claims,
1475
+ row.expectedOutput,
1476
+ !row.mode && row.label ? row.label : undefined,
1477
+ ].filter((value): value is string => Boolean(value));
1478
+ 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(" · ")}`)}` : ""}`;
1479
+ }
1480
+
1481
+ function compactWorkflowShortId(job: AsyncJobState): string {
1482
+ return job.asyncId.slice(0, 8);
1483
+ }
1484
+
1485
+ function compactWorkflowDisplayLabel(job: AsyncJobState): string {
1486
+ return boundedLaneValue(job.description, 64) ?? compactWorkflowShortId(job);
1487
+ }
1488
+
1489
+ function compactWorkflowStats(job: AsyncJobState, rows: readonly CompactWorkflowLaneRow[], theme: Theme, projection: WorkflowWidgetProjection): string {
1490
+ const counts = compactWorkflowLaneCounts(rows);
1491
+ const total = counts.total || projection.stageProgress?.total || job.stepsTotal || job.agents?.length || 0;
1492
+ const progress = [
1493
+ total > 0 ? `${counts.done}/${total} done` : undefined,
1494
+ counts.active ? `${counts.active} active` : undefined,
1495
+ counts.queued ? `${counts.queued} queued` : undefined,
1496
+ counts.blocked ? `${counts.blocked} blocked` : undefined,
1497
+ counts.failed ? `${counts.failed} failed` : undefined,
1498
+ counts.paused ? `${counts.paused} paused` : undefined,
1499
+ counts.stopped ? `${counts.stopped} stopped` : undefined,
1500
+ ].filter((value): value is string => Boolean(value));
1501
+ const rowToolUses = rows.map((row) => row.toolUses).filter((value): value is number => value !== undefined);
1502
+ const toolUses = job.toolCount ?? (rowToolUses.length ? rowToolUses.reduce((sum, value) => sum + value, 0) : undefined);
1503
+ const rowDuration = rows.map((row) => row.durationMs).filter((value): value is number => value !== undefined);
1504
+ const durationMs = job.startedAt !== undefined && job.updatedAt !== undefined
1505
+ ? Math.max(0, job.updatedAt - job.startedAt)
1506
+ : rowDuration.length ? Math.max(...rowDuration) : undefined;
1507
+ return statJoin(theme, [
1508
+ `id: ${compactWorkflowShortId(job)}`,
1509
+ ...progress,
1510
+ toolUses !== undefined ? formatToolUseStat(toolUses) : undefined,
1511
+ durationMs !== undefined ? formatDuration(durationMs) : undefined,
1512
+ ].filter((value): value is string => Boolean(value)));
1513
+ }
1514
+
1515
+ interface CompactWorkflowBottleneck {
1516
+ text: string;
1517
+ tone: "warning" | "error";
1518
+ }
1519
+
1520
+ function compactWorkflowBottleneck(rows: readonly CompactWorkflowLaneRow[], job: AsyncJobState): CompactWorkflowBottleneck | undefined {
1521
+ let row: CompactWorkflowLaneRow | undefined;
1522
+ for (const candidate of rows) {
1523
+ if (candidate.state !== "blocked" && candidate.state !== "failed" && candidate.state !== "paused" && candidate.state !== "stopped") continue;
1524
+ if (!row || workflowChecklistItemPriority(candidate.state) < workflowChecklistItemPriority(row.state)) row = candidate;
1525
+ }
1526
+ if (row) return { text: `${row.key} · ${row.state}`, tone: row.state === "blocked" || row.state === "failed" ? "error" : "warning" };
1527
+ 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" };
1528
+ if (job.status === "failed" || job.status === "partial" || job.status === "rejected") return { text: `workflow · ${job.status}`, tone: "error" };
1529
+ if (job.status === "paused" || job.status === "stopped") return { text: `workflow · ${job.status}`, tone: "warning" };
1530
+ return undefined;
1531
+ }
1532
+
1533
+ function compactWorkflowHeaderLine(job: AsyncJobState, theme: Theme, width?: number): string {
1534
+ const label = compactWorkflowDisplayLabel(job);
1535
+ const full = `${theme.fg("toolTitle", themeBold(theme, `async workflow: ${label}`))} ${theme.fg("dim", "─ background")}`;
1536
+ if (width === undefined || visibleWidth(full) <= width) return full;
1537
+ return `${theme.fg("toolTitle", themeBold(theme, `async workflow ${label}`))} ${theme.fg("dim", "· background")}`;
1538
+ }
1539
+
1540
+ function compactWorkflowWidgetBodyLines(job: AsyncJobState, theme: Theme, frame: number | undefined, projection: WorkflowWidgetProjection): string[] {
1541
+ const rows = compactWorkflowLaneRows(job, projection.checklist);
1542
+ const lines = [` ${compactWorkflowStats(job, rows, theme, projection)}`];
1543
+ if (rows.length) {
1544
+ for (const row of rows) lines.push(compactWorkflowLaneLine(row, theme, " ", frame));
1545
+ } else {
1546
+ lines.push(` ${theme.fg("dim", "◦ waiting for workflow lanes")}`);
1547
+ }
1548
+ const bottleneck = compactWorkflowBottleneck(rows, job);
1549
+ if (bottleneck) lines.push(` ${theme.fg(bottleneck.tone, `bottleneck · ${bottleneck.text}`)}`);
1550
+ if (job.status === "running" || rows.some((row) => row.state === "running")) lines.push(` ${theme.fg("accent", workflowDetailHintText())}`);
1551
+ return lines;
1552
+ }
1553
+
1343
1554
  function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], snapshotNow?: number): string {
1344
1555
  const facts: string[] = [];
1345
1556
  if (step.currentTool && step.currentToolStartedAt !== undefined && snapshotNow !== undefined) facts.push(`${step.currentTool} ${formatDuration(Math.max(0, snapshotNow - step.currentToolStartedAt))}`);
@@ -2154,19 +2365,13 @@ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string):
2154
2365
 
2155
2366
  function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
2156
2367
  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;
2368
+ if (!expanded && job.mode === "workflow") {
2369
+ return compactWorkflowWidgetBodyLines(job, theme, frame, projection);
2165
2370
  }
2166
2371
  if (!steps.length) {
2167
2372
  const lane = projectAsyncLane(job, laneStepForJob(job, steps));
2168
2373
  return [
2169
- ...workflowPreflightLines(job, expanded),
2374
+ ...(expanded ? workflowPreflightLines(job) : []),
2170
2375
  ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
2171
2376
  ...(lane ? formatLaneProjectionLines(lane, theme, " ") : []),
2172
2377
  ...hostStepWidgetLines(job, theme, " "),
@@ -2176,8 +2381,8 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
2176
2381
  }
2177
2382
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
2178
2383
  const lines: string[] = [
2179
- ...workflowPreflightLines(job, expanded),
2180
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
2384
+ ...(expanded ? workflowPreflightLines(job) : []),
2385
+ ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded }),
2181
2386
  ];
2182
2387
  const group = activeParallelWidgetGroup(job);
2183
2388
  if (group) {
@@ -2210,6 +2415,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
2210
2415
  }
2211
2416
 
2212
2417
  function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
2418
+ if (!expanded && job.mode === "workflow") return [compactWorkflowHeaderLine(job, theme, width), ...compactWorkflowWidgetBodyLines(job, theme, frame, projection)].map((line) => truncLine(line, width));
2213
2419
  const stats = widgetStats(job, theme, projection, !expanded);
2214
2420
  const count = job.mode === "workflow"
2215
2421
  ? projection.checklist?.total ?? projection.stageProgress?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length
@@ -2386,11 +2592,17 @@ function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: numbe
2386
2592
  }
2387
2593
 
2388
2594
  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);
2595
+ const compactWorkflow = job.mode === "workflow";
2596
+ const compactRows = compactWorkflow ? compactWorkflowLaneRows(job, projection.checklist) : undefined;
2597
+ const stats = compactWorkflow ? compactWorkflowStats(job, compactRows ?? [], theme, projection) : widgetStats(job, theme, projection, true);
2598
+ if (compactWorkflow) {
2599
+ const bottleneck = compactWorkflowBottleneck(compactRows ?? [], job);
2600
+ const suffix = [stats, bottleneck ? theme.fg(bottleneck.tone, `bottleneck · ${bottleneck.text}`) : undefined].filter((value): value is string => Boolean(value)).join(` ${theme.fg("dim", "·")} `);
2601
+ return truncLine(` ${widgetStatusGlyph(job, theme, frame)} ${compactWorkflowHeaderLine(job, theme, Math.max(0, width - 4))}${suffix ? ` ${theme.fg("dim", "·")} ${suffix}` : ""}`, width);
2602
+ }
2603
+ const activity = widgetActivity(job);
2392
2604
  const status = job.status === "complete" ? "done" : job.status;
2393
- const lane = checklistPrimary ? undefined : projectAsyncLane(job, laneStepForJob(job, projection.steps));
2605
+ const lane = projectAsyncLane(job, laneStepForJob(job, projection.steps));
2394
2606
  const laneSummary = lane
2395
2607
  ? [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
2608
  : "";
@@ -2557,21 +2769,22 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2557
2769
  let slots = MAX_WIDGET_JOBS;
2558
2770
  const appendJob = (job: AsyncJobState): void => {
2559
2771
  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
2772
+ const compactWorkflow = !expanded && job.mode === "workflow";
2773
+ const stats = compactWorkflow ? "" : widgetStats(job, theme, projection, !expanded);
2774
+ const details = compactWorkflow
2563
2775
  ? [
2564
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", false, frame, false),
2565
- ...(job.status === "running" || projection.checklist!.running > 0 ? [` ${theme.fg("accent", liveDetailHintText())}`] : []),
2776
+ ...compactWorkflowWidgetBodyLines(job, theme, frame, projection),
2566
2777
  ]
2567
2778
  : [
2568
2779
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
2569
2780
  ...widgetLaneDetailLines(job, theme, projection),
2570
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
2781
+ ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded || !job.steps?.length }),
2571
2782
  ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
2572
2783
  ];
2573
2784
  items.push([
2574
- `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
2785
+ compactWorkflow
2786
+ ? compactWorkflowHeaderLine(job, theme, width)
2787
+ : `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
2575
2788
  ...details,
2576
2789
  ]);
2577
2790
  };
@@ -2789,7 +3002,7 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
2789
3002
  if (d.preflight) c.addChild(new Text(truncLine(theme.fg("dim", formatWorkflowPreflightPlanSummary(d.preflight, { indent: rowIndent })), width), 0, 0));
2790
3003
  if (phase) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Phase ${phase}`), width), 0, 0));
2791
3004
  const checklist = projectWorkflowChecklist({ hostSteps: workflow?.receipt?.hostSteps, preflight: d.preflight, trace: workflow?.trace, now: Date.now() });
2792
- for (const line of workflowChecklistWidgetLines(checklist, theme, rowIndent, false, frame, true, !expanded, expanded)) {
3005
+ for (const line of workflowChecklistWidgetLines(checklist, theme, rowIndent, false, frame, { limitPhases: !expanded, includeBottleneckOutput: expanded })) {
2793
3006
  c.addChild(new Text(truncLine(line, width), 0, 0));
2794
3007
  }
2795
3008
  if (rows.length === 0) {
@@ -2871,7 +3084,7 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
2871
3084
  const detailIndent = mainWindowIndent(layout, 2);
2872
3085
  c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
2873
3086
  if (checklistPrimary) {
2874
- for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, rowIndent, false, frame, false)) {
3087
+ for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, rowIndent, false, frame, { includeSummary: false })) {
2875
3088
  c.addChild(new Text(truncLine(line, width), 0, 0));
2876
3089
  }
2877
3090
  if (hasRunning || workflowChecklist.running > 0) c.addChild(new Text(truncLine(theme.fg("accent", `${rowIndent}${liveDetailHintText()}`), width), 0, 0));
@@ -3222,7 +3435,7 @@ export function renderSubagentResult(
3222
3435
  c.addChild(new Text(fit(` ${chainVis}`), 0, 0));
3223
3436
  }
3224
3437
  const workflowChecklist = foregroundWorkflowChecklist(d);
3225
- for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, " ", true, frame)) {
3438
+ for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, " ", true, frame, { includeItemErrors: false })) {
3226
3439
  c.addChild(new Text(fit(line), 0, 0));
3227
3440
  }
3228
3441
 
@@ -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;