pi-subagents 0.47.0 → 0.48.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 (57) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +2 -0
  3. package/agents/reviewer.md +3 -4
  4. package/docs/configuration.md +46 -2
  5. package/docs/observability.md +1 -1
  6. package/docs/tool-reference.md +1 -1
  7. package/package.json +1 -1
  8. package/src/agents/agents.ts +9 -3
  9. package/src/extension/config.ts +16 -0
  10. package/src/extension/doctor.ts +40 -0
  11. package/src/extension/index.ts +30 -11
  12. package/src/extension/public-execution.ts +5 -0
  13. package/src/extension/rpc.ts +7 -9
  14. package/src/extension/schemas.ts +4 -4
  15. package/src/intercom/intercom-bridge.ts +4 -1
  16. package/src/intercom/native-supervisor-channel.ts +102 -4
  17. package/src/missions/lifecycle.ts +4 -7
  18. package/src/missions/store.ts +4 -4
  19. package/src/missions/workflow-state.ts +6 -2
  20. package/src/runs/background/active-async-capacity.ts +374 -0
  21. package/src/runs/background/active-run-index.ts +46 -0
  22. package/src/runs/background/async-execution.ts +117 -29
  23. package/src/runs/background/async-job-tracker.ts +279 -134
  24. package/src/runs/background/async-resume.ts +7 -1
  25. package/src/runs/background/async-status.ts +33 -4
  26. package/src/runs/background/chain-append.ts +33 -15
  27. package/src/runs/background/control-channel.ts +55 -17
  28. package/src/runs/background/owned-process-tree.ts +104 -0
  29. package/src/runs/background/process-terminal.ts +17 -3
  30. package/src/runs/background/result-watcher.ts +87 -6
  31. package/src/runs/background/run-status.ts +23 -1
  32. package/src/runs/background/scheduled-runs.ts +23 -0
  33. package/src/runs/background/stale-run-reconciler.ts +10 -3
  34. package/src/runs/background/subagent-runner.ts +75 -33
  35. package/src/runs/foreground/async-dismiss-action.ts +85 -0
  36. package/src/runs/foreground/async-steering-action.ts +6 -7
  37. package/src/runs/foreground/chain-execution.ts +37 -2
  38. package/src/runs/foreground/execution.ts +90 -19
  39. package/src/runs/foreground/foreground-control.ts +12 -0
  40. package/src/runs/foreground/prompt-audit.ts +171 -0
  41. package/src/runs/foreground/subagent-executor.ts +648 -195
  42. package/src/runs/shared/acceptance.ts +13 -4
  43. package/src/runs/shared/llm-intent-arbiter.ts +286 -0
  44. package/src/runs/shared/parallel-utils.ts +2 -0
  45. package/src/runs/shared/pi-args.ts +44 -1
  46. package/src/runs/shared/run-fanout-budget.ts +280 -0
  47. package/src/runs/shared/single-output.ts +4 -2
  48. package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
  49. package/src/runs/shared/task-intent.ts +19 -3
  50. package/src/runs/shared/worktree.ts +17 -5
  51. package/src/shared/artifacts.ts +1 -1
  52. package/src/shared/file-coalescer.ts +9 -0
  53. package/src/shared/types.ts +97 -1
  54. package/src/shared/utils.ts +3 -1
  55. package/src/tui/fleet-status.ts +7 -5
  56. package/src/tui/fleet.ts +225 -12
  57. package/src/workflows/scripted-workflow.ts +26 -6
package/src/tui/fleet.ts CHANGED
@@ -17,12 +17,14 @@ import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.
17
17
  import { FLEET_STATUS_WIDGET_KEY } from "./fleet-status.ts";
18
18
  import { readFleetTranscript, renderFleetTranscript, type FleetTranscript } from "./fleet-transcript.ts";
19
19
  import { handleHerdrInspectorAction } from "../inspectors/herdr/actions.ts";
20
+ import { getLivePromptAudit, type LivePromptAudit, type PromptAuditView } from "../runs/foreground/prompt-audit.ts";
20
21
 
21
22
  const REFRESH_MS = 750;
22
23
  const MAX_RECENT_ASYNC_RUNS = 20;
23
24
  const MAX_FLEET_HISTORY_CANDIDATES = 100;
