pi-subagents 0.62.0 → 0.63.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 (36) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/docs/agents.md +5 -4
  3. package/docs/configuration.md +28 -2
  4. package/docs/models.md +5 -5
  5. package/docs/observability.md +4 -1
  6. package/docs/tool-reference.md +2 -2
  7. package/package.json +1 -1
  8. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  9. package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
  10. package/src/agents/agent-management.ts +22 -4
  11. package/src/agents/agents.ts +107 -125
  12. package/src/api/shared-types.ts +3 -0
  13. package/src/extension/config.ts +20 -0
  14. package/src/inspectors/herdr/inspector-runner.ts +19 -13
  15. package/src/runs/background/active-async-capacity.ts +0 -1
  16. package/src/runs/background/async-execution.ts +53 -7
  17. package/src/runs/background/async-resume.ts +3 -0
  18. package/src/runs/background/async-status.ts +18 -2
  19. package/src/runs/background/notify.ts +13 -1
  20. package/src/runs/background/run-status.ts +22 -2
  21. package/src/runs/background/subagent-runner.ts +30 -2
  22. package/src/runs/background/wait-completions.ts +13 -0
  23. package/src/runs/foreground/subagent-executor.ts +35 -3
  24. package/src/runs/shared/acceptance.ts +15 -9
  25. package/src/runs/shared/lane-metadata.ts +24 -3
  26. package/src/runs/shared/parallel-handoff.ts +4 -0
  27. package/src/runs/shared/pi-args.ts +8 -2
  28. package/src/runs/shared/task-intent.ts +5 -2
  29. package/src/runs/shared/worktree.ts +467 -63
  30. package/src/shared/types.ts +29 -0
  31. package/src/shared/utils.ts +18 -7
  32. package/src/slash/subagents-admin.ts +24 -12
  33. package/src/tui/fleet-status.ts +61 -2
  34. package/src/tui/fleet.ts +12 -7
  35. package/src/tui/render.ts +222 -14
  36. package/src/workflows/workflow-checklist.ts +441 -0
@@ -382,6 +382,17 @@ export type SubagentResultStatus = "completed" | "failed" | "paused" | "stopped"
382
382
  export type SubagentOutputState = "present" | "absent" | "unknown";
383
383
  export type SubagentRunMode = "single" | "parallel" | "chain" | "workflow";
384
384
  export type SubagentResultMode = SubagentRunMode;
385
+ export type WorktreeProvider = "auto" | "native" | "worktrunk";
386
+ export type ManagedWorktreeProvider = Exclude<WorktreeProvider, "auto">;
387
+
388
+ export interface WorktreeNaming {
389
+ requestedBranch: string;
390
+ branchPrefix: string;
391
+ label: string;
392
+ sanitizedPathComponent: string;
393
+ collision?: "branch" | "path" | "both";
394
+ collisionSuffix?: string;
395
+ }
385
396
 
386
397
  export interface ParallelHandoffPatch {
387
398
  path: string;
@@ -425,6 +436,10 @@ export interface ParallelHandoffCleanupTask {
425
436
  index: number;
426
437
  path: string;
427
438
  branch: string;
439
+ /** Provider that allocated this worktree; omitted in old manifests. */
440
+ provider?: ManagedWorktreeProvider;
441
+ /** Branch/path naming evidence retained with the cleanup authority. */
442
+ naming?: WorktreeNaming;
428
443
  worktreeRemoved: boolean;
429
444
  branchRemoved: boolean;
430
445
  preserved?: boolean;
@@ -1202,6 +1217,8 @@ export interface SingleResult {
1202
1217
  * result row's array position.
1203
1218
  */
1204
1219
  index: number;
1220
+ /** Workflow child key that owns this result when returned from workflow details. */
1221
+ workflowKey?: string;
1205
1222
  agent: string;
1206
1223
  task: string;
1207
1224
  /** Human-readable display name for the child's own session (agent + task
@@ -1307,6 +1324,8 @@ export interface WaitCompletionChild {
1307
1324
  sessionFile?: string;
1308
1325
  success?: boolean;
1309
1326
  outputState?: SubagentOutputState;
1327
+ structuredOutput?: unknown;
1328
+ structuredOutputPath?: string;
1310
1329
  error?: string;
1311
1330
  model?: string;
1312
1331
  contextOverflow?: boolean;
@@ -1841,6 +1860,10 @@ export interface AsyncStatus {
1841
1860
  worktreePath?: string;
1842
1861
  /** Display-only branch copied at launch; handoff remains authoritative. */
1843
1862
  branch?: string;
1863
+ /** Display-only provisioning provider copied at launch; handoff remains authoritative. */
1864
+ provider?: ManagedWorktreeProvider;
1865
+ /** Display-only naming evidence copied at launch; handoff remains authoritative. */
1866
+ naming?: WorktreeNaming;
1844
1867
  /** Child run identity for workflow capacity reconciliation. */
1845
1868
  runId?: string;
1846
1869
  /** True only when this workflow child owns a detached async runner. */
@@ -2554,6 +2577,12 @@ export interface ExtensionConfig {
2554
2577
  worktreeSetupHook?: string;
2555
2578
  worktreeSetupHookTimeoutMs?: number;
2556
2579
  worktreeBaseDir?: string;
2580
+ /** Enable managed worktrees when a launch does not provide an explicit value. */
2581
+ worktree?: boolean;
2582
+ /** Worktree allocator selection. Defaults to auto. */
2583
+ worktreeProvider?: WorktreeProvider;
2584
+ /** Namespace used by managed worktree branches. Defaults to pi-subagents/. */
2585
+ worktreeBranchPrefix?: string;
2557
2586
  /** Where to store subagent artifact files. Defaults to "session" (the pi session directory, or OS temp when unavailable). Set to "project" for cwd/.pi/subagents. */
2558
2587
  artifactDir?: ArtifactDirPreference;
2559
2588
  /** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
@@ -237,6 +237,12 @@ export function findLatestSessionFile(sessionDir: string): string | null {
237
237
  return latest ? latest.path : null;
238
238
  }
239
239
 
240
+ const PI_TURN_TIMING_FOOTER = /(?:\r?\n)*\x1b\[38;2;136;136;136m✻ Turn took [^()\r\n]+ \(Total time [^·\r\n]+ · \d+ turns?\)\x1b\[0m[ \t]*$/u;
241
+
242
+ function stripPiTurnTimingFooter(text: string): string {
243
+ return text.replace(PI_TURN_TIMING_FOOTER, "");
244
+ }
245
+
240
246
  export function getFinalOutput(messages: Message[]): string {
241
247
  const validTextParts: string[] = [];
242
248
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -246,21 +252,26 @@ export function getFinalOutput(messages: Message[]): string {
246
252
  || ("stopReason" in msg && msg.stopReason === "error");
247
253
  if (hasAssistantError) continue;
248
254
  const messageText = msg.content
249
- .filter((part) => part.type === "text" && part.text.trim().length > 0)
250
- .map((part) => part.type === "text" ? part.text : "")
255
+ .flatMap((part) => {
256
+ if (part.type !== "text") return [];
257
+ const text = stripPiTurnTimingFooter(part.text);
258
+ return text.trim().length > 0 ? [text] : [];
259
+ })
251
260
  .join("\n");
252
261
  for (let j = msg.content.length - 1; j >= 0; j--) {
253
262
  const part = msg.content[j];
254
- if (!part || part.type !== "text" || part.text.trim().length === 0) continue;
255
- validTextParts.push(part.text);
256
- if (/```acceptance[-_]report\s*\n[\s\S]*?```/i.test(part.text)) return messageText;
257
- for (const match of part.text.matchAll(/```(?:json|jsonc|json5)\s*\n([\s\S]*?)```/gi)) {
263
+ if (!part || part.type !== "text") continue;
264
+ const text = stripPiTurnTimingFooter(part.text);
265
+ if (text.trim().length === 0) continue;
266
+ validTextParts.push(text);
267
+ if (/```acceptance[-_]report\s*\n[\s\S]*?```/i.test(text)) return messageText;
268
+ for (const match of text.matchAll(/```(?:json|jsonc|json5)\s*\n([\s\S]*?)```/gi)) {
258
269
  const body = match[1] ?? "";
259
270
  if (/"(?:criteriaSatisfied|criteria_satisfied)"/.test(body) && /"(?:changedFiles|changed_files|testsAddedOrUpdated|tests_added_or_updated|commandsRun|commands_run|validationOutput|validation_output|residualRisks|residual_risks|noStagedFiles|no_staged_files|diffSummary|diff_summary|reviewFindings|review_findings|manualNotes|manual_notes)"/.test(body)) {
260
271
  return messageText;
261
272
  }
262
273
  }
263
- if (/ACCEPTANCE_REPORT\s*:/i.test(part.text)) return messageText;
274
+ if (/ACCEPTANCE_REPORT\s*:/i.test(text)) return messageText;
264
275
  }
265
276
  }
266
277
  return validTextParts[0] ?? "";
@@ -2,7 +2,6 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import {
5
- agentHasFrontmatterField,
6
5
  discoverAgentsAll,
7
6
  EXTRA_AGENT_DIRS_ENV,
8
7
  frontmatterNameForConfig,
@@ -116,18 +115,15 @@ type AgentSelection =
116
115
 
117
116
  function savesThroughSettings(agent: AgentConfig, field: EditableOverrideField): boolean {
118
117
  if (agent.source === "builtin") return true;
119
- if (agent.source === "package") {
120
- if (field === "systemPrompt") return false;
121
- return !agentHasFrontmatterField(agent, field);
122
- }
118
+ if (agent.source === "package") return true;
123
119
  if (!agent.override) return false;
120
+ if (agent.override.fields?.includes(field) === true) return true;
124
121
  // A lower-scope override can flow into a higher-scope custom agent with the
125
122
  // same name. Persist that agent's edits in its own frontmatter instead of
126
123
  // rewriting the shared lower-scope override used by another agent.
127
124
  if (agent.source !== agent.override.scope) return false;
128
- // Custom-agent overrides fill only fields absent from frontmatter. Compare the
129
- // effective value with the pre-override base so an override on one field does
130
- // not redirect edits to an unrelated frontmatter-owned field.
125
+ // Compare the effective value with the pre-override base so an override on one
126
+ // field does not redirect edits to an unrelated frontmatter-owned field.
131
127
  return agent[field] !== agent.override.base[field];
132
128
  }
133
129
 
@@ -256,10 +252,12 @@ async function chooseThinking(ctx: ExtensionContext, agent: AgentConfig): Promis
256
252
  }
257
253
 
258
254
  async function chooseOverrideScope(ctx: ExtensionContext, agent: AgentConfig): Promise<"user" | "project" | undefined> {
259
- if (agent.override?.scope) return agent.override.scope;
260
255
  const d = discoverAgentsAll(ctx.cwd);
256
+ if (agent.override?.scope) {
257
+ return agent.source === "project" && agent.override.scope === "user" && d.projectSettingsPath ? "project" : agent.override.scope;
258
+ }
261
259
  if (!d.projectSettingsPath || !ctx.hasUI) return "user";
262
- const choice = await ctx.ui.select(`Save builtin override for ${agent.name}`, ["user", "project"]);
260
+ const choice = await ctx.ui.select(`Save subagent override for ${agent.name}`, ["user", "project"]);
263
261
  return choice === "user" || choice === "project" ? choice : undefined;
264
262
  }
265
263
 
@@ -271,6 +269,18 @@ function persistSettingsField(
271
269
  value: string | undefined,
272
270
  ): { filePath: string; overridden: boolean } {
273
271
  const base = agent.override?.base ?? buildBuiltinBase(agent);
272
+ const shadowsLowerScope = scope === "project" && agent.override?.fieldScopes?.[field]?.includes("user") === true;
273
+ if (shadowsLowerScope && (value === undefined || value === base[field])) {
274
+ const filePath = field === "model"
275
+ ? mergeBuiltinAgentOverride(ctx.cwd, agent.name, scope, { model: value ?? base.model ?? false })
276
+ : field === "thinking"
277
+ ? mergeBuiltinAgentOverride(ctx.cwd, agent.name, scope, { thinking: value ?? (base.thinking === false ? "off" : base.thinking) ?? false })
278
+ : mergeBuiltinAgentOverride(ctx.cwd, agent.name, scope, { systemPrompt: value ?? base.systemPrompt });
279
+ return {
280
+ filePath,
281
+ overridden: true,
282
+ };
283
+ }
274
284
  if (value === undefined || value === base[field]) {
275
285
  return {
276
286
  filePath: removeBuiltinAgentOverrideFields(ctx.cwd, agent.name, scope, [field]).path,
@@ -358,13 +368,15 @@ async function saveAgentSystemPrompt(ctx: ExtensionContext, agent: AgentConfig,
358
368
  }
359
369
 
360
370
  async function editSystemPrompt(ctx: ExtensionContext, agent: AgentConfig): Promise<string | null> {
361
- if (!savesThroughSettings(agent, "systemPrompt")) {
371
+ const saveThroughSettings = savesThroughSettings(agent, "systemPrompt");
372
+ if (!saveThroughSettings) {
362
373
  const readOnlyMessage = readOnlyAgentMessage(agent, "systemPrompt");
363
374
  if (readOnlyMessage) return readOnlyMessage;
364
375
  }
365
376
  const edited = await ctx.ui.editor(`Edit '${agent.name}' system prompt`, agent.systemPrompt ?? "");
366
377
  if (edited === undefined) return null;
367
- if (edited.replace(/\s+$/, "") === (agent.systemPrompt ?? "").replace(/\s+$/, "")) {
378
+ const projectOwnsLowerScopeOverride = agent.source === "project" && agent.override?.scope === "user" && discoverAgentsAll(ctx.cwd).projectSettingsPath;
379
+ if (edited.replace(/\s+$/, "") === (agent.systemPrompt ?? "").replace(/\s+$/, "") && !(saveThroughSettings && projectOwnsLowerScopeOverride)) {
368
380
  return `System prompt for '${agent.name}' left unchanged.`;
369
381
  }
370
382
  return saveAgentSystemPrompt(ctx, agent, edited);
@@ -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 { formatWorkflowChecklistBottleneck, formatWorkflowChecklistPhase, formatWorkflowChecklistSummary, projectWorkflowChecklist, type WorkflowChecklistPhase, type WorkflowChecklistProjection } from "../workflows/workflow-checklist.ts";
11
12
 
12
13
  export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
13
14
 
@@ -35,6 +36,7 @@ type FleetStatusEntry = {
35
36
  projectPane?: HerdrProjectPaneSnapshot;
36
37
  nestedChildren?: NestedRunSummary[];
37
38
  workflowRows?: AsyncStatusWorkflowRow[];
39
+ workflowChecklist?: WorkflowChecklistProjection;
38
40
  };
39
41
 
40
42
  type FleetNestedRow = {
@@ -50,6 +52,7 @@ type FleetNestedRow = {
50
52
  type FleetTreeRow =
51
53
  | { kind: "owner"; entry: FleetStatusEntry }
52
54
  | { kind: "child"; entry: FleetStatusEntry; last: boolean }
55
+ | { kind: "workflow-phase"; ownerKey: string; phase: WorkflowChecklistPhase; last: boolean }
53
56
  | { kind: "workflow"; ownerKey: string; row: AsyncStatusWorkflowRow; last: boolean }
54
57
  | { kind: "nested"; ownerKey: string; row: FleetNestedRow; last: boolean };
55
58
 
@@ -117,6 +120,18 @@ function visibleWorkflowRows(rows: AsyncStatusWorkflowRow[] | undefined, visible
117
120
  return [{ name: `… +${rows.length - visible.length} hidden workflow steps`, state: "complete", overflow: rows.length - visible.length }, ...visible];
118
121
  }
119
122
 
123
+ function visibleWorkflowPhases(checklist: WorkflowChecklistProjection | undefined, visibleLimit: number): WorkflowChecklistPhase[] {
124
+ const phases = checklist?.phases ?? [];
125
+ if (phases.length <= visibleLimit) return phases;
126
+ const selected = new Set<number>();
127
+ for (const [index, phase] of phases.entries()) {
128
+ if (phase.state !== "complete") selected.add(index);
129
+ if (selected.size >= visibleLimit) break;
130
+ }
131
+ for (let index = phases.length - 1; index >= 0 && selected.size < visibleLimit; index--) selected.add(index);
132
+ return [...selected].sort((left, right) => left - right).map((index) => phases[index]!);
133
+ }
134
+
120
135
  function isWorkflowRowTerminal(row: AsyncStatusWorkflowRow): boolean {
121
136
  if (row.kind) return row.state === "done" || row.state === "cancelled" || row.state === "error";
122
137
  return row.state === "complete" || row.state === "completed";
@@ -224,10 +239,11 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
224
239
  if (entry.parentKey && entryKeys.has(entry.parentKey)) continue;
225
240
  rows.push({ kind: "owner", entry });
226
241
  const attached = childrenByParent.get(entry.key) ?? [];
242
+ const workflowPhases = visibleWorkflowPhases(entry.workflowChecklist, attached.length > 0 ? 2 : 4);
227
243
  const workflowRows = visibleWorkflowRows(entry.workflowRows, attached.length > 0 ? 2 : 4);
228
244
  for (const [index, child] of attached.entries()) {
229
245
  const nested = nestedFleetRows(child.nestedChildren, 3);
230
- const laterRows = index < attached.length - 1 || workflowRows.length > 0 || Boolean(entry.nestedChildren?.length);
246
+ const laterRows = index < attached.length - 1 || workflowPhases.length > 0 || workflowRows.length > 0 || Boolean(entry.nestedChildren?.length);
231
247
  rows.push({ kind: "child", entry: child, last: !laterRows && nested.length === 0 });
232
248
  for (const [nestedIndex, row] of nested.entries()) rows.push({
233
249
  kind: "nested",
@@ -236,6 +252,12 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
236
252
  last: nestedIndex === nested.length - 1 && !laterRows,
237
253
  });
238
254
  }
255
+ for (const [index, phase] of workflowPhases.entries()) rows.push({
256
+ kind: "workflow-phase",
257
+ ownerKey: entry.key,
258
+ phase,
259
+ last: index === workflowPhases.length - 1 && workflowRows.length === 0 && !entry.nestedChildren?.length,
260
+ });
239
261
  for (const [index, row] of workflowRows.entries()) rows.push({ kind: "workflow", ownerKey: entry.key, row, last: index === workflowRows.length - 1 && !entry.nestedChildren?.length });
240
262
  const nested = nestedFleetRows(entry.nestedChildren, attached.length > 0 ? 3 : 4);
241
263
  for (const [index, row] of nested.entries()) rows.push({ kind: "nested", ownerKey: entry.key, row, last: index === nested.length - 1 });
@@ -364,6 +386,14 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
364
386
  const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
365
387
  const workflowSteps = workflowStepsWithoutMaterializedChildren(job.steps, materializedChildrenByWorkflow.get(`async:${job.asyncId}`));
366
388
  const workflowRows = projectAsyncWorkflowRows(workflowSteps, job.workflowGraph ?? job.hostSteps, job.preflight);
389
+ const workflowChecklist = projectWorkflowChecklist({
390
+ graph: job.workflowGraph,
391
+ steps: job.steps,
392
+ hostSteps: job.hostSteps,
393
+ preflight: job.preflight,
394
+ trace: job.workflow?.trace,
395
+ now: job.updatedAt ?? Date.now(),
396
+ });
367
397
  entries.push({
368
398
  key: `async:${job.asyncId}`,
369
399
  ...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
@@ -375,6 +405,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
375
405
  ...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
376
406
  state: job.status,
377
407
  ...(workflowRows.length ? { workflowRows } : {}),
408
+ ...(workflowChecklist.total ? { workflowChecklist } : {}),
378
409
  ...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
379
410
  });
380
411
  continue;
@@ -668,6 +699,8 @@ export class SubagentFleetStatus {
668
699
  lines.push(this.renderEntry(rosterIndex, selectedIndex, row.entry, width, theme, row.kind === "child" ? (row.last ? "└─" : "├─") : undefined));
669
700
  } else if (row.kind === "workflow") {
670
701
  lines.push(this.renderWorkflowRow(row.row, row.last, width, theme));
702
+ } else if (row.kind === "workflow-phase") {
703
+ lines.push(this.renderWorkflowPhaseRow(row.phase, row.last, width, theme));
671
704
  } else {
672
705
  lines.push(this.renderNestedRow(row.row, row.last, width, theme));
673
706
  }
@@ -691,7 +724,10 @@ export class SubagentFleetStatus {
691
724
  private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme, branch?: string): string {
692
725
  const agent = entry.modelThinking ? `${entry.agent} (${entry.modelThinking})` : entry.agent;
693
726
  const prefix = branch ? ` ${branch}` : " ";
694
- const left = `${prefix} ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}`;
727
+ const checklist = entry.workflowWrapper && entry.workflowChecklist
728
+ ? ` · checklist ${formatWorkflowChecklistSummary(entry.workflowChecklist)}${entry.workflowChecklist.bottleneck ? ` · bottleneck ${formatWorkflowChecklistBottleneck(entry.workflowChecklist.bottleneck)}` : ""}`
729
+ : "";
730
+ const left = `${prefix} ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}${checklist}`;
695
731
  const elapsed = Date.now() - entry.startedAt;
696
732
  const rightText = entry.projectPane
697
733
  ? `${entry.projectPane.summary ?? "—"} · ${formatFleetElapsed(Date.now() - entry.projectPane.refreshedAt)} ago`
@@ -730,6 +766,20 @@ export class SubagentFleetStatus {
730
766
  return theme.fg("warning", state);
731
767
  }
732
768
 
769
+ private renderWorkflowPhaseRow(phase: WorkflowChecklistPhase, last: boolean, width: number, theme: Theme): string {
770
+ const marker = last ? "└─" : "├─";
771
+ const glyph = phase.state === "complete"
772
+ ? theme.fg("success", "✓")
773
+ : phase.state === "running"
774
+ ? theme.fg("accent", "●")
775
+ : phase.state === "blocked" || phase.state === "failed"
776
+ ? theme.fg("error", phase.state === "blocked" ? "!" : "✗")
777
+ : phase.state === "queued"
778
+ ? theme.fg("muted", "◦")
779
+ : theme.fg("warning", "■");
780
+ return truncateToWidth(` ${marker} ${glyph} ${theme.fg("muted", formatWorkflowChecklistPhase(phase))}`, width);
781
+ }
782
+
733
783
  private renderWorkflowRow(row: AsyncStatusWorkflowRow, last: boolean, width: number, theme: Theme): string {
734
784
  const marker = last ? "└─" : "├─";
735
785
  const indent = " ";
@@ -809,6 +859,15 @@ export class SubagentFleetStatus {
809
859
  entry.external,
810
860
  Math.round((now - entry.startedAt) / 1000),
811
861
  entry.tokens,
862
+ entry.workflowChecklist ? [
863
+ entry.workflowChecklist.total,
864
+ entry.workflowChecklist.done,
865
+ entry.workflowChecklist.running,
866
+ entry.workflowChecklist.queued,
867
+ entry.workflowChecklist.blocked,
868
+ entry.workflowChecklist.failed,
869
+ entry.workflowChecklist.phases.map((phase) => [phase.key, phase.state, phase.done, phase.total, phase.running, phase.queued, phase.blocked, phase.failed, phase.items.map((item) => [item.key, item.state, item.currentTool, item.currentPath, item.durationMs, item.toolCount, item.error])]),
870
+ ] : undefined,
812
871
  visibleWorkflowRows(entry.workflowRows, entry.parentKey ? 2 : 4).map((row) => [
813
872
  row.kind,
814
873
  row.name,
package/src/tui/fleet.ts CHANGED
@@ -42,7 +42,7 @@ export const DEFAULT_FLEET_KEYBINDINGS: Record<FleetKeybindingAction, string[]>
42
42
  pageDown: ["pageDown"],
43
43
  refresh: ["r", "R"],
44
44
  steer: ["s"],
45
- inspect: ["H"],
45
+ inspect: ["return", "H"],
46
46
  stop: ["D"],
47
47
  toggleTools: ["x", "X", "ctrl+o"],
48
48
  };
@@ -952,6 +952,12 @@ export class SubagentFleetComponent implements Component {
952
952
  return { runId: parent.asyncId, asyncDir: parent.asyncDir };
953
953
  }
954
954
 
955
+ private inspectSelectedHerdr(): void {
956
+ const target = this.selectedHerdrInspectAction();
957
+ 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 });
958
+ else this.runAction(() => this.options.actions!.inspect!(target));
959
+ }
960
+
955
961
  private actionLines(): string[] {
956
962
  const lines: string[] = [];
957
963
  if (this.actionBusy) lines.push(this.theme.fg("accent", "Action pending..."));
@@ -1179,12 +1185,6 @@ export class SubagentFleetComponent implements Component {
1179
1185
  }
1180
1186
  return;
1181
1187
  }
1182
- if (matchesFleetAction(data, this.keybindings, "inspect")) {
1183
- const target = this.selectedHerdrInspectAction();
1184
- 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 });
1185
- else this.runAction(() => this.options.actions!.inspect!(target));
1186
- return;
1187
- }
1188
1188
  if (matchesFleetAction(data, this.keybindings, "stop")) {
1189
1189
  const target = this.selectedAsyncAction();
1190
1190
  if ("reason" in target || !this.options.actions) this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
@@ -1201,6 +1201,11 @@ export class SubagentFleetComponent implements Component {
1201
1201
  this.expandedTools = !this.expandedTools;
1202
1202
  this.transcriptCache = undefined;
1203
1203
  this.tui.requestRender();
1204
+ return;
1205
+ }
1206
+ if (matchesFleetAction(data, this.keybindings, "inspect")) {
1207
+ this.inspectSelectedHerdr();
1208
+ return;
1204
1209
  }
1205
1210
  }
1206
1211