pi-subagents 0.49.0 → 0.51.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 (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. package/src/runs/foreground/chain-execution.ts +0 -1565
package/src/tui/fleet.ts CHANGED
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import { getMarkdownTheme, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type MarkdownTheme } from "@earendil-works/pi-tui";
6
+ import { snapshotExternalRuns, type ExternalRun } from "../api/external-runs.ts";
6
7
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
7
8
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../shared/formatters.ts";
8
9
  import { DIRS, type AsyncJobState, type Details, type FleetKeybindingAction, type FleetKeybindingsConfig, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
@@ -13,13 +14,16 @@ import { listAsyncRuns, type AsyncRunSummary } from "../runs/background/async-st
13
14
  import { steerAsyncRun } from "../runs/foreground/async-steering-action.ts";
14
15
  import type { SteerDeliveryMode } from "../runs/background/control-channel.ts";
15
16
  import { stopAsyncRun } from "../runs/foreground/async-stop-action.ts";
17
+ import { resolveWorkflowForegroundSteeringTarget, steerWorkflowForegroundTarget } from "../runs/foreground/workflow-foreground-steering.ts";
16
18
  import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.ts";
17
19
  import { FLEET_STATUS_WIDGET_KEY } from "./fleet-status.ts";
18
20
  import { readFleetTranscript, renderFleetTranscript, type FleetTranscript } from "./fleet-transcript.ts";
19
21
  import { handleHerdrInspectorAction } from "../inspectors/herdr/actions.ts";
22
+ import type { HerdrClient } from "../inspectors/herdr/client.ts";
20
23
  import { getLivePromptAudit, type LivePromptAudit, type PromptAuditView } from "../runs/foreground/prompt-audit.ts";
21
24
 
22
25
  const REFRESH_MS = 750;
26
+ const MIN_REFRESH_MS = 250;
23
27
  const MAX_RECENT_ASYNC_RUNS = 20;
24
28
  const MAX_FLEET_HISTORY_CANDIDATES = 100;
25
29
  const TRANSCRIPT_LINES = 200;
@@ -77,6 +81,7 @@ export type FleetItem = (
77
81
  | { key: string; kind: "foreground-active"; runId: string; index?: number; agent: string; state: "running"; updatedAt: number; control: ForegroundRunControl; activeChild?: ForegroundChildControl }
78
82
  | { key: string; kind: "foreground-recent"; runId: string; index: number; agent: string; state: ForegroundResumeChild["status"]; updatedAt: number; run: ForegroundResumeRun; child: ForegroundResumeChild }
79
83
  | { key: string; kind: "async"; runId: string; index?: number; agent: string; state: string; updatedAt: number; run: AsyncRunSummary; step?: AsyncStep }
84
+ | { key: string; kind: "external"; runId: string; agent: string; state: ExternalRun["state"]; updatedAt: number; run: ExternalRun }
80
85
  ) & { description?: string };
81
86
 
82
87
  export interface FleetSnapshot {
@@ -105,6 +110,7 @@ export interface FleetViewOptions {
105
110
  fleetKeybindings?: FleetKeybindingsConfig;
106
111
  actions?: FleetActionHandlers;
107
112
  copyText?: (text: string) => Promise<void> | void;
113
+ herdrClient?: HerdrClient;
108
114
  }
109
115
 
110
116
  function belongsToCurrentSession(sessionId: string | undefined, currentSessionId: string | null): boolean {
@@ -186,8 +192,26 @@ export function collectFleetSnapshot(
186
192
  ): FleetSnapshot {
187
193
  const items: FleetItem[] = [];
188
194
  const activeForegroundIds = new Set<string>();
195
+ const trackedJobs = state.fleetJobs ?? state.asyncJobs;
196
+ const workflowParentIds = new Set([...trackedJobs.values()]
197
+ .filter((job) => job.mode === "workflow" && belongsToCurrentSession(job.sessionId, state.currentSessionId))
198
+ .map((job) => job.asyncId));
199
+ const workflowForegroundChildCounts = new Map<string, number>();
200
+ const liveWorkflowForegroundControls = new Set<ForegroundRunControl>();
201
+ for (const control of state.foregroundControls.values()) {
202
+ const activeChildCount = control.activeChildren?.size ?? 0;
203
+ if (!control.parentWorkflowRunId
204
+ || !workflowParentIds.has(control.parentWorkflowRunId)
205
+ || !belongsToCurrentSession(control.sessionId, state.currentSessionId)
206
+ || !control.workflowSteeringDir
207
+ || activeChildCount === 0) continue;
208
+ liveWorkflowForegroundControls.add(control);
209
+ workflowForegroundChildCounts.set(control.parentWorkflowRunId, (workflowForegroundChildCounts.get(control.parentWorkflowRunId) ?? 0) + activeChildCount);
210
+ }
189
211
  for (const control of [...state.foregroundControls.values()].sort((left, right) => right.updatedAt - left.updatedAt)) {
190
212
  activeForegroundIds.add(control.runId);
213
+ if (control.parentWorkflowRunId && workflowParentIds.has(control.parentWorkflowRunId)
214
+ && ((workflowForegroundChildCounts.get(control.parentWorkflowRunId) ?? 0) <= 1 || !liveWorkflowForegroundControls.has(control))) continue;
191
215
  if (control.activeChildren) {
192
216
  for (const child of [...control.activeChildren.values()].sort((left, right) => left.index - right.index)) {
193
217
  items.push({
@@ -222,7 +246,7 @@ export function collectFleetSnapshot(
222
246
  try {
223
247
  let runs: AsyncRunSummary[];
224
248
  const descriptions = new Map<string, string>();
225
- const tracked = [...(state.fleetJobs ?? state.asyncJobs).values()]
249
+ const tracked = [...trackedJobs.values()]
226
250
  .filter((job) => belongsToCurrentSession(job.sessionId, state.currentSessionId));
227
251
  const byUpdate = (left: AsyncJobState, right: AsyncJobState) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0);
228
252
  const active = tracked.filter((job) => job.status === "queued" || job.status === "running").sort(byUpdate);
@@ -255,6 +279,26 @@ export function collectFleetSnapshot(
255
279
  error = cause instanceof Error ? cause.message : String(cause);
256
280
  }
257
281
 
282
+ if (state.currentSessionId) {
283
+ try {
284
+ for (const run of snapshotExternalRuns(state.currentSessionId, { ignoreMalformed: true, onMalformedRecord: (message) => console.warn(`[pi-subagents] Removed ${message}`) })) {
285
+ items.push({
286
+ key: `external:${run.id}`,
287
+ kind: "external",
288
+ runId: run.id,
289
+ agent: run.label,
290
+ state: run.state,
291
+ updatedAt: run.updatedAt ?? run.endedAt ?? run.startedAt,
292
+ run,
293
+ ...(run.currentAction ? { description: run.currentAction } : {}),
294
+ });
295
+ }
296
+ } catch (cause) {
297
+ const message = `Failed to inspect external jobs: ${cause instanceof Error ? cause.message : String(cause)}`;
298
+ error = error ? `${error}; ${message}` : message;
299
+ }
300
+ }
301
+
258
302
  const recentForeground = [...(state.foregroundRuns?.values() ?? [])]
259
303
  .filter((run) => belongsToCurrentSession(run.sessionId, state.currentSessionId) && !activeForegroundIds.has(run.runId))
260
304
  .sort((left, right) => right.updatedAt - left.updatedAt);
@@ -276,6 +320,27 @@ export function collectFleetSnapshot(
276
320
  return { items, ...(error ? { error } : {}) };
277
321
  }
278
322
 
323
+ function visibleWorkflowParentKeyForForegroundKey(state: SubagentState, key: string, items: FleetItem[]): string | undefined {
324
+ for (const control of state.foregroundControls.values()) {
325
+ if (!control.parentWorkflowRunId) continue;
326
+ let matches = false;
327
+ if (control.activeChildren) {
328
+ for (const child of control.activeChildren.values()) {
329
+ if (`foreground-active:${control.runId}:${child.index}` === key) {
330
+ matches = true;
331
+ break;
332
+ }
333
+ }
334
+ } else {
335
+ matches = `foreground-active:${control.runId}:${control.currentIndex ?? 0}` === key;
336
+ }
337
+ if (!matches) continue;
338
+ const parentKey = `async:${control.parentWorkflowRunId}`;
339
+ return items.some((item) => item.key === parentKey) ? parentKey : undefined;
340
+ }
341
+ return undefined;
342
+ }
343
+
279
344
  function statusGlyph(item: FleetItem, theme: Theme): string {
280
345
  if (item.state === "running") return theme.fg("accent", "●");
281
346
  if (item.state === "queued" || item.state === "pending") return theme.fg("muted", "◦");
@@ -424,10 +489,20 @@ function foregroundRecentDetail(item: Extract<FleetItem, { kind: "foreground-rec
424
489
  return lines.filter((line): line is string => line !== undefined);
425
490
  }
426
491
 
427
- function asyncDetail(item: Extract<FleetItem, { kind: "async" }>): string[] {
492
+ function externalElapsedEnd(run: ExternalRun): number {
493
+ const terminal = run.state !== "queued" && run.state !== "running";
494
+ return terminal ? (run.endedAt ?? run.updatedAt ?? Date.now()) : Date.now();
495
+ }
496
+
497
+ function asyncDetail(item: Extract<FleetItem, { kind: "async" }>, state: SubagentState): string[] {
428
498
  const status = readStatus(item.run.asyncDir);
429
499
  if (status) {
430
- return formatAsyncRunTranscript(status, item.run.asyncDir, { index: item.index, lines: TRANSCRIPT_LINES }).split("\n");
500
+ const trackedJob = state.fleetJobs?.get(item.runId) ?? state.asyncJobs.get(item.runId);
501
+ return formatAsyncRunTranscript(status, item.run.asyncDir, {
502
+ index: item.index,
503
+ lines: TRANSCRIPT_LINES,
504
+ sessionRoots: uniquePaths([...(state.trustedSessionRoots ?? []), trackedJob?.sessionRoot]),
505
+ }).split("\n");
431
506
  }
432
507
  const outputPath = item.index !== undefined ? path.join(item.run.asyncDir, `output-${item.index}.log`) : undefined;
433
508
  return [
@@ -444,13 +519,36 @@ function asyncDetail(item: Extract<FleetItem, { kind: "async" }>): string[] {
444
519
  ].filter((line): line is string => line !== undefined);
445
520
  }
446
521
 
522
+ function externalDetail(item: Extract<FleetItem, { kind: "external" }>): string[] {
523
+ return [
524
+ `Run: ${item.runId}`,
525
+ "Source: external · display-only",
526
+ `Owner: ${item.run.source}`,
527
+ `State: ${item.state}`,
528
+ `Started: ${new Date(item.run.startedAt).toISOString()}`,
529
+ item.run.updatedAt !== undefined ? `Updated: ${new Date(item.run.updatedAt).toISOString()}` : undefined,
530
+ item.run.endedAt !== undefined ? `Ended: ${new Date(item.run.endedAt).toISOString()}` : undefined,
531
+ `Elapsed: ${formatDuration(Math.max(0, externalElapsedEnd(item.run) - item.run.startedAt))}`,
532
+ item.run.currentAction ? `Current action: ${item.run.currentAction}` : undefined,
533
+ item.run.reportPath ? `Report path: ${item.run.reportPath}` : undefined,
534
+ item.run.transcriptPath ? `Transcript path: ${item.run.transcriptPath}` : undefined,
535
+ "",
536
+ "Preview",
537
+ item.run.preview ?? "(no preview supplied)",
538
+ "",
539
+ "The owning extension controls execution, persistence, cancellation, and results.",
540
+ ].filter((line): line is string => line !== undefined);
541
+ }
542
+
447
543
  function detailLines(item: FleetItem | undefined, error: string | undefined, state: SubagentState): string[] {
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."];
544
+ if (!item) return [error ? `Fleet scan failed: ${error}` : "No current-session Fleet jobs.", "", "New jobs appear here automatically while this inspector remains open."];
449
545
  const lines = item.kind === "foreground-active"
450
546
  ? foregroundActiveDetail(item, state)
451
547
  : item.kind === "foreground-recent"
452
548
  ? foregroundRecentDetail(item, state)
453
- : asyncDetail(item);
549
+ : item.kind === "external"
550
+ ? externalDetail(item)
551
+ : asyncDetail(item, state);
454
552
  if (error) lines.unshift(`Fleet scan warning: ${error}`, "");
455
553
  return lines;
456
554
  }
@@ -478,6 +576,7 @@ function fleetArtifactsRoot(state: SubagentState, cwd: string): string {
478
576
  }
479
577
 
480
578
  function transcriptTarget(item: FleetItem, state: SubagentState): { path: string; trustedRoots: string[] } | undefined {
579
+ if (item.kind === "external") return undefined;
481
580
  if (item.kind === "foreground-active") {
482
581
  const artifactsRoot = fleetArtifactsRoot(state, item.control.cwd ?? state.baseCwd);
483
582
  return {
@@ -522,10 +621,12 @@ function itemContext(item: FleetItem): string | undefined {
522
621
  }
523
622
 
524
623
  function itemMode(item: FleetItem): string {
624
+ if (item.kind === "external") return "display-only";
525
625
  return item.kind === "foreground-active" ? item.control.mode : item.run.mode;
526
626
  }
527
627
 
528
628
  function itemSource(item: FleetItem): string {
629
+ if (item.kind === "external") return `external · ${item.run.source}`;
529
630
  if (item.kind === "async") return "background";
530
631
  return item.kind === "foreground-active" ? "foreground · live" : "foreground · recent";
531
632
  }
@@ -545,6 +646,8 @@ function itemStats(item: FleetItem): string[] {
545
646
  model = formatModelThinking(item.child.model, item.child.thinking) || undefined;
546
647
  tokens = item.child.tokens;
547
648
  tools = item.child.toolCount;
649
+ } else if (item.kind === "external") {
650
+ durationMs = Math.max(0, externalElapsedEnd(item.run) - item.run.startedAt);
548
651
  } else {
549
652
  model = formatModelThinking(item.step?.model, item.step?.thinking) || undefined;
550
653
  tokens = item.step?.tokens?.total ?? (item.index === undefined ? item.run.totalTokens?.total : undefined);
@@ -564,7 +667,7 @@ function itemStats(item: FleetItem): string[] {
564
667
  function structuredHeader(item: FleetItem, width: number, theme: Theme, conversationState: string, promptSummary?: string): string[] {
565
668
  const lines: string[] = [];
566
669
  lines.push(rightAligned(` ${statusGlyph(item, theme)} ${theme.bold(item.agent)}`, theme.fg("dim", item.state), width));
567
- const child = item.index !== undefined ? ` · child ${item.index + 1}` : "";
670
+ const child = "index" in item && item.index !== undefined ? ` · child ${item.index + 1}` : "";
568
671
  const context = itemContext(item);
569
672
  const identity = `${itemSource(item)} · ${item.runId.slice(0, 8)}${child} · ${itemMode(item)}${context ? ` ${context}` : ""}`;
570
673
  lines.push(` ${theme.fg("dim", identity)}`);
@@ -629,7 +732,8 @@ export class SubagentFleetComponent implements Component {
629
732
  private actionBusy = false;
630
733
  private transcriptCache: FleetTranscriptCache | undefined;
631
734
  private disposed = false;
632
- private readonly timer: ReturnType<typeof setInterval>;
735
+ private refreshTimer: ReturnType<typeof setTimeout> | undefined;
736
+ private readonly refreshMs: number;
633
737
  private readonly tui: FleetTui;
634
738
  private readonly theme: Theme;
635
739
  private readonly markdownTheme: MarkdownTheme;
@@ -652,20 +756,41 @@ export class SubagentFleetComponent implements Component {
652
756
  this.done = done;
653
757
  this.options = options;
654
758
  this.keybindings = resolveFleetKeybindings(options.fleetKeybindings);
759
+ this.refreshMs = Math.max(MIN_REFRESH_MS, options.refreshMs ?? REFRESH_MS);
655
760
  this.selectedKey = options.initialKey;
656
761
  this.refresh();
657
- this.timer = setInterval(() => {
762
+ this.scheduleRefresh();
763
+ }
764
+
765
+ private scheduleRefresh(): void {
766
+ if (this.disposed || this.refreshTimer) return;
767
+ this.refreshTimer = setTimeout(() => {
768
+ this.refreshTimer = undefined;
658
769
  if (this.disposed) return;
659
- this.invalidate();
660
- this.tui.requestRender();
661
- }, options.refreshMs ?? REFRESH_MS);
662
- this.timer.unref?.();
770
+ try {
771
+ this.invalidate();
772
+ this.tui.requestRender();
773
+ } finally {
774
+ this.scheduleRefresh();
775
+ }
776
+ }, this.refreshMs);
777
+ this.refreshTimer.unref?.();
778
+ }
779
+
780
+ private stopRefresh(): void {
781
+ this.disposed = true;
782
+ if (this.refreshTimer) clearTimeout(this.refreshTimer);
783
+ this.refreshTimer = undefined;
663
784
  }
664
785
 
665
786
  private refresh(): void {
666
787
  const previousKey = this.snapshot.items[this.selected]?.key ?? this.selectedKey;
667
788
  this.snapshot = collectFleetSnapshot(this.state, this.options);
668
- const preserved = previousKey ? this.snapshot.items.findIndex((item) => item.key === previousKey) : -1;
789
+ let preserved = previousKey ? this.snapshot.items.findIndex((item) => item.key === previousKey) : -1;
790
+ if (preserved < 0 && previousKey) {
791
+ const parentKey = visibleWorkflowParentKeyForForegroundKey(this.state, previousKey, this.snapshot.items);
792
+ if (parentKey) preserved = this.snapshot.items.findIndex((item) => item.key === parentKey);
793
+ }
669
794
  this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.snapshot.items.length - 1));
670
795
  this.selectedKey = this.snapshot.items[this.selected]?.key;
671
796
  }
@@ -725,14 +850,28 @@ export class SubagentFleetComponent implements Component {
725
850
  private selectedAsyncAction(): { item: Extract<FleetItem, { kind: "async" }> } | { reason: string } {
726
851
  const item = this.snapshot.items[this.selected];
727
852
  if (!item) return { reason: "No child is selected." };
853
+ if (item.kind === "external") return { reason: "External jobs are display-only and remain controlled by their owning extension." };
728
854
  if (item.kind !== "async") return { reason: "Fleet controls are available for current-session top-level async runs only." };
729
855
  if (!isActionableAsyncState(item.run.state) || !isActionableAsyncState(item.state)) return { reason: `Selected child is ${item.state}; controls require a running or queued async child.` };
730
856
  return { item };
731
857
  }
732
858
 
859
+ private selectedSteerAction(): { runId: string; asyncDir: string; index?: number } | { reason: string } {
860
+ const item = this.snapshot.items[this.selected];
861
+ if (item?.kind === "foreground-active" && item.control.parentWorkflowRunId) {
862
+ const parent = this.state.asyncJobs.get(item.control.parentWorkflowRunId) ?? this.state.fleetJobs?.get(item.control.parentWorkflowRunId);
863
+ if (!parent || !isActionableAsyncState(parent.status)) return { reason: "The parent workflow is no longer available for steering." };
864
+ return { runId: item.runId, asyncDir: parent.asyncDir, ...(item.index !== undefined ? { index: item.index } : {}) };
865
+ }
866
+ const target = this.selectedAsyncAction();
867
+ if ("reason" in target) return target;
868
+ return { runId: target.item.runId, asyncDir: target.item.run.asyncDir, ...(target.item.index !== undefined ? { index: target.item.index } : {}) };
869
+ }
870
+
733
871
  private selectedHerdrInspectAction(): { runId: string; asyncDir: string; index?: number } | { reason: string } {
734
872
  const item = this.snapshot.items[this.selected];
735
873
  if (!item) return { reason: "No child is selected." };
874
+ if (item.kind === "external") return { reason: "External jobs are display-only and have no Herdr controls." };
736
875
  if (item.kind === "async") {
737
876
  if (!isActionableAsyncState(item.run.state) || !isActionableAsyncState(item.state)) return { reason: `Selected child is ${item.state}; controls require a running or queued async child.` };
738
877
  return { runId: item.runId, asyncDir: item.run.asyncDir, ...(item.index !== undefined ? { index: item.index } : {}) };
@@ -886,12 +1025,12 @@ export class SubagentFleetComponent implements Component {
886
1025
  this.setActionNotice({ text: "Steer message cannot be empty.", isError: true });
887
1026
  return;
888
1027
  }
889
- const target = this.selectedAsyncAction();
1028
+ const target = this.selectedSteerAction();
890
1029
  if ("reason" in target || !this.options.actions) {
891
1030
  this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
892
1031
  return;
893
1032
  }
894
- this.runAction(() => this.options.actions!.steer({ runId: target.item.runId, asyncDir: target.item.run.asyncDir, ...(target.item.index !== undefined ? { index: target.item.index } : {}), message, mode: this.steerMode }));
1033
+ this.runAction(() => this.options.actions!.steer({ ...target, message, mode: this.steerMode }));
895
1034
  return;
896
1035
  }
897
1036
  if (matchesKey(data, "tab") || data === "\t") {
@@ -928,6 +1067,7 @@ export class SubagentFleetComponent implements Component {
928
1067
  return;
929
1068
  }
930
1069
  if (matchesFleetAction(data, this.keybindings, "close")) {
1070
+ this.stopRefresh();
931
1071
  this.done(undefined);
932
1072
  return;
933
1073
  }
@@ -958,7 +1098,7 @@ export class SubagentFleetComponent implements Component {
958
1098
  return;
959
1099
  }
960
1100
  if (matchesFleetAction(data, this.keybindings, "steer")) {
961
- const target = this.selectedAsyncAction();
1101
+ const target = this.selectedSteerAction();
962
1102
  if ("reason" in target || !this.options.actions) this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
963
1103
  else {
964
1104
  this.actionNotice = undefined;
@@ -1117,7 +1257,9 @@ export class SubagentFleetComponent implements Component {
1117
1257
  ];
1118
1258
  const lines = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
1119
1259
  const selected = this.snapshot.items[this.selected];
1120
- const title = ` ${this.theme.bold("Subagent fleet inspector")} ${this.theme.fg("dim", "· live controls")}`;
1260
+ const title = selected?.kind === "external"
1261
+ ? ` ${this.theme.bold("Fleet inspector")} ${this.theme.fg("dim", "· external display-only")}`
1262
+ : ` ${this.theme.bold("Subagent fleet inspector")} ${this.theme.fg("dim", "· live controls")}`;
1121
1263
  const selectedStatus = selected
1122
1264
  ? `${statusGlyph(selected, this.theme)} ${selected.agent} · ${selected.state} `
1123
1265
  : this.theme.fg("dim", "no children ");
@@ -1136,7 +1278,9 @@ export class SubagentFleetComponent implements Component {
1136
1278
  const position = this.snapshot.items.length ? `${this.selected + 1}/${this.snapshot.items.length}` : "0/0";
1137
1279
  const footer = this.promptAuditOpen
1138
1280
  ? ` j/k child · 1/2/3 view · g redo with guidance · c copy · Esc close Prompt Audit · ${position}`
1139
- : ` ${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}`;
1281
+ : selected?.kind === "external"
1282
+ ? ` ${bindingLabel(this.keybindings, "selectUp")}/${bindingLabel(this.keybindings, "selectDown")} job · display-only · ${bindingLabel(this.keybindings, "refresh")} refresh · ${bindingLabel(this.keybindings, "close")} close · ${position}`
1283
+ : ` ${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}`;
1140
1284
  lines.push(this.theme.fg("border", "│") + fit(this.theme.fg("dim", footer), innerWidth) + this.theme.fg("border", "│"));
1141
1285
  lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
1142
1286
  return lines.map((line) => truncateToWidth(line, width));
@@ -1148,8 +1292,7 @@ export class SubagentFleetComponent implements Component {
1148
1292
  }
1149
1293
 
1150
1294
  dispose(): void {
1151
- this.disposed = true;
1152
- clearInterval(this.timer);
1295
+ this.stopRefresh();
1153
1296
  }
1154
1297
  }
1155
1298
 
@@ -1164,23 +1307,38 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
1164
1307
  await copyToClipboard(text);
1165
1308
  });
1166
1309
  const actions = options.actions ?? {
1167
- steer: async (input: { runId: string; asyncDir: string; index?: number; message: string; mode: SteerDeliveryMode }) => firstToolResultText(await steerAsyncRun({
1168
- state,
1169
- runId: input.runId,
1170
- ...(input.index !== undefined ? { index: input.index } : {}),
1171
- message: input.message,
1172
- mode: input.mode,
1173
- location: { asyncDir: input.asyncDir, resolvedId: input.runId } as Parameters<typeof steerAsyncRun>[0]["location"],
1174
- }), `Failed to steer async run ${input.runId}.`),
1310
+ steer: async (input: { runId: string; asyncDir: string; index?: number; message: string; mode: SteerDeliveryMode }) => {
1311
+ const status = readStatus(input.asyncDir);
1312
+ const liveWorkflowRunId = status?.mode === "workflow" && state.workflowControllers?.has(status.runId || input.runId)
1313
+ ? status.runId || input.runId
1314
+ : undefined;
1315
+ if (liveWorkflowRunId) {
1316
+ const route = state.foregroundControls.get(input.runId)?.parentWorkflowRunId === liveWorkflowRunId
1317
+ ? resolveWorkflowForegroundSteeringTarget({ state, childRunId: input.runId, asyncDirRoot: options.asyncDirRoot ?? DIRS.async })
1318
+ : resolveWorkflowForegroundSteeringTarget({ state, workflowRunId: liveWorkflowRunId, asyncDirRoot: options.asyncDirRoot ?? DIRS.async });
1319
+ if (!route.ok) return { text: route.message, isError: true };
1320
+ return firstToolResultText(await steerWorkflowForegroundTarget({ target: route.target, message: input.message, mode: input.mode, ...(input.index !== undefined ? { index: input.index } : {}) }), `Failed to steer foreground run ${input.runId}.`);
1321
+ }
1322
+ return firstToolResultText(await steerAsyncRun({
1323
+ state,
1324
+ runId: input.runId,
1325
+ ...(input.index !== undefined ? { index: input.index } : {}),
1326
+ message: input.message,
1327
+ mode: input.mode,
1328
+ location: { asyncDir: input.asyncDir, resolvedId: input.runId } as Parameters<typeof steerAsyncRun>[0]["location"],
1329
+ }), `Failed to steer async run ${input.runId}.`);
1330
+ },
1175
1331
  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}.`),
1176
1332
  inspect: async (input: { runId: string; asyncDir: string; index?: number }) => firstToolResultText(await handleHerdrInspectorAction("inspector.open", {
1177
1333
  id: input.runId,
1178
1334
  dir: input.asyncDir,
1335
+ focus: true,
1179
1336
  ...(input.index !== undefined ? { index: input.index } : {}),
1180
1337
  }, {
1181
1338
  state,
1182
1339
  sessionRoots: state.trustedSessionRoots,
1183
1340
  cwd: state.baseCwd,
1341
+ ...(options.herdrClient ? { client: options.herdrClient } : {}),
1184
1342
  ...(state.authorityPolicy ? { authorityPolicy: state.authorityPolicy } : {}),
1185
1343
  ...(state.missionStoreConfig ? { missions: state.missionStoreConfig } : {}),
1186
1344
  }), `Failed to open Herdr inspector for async run ${input.runId}.`),
package/src/tui/render.ts CHANGED
@@ -28,6 +28,7 @@ import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
28
28
  import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
29
29
  import { contextModeBadge, contextModePrefix } from "../runs/shared/context-mode.ts";
30
30
  import { buildWorkflowChatProgressRows, type WorkflowChatProgressRow } from "../workflows/chat-progress.ts";
31
+ import { encodeAsyncStatusSnapshotWidget } from "../runs/background/async-status-snapshot.ts";
31
32
 
32
33
  type Theme = ExtensionContext["ui"]["theme"];
33
34
 
@@ -71,6 +72,22 @@ function liveDetailHintText(): string {
71
72
  return `Press ${liveDetailKeyText()} for live detail`;
72
73
  }
73
74
 
75
+ function foregroundSingleHintText(shortcut?: string): string {
76
+ if (!shortcut) return liveDetailHintText();
77
+ const label = shortcut
78
+ .split("+")
79
+ .map((part) => {
80
+ const normalized = part.trim().toLowerCase();
81
+ if (normalized === "ctrl") return "Ctrl";
82
+ if (normalized === "alt") return "Alt";
83
+ if (normalized === "shift") return "Shift";
84
+ if (normalized === "super") return "Super";
85
+ return normalized.length === 1 ? normalized.toUpperCase() : part.trim();
86
+ })
87
+ .join("+");
88
+ return `${liveDetailHintText()} · ${label} to run in background`;
89
+ }
90
+
74
91
  function getTermWidth(): number {
75
92
  return process.stdout.columns || 120;
76
93
  }
@@ -341,7 +358,7 @@ function visibleWorkflowRows(rows: WorkflowChatProgressRow[]): { rows: WorkflowC
341
358
  selected.add(row.key);
342
359
  };
343
360
  for (const row of [...rows].reverse()) {
344
- if (row.state === "failed") add(row);
361
+ if (row.state === "failed" || row.state === "detached") add(row);
345
362
  }
346
363
  for (const row of [...rows].reverse()) add(row);
347
364
  return {
@@ -1585,10 +1602,21 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
1585
1602
  return;
1586
1603
  }
1587
1604
  if (!ctx.hasUI) return;
1605
+ if ((ctx as { mode?: string }).mode === "rpc") {
1606
+ ctx.ui.setWidget(WIDGET_KEY, encodeAsyncStatusSnapshotWidget(jobs));
1607
+ return;
1608
+ }
1588
1609
  ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
1589
1610
  }
1590
1611
 
1591
- function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
1612
+ function renderSingleCompact(
1613
+ d: Details,
1614
+ r: Details["results"][number],
1615
+ theme: Theme,
1616
+ layout: MainWindowRenderLayout,
1617
+ frame?: number,
1618
+ foregroundDetachShortcut?: string,
1619
+ ): Component {
1592
1620
  const output = r.truncation?.text || getSingleResultOutput(r);
1593
1621
  const progress = r.progress || r.progressSummary;
1594
1622
  const isRunning = isResultRunning(r);
@@ -1613,7 +1641,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1613
1641
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, progressSnapshotNow)) {
1614
1642
  c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1615
1643
  }
1616
- c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}${liveDetailHintText()}`), width), 0, 0));
1644
+ c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}${foregroundSingleHintText(foregroundDetachShortcut)}`), width), 0, 0));
1617
1645
  if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1618
1646
  return c;
1619
1647
  }
@@ -1635,7 +1663,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1635
1663
  function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: number): string {
1636
1664
  if (row.state === "running") return theme.fg("accent", runningGlyph(frame));
1637
1665
  if (row.state === "complete") return theme.fg("success", "✓");
1638
- if (row.state === "stopped") return theme.fg("warning", "■");
1666
+ if (row.state === "detached" || row.state === "stopped") return theme.fg("warning", "■");
1639
1667
  return theme.fg("error", "✗");
1640
1668
  }
1641
1669
 
@@ -1643,12 +1671,13 @@ function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): stri
1643
1671
  const label = (row.state === "complete" ? "complete" : row.state).padEnd(8);
1644
1672
  if (row.state === "running") return theme.fg("accent", label);
1645
1673
  if (row.state === "complete") return theme.fg("success", label);
1646
- if (row.state === "stopped") return theme.fg("warning", label);
1674
+ if (row.state === "detached" || row.state === "stopped") return theme.fg("warning", label);
1647
1675
  return theme.fg("error", label);
1648
1676
  }
1649
1677
 
1650
- function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue: boolean, isError?: boolean): "running" | "complete" | "failed" {
1678
+ function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue: boolean, isError?: boolean): "running" | "complete" | "failed" | "paused" {
1651
1679
  if (isError || rows.some((row) => row.state === "failed")) return "failed";
1680
+ if (rows.some((row) => row.state === "detached")) return "paused";
1652
1681
  if ((rows.length > 0 && rows.every((row) => row.state === "complete")) || hasTerminalValue) return "complete";
1653
1682
  return "running";
1654
1683
  }
@@ -1657,7 +1686,7 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1657
1686
  const workflow = d.workflow;
1658
1687
  const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace) : [];
1659
1688
  const state = workflowOverallState(rows, workflow?.value !== undefined, result.isError);
1660
- const glyph = state === "running" ? theme.fg("accent", runningGlyph(frame)) : state === "complete" ? theme.fg("success", "✓") : theme.fg("error", "✗");
1689
+ const glyph = state === "running" ? theme.fg("accent", runningGlyph(frame)) : state === "complete" ? theme.fg("success", "✓") : state === "paused" ? theme.fg("warning", "■") : theme.fg("error", "✗");
1661
1690
  const width = getTermWidth() - 4;
1662
1691
  const runId = d.runId ? d.runId.slice(0, 12) : "workflow";
1663
1692
  const repoLabel = d.chatProgress?.repoLabel ?? (d.chatProgress?.repoRelation === "same" ? "same repo" : "other repo");
@@ -1678,7 +1707,7 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1678
1707
  const label = row.label && row.label !== row.key ? ` ${oneLine(row.label)}` : "";
1679
1708
  const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
1680
1709
  const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
1681
- const error = row.error ? ` ${theme.fg("error", `· ${compactWorkflowError(row.error)}`)}` : "";
1710
+ const error = row.error ? ` ${theme.fg(row.state === "detached" ? "warning" : "error", `· ${compactWorkflowError(row.error)}`)}` : "";
1682
1711
  c.addChild(new Text(truncLine(`${rowIndent}${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
1683
1712
  }
1684
1713
  if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Emits ${workflow.emits.length}`), width), 0, 0));
@@ -1844,6 +1873,7 @@ export function renderSubagentResult(
1844
1873
  theme: Theme,
1845
1874
  frame?: number,
1846
1875
  rendererConfig?: MainWindowRendererConfig,
1876
+ foregroundDetachShortcut?: string,
1847
1877
  ): Component {
1848
1878
  const layout = resolveMainWindowRenderLayout(rendererConfig);
1849
1879
  const compact = (component: Component): Component => capCompactMainWindowResult(component, layout, theme, !options.expanded);
@@ -1877,8 +1907,9 @@ export function renderSubagentResult(
1877
1907
 
1878
1908
  if (d.mode === "single" && d.results.length === 1) {
1879
1909
  const r = d.results[0];
1910
+ const detachableShortcut = d.asyncId || d.background ? undefined : foregroundDetachShortcut;
1880
1911
  if (!r) return compact(renderMultiCompact(d, theme, layout, frame));
1881
- if (!expanded) return compact(renderSingleCompact(d, r, theme, layout, frame));
1912
+ if (!expanded) return compact(renderSingleCompact(d, r, theme, layout, frame, detachableShortcut));
1882
1913
  const isRunning = isResultRunning(r);
1883
1914
  const contextBadge = contextModeBadge(theme, r.context ?? d.context);
1884
1915
  const output = r.truncation?.text || getSingleResultOutput(r);
@@ -1918,7 +1949,7 @@ export function renderSubagentResult(
1918
1949
  if (liveStatusLine) {
1919
1950
  c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
1920
1951
  }
1921
- c.addChild(new Text(fit(theme.fg("accent", liveDetailHintText())), 0, 0));
1952
+ c.addChild(new Text(fit(theme.fg("accent", foregroundSingleHintText(detachableShortcut))), 0, 0));
1922
1953
  if (r.artifactPaths) {
1923
1954
  c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
1924
1955
  }
@@ -90,13 +90,13 @@ export function resolveWorkflowChatProgress(input: ResolveWorkflowChatProgressIn
90
90
  else mode = requestedMode;
91
91
 
92
92
  if (mode === "live-card" && !sameRepo) return { error: "chatProgress: 'live-card' is only available for workflowScript runs in the same Git repository." };
93
- if (mode === "live-card" && input.background) return { error: "chatProgress: 'live-card' requires a watched foreground workflow; pass async:false." };
93
+ if (mode === "live-card" && input.background) return { error: "chatProgress: 'live-card' is unavailable for async workflowScript. Async workflows have no inline live card; omit chatProgress or use auto/off. Use async:false only when the parent must block." };
94
94
  return { projection: { mode, repoRelation, ...(repoLabel ? { repoLabel } : {}) } };
95
95
  }
96
96
 
97
97
  export interface WorkflowChatProgressRow {
98
98
  key: string;
99
- state: "running" | "complete" | "failed" | "stopped";
99
+ state: "running" | "complete" | "failed" | "detached" | "stopped";
100
100
  label?: string;
101
101
  phase?: string;
102
102
  runId?: string;
@@ -127,9 +127,11 @@ export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workfl
127
127
  ? "complete"
128
128
  : entry.state === "failed"
129
129
  ? "failed"
130
- : entry.state === "stopped"
131
- ? "stopped"
132
- : "running";
130
+ : entry.state === "detached"
131
+ ? "detached"
132
+ : entry.state === "stopped"
133
+ ? "stopped"
134
+ : "running";
133
135
  const label = cleanLabel(entry.label);
134
136
  const phase = cleanLabel(entry.phase);
135
137
  if (label) next.label = label;