24
25
  const TRANSCRIPT_LINES = 200;
25
26
  const OUTPUT_TAIL_BYTES = 64 * 1024;
27
+ const PROMPT_AUDIT_SUMMARY_WIDTH = 160;
26
28
 
27
29
  export const DEFAULT_FLEET_KEYBINDINGS: Record<FleetKeybindingAction, string[]> = {
28
30
  close: ["escape", "ctrl+c", "q"],
@@ -91,6 +93,7 @@ export interface FleetActionHandlers {
91
93
  steer(input: { runId: string; asyncDir: string; index?: number; message: string; mode: SteerDeliveryMode }): Promise<FleetActionResult>;
92
94
  stop(input: { runId: string; asyncDir: string; index?: number }): Promise<FleetActionResult> | FleetActionResult;
93
95
  inspect?(input: { runId: string; asyncDir: string; index?: number }): Promise<FleetActionResult>;
96
+ redoPrompt?(input: { runId: string; index: number; guidance: string; control?: ForegroundRunControl }): Promise<FleetActionResult>;
94
97
  }
95
98
 
96
99
  export interface FleetViewOptions {
@@ -101,6 +104,7 @@ export interface FleetViewOptions {
101
104
  markdownTheme?: MarkdownTheme;
102
105
  fleetKeybindings?: FleetKeybindingsConfig;
103
106
  actions?: FleetActionHandlers;
107
+ copyText?: (text: string) => Promise<void> | void;
104
108
  }
105
109
 
106
110
  function belongsToCurrentSession(sessionId: string | undefined, currentSessionId: string | null): boolean {
@@ -280,23 +284,62 @@ function statusGlyph(item: FleetItem, theme: Theme): string {
280
284
  return theme.fg("error", "✗");
281
285
  }
282
286
 
283
- function foregroundActiveDetail(item: Extract<FleetItem, { kind: "foreground-active" }>): string[] {
287
+ function foregroundPromptAuditCount(item: Extract<FleetItem, { kind: "foreground-active" }>, state: SubagentState): number {
288
+ if (!state.currentSessionId || item.control.sessionId !== state.currentSessionId) return 0;
289
+ if (!item.control.activeChildren) return getLivePromptAudit(item.control, item.index ?? 0) ? 1 : 0;
290
+ return [...item.control.activeChildren.keys()].filter((index) => getLivePromptAudit(item.control, index)).length;
291
+ }
292
+
293
+ function authoredPromptSummary(text: string): string | undefined {
294
+ const summary = text.replace(/\s+/g, " ").trim();
295
+ return summary ? truncateToWidth(summary, PROMPT_AUDIT_SUMMARY_WIDTH, "…") : undefined;
296
+ }
297
+
298
+ function foregroundAuthoredPromptSummary(item: Extract<FleetItem, { kind: "foreground-active" }>, state: SubagentState): string | undefined {
299
+ if (!state.currentSessionId || item.control.sessionId !== state.currentSessionId) return undefined;
300
+ const prompt = getLivePromptAudit(item.control, item.index ?? 0);
301
+ return prompt ? authoredPromptSummary(prompt.authoredTask) : undefined;
302
+ }
303
+
304
+ function promptAuditText(prompt: LivePromptAudit, view: PromptAuditView): string {
305
+ switch (view) {
306
+ case "authored": return prompt.authoredTask;
307
+ case "runtime": return prompt.runtimeAdditions;
308
+ case "effective": return prompt.finalEffectivePrompt;
309
+ }
310
+ }
311
+
312
+ function promptAuditViewLabel(view: PromptAuditView): string {
313
+ switch (view) {
314
+ case "authored": return "[1] Authored task";
315
+ case "runtime": return "[2] Runtime additions";
316
+ case "effective": return "[3] Final effective prompt";
317
+ }
318
+ }
319
+
320
+ function foregroundActiveDetail(item: Extract<FleetItem, { kind: "foreground-active" }>, state: SubagentState): string[] {
284
321
  const { control } = item;
285
322
  const live = item.activeChild ?? control;
286
323
  const modelThinking = formatModelThinking(live.model, live.thinking);
324
+ const promptAuditCount = foregroundPromptAuditCount(item, state);
325
+ const promptSummary = foregroundAuthoredPromptSummary(item, state);
287
326
  const lines = [
288
327
  `Run: ${item.runId}`,
289
328
  "Source: foreground",
290
329
  `State: running`,
291
330
  `Mode: ${control.mode}`,
292
331
  control.parentWorkflowRunId ? `Workflow child of: ${control.parentWorkflowRunId}${control.workflowKey ? ` (${control.workflowKey})` : ""}` : undefined,
332
+ control.sourceRunId ? `Redo source: ${control.sourceRunId}` : undefined,
333
+ control.supersededByRunId ? `Superseded by: ${control.supersededByRunId}` : undefined,
293
334
  item.index !== undefined ? `Child: ${item.index} (${item.agent})` : `Agent: ${item.agent}`,
294
335
  modelThinking ? `Model: ${modelThinking}` : undefined,
336
+ promptSummary ? `Task: ${promptSummary}` : undefined,
295
337
  `Started: ${new Date(live.startedAt).toISOString()}`,
296
338
  live.currentTool ? `Current tool: ${live.currentTool}${live.currentPath ? ` · ${shortenPath(live.currentPath)}` : ""}` : undefined,
297
339
  live.turnCount !== undefined ? `Turns: ${live.turnCount}` : undefined,
298
340
  live.toolCount !== undefined ? `Tools: ${live.toolCount}` : undefined,
299
341
  live.tokens !== undefined ? `Tokens: ${formatTokens(live.tokens)}` : undefined,
342
+ promptAuditCount > 0 ? `Prompt audit: ${promptAuditCount} live · 3 views · p opens` : undefined,
300
343
  "",
301
344
  "Transcript",
302
345
  "Live foreground output remains in the expanded subagent tool result. Persisted output and session paths appear here after the child settles.",
@@ -404,7 +447,7 @@ function asyncDetail(item: Extract<FleetItem, { kind: "async" }>): string[] {
404
447
  function detailLines(item: FleetItem | undefined, error: string | undefined, state: SubagentState): string[] {
405
448
  if (!item) return [error ? `Fleet scan failed: ${error}` : "No current-session foreground or recent async children.", "", "New runs appear here automatically while this inspector remains open."];
406
449
  const lines = item.kind === "foreground-active"
407
- ? foregroundActiveDetail(item)
450
+ ? foregroundActiveDetail(item, state)
408
451
  : item.kind === "foreground-recent"
409
452
  ? foregroundRecentDetail(item, state)
410
453
  : asyncDetail(item);
@@ -517,7 +560,7 @@ function itemStats(item: FleetItem): string[] {
517
560
  ].filter((value): value is string => Boolean(value));
518
561
  }
519
562
 
520
- function structuredHeader(item: FleetItem, width: number, theme: Theme, conversationState: string): string[] {
563
+ function structuredHeader(item: FleetItem, width: number, theme: Theme, conversationState: string, promptSummary?: string): string[] {
521
564
  const lines: string[] = [];
522
565
  lines.push(rightAligned(` ${statusGlyph(item, theme)} ${theme.bold(item.agent)}`, theme.fg("dim", item.state), width));
523
566
  const child = item.index !== undefined ? ` · child ${item.index + 1}` : "";
@@ -526,10 +569,7 @@ function structuredHeader(item: FleetItem, width: number, theme: Theme, conversa
526
569
  lines.push(` ${theme.fg("dim", identity)}`);
527
570
  const stats = itemStats(item);
528
571
  if (stats.length) lines.push(` ${theme.fg("muted", stats.join(" · "))}`);
529
- if (item.description) {
530
- const task = item.description.replace(/\s+/g, " ").trim();
531
- lines.push(` ${theme.fg("dim", "Task")} ${task}`);
532
- }
572
+ if (promptSummary) lines.push(` ${theme.fg("muted", `Task: ${promptSummary}`)}`);
533
573
  lines.push(`${theme.fg("accent", "Conversation")} ${theme.fg("dim", `· ${conversationState}`)}`);
534
574
  return lines.map((line) => truncateToWidth(line, width));
535
575
  }
@@ -578,8 +618,11 @@ export class SubagentFleetComponent implements Component {
578
618
  private detailViewportHeight = 8;
579
619
  private bodyHeight = 8;
580
620
  private expandedTools = false;
621
+ private promptAuditOpen = false;
622
+ private promptAuditView: PromptAuditView = "authored";
581
623
  private actionNotice: FleetActionResult | undefined;
582
624
  private steerDraft: string | undefined;
625
+ private redoGuidanceDraft: string | undefined;
583
626
  private steerMode: SteerDeliveryMode = "steer";
584
627
  private stopConfirming = false;
585
628
  private actionBusy = false;
@@ -635,8 +678,45 @@ export class SubagentFleetComponent implements Component {
635
678
  this.tui.requestRender();
636
679
  }
637
680
 
681
+ private selectedPromptAudit(): LivePromptAudit | undefined {
682
+ const item = this.snapshot.items[this.selected];
683
+ if (item?.kind !== "foreground-active") return undefined;
684
+ if (!this.state.currentSessionId || item.control.sessionId !== this.state.currentSessionId) return undefined;
685
+ return getLivePromptAudit(item.control, item.index ?? 0);
686
+ }
687
+
688
+ private promptAuditItems(): Array<{ item: Extract<FleetItem, { kind: "foreground-active" }>; prompt: LivePromptAudit }> {
689
+ const selected = this.snapshot.items[this.selected];
690
+ if (selected?.kind !== "foreground-active" || !this.state.currentSessionId || selected.control.sessionId !== this.state.currentSessionId) return [];
691
+ return this.snapshot.items.flatMap((item) => {
692
+ if (item.kind !== "foreground-active" || item.control !== selected.control) return [];
693
+ const prompt = getLivePromptAudit(item.control, item.index ?? 0);
694
+ return prompt ? [{ item, prompt }] : [];
695
+ });
696
+ }
697
+
698
+ private selectedPromptText(): string | undefined {
699
+ const prompt = this.selectedPromptAudit();
700
+ return prompt ? promptAuditText(prompt, this.promptAuditView) : undefined;
701
+ }
702
+
703
+ private movePromptSelection(delta: number): void {
704
+ const items = this.promptAuditItems();
705
+ if (items.length === 0) return;
706
+ const currentKey = this.snapshot.items[this.selected]?.key;
707
+ const current = Math.max(0, items.findIndex(({ item }) => item.key === currentKey));
708
+ const target = items[Math.max(0, Math.min(items.length - 1, current + delta))]?.item;
709
+ if (!target) return;
710
+ this.selected = this.snapshot.items.findIndex((item) => item.key === target.key);
711
+ this.selectedKey = target.key;
712
+ this.detailScroll = 0;
713
+ this.detailAutoFollow = false;
714
+ this.tui.requestRender();
715
+ }
716
+
638
717
  private resetActionInput(): void {
639
718
  this.steerDraft = undefined;
719
+ this.redoGuidanceDraft = undefined;
640
720
  this.steerMode = "steer";
641
721
  this.stopConfirming = false;
642
722
  }
@@ -668,6 +748,9 @@ export class SubagentFleetComponent implements Component {
668
748
  if (this.steerDraft !== undefined) {
669
749
  lines.push(this.theme.fg("accent", `Steer message (${this.steerMode}): ${this.steerDraft}${this.theme.fg("dim", "▌")}`));
670
750
  lines.push(this.theme.fg("dim", "Enter sends · Tab changes mode · Esc cancels · Backspace edits"));
751
+ } else if (this.redoGuidanceDraft !== undefined) {
752
+ lines.push(this.theme.fg("accent", `Redo guidance: ${this.redoGuidanceDraft}${this.theme.fg("dim", "▌")}`));
753
+ lines.push(this.theme.fg("dim", "Enter rewrites and reruns · Esc cancels · Backspace edits"));
671
754
  } else if (this.stopConfirming) {
672
755
  const selected = this.snapshot.items[this.selected];
673
756
  lines.push(this.theme.fg("warning", `Confirm stop for async run ${selected?.runId ?? "selected run"}?`));
@@ -714,6 +797,82 @@ export class SubagentFleetComponent implements Component {
714
797
  }
715
798
 
716
799
  handleInput(data: string): void {
800
+ if (this.promptAuditOpen && this.redoGuidanceDraft !== undefined) {
801
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
802
+ this.resetActionInput();
803
+ this.tui.requestRender();
804
+ return;
805
+ }
806
+ if (matchesKey(data, "return") || data === "\r" || data === "\n") {
807
+ const guidance = this.redoGuidanceDraft.trim();
808
+ if (!guidance) {
809
+ this.setActionNotice({ text: "Redo guidance cannot be empty.", isError: true });
810
+ return;
811
+ }
812
+ const selected = this.snapshot.items[this.selected];
813
+ if (selected?.kind !== "foreground-active" || !this.options.actions?.redoPrompt) {
814
+ this.setActionNotice({ text: "Redo is unavailable for this prompt.", isError: true });
815
+ return;
816
+ }
817
+ this.runAction(() => this.options.actions!.redoPrompt!({ runId: selected.runId, index: selected.index ?? 0, guidance, control: selected.control }));
818
+ return;
819
+ }
820
+ if (matchesKey(data, "backspace") || data === "\x7f") {
821
+ this.redoGuidanceDraft = this.redoGuidanceDraft.slice(0, -1);
822
+ this.tui.requestRender();
823
+ return;
824
+ }
825
+ if (data.length === 1 && data >= " " && data !== "\x7f") {
826
+ this.redoGuidanceDraft += data;
827
+ this.tui.requestRender();
828
+ }
829
+ return;
830
+ }
831
+ if (this.promptAuditOpen) {
832
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
833
+ this.promptAuditOpen = false;
834
+ this.detailAutoFollow = true;
835
+ this.tui.requestRender();
836
+ return;
837
+ }
838
+ if (data === "j") return this.movePromptSelection(1);
839
+ if (data === "k") return this.movePromptSelection(-1);
840
+ if (data === "1" || data === "2" || data === "3") {
841
+ this.promptAuditView = data === "1" ? "authored" : data === "2" ? "runtime" : "effective";
842
+ this.detailScroll = 0;
843
+ this.tui.requestRender();
844
+ return;
845
+ }
846
+ if (data === "g") {
847
+ const prompt = this.selectedPromptAudit();
848
+ if (!prompt) this.setActionNotice({ text: "Prompt Audit is available only for a live child owned by this session.", isError: true });
849
+ else if (!prompt.rerun) this.setActionNotice({ text: "Redo is not safe for this prompt in this slice.", isError: true });
850
+ else if (!this.options.actions?.redoPrompt) this.setActionNotice({ text: "Redo controls are unavailable in this context.", isError: true });
851
+ else {
852
+ this.actionNotice = undefined;
853
+ this.redoGuidanceDraft = "";
854
+ this.detailAutoFollow = false;
855
+ this.detailScroll = 0;
856
+ this.tui.requestRender();
857
+ }
858
+ return;
859
+ }
860
+ if (data === "c") {
861
+ const text = this.selectedPromptText();
862
+ if (!text) this.setActionNotice({ text: "No prompt view is available to copy.", isError: true });
863
+ else if (!this.options.copyText) this.setActionNotice({ text: "Clipboard is unavailable in this context.", isError: true });
864
+ else this.runAction(async () => {
865
+ await this.options.copyText!(text);
866
+ return { text: "Copied visible prompt view." };
867
+ });
868
+ return;
869
+ }
870
+ if (matchesFleetAction(data, this.keybindings, "scrollUp")) return this.scrollDetail(-1);
871
+ if (matchesFleetAction(data, this.keybindings, "scrollDown")) return this.scrollDetail(1);
872
+ if (matchesFleetAction(data, this.keybindings, "pageUp")) return this.scrollDetail(-this.detailViewportHeight);
873
+ if (matchesFleetAction(data, this.keybindings, "pageDown")) return this.scrollDetail(this.detailViewportHeight);
874
+ return;
875
+ }
717
876
  if (this.steerDraft !== undefined) {
718
877
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
719
878
  this.resetActionInput();
@@ -785,6 +944,18 @@ export class SubagentFleetComponent implements Component {
785
944
  this.tui.requestRender();
786
945
  return;
787
946
  }
947
+ if (data === "p") {
948
+ if (!this.selectedPromptAudit()) this.setActionNotice({ text: "Prompt Audit is available only for a live child owned by this session.", isError: true });
949
+ else {
950
+ this.promptAuditOpen = true;
951
+ this.promptAuditView = "authored";
952
+ this.detailScroll = 0;
953
+ this.detailAutoFollow = false;
954
+ this.actionNotice = undefined;
955
+ this.tui.requestRender();
956
+ }
957
+ return;
958
+ }
788
959
  if (matchesFleetAction(data, this.keybindings, "steer")) {
789
960
  const target = this.selectedAsyncAction();
790
961
  if ("reason" in target || !this.options.actions) this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
@@ -830,7 +1001,7 @@ export class SubagentFleetComponent implements Component {
830
1001
  const marker = index === this.selected ? this.theme.fg("accent", "›") : " ";
831
1002
  const context = item.kind === "async" ? contextModeBadge(this.theme, item.step?.context ?? item.run.context) : item.kind === "foreground-recent" ? contextModeBadge(this.theme, item.child.context) : "";
832
1003
  const agent = index === this.selected ? this.theme.bold(item.agent) : item.agent;
833
- const identity = item.description?.replace(/\s+/g, " ").trim() || item.runId.slice(0, 8);
1004
+ const identity = item.runId.slice(0, 8);
834
1005
  const left = `${marker} ${statusGlyph(item, this.theme)} ${agent}${context} ${this.theme.fg("dim", `· ${identity}`)}`;
835
1006
  return rightAligned(left, this.theme.fg("dim", item.state), width);
836
1007
  });
@@ -853,7 +1024,35 @@ export class SubagentFleetComponent implements Component {
853
1024
  return { transcript, body: [...body] };
854
1025
  }
855
1026
 
1027
+ private promptAuditDetail(width: number): FleetDetailSections {
1028
+ const selected = this.snapshot.items[this.selected];
1029
+ const prompt = this.selectedPromptAudit();
1030
+ const items = this.promptAuditItems();
1031
+ const selectedPosition = selected ? items.findIndex(({ item }) => item.key === selected.key) : -1;
1032
+ const live = selected?.kind === "foreground-active" ? selected.activeChild ?? selected.control : undefined;
1033
+ const viewLabel = promptAuditViewLabel(this.promptAuditView);
1034
+ const promptText = this.selectedPromptText()
1035
+ ?? this.theme.fg("muted", "Selected prompt unavailable.");
1036
+ const raw = [
1037
+ this.theme.bold("Prompt Audit"),
1038
+ this.theme.fg("dim", "Retention: live memory only · no storage"),
1039
+ selected?.kind === "foreground-active" ? `Run: ${selected.runId}${selected.index !== undefined ? ` · Child: ${selected.index}` : ""} · Agent: ${selected.agent}` : "Selected prompt unavailable",
1040
+ live ? `Started: ${new Date(live.startedAt).toISOString()}` : undefined,
1041
+ live ? `Model: ${formatModelThinking(live.model, live.thinking) || "default"}` : undefined,
1042
+ prompt?.cwd ? `Cwd: ${prompt.cwd}` : undefined,
1043
+ prompt?.outputPath ? `Output: ${prompt.outputPath}` : undefined,
1044
+ this.theme.fg("dim", `Live children: ${items.map(({ item }) => item.agent).join(", ") || "none"}`),
1045
+ this.theme.fg("dim", selectedPosition >= 0 ? `Selected: ${selectedPosition + 1}/${items.length}` : "Selected prompt unavailable"),
1046
+ "",
1047
+ this.theme.fg("accent", viewLabel),
1048
+ promptText,
1049
+ ];
1050
+ const body = raw.filter((line): line is string => line !== undefined).flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width)));
1051
+ return { header: [], body: this.withActionLines(body) };
1052
+ }
1053
+
856
1054
  private wrappedDetail(width: number): FleetDetailSections {
1055
+ if (this.promptAuditOpen) return this.promptAuditDetail(width);
857
1056
  const selected = this.snapshot.items[this.selected];
858
1057
  let transcriptWarning: string | undefined;
859
1058
  if (selected) {
@@ -871,7 +1070,8 @@ export class SubagentFleetComponent implements Component {
871
1070
  : latest?.kind === "tool"
872
1071
  ? `${latest.name} · ${latest.status}`
873
1072
  : "activity";
874
- return { header: structuredHeader(selected, width, this.theme, conversationState), body: this.withActionLines(body) };
1073
+ const promptSummary = selected.kind === "foreground-active" ? foregroundAuthoredPromptSummary(selected, this.state) : undefined;
1074
+ return { header: structuredHeader(selected, width, this.theme, conversationState, promptSummary), body: this.withActionLines(body) };
875
1075
  }
876
1076
  }
877
1077
  }
@@ -880,7 +1080,7 @@ export class SubagentFleetComponent implements Component {
880
1080
  if (transcriptWarning) raw.unshift(`Transcript preview warning: ${transcriptWarning}`, "");
881
1081
  const lines: string[] = [];
882
1082
  for (const line of raw) {
883
- const styled = /^(Run|State|Mode|Source|Child|Agent|Model):/.test(line)
1083
+ const styled = /^(Run|State|Mode|Source|Child|Agent|Model|Task):/.test(line)
884
1084
  ? this.theme.bold(line)
885
1085
  : /^(Transcript|Result transcript tail)/.test(line)
886
1086
  ? this.theme.fg("accent", line)
@@ -933,7 +1133,9 @@ export class SubagentFleetComponent implements Component {
933
1133
  }
934
1134
  lines.push(this.theme.fg("border", `├${"─".repeat(rosterWidth)}┴${"─".repeat(detailWidth)}┤`));
935
1135
  const position = this.snapshot.items.length ? `${this.selected + 1}/${this.snapshot.items.length}` : "0/0";
936
- const footer = ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} agent · ${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}`;
1136
+ const footer = this.promptAuditOpen
1137
+ ? ` j/k child · 1/2/3 view · g redo with guidance · c copy · Esc close Prompt Audit · ${position}`
1138
+ : ` ${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}`;
937
1139
  lines.push(this.theme.fg("border", "│") + fit(this.theme.fg("dim", footer), innerWidth) + this.theme.fg("border", "│"));
938
1140
  lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
939
1141
  return lines.map((line) => truncateToWidth(line, width));
@@ -954,6 +1156,12 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
954
1156
  const wasOpen = state.fleetInspectorOpen === true;
955
1157
  state.fleetInspectorOpen = true;
956
1158
  if (typeof ctx.ui.setWidget === "function") ctx.ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
1159
+ const copyText = options.copyText ?? (async (text: string) => {
1160
+ const module = await import("@earendil-works/pi-coding-agent");
1161
+ const copyToClipboard = (module as { copyToClipboard?: (value: string) => Promise<void> | void }).copyToClipboard;
1162
+ if (!copyToClipboard) throw new Error("Clipboard is unavailable in this Pi version.");
1163
+ await copyToClipboard(text);
1164
+ });
957
1165
  const actions = options.actions ?? {
958
1166
  steer: async (input: { runId: string; asyncDir: string; index?: number; message: string; mode: SteerDeliveryMode }) => firstToolResultText(await steerAsyncRun({
959
1167
  state,
@@ -974,10 +1182,15 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
974
1182
  ...(state.authorityPolicy ? { authorityPolicy: state.authorityPolicy } : {}),
975
1183
  ...(state.missionStoreConfig ? { missions: state.missionStoreConfig } : {}),
976
1184
  }), `Failed to open Herdr inspector for async run ${input.runId}.`),
1185
+ redoPrompt: async (input: { runId: string; index: number; guidance: string; control?: ForegroundRunControl }) => {
1186
+ const control = input.control ?? state.foregroundControls.get(input.runId);
1187
+ if (!control?.promptAuditRedo) return { text: "Redo is not available for this live child.", isError: true };
1188
+ return control.promptAuditRedo(input.index, input.guidance);
1189
+ },
977
1190
  } satisfies FleetActionHandlers;
978
1191
  try {
979
1192
  await ctx.ui.custom<undefined>(
980
- (tui, theme, _keybindings, done) => new SubagentFleetComponent(tui, theme, state, done, { ...options, actions }),
1193
+ (tui, theme, _keybindings, done) => new SubagentFleetComponent(tui, theme, state, done, { ...options, actions, copyText }),
981
1194
  {
982
1195
  overlay: true,
983
1196
  overlayOptions: { anchor: "center", width: "95%", minWidth: 60, maxHeight: "85%", margin: 1 },
@@ -131,11 +131,11 @@ function hostCall(method, args, observation) {
131
131
  : promise;
132
132
  }
133
133
 
134
- function runHostCall(key, params, collectFailure) {
134
+ function runHostCall(key, params, collectFailure, batch) {
135
135
  const callId = ++nextCallId;
136
136
  const promise = new Promise((resolve, reject) => {
137
137
  pending.set(callId, { resolve, reject });
138
- parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}) } });
138
+ parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}), ...(batch ? { batch } : {}) } });
139
139
  });
140
140
  return { key, callId, promise };
141
141
  }
@@ -188,7 +188,8 @@ const runs = Object.freeze({
188
188
  calls.push({ key, params });
189
189
  }
190
190
  for (const { key, params } of calls) runFingerprints.set(key, stableRunJson(params));
191
- const launched = calls.map(({ key, params }) => runHostCall(key, params, true));
191
+ const batch = { id: "batch-" + (++nextCallId), calls };
192
+ const launched = calls.map(({ key, params }) => runHostCall(key, params, true, batch));
192
193
  return trackRunObservation(launched.map(({ key, callId }) => ({ key, callId })), Promise.all(launched.map(({ promise }) => promise)));
193
194
  },
194
195
  status(keyOrRunId) { return hostCall("status", { keyOrRunId }); },
@@ -363,7 +364,8 @@ export interface RunWorkflowScriptOptions {
363
364
  script: string;
364
365
  timeoutMs?: number;
365
366
  signal?: AbortSignal;
366
- launch: (key: string, params: Record<string, unknown>, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
367
+ admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
368
+ launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean }) => Promise<WorkflowScriptChildResult>;
367
369
  status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
368
370
  state?: {
369
371
  get: (key: string) => unknown | Promise<unknown>;
@@ -469,10 +471,13 @@ function validateKey(value: unknown, owner = "runs.run"): string {
469
471
  return value;
470
472
  }
471
473
 
472
- function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label"> {
474
+ function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
473
475
  return {
474
476
  ...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
475
477
  ...(typeof params.label === "string" && params.label.trim() ? { label: params.label.trim() } : {}),
478
+ // Requested agent name, so a child is identifiable while it runs. Launch
479
+ // resolution overwrites this with the canonical name on the terminal entry.
480
+ ...(typeof params.agent === "string" && params.agent.trim() ? { agent: params.agent.trim() } : {}),
476
481
  };
477
482
  }
478
483
 
@@ -487,6 +492,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
487
492
  const children = new Map<string, WorkflowScriptChildResult>();
488
493
  const childOrder: string[] = [];
489
494
  const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean }>();
495
+ const batchAdmissions = new Map<string, Promise<void>>();
490
496
  const observedRunCalls = new Set<number>();
491
497
  const childController = new AbortController();
492
498
  let settled = false;
@@ -677,7 +683,21 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
677
683
  childOrder.push(key);
678
684
  trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
679
685
  traceChanged();
680
- const promise = Promise.resolve().then(() => options.launch(key, { ...params, async: params.async ?? false }, childController.signal)).then((result) => {
686
+ const batch = isRecord(message.args.batch) && typeof message.args.batch.id === "string" && Array.isArray(message.args.batch.calls)
687
+ ? { id: message.args.batch.id, calls: message.args.batch.calls.filter((call): call is { key: string; params: Record<string, unknown> } => isRecord(call) && typeof call.key === "string" && isRecord(call.params)) }
688
+ : undefined;
689
+ let admission = batch ? batchAdmissions.get(batch.id) : undefined;
690
+ if (!admission) {
691
+ const seenKeys = new Set<string>();
692
+ const calls = (batch?.calls ?? [{ key, params }]).filter((call) => {
693
+ if (seenKeys.has(call.key) || launches.has(call.key) || call.params.resume !== undefined) return false;
694
+ seenKeys.add(call.key);
695
+ return true;
696
+ });
697
+ admission = Promise.resolve().then(() => options.admit?.(calls));
698
+ if (batch) batchAdmissions.set(batch.id, admission);
699
+ }
700
+ const promise = admission.then(() => options.launch(key, { ...params, async: params.async ?? false }, childController.signal, { admitted: true })).then((result) => {
681
701
  const normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
682
702
  children.set(key, normalized);
683
703
  trace.push({ operation: "run", key, state: normalized.ok ? "completed" : "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });