pi-subagents 0.65.1 → 0.67.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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -8,6 +8,7 @@ import { contextModeLabel } from "../runs/shared/context-mode.ts";
8
8
  import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
9
9
  import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
10
10
  import { isStaleExtensionContextError } from "../shared/extension-context.ts";
11
+ import { inlineWorkflowRenderKey } from "./render.ts";
11
12
  import { formatWorkflowChecklistBottleneck, formatWorkflowChecklistPhase, formatWorkflowChecklistSummary, projectWorkflowChecklist, type WorkflowChecklistPhase, type WorkflowChecklistProjection } from "../workflows/workflow-checklist.ts";
12
13
 
13
14
  export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
@@ -17,6 +18,35 @@ const MAX_AGENT_ROWS = 6;
17
18
  const REFRESH_MS = 500;
18
19
 
19
20
  type Theme = ExtensionContext["ui"]["theme"];
21
+
22
+ const FLEET_AGENT_IDENTITY_COLORS = [
23
+ "mdLink",
24
+ "mdHeading",
25
+ "syntaxFunction",
26
+ "syntaxKeyword",
27
+ "syntaxNumber",
28
+ "syntaxType",
29
+ "syntaxVariable",
30
+ "customMessageLabel",
31
+ "toolTitle",
32
+ "thinkingMedium",
33
+ "thinkingHigh",
34
+ "mdQuote",
35
+ "bashMode",
36
+ "userMessageText",
37
+ "mdCode",
38
+ "syntaxOperator",
39
+ ] as const satisfies readonly Exclude<Parameters<Theme["fg"]>[0], "accent" | "success" | "error" | "warning" | "muted" | "dim">[];
40
+
41
+ export function fleetAgentIdentityColor(identity: string): (typeof FLEET_AGENT_IDENTITY_COLORS)[number] {
42
+ let hash = 2166136261;
43
+ for (let i = 0; i < identity.length; i++) {
44
+ hash ^= identity.charCodeAt(i);
45
+ hash = Math.imul(hash, 16777619);
46
+ }
47
+ return FLEET_AGENT_IDENTITY_COLORS[(hash >>> 0) % FLEET_AGENT_IDENTITY_COLORS.length]!;
48
+ }
49
+
20
50
  type FleetStatusTui = {
21
51
  requestRender(): void;
22
52
  };
@@ -26,6 +56,7 @@ type FleetStatusEntry = {
26
56
  parentKey?: string;
27
57
  workflowWrapper?: boolean;
28
58
  agent: string;
59
+ displayLabel?: string;
29
60
  modelThinking?: string;
30
61
  description?: string;
31
62
  startedAt: number;
@@ -41,10 +72,12 @@ type FleetStatusEntry = {
41
72
 
42
73
  type FleetNestedRow = {
43
74
  name: string;
75
+ agentIdentity?: string;
44
76
  state: NestedRunSummary["state"] | NestedStepSummary["status"];
45
77
  modelThinking?: string;
46
78
  activity?: string;
47
79
  startedAt?: number;
80
+ endedAt?: number;
48
81
  depth: number;
49
82
  overflow?: number;
50
83
  };
@@ -60,6 +93,7 @@ export interface FleetStatusOptions {
60
93
  refreshMs?: number;
61
94
  maxAgentRows?: number;
62
95
  placement?: FleetViewPlacement;
96
+ onWorkflowCoverageChange?: (ui: ExtensionContext["ui"], coverage: ReadonlyMap<string, string>) => void;
63
97
  }
64
98
 
65
99
  export function resolveFleetViewPlacement(value: unknown): FleetViewPlacement {
@@ -70,14 +104,21 @@ export function formatFleetElapsed(ms: number): string {
70
104
  return `${Math.max(0, Math.round(ms / 1000))}s`;
71
105
  }
72
106
 
73
- export function formatFleetTokens(count: number, window?: number): string {
107
+ function detailElapsed(row: Pick<AsyncStatusWorkflowRow, "startedAt" | "endedAt" | "durationMs"> & { state: string }): string | undefined {
108
+ const duration = row.state === "running" && row.startedAt !== undefined
109
+ ? Date.now() - row.startedAt
110
+ : row.durationMs ?? (row.startedAt !== undefined && row.endedAt !== undefined ? row.endedAt - row.startedAt : undefined);
111
+ return duration !== undefined ? formatFleetElapsed(duration) : undefined;
112
+ }
113
+
114
+ export function formatFleetTokens(count: number, window?: number, windowCount = 1): string {
74
115
  const compact = (value: number): string => value >= 1_000_000
75
116
  ? `${(value / 1_000_000).toFixed(1)}M`
76
117
  : value >= 1_000
77
118
  ? `${(value / 1_000).toFixed(1)}k`
78
119
  : `${Math.max(0, Math.round(value))}`;
79
120
  return window !== undefined
80
- ? `↓ ${compact(window)} window · ${compact(count)} spent`
121
+ ? `↓ ${compact(window)} ${windowCount > 1 ? "Σ windows" : "window"} · ${compact(count)} spent`
81
122
  : `↓ ${compact(count)} tokens`;
82
123
  }
83
124
 
@@ -184,11 +225,13 @@ function nestedFleetRows(children: NestedRunSummary[] | undefined, visibleLimit:
184
225
  const activity = nestedActivity(step);
185
226
  rows.push({
186
227
  name: step.agent,
228
+ agentIdentity: step.agent,
187
229
  state: step.status,
188
230
  depth,
189
231
  ...(modelThinking ? { modelThinking } : {}),
190
232
  ...(activity ? { activity } : {}),
191
233
  ...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
234
+ ...(step.endedAt !== undefined ? { endedAt: step.endedAt } : {}),
192
235
  });
193
236
  if (!appendRuns(step.children, depth + 1)) {
194
237
  omitted += nestedStepDisplayCount(steps, stepIndex + 1);
@@ -206,11 +249,13 @@ function nestedFleetRows(children: NestedRunSummary[] | undefined, visibleLimit:
206
249
  const activity = nestedActivity(child);
207
250
  rows.push({
208
251
  name: nestedRunLabel(child),
252
+ agentIdentity: child.agent ?? child.agents?.join("\0") ?? child.id,
209
253
  state: child.state,
210
254
  depth,
211
255
  ...(modelThinking ? { modelThinking } : {}),
212
256
  ...(activity ? { activity } : {}),
213
257
  ...(child.startedAt !== undefined ? { startedAt: child.startedAt } : {}),
258
+ ...(child.endedAt !== undefined ? { endedAt: child.endedAt } : {}),
214
259
  });
215
260
  }
216
261
  if (!appendRuns(child.children, depth + 1)) {
@@ -320,6 +365,7 @@ function projectPaneEntries(state: SubagentState): FleetStatusEntry[] {
320
365
  }
321
366
 
322
367
  export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntry[] {
368
+ const now = Date.now();
323
369
  const entries: FleetStatusEntry[] = [];
324
370
  const activeWorkflowKeys = new Set([...state.asyncJobs.values()]
325
371
  .filter((job) => job.mode === "workflow" && isActiveState(job.status))
@@ -380,7 +426,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
380
426
 
381
427
  for (const job of state.asyncJobs.values()) {
382
428
  if (!isActiveState(job.status)) continue;
383
- const startedAt = job.startedAt ?? job.updatedAt ?? Date.now();
429
+ const startedAt = job.startedAt ?? job.updatedAt ?? now;
384
430
  const linkedParentKey = linkedWorkflowParentKey(job.parentWorkflowRunId, activeWorkflowKeys);
385
431
  if (job.mode === "workflow") {
386
432
  const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
@@ -392,7 +438,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
392
438
  hostSteps: job.hostSteps,
393
439
  preflight: job.preflight,
394
440
  trace: job.workflow?.trace,
395
- now: job.updatedAt ?? Date.now(),
441
+ now,
396
442
  });
397
443
  entries.push({
398
444
  key: `async:${job.asyncId}`,
@@ -439,7 +485,8 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
439
485
  entries.push({
440
486
  key: `async:${job.asyncId}:${index}`,
441
487
  ...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
442
- agent: step.label ? `${step.label} (${step.agent})` : step.agent,
488
+ agent: step.agent,
489
+ ...(step.label ? { displayLabel: `${step.label} (${step.agent})` } : {}),
443
490
  ...(modelThinking ? { modelThinking } : {}),
444
491
  description: step.description ?? job.description,
445
492
  startedAt: step.startedAt ?? startedAt,
@@ -492,6 +539,8 @@ export class SubagentFleetStatus {
492
539
  private inspectorOpen = false;
493
540
  private lastRenderKey = "";
494
541
  private entries: FleetStatusEntry[] = [];
542
+ private workflowSnapshots = new Map<string, { snapshot: string; childRows: Set<string> }>();
543
+ private readonly onWorkflowCoverageChange: FleetStatusOptions["onWorkflowCoverageChange"];
495
544
  private readonly state: SubagentState;
496
545
  private readonly openInspector: (itemKey: string) => Promise<void> | void;
497
546
  private readonly refreshMs: number;
@@ -508,10 +557,14 @@ export class SubagentFleetStatus {
508
557
  this.refreshMs = options.refreshMs ?? REFRESH_MS;
509
558
  this.maxAgentRows = options.maxAgentRows ?? MAX_AGENT_ROWS;
510
559
  this.placement = options.placement ?? "belowEditor";
560
+ this.onWorkflowCoverageChange = options.onWorkflowCoverageChange;
511
561
  }
512
562
 
513
563
  setContext(ctx: ExtensionContext): void {
514
- if (!ctx.hasUI) return;
564
+ if (!ctx.hasUI) {
565
+ this.clearUiRegistration();
566
+ return;
567
+ }
515
568
  const ui = ctx.ui;
516
569
  if (this.ui === ui) {
517
570
  this.ctx = ctx;
@@ -548,6 +601,52 @@ export class SubagentFleetStatus {
548
601
  return;
549
602
  }
550
603
  this.entries = collectFleetStatusEntries(this.state);
604
+ this.workflowSnapshots.clear();
605
+ if (this.active && !this.inspectorOpen && !this.state.fleetInspectorOpen && this.onWorkflowCoverageChange) {
606
+ const childrenByParent = new Map<string, AsyncJobState[]>();
607
+ for (const child of this.state.asyncJobs.values()) {
608
+ if (!child.parentWorkflowRunId) continue;
609
+ const children = childrenByParent.get(child.parentWorkflowRunId) ?? [];
610
+ children.push(child);
611
+ childrenByParent.set(child.parentWorkflowRunId, children);
612
+ }
613
+ const attachedByParent = new Map<string, FleetStatusEntry[]>();
614
+ for (const entry of this.entries) {
615
+ if (!entry.parentKey) continue;
616
+ const attached = attachedByParent.get(entry.parentKey) ?? [];
617
+ attached.push(entry);
618
+ attachedByParent.set(entry.parentKey, attached);
619
+ }
620
+ for (const job of this.state.asyncJobs.values()) {
621
+ // Fleet does not expand attached workflow wrappers or step descendants.
622
+ // Unknown/unsupported coverage deliberately retains the async tree.
623
+ if (job.mode !== "workflow" || !isActiveState(job.status) || job.parentWorkflowRunId
624
+ || job.nestedChildren?.length || job.steps?.some((step) => step.children?.length)) continue;
625
+ const key = `async:${job.asyncId}`;
626
+ const children = childrenByParent.get(job.asyncId) ?? [];
627
+ const attached = attachedByParent.get(key) ?? [];
628
+ if (children.length && job.status !== "running") continue;
629
+ // Even without workflow rows, these children plus their owner cannot fit.
630
+ if (attached.length >= this.maxAgentRows) continue;
631
+ const rowKeys = new Set<string>();
632
+ let unsupported = false;
633
+ for (const child of children) {
634
+ if (!isActiveState(child.status) || (child.mode !== "single" && child.mode !== "parallel" && child.mode !== "chain")
635
+ || child.hostSteps?.length || child.workflowGraph
636
+ || childrenByParent.has(child.asyncId) || child.nestedChildren?.length
637
+ || child.steps?.some((step) => step.children?.length || step.runner || !isActiveState(step.status))) {
638
+ unsupported = true; break;
639
+ }
640
+ const count = child.steps?.length || child.agents?.length || 0;
641
+ if (!count || (child.stepsTotal ?? 0) > count || (child.chainStepCount ?? 0) > count) {
642
+ unsupported = true; break;
643
+ }
644
+ for (let index = 0; index < count; index++) rowKeys.add(`async:${child.asyncId}:${child.steps?.[index]?.index ?? index}`);
645
+ }
646
+ if (unsupported || attached.length !== rowKeys.size || attached.some((entry) => !rowKeys.has(entry.key))) continue;
647
+ this.workflowSnapshots.set(key, { snapshot: inlineWorkflowRenderKey(job, children), childRows: rowKeys });
648
+ }
649
+ }
551
650
  this.clampSelection();
552
651
  if (this.inspectorOpen || this.state.fleetInspectorOpen) {
553
652
  this.lastRenderKey = "";
@@ -563,6 +662,7 @@ export class SubagentFleetStatus {
563
662
  }
564
663
 
565
664
  const renderKey = this.getRenderKey();
665
+ if (!this.active || renderKey !== this.lastRenderKey) this.clearWorkflowCoverage();
566
666
  if (!this.widgetRegistered) {
567
667
  ctx.ui.setWidget(FLEET_STATUS_WIDGET_KEY, (tui, theme) => {
568
668
  this.tui = tui;
@@ -573,6 +673,7 @@ export class SubagentFleetStatus {
573
673
  },
574
674
  dispose: () => {
575
675
  if (this.tui !== tui) return;
676
+ this.clearWorkflowCoverage();
576
677
  this.widgetRegistered = false;
577
678
  this.tui = undefined;
578
679
  },
@@ -654,17 +755,24 @@ export class SubagentFleetStatus {
654
755
  }
655
756
 
656
757
  render(width: number, theme: Theme): string[] {
657
- if (!this.hasInlineSurface()) return [];
758
+ if (!this.hasInlineSurface() || this.state.widgetsSuspended || this.inspectorOpen || this.state.fleetInspectorOpen) {
759
+ this.clearWorkflowCoverage();
760
+ return [];
761
+ }
658
762
  if (!this.active) {
763
+ this.clearWorkflowCoverage();
659
764
  const workEntries = this.entries.filter((entry) => !entry.surface);
660
765
  const projectEntries = this.entries.filter((entry) => entry.surface === "project-pane");
661
- const tokens = workEntries.reduce((total, entry) => total + entry.tokens, 0);
662
- const nativeEntries = workEntries.filter((entry) => !entry.external);
766
+ // Workflow totals can overlap child usage and omit live lanes. Do not
767
+ // present either wrapper totals or an active-only sum as workflow spend.
768
+ const hasWorkflow = workEntries.some((entry) => entry.workflowWrapper);
769
+ const nativeEntries = workEntries.filter((entry) => !entry.external && !entry.workflowWrapper && !entry.parentKey);
770
+ const tokens = nativeEntries.reduce((total, entry) => total + entry.tokens, 0);
663
771
  const window = nativeEntries.length > 0 && nativeEntries.every((entry) => entry.window !== undefined)
664
772
  ? nativeEntries.reduce((total, entry) => total + entry.window!, 0)
665
773
  : undefined;
666
774
  const capacity = this.state.activeAsyncCapacity;
667
- const hasNativeRows = workEntries.some((entry) => !entry.external);
775
+ const hasNativeRows = nativeEntries.length > 0 || hasWorkflow;
668
776
  const showNativeSummary = hasNativeRows || Boolean(capacity?.used);
669
777
  const asyncRuns = capacity && showNativeSummary && (capacity.used > 0 || capacity.limit > 0) ? `Async runs ${capacity.used}/${capacity.limit || "∞"}` : "";
670
778
  const activeEntries = activeLeafAgentCount(workEntries);
@@ -673,7 +781,11 @@ export class SubagentFleetStatus {
673
781
  const paneAttention = projectEntries.filter((entry) => entry.projectPane && projectPaneNeedsAttention(entry.projectPane)).length;
674
782
  const panes = projectEntries.length > 0 ? `${projectEntries.length} pane${projectEntries.length === 1 ? "" : "s"}${paneAttention ? ` (${paneAttention} ⚠)` : ""}` : "";
675
783
  const label = [agents, asyncRuns, panes].filter(Boolean).join(" · ");
676
- const detail = [showNativeSummary ? formatFleetTokens(tokens, window) : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
784
+ const nativeUsage = formatFleetTokens(tokens, window, nativeEntries.length);
785
+ const usage = hasWorkflow
786
+ ? nativeEntries.length > 0 ? `standalone: ${nativeUsage} · workflow usage on child rows` : "usage on child rows"
787
+ : nativeUsage;
788
+ const detail = [showNativeSummary ? usage : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
677
789
  return [truncateToWidth(` ${theme.fg("muted", label)}${label && detail ? " · " : ""}${theme.fg("dim", detail)}`, width)];
678
790
  }
679
791
  const roster = this.rosterKeys();
@@ -706,6 +818,26 @@ export class SubagentFleetStatus {
706
818
  }
707
819
  }
708
820
  if (hiddenBelow > 0) lines.push(rightAlign("", theme.fg("dim", `↓ ${hiddenBelow} more`), width));
821
+ if (this.ui && this.widgetRegistered && this.onWorkflowCoverageChange) {
822
+ const coverage = new Map<string, string>();
823
+ for (const [key, { snapshot, childRows }] of this.workflowSnapshots) {
824
+ const ownerIndex = tree.findIndex((row) => row.kind === "owner" && row.entry.key === key);
825
+ const owner = tree[ownerIndex];
826
+ if (owner?.kind !== "owner" || ownerIndex < start) continue;
827
+ const count = childRows.size + (owner.entry.workflowRows?.length ?? 0) + (owner.entry.workflowChecklist?.phases.length ?? 0);
828
+ if (!count || ownerIndex + count >= start + visibleCount) continue;
829
+ const descendants = tree.slice(ownerIndex + 1, ownerIndex + count + 1);
830
+ if (!descendants.every((row) => (row.kind === "workflow" && row.ownerKey === key && !row.row.overflow
831
+ && visibleWidth(this.renderWorkflowRow(row.row, row.last, Infinity, theme)) <= width)
832
+ || (row.kind === "workflow-phase" && row.ownerKey === key
833
+ && visibleWidth(this.renderWorkflowPhaseRow(row.phase, row.last, Infinity, theme)) <= width)
834
+ || (row.kind === "child" && childRows.has(row.entry.key)
835
+ && visibleWidth(this.renderEntry(rosterIndexByKey.get(row.entry.key) ?? 0, selectedIndex, row.entry, 0, theme, row.last ? "└─" : "├─", true)) <= width))) continue;
836
+ if (childRows.size && visibleWidth(this.renderEntry(rosterIndexByKey.get(key) ?? 0, selectedIndex, owner.entry, 0, theme, undefined, true)) > width) continue;
837
+ coverage.set(key.slice("async:".length), snapshot);
838
+ }
839
+ this.onWorkflowCoverageChange(this.ui, coverage);
840
+ }
709
841
  this.renderProjectPaneSection(lines, selectedIndex, width, theme, rosterIndexByKey);
710
842
  return lines;
711
843
  }
@@ -721,18 +853,20 @@ export class SubagentFleetStatus {
721
853
  }
722
854
 
723
855
 
724
- private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme, branch?: string): string {
725
- const agent = entry.modelThinking ? `${entry.agent} (${entry.modelThinking})` : entry.agent;
856
+ private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme, branch?: string, unclipped = false): string {
857
+ const label = entry.displayLabel ?? entry.agent;
858
+ const agent = entry.modelThinking ? `${label} (${entry.modelThinking})` : label;
726
859
  const prefix = branch ? ` ${branch}` : " ";
727
860
  const checklist = entry.workflowWrapper && entry.workflowChecklist
728
861
  ? ` · checklist ${formatWorkflowChecklistSummary(entry.workflowChecklist)}${entry.workflowChecklist.bottleneck ? ` · bottleneck ${formatWorkflowChecklistBottleneck(entry.workflowChecklist.bottleneck)}` : ""}`
729
862
  : "";
730
- const left = `${prefix} ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}${checklist}`;
863
+ const left = `${prefix} ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg(fleetAgentIdentityColor(entry.agent), agent)} · ${entry.state}${checklist}`;
731
864
  const elapsed = Date.now() - entry.startedAt;
732
865
  const rightText = entry.projectPane
733
866
  ? `${entry.projectPane.summary ?? "—"} · ${formatFleetElapsed(Date.now() - entry.projectPane.refreshedAt)} ago`
734
- : entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens, entry.window)}`;
867
+ : entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${entry.workflowWrapper ? "usage on child rows" : formatFleetTokens(entry.tokens, entry.window)}`;
735
868
  const right = theme.fg("dim", rightText);
869
+ if (unclipped) return `${left} ${right}`;
736
870
  return rightAlign(left, right, width);
737
871
  }
738
872
 
@@ -742,9 +876,9 @@ export class SubagentFleetStatus {
742
876
  if (row.overflow !== undefined) return truncateToWidth(`${indent}${marker} ${theme.fg("dim", `+${row.overflow} nested leaves`)}`, width);
743
877
  const modelThinking = row.modelThinking ? ` (${row.modelThinking})` : "";
744
878
  const activity = row.activity ? ` · ${row.activity}` : "";
745
- const left = `${indent}${marker} ${nestedStatusGlyph(row.state, theme)} ${theme.fg("muted", `${row.name}${modelThinking}`)} · ${row.state}${activity}`;
746
- const elapsed = row.startedAt !== undefined ? ` · ${formatFleetElapsed(Date.now() - row.startedAt)}` : "";
747
- return truncateToWidth(`${left}${theme.fg("dim", elapsed)}`, width);
879
+ const left = `${indent}${marker} ${nestedStatusGlyph(row.state, theme)} ${theme.fg(fleetAgentIdentityColor(row.agentIdentity ?? row.name), `${row.name}${modelThinking}`)} · ${row.state}${activity}`;
880
+ const elapsed = detailElapsed(row);
881
+ return truncateToWidth(`${left}${elapsed !== undefined ? theme.fg("dim", ` · ${elapsed}`) : ""}`, width);
748
882
  }
749
883
 
750
884
  private workflowRowGlyph(row: AsyncStatusWorkflowRow, theme: Theme): string {
@@ -797,7 +931,7 @@ export class SubagentFleetStatus {
797
931
  ].filter((value): value is string => Boolean(value)).join(" · ") : "";
798
932
  const left = `${indent}${marker} ${this.workflowRowGlyph(row, theme)} ${theme.fg("muted", `${kind}${row.name}${context ? ` ${context}` : ""}${modelThinking}`)} · ${this.workflowRowStateLabel(row, theme)}${activity}${hints ? ` · ${hints}` : ""}`;
799
933
  const details = [
800
- row.startedAt !== undefined ? formatFleetElapsed(Date.now() - row.startedAt) : undefined,
934
+ detailElapsed(row),
801
935
  row.tokens !== undefined ? formatFleetTokens(row.tokens, row.window) : undefined,
802
936
  row.provider ? `provider:${row.provider}` : undefined,
803
937
  row.role ? `role:${row.role}` : undefined,
@@ -853,6 +987,7 @@ export class SubagentFleetStatus {
853
987
  entry.surface,
854
988
  entry.parentKey,
855
989
  entry.agent,
990
+ entry.displayLabel,
856
991
  entry.state,
857
992
  entry.modelThinking,
858
993
  entry.description,
@@ -876,6 +1011,8 @@ export class SubagentFleetStatus {
876
1011
  row.modelThinking,
877
1012
  row.activity,
878
1013
  row.startedAt,
1014
+ row.endedAt,
1015
+ row.durationMs,
879
1016
  row.tokens,
880
1017
  row.provider,
881
1018
  row.role,
@@ -889,10 +1026,12 @@ export class SubagentFleetStatus {
889
1026
  ]),
890
1027
  nestedFleetRows(entry.nestedChildren, entry.parentKey ? 3 : 4).map((row) => [
891
1028
  row.name,
1029
+ row.agentIdentity,
892
1030
  row.state,
893
1031
  row.modelThinking,
894
1032
  row.activity,
895
1033
  row.startedAt,
1034
+ row.endedAt,
896
1035
  row.depth,
897
1036
  row.overflow,
898
1037
  ]),
@@ -918,6 +1057,7 @@ export class SubagentFleetStatus {
918
1057
  }
919
1058
 
920
1059
  private clearWidget(): void {
1060
+ this.clearWorkflowCoverage();
921
1061
  if (!this.widgetRegistered) return;
922
1062
  try {
923
1063
  this.ui?.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
@@ -931,6 +1071,7 @@ export class SubagentFleetStatus {
931
1071
  }
932
1072
 
933
1073
  private clearUiRegistration(): void {
1074
+ this.clearWorkflowCoverage();
934
1075
  if (this.timer) clearInterval(this.timer);
935
1076
  this.timer = undefined;
936
1077
 
@@ -961,4 +1102,8 @@ export class SubagentFleetStatus {
961
1102
  throw new AggregateError(cleanupErrors, "Failed to clean up FleetView UI registration");
962
1103
  }
963
1104
  }
1105
+
1106
+ private clearWorkflowCoverage(): void {
1107
+ if (this.ui) this.onWorkflowCoverageChange?.(this.ui, new Map());
1108
+ }
964
1109
  }
package/src/tui/fleet.ts CHANGED
@@ -18,8 +18,8 @@ import { resolveWorkflowForegroundSteeringTarget, steerWorkflowForegroundTarget
18
18
  import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.ts";
19
19
  import { FLEET_STATUS_WIDGET_KEY } from "./fleet-status.ts";
20
20
  import { readFleetTranscript, renderFleetTranscript, type FleetTranscript } from "./fleet-transcript.ts";
21
- import { handleHerdrInspectorAction } from "../inspectors/herdr/actions.ts";
22
- import type { HerdrClient } from "../inspectors/herdr/client.ts";
21
+ import { handleInspectorAction } from "../inspectors/actions.ts";
22
+ import type { InspectorPlugin } from "../inspectors/types.ts";
23
23
  import { getLivePromptAudit, type LivePromptAudit, type PromptAuditView } from "../runs/foreground/prompt-audit.ts";
24
24
 
25
25
  const REFRESH_MS = 750;
@@ -110,7 +110,8 @@ export interface FleetViewOptions {
110
110
  fleetKeybindings?: FleetKeybindingsConfig;
111
111
  actions?: FleetActionHandlers;
112
112
  copyText?: (text: string) => Promise<void> | void;
113
- herdrClient?: HerdrClient;
113
+ inspectorPlugins?: readonly InspectorPlugin[];
114
+ inspectorEnv?: NodeJS.ProcessEnv;
114
115
  }
115
116
 
116
117
  function belongsToCurrentSession(sessionId: string | undefined, currentSessionId: string | null): boolean {
@@ -179,10 +180,16 @@ function asyncItems(run: AsyncRunSummary, description?: string): FleetItem[] {
179
180
  }));
180
181
  }
181
182
 
182
- function orderFleetAsyncRuns(runs: AsyncRunSummary[], terminalLimit: number): AsyncRunSummary[] {
183
+ function orderFleetAsyncRuns(runs: AsyncRunSummary[], terminalLimit: number, trackedJobs: Map<string, AsyncJobState>): AsyncRunSummary[] {
183
184
  const updatedAt = (run: AsyncRunSummary) => run.lastUpdate ?? run.endedAt ?? run.startedAt;
184
185
  const byNewest = (left: AsyncRunSummary, right: AsyncRunSummary) => updatedAt(right) - updatedAt(left);
185
- const active = runs.filter((run) => run.state === "queued" || run.state === "running").sort(byNewest);
186
+ // Missing tracked starts must not inherit the summary's heartbeat-based display fallback.
187
+ const startedAt = (run: AsyncRunSummary) => {
188
+ const job = trackedJobs.get(run.id);
189
+ return job ? job.startedAt ?? 0 : run.startedAt;
190
+ };
191
+ const byStartedAt = (left: AsyncRunSummary, right: AsyncRunSummary) => startedAt(left) - startedAt(right) || left.id.localeCompare(right.id);
192
+ const active = runs.filter((run) => run.state === "queued" || run.state === "running").sort(byStartedAt);
186
193
  const terminal = runs.filter((run) => run.state !== "queued" && run.state !== "running").sort(byNewest);
187
194
  return [...active, ...terminal.slice(0, terminalLimit)];
188
195
  }
@@ -208,7 +215,7 @@ export function collectFleetSnapshot(
208
215
  liveWorkflowForegroundControls.add(control);
209
216
  workflowForegroundChildCounts.set(control.parentWorkflowRunId, (workflowForegroundChildCounts.get(control.parentWorkflowRunId) ?? 0) + activeChildCount);
210
217
  }
211
- for (const control of [...state.foregroundControls.values()].sort((left, right) => right.updatedAt - left.updatedAt)) {
218
+ for (const control of [...state.foregroundControls.values()].sort((left, right) => left.startedAt - right.startedAt || left.runId.localeCompare(right.runId))) {
212
219
  activeForegroundIds.add(control.runId);
213
220
  if (control.parentWorkflowRunId && workflowParentIds.has(control.parentWorkflowRunId)
214
221
  && ((workflowForegroundChildCounts.get(control.parentWorkflowRunId) ?? 0) <= 1 || !liveWorkflowForegroundControls.has(control))) continue;
@@ -249,7 +256,7 @@ export function collectFleetSnapshot(
249
256
  const tracked = [...trackedJobs.values()]
250
257
  .filter((job) => belongsToCurrentSession(job.sessionId, state.currentSessionId));
251
258
  const byUpdate = (left: AsyncJobState, right: AsyncJobState) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0);
252
- const active = tracked.filter((job) => job.status === "queued" || job.status === "running").sort(byUpdate);
259
+ const active = tracked.filter((job) => job.status === "queued" || job.status === "running");
253
260
  const recent = tracked.filter((job) => job.status !== "queued" && job.status !== "running").sort(byUpdate).slice(0, options.limit ?? MAX_RECENT_ASYNC_RUNS);
254
261
  const trackedRuns: AsyncRunSummary[] = [];
255
262
  for (const job of [...active, ...recent]) {
@@ -272,7 +279,7 @@ export function collectFleetSnapshot(
272
279
  } else {
273
280
  runs = trackedRuns;
274
281
  }
275
- for (const run of orderFleetAsyncRuns(runs, options.limit ?? MAX_RECENT_ASYNC_RUNS)) {
282
+ for (const run of orderFleetAsyncRuns(runs, options.limit ?? MAX_RECENT_ASYNC_RUNS, trackedJobs)) {
276
283
  items.push(...asyncItems(run, descriptions.get(run.id)));
277
284
  }
278
285
  } catch (cause) {
@@ -937,23 +944,23 @@ export class SubagentFleetComponent implements Component {
937
944
  return { runId: target.item.runId, asyncDir: target.item.run.asyncDir, ...(target.item.index !== undefined ? { index: target.item.index } : {}) };
938
945
  }
939
946
 
940
- private selectedHerdrInspectAction(): { runId: string; asyncDir: string; index?: number } | { reason: string } {
947
+ private selectedInspectAction(): { runId: string; asyncDir: string; index?: number } | { reason: string } {
941
948
  const item = this.snapshot.items[this.selected];
942
949
  if (!item) return { reason: "No child is selected." };
943
- if (item.kind === "external") return { reason: "External jobs are display-only and have no Herdr controls." };
950
+ if (item.kind === "external") return { reason: "External jobs are display-only and have no inspector controls." };
944
951
  if (item.kind === "async") {
945
952
  if (!isActionableAsyncState(item.run.state) || !isActionableAsyncState(item.state)) return { reason: `Selected child is ${item.state}; controls require a running or queued async child.` };
946
953
  return { runId: item.runId, asyncDir: item.run.asyncDir, ...(item.index !== undefined ? { index: item.index } : {}) };
947
954
  }
948
955
  if (item.kind !== "foreground-active" || !item.control.parentWorkflowRunId) return { reason: "Fleet controls are available for current-session top-level async runs only." };
949
956
  const parent = this.state.asyncJobs.get(item.control.parentWorkflowRunId) ?? this.state.fleetJobs?.get(item.control.parentWorkflowRunId);
950
- if (!parent || !isActionableAsyncState(parent.status)) return { reason: "The parent workflow is no longer available for Herdr inspection." };
957
+ if (!parent || !isActionableAsyncState(parent.status)) return { reason: "The parent workflow is no longer available for inspection." };
951
958
  return { runId: parent.asyncId, asyncDir: parent.asyncDir };
952
959
  }
953
960
 
954
- private inspectSelectedHerdr(): void {
955
- const target = this.selectedHerdrInspectAction();
956
- if ("reason" in target || !this.options.actions?.inspect) this.setActionNotice({ text: "reason" in target ? target.reason : "Herdr inspector controls are unavailable in this context.", isError: true });
961
+ private inspectSelected(): void {
962
+ const target = this.selectedInspectAction();
963
+ if ("reason" in target || !this.options.actions?.inspect) this.setActionNotice({ text: "reason" in target ? target.reason : "Inspector controls are unavailable in this context.", isError: true });
957
964
  else this.runAction(() => this.options.actions!.inspect!(target));
958
965
  }
959
966
 
@@ -1203,7 +1210,7 @@ export class SubagentFleetComponent implements Component {
1203
1210
  return;
1204
1211
  }
1205
1212
  if (matchesFleetAction(data, this.keybindings, "inspect")) {
1206
- this.inspectSelectedHerdr();
1213
+ this.inspectSelected();
1207
1214
  return;
1208
1215
  }
1209
1216
  }
@@ -1358,7 +1365,7 @@ export class SubagentFleetComponent implements Component {
1358
1365
  ? ` j/k child · 1/2/3 view · g redo with guidance · c copy · Esc close Prompt Audit · ${position}`
1359
1366
  : selected?.kind === "external"
1360
1367
  ? ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} job · display-only · ${bindingLabel(this.keybindings, "refresh")} refresh · ${bindingLabel(this.keybindings, "close")} close · ${position}`
1361
- : ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} agent · p Prompt Audit · ${bindingLabel(this.keybindings, "inspect")} Herdr · ${bindingLabel(this.keybindings, "steer")} steer · ${bindingLabel(this.keybindings, "stop")} stop · ${bindingLabel(this.keybindings, "toggleTools")} tools · ${bindingLabel(this.keybindings, "refresh")} refresh · ${bindingLabel(this.keybindings, "close")} close · ${position}`;
1368
+ : ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} agent · p Prompt Audit · ${bindingLabel(this.keybindings, "inspect")} Inspect · ${bindingLabel(this.keybindings, "steer")} steer · ${bindingLabel(this.keybindings, "stop")} stop · ${bindingLabel(this.keybindings, "toggleTools")} tools · ${bindingLabel(this.keybindings, "refresh")} refresh · ${bindingLabel(this.keybindings, "close")} close · ${position}`;
1362
1369
  lines.push(this.theme.fg("border", "│") + fit(this.theme.fg("dim", footer), innerWidth) + this.theme.fg("border", "│"));
1363
1370
  lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
1364
1371
  return lines.map((line) => truncateToWidth(line, width));
@@ -1407,7 +1414,7 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
1407
1414
  }), `Failed to steer async run ${input.runId}.`);
1408
1415
  },
1409
1416
  stop: (input: { runId: string; asyncDir: string; index?: number }) => firstToolResultText(stopAsyncRun(state, input.runId, undefined, { asyncDir: input.asyncDir, resolvedId: input.runId }), `Failed to stop async run ${input.runId}.`),
1410
- inspect: async (input: { runId: string; asyncDir: string; index?: number }) => firstToolResultText(await handleHerdrInspectorAction("inspector.open", {
1417
+ inspect: async (input: { runId: string; asyncDir: string; index?: number }) => firstToolResultText(await handleInspectorAction("inspector.open", {
1411
1418
  id: input.runId,
1412
1419
  dir: input.asyncDir,
1413
1420
  focus: true,
@@ -1416,10 +1423,11 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
1416
1423
  state,
1417
1424
  sessionRoots: state.trustedSessionRoots,
1418
1425
  cwd: state.baseCwd,
1419
- ...(options.herdrClient ? { client: options.herdrClient } : {}),
1420
1426
  ...(state.authorityPolicy ? { authorityPolicy: state.authorityPolicy } : {}),
1421
1427
  ...(state.missionStoreConfig ? { missions: state.missionStoreConfig } : {}),
1422
- }), `Failed to open Herdr inspector for async run ${input.runId}.`),
1428
+ ...(options.inspectorPlugins ? { plugins: options.inspectorPlugins } : {}),
1429
+ ...(options.inspectorEnv ? { env: options.inspectorEnv } : {}),
1430
+ }), `Failed to open inspector for async run ${input.runId}.`),
1423
1431
  redoPrompt: async (input: { runId: string; index: number; guidance: string; control?: ForegroundRunControl }) => {
1424
1432
  const control = input.control ?? state.foregroundControls.get(input.runId);
1425
1433
  if (!control?.promptAuditRedo) return { text: "Redo is not available for this live child.", isError: true };