pi-subagents 0.66.0 → 0.68.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 (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
package/src/tui/render.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  WIDGET_KEY,
27
27
  } from "../shared/types.ts";
28
28
  import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
29
- import { FLEET_OPEN_SHORTCUT, formatShortcutLabel } from "../shared/shortcuts.ts";
29
+ import { formatShortcutLabel } from "../shared/shortcuts.ts";
30
30
  import { formatContextUsage, formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
31
31
  import { getDisplayItems, getSingleResultOutput, PROMPT_REDACTED } from "../shared/utils.ts";
32
32
  import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
@@ -45,6 +45,10 @@ import { formatWorkflowChecklistBottleneck, formatWorkflowChecklistPhase, format
45
45
  type Theme = ExtensionContext["ui"]["theme"];
46
46
 
47
47
  interface WorkflowWidgetProjection {
48
+ now: number;
49
+ inlineFleetCovered?: boolean;
50
+ children?: AsyncJobState[];
51
+ materializedKeys?: Set<string>;
48
52
  stages: WorkflowGraphNode[];
49
53
  steps: AsyncJobStep[];
50
54
  stageProgress?: { total: number; current?: number };
@@ -91,11 +95,11 @@ function liveDetailKeyText(): string {
91
95
  }
92
96
 
93
97
  export function liveDetailHintText(): string {
94
- return `Press ${liveDetailKeyText()} for live detail · ${formatShortcutLabel(FLEET_OPEN_SHORTCUT)} Fleet`;
98
+ return `Press ${liveDetailKeyText()} for live detail`;
95
99
  }
96
100
 
97
101
  function workflowDetailHintText(): string {
98
- return `Press ${liveDetailKeyText()} for details · ${formatShortcutLabel(FLEET_OPEN_SHORTCUT)} Fleet`;
102
+ return `Press ${liveDetailKeyText()} for details`;
99
103
  }
100
104
 
101
105
  function foregroundSingleHintText(shortcut?: string): string {
@@ -443,11 +447,14 @@ function workflowStageProgress(job: AsyncJobState, stages = job.mode === "workfl
443
447
  return { total: stages.length };
444
448
  }
445
449
 
446
- function buildWorkflowWidgetProjection(job: AsyncJobState): WorkflowWidgetProjection {
450
+ function buildWorkflowWidgetProjection(job: AsyncJobState, now = Date.now()): WorkflowWidgetProjection {
447
451
  const stages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
448
452
  const stageProgress = workflowStageProgress(job, stages);
449
- const steps = workflowWidgetSteps(job, stages);
450
- const projection: WorkflowWidgetProjection = { stages, steps };
453
+ const clock = job.status === "running" ? now : job.updatedAt;
454
+ const loadedSteps = workflowWidgetSteps(job, stages);
455
+ const steps = job.mode === "workflow" ? loadedSteps.map((step) => step.status === "running" && step.startedAt !== undefined && clock !== undefined
456
+ ? { ...step, durationMs: Math.max(0, clock - step.startedAt) } : step) : loadedSteps;
457
+ const projection: WorkflowWidgetProjection = { stages, steps, now };
451
458
  if (stageProgress) projection.stageProgress = stageProgress;
452
459
  if (stages.length) projection.plannedKeys = new Set(stages.map((node) => node.id));
453
460
  if (job.mode === "workflow") {
@@ -457,19 +464,27 @@ function buildWorkflowWidgetProjection(job: AsyncJobState): WorkflowWidgetProjec
457
464
  hostSteps: job.hostSteps,
458
465
  preflight: job.preflight,
459
466
  trace: job.workflow?.trace,
460
- now: job.updatedAt ?? Date.now(),
467
+ now: clock,
461
468
  });
462
469
  if (checklist.total > 0) projection.checklist = checklist;
463
470
  }
464
471
  return projection;
465
472
  }
466
473
 
467
- function workflowWidgetProjectionLookup(): WorkflowWidgetProjectionLookup {
474
+ function workflowWidgetProjectionLookup(now = Date.now(), childrenByParent = new Map<string, AsyncJobState[]>()): WorkflowWidgetProjectionLookup {
468
475
  const projections = new WeakMap<AsyncJobState, WorkflowWidgetProjection>();
469
476
  return (job: AsyncJobState) => {
470
477
  const cached = projections.get(job);
471
478
  if (cached) return cached;
472
- const projection = buildWorkflowWidgetProjection(job);
479
+ const projection = buildWorkflowWidgetProjection(job, now);
480
+ const children = childrenByParent.get(job.asyncId);
481
+ if (children?.length) {
482
+ projection.children = children;
483
+ projection.materializedKeys = new Set(children.flatMap((child) => [child.asyncId, ...(child.workflowKey ? [child.workflowKey] : [])]));
484
+ for (const step of projection.steps) {
485
+ if (step.runId && projection.materializedKeys.has(step.runId) && step.workflowKey) projection.materializedKeys.add(step.workflowKey);
486
+ }
487
+ }
473
488
  projections.set(job, projection);
474
489
  return projection;
475
490
  };
@@ -1051,9 +1066,11 @@ function hostStepRenderKey(row: AsyncStatusWorkflowRow): unknown[] {
1051
1066
  }
1052
1067
 
1053
1068
  export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
1054
- const projection = buildWorkflowWidgetProjection(job);
1069
+ const projection = buildWorkflowWidgetProjection(job, job.updatedAt ?? 0);
1055
1070
  return JSON.stringify({
1056
1071
  asyncDir: job.asyncDir,
1072
+ parentWorkflowRunId: job.parentWorkflowRunId,
1073
+ workflowKey: job.workflowKey,
1057
1074
  status: job.status,
1058
1075
  description: job.mode === "workflow" ? job.description : undefined,
1059
1076
  activityState: job.activityState,
@@ -1501,9 +1518,10 @@ function compactWorkflowStats(job: AsyncJobState, rows: readonly CompactWorkflow
1501
1518
  const rowToolUses = rows.map((row) => row.toolUses).filter((value): value is number => value !== undefined);
1502
1519
  const toolUses = job.toolCount ?? (rowToolUses.length ? rowToolUses.reduce((sum, value) => sum + value, 0) : undefined);
1503
1520
  const rowDuration = rows.map((row) => row.durationMs).filter((value): value is number => value !== undefined);
1504
- const durationMs = job.startedAt !== undefined && job.updatedAt !== undefined
1505
- ? Math.max(0, job.updatedAt - job.startedAt)
1506
- : rowDuration.length ? Math.max(...rowDuration) : undefined;
1521
+ const end = job.status === "running" ? projection.now : job.updatedAt;
1522
+ const durationMs = job.status !== "queued" && job.startedAt !== undefined && end !== undefined
1523
+ ? Math.max(0, end - job.startedAt)
1524
+ : job.status !== "queued" && rowDuration.length ? Math.max(...rowDuration) : undefined;
1507
1525
  return statJoin(theme, [
1508
1526
  `id: ${compactWorkflowShortId(job)}`,
1509
1527
  ...progress,
@@ -1540,8 +1558,11 @@ function compactWorkflowHeaderLine(job: AsyncJobState, theme: Theme, width?: num
1540
1558
  function compactWorkflowWidgetBodyLines(job: AsyncJobState, theme: Theme, frame: number | undefined, projection: WorkflowWidgetProjection): string[] {
1541
1559
  const rows = compactWorkflowLaneRows(job, projection.checklist);
1542
1560
  const lines = [` ${compactWorkflowStats(job, rows, theme, projection)}`];
1561
+ if (projection.inlineFleetCovered) return [...lines, ` ${theme.fg("dim", "Workflow children shown in Fleet roster")}`];
1543
1562
  if (rows.length) {
1544
- for (const row of rows) lines.push(compactWorkflowLaneLine(row, theme, " ", frame));
1563
+ for (const row of rows) {
1564
+ if (!projection.materializedKeys?.has(row.key)) lines.push(compactWorkflowLaneLine(row, theme, " ", frame));
1565
+ }
1545
1566
  } else {
1546
1567
  lines.push(` ${theme.fg("dim", "◦ waiting for workflow lanes")}`);
1547
1568
  }
@@ -2085,7 +2106,8 @@ function widgetStats(job: AsyncJobState, theme: Theme, projection = buildWorkflo
2085
2106
  if (projection.checklist && !checklistPrimary) parts.push(formatWorkflowChecklistSummary(projection.checklist));
2086
2107
  if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
2087
2108
  if (job.totalTokens?.total) parts.push(formatTokenUsage(job.totalTokens, "token"));
2088
- if (job.startedAt !== undefined && job.updatedAt !== undefined) parts.push(formatDuration(Math.max(0, job.updatedAt - job.startedAt)));
2109
+ const end = job.status === "running" && (job.mode === "workflow" || job.parentWorkflowRunId) ? projection.now : job.updatedAt;
2110
+ if (job.status !== "queued" && job.startedAt !== undefined && end !== undefined) parts.push(formatDuration(Math.max(0, end - job.startedAt)));
2089
2111
  return statJoin(theme, parts);
2090
2112
  }
2091
2113
 
@@ -2364,7 +2386,8 @@ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string):
2364
2386
  }
2365
2387
 
2366
2388
  function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
2367
- const { steps } = projection;
2389
+ const steps = projection.steps.filter((step) => !projection.materializedKeys?.has(step.workflowKey ?? "") && !projection.materializedKeys?.has(step.runId ?? ""));
2390
+ const checklist = widgetChecklistWithoutMaterializedChildren(projection);
2368
2391
  if (!expanded && job.mode === "workflow") {
2369
2392
  return compactWorkflowWidgetBodyLines(job, theme, frame, projection);
2370
2393
  }
@@ -2372,7 +2395,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
2372
2395
  const lane = projectAsyncLane(job, laneStepForJob(job, steps));
2373
2396
  return [
2374
2397
  ...(expanded ? workflowPreflightLines(job) : []),
2375
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
2398
+ ...workflowChecklistWidgetLines(checklist, theme, " ", expanded, frame),
2376
2399
  ...(lane ? formatLaneProjectionLines(lane, theme, " ") : []),
2377
2400
  ...hostStepWidgetLines(job, theme, " "),
2378
2401
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
@@ -2382,7 +2405,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
2382
2405
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
2383
2406
  const lines: string[] = [
2384
2407
  ...(expanded ? workflowPreflightLines(job) : []),
2385
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded }),
2408
+ ...workflowChecklistWidgetLines(checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded }),
2386
2409
  ];
2387
2410
  const group = activeParallelWidgetGroup(job);
2388
2411
  if (group) {
@@ -2415,7 +2438,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
2415
2438
  }
2416
2439
 
2417
2440
  function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
2418
- if (!expanded && job.mode === "workflow") return [compactWorkflowHeaderLine(job, theme, width), ...compactWorkflowWidgetBodyLines(job, theme, frame, projection)].map((line) => truncLine(line, width));
2441
+ if ((!expanded || projection.inlineFleetCovered) && job.mode === "workflow") return [compactWorkflowHeaderLine(job, theme, width), ...compactWorkflowWidgetBodyLines(job, theme, frame, projection)].map((line) => truncLine(line, width));
2419
2442
  const stats = widgetStats(job, theme, projection, !expanded);
2420
2443
  const count = job.mode === "workflow"
2421
2444
  ? projection.checklist?.total ?? projection.stageProgress?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length
@@ -2522,6 +2545,10 @@ function buildSingleLineWidgetLines(jobs: AsyncJobState[], theme: Theme, width:
2522
2545
  if (counts.failed.length > 0) parts.push(`${counts.failed.length} failed`);
2523
2546
  if (counts.stopped.length > 0) parts.push(`${counts.stopped.length} stopped`);
2524
2547
  if (counts.paused.length > 0) parts.push(`${counts.paused.length} paused`);
2548
+ for (const status of ["partial", "rejected"] as const) {
2549
+ const count = jobs.filter((job) => job.status === status).length;
2550
+ if (count > 0) parts.push(`${count} ${status}`);
2551
+ }
2525
2552
  if (!hasActive && counts.complete.length > 0) parts.push(`${counts.complete.length}/${jobs.length} done`);
2526
2553
  return [truncLine(`${theme.fg(hasActive ? "accent" : "dim", glyph)} ${theme.fg(hasActive ? "accent" : "dim", "subagents")} (${parts.join(", ") || `${jobs.length} total`})`, width)];
2527
2554
  }
@@ -2723,6 +2750,68 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
2723
2750
  }
2724
2751
 
2725
2752
  const asyncWidgetUpdates = new WeakMap<ExtensionContext["ui"], (jobs: AsyncJobState[]) => void>();
2753
+ const inlineWorkflowCoverage = new WeakMap<ExtensionContext["ui"], ReadonlyMap<string, string>>();
2754
+ const asyncWidgetInvalidations = new WeakMap<ExtensionContext["ui"], () => void>();
2755
+
2756
+ /** Include child identity and freshness in the roster's existing snapshot. */
2757
+ export function inlineWorkflowRenderKey(job: AsyncJobState, children: AsyncJobState[]): string {
2758
+ if (!children.length) return widgetRenderKey(job);
2759
+ return JSON.stringify([widgetRenderKey(job), children.map((child) => [
2760
+ child.asyncId, widgetRenderKey(child, true), child.context,
2761
+ child.steps?.map((step) => [Boolean(step.runner), step.tokens?.window]), Boolean(child.workflowGraph),
2762
+ ])]);
2763
+ }
2764
+
2765
+ /** Presentation-only coverage from the mounted inline Fleet roster, never configuration. */
2766
+ export function setInlineWorkflowCoverage(ui: ExtensionContext["ui"], coverage: ReadonlyMap<string, string>): void {
2767
+ const previous = inlineWorkflowCoverage.get(ui);
2768
+ if ((previous?.size ?? 0) === coverage.size && [...coverage].every(([id, key]) => previous?.get(id) === key)) return;
2769
+ if (coverage.size) inlineWorkflowCoverage.set(ui, coverage);
2770
+ else inlineWorkflowCoverage.delete(ui);
2771
+ asyncWidgetInvalidations.get(ui)?.();
2772
+ }
2773
+
2774
+ /** Attach only to loaded workflow parents; orphan jobs remain top-level. */
2775
+ function widgetJobTree(jobs: AsyncJobState[], now: number): { roots: AsyncJobState[]; projectionFor: WorkflowWidgetProjectionLookup } {
2776
+ const parents = new Map(jobs.filter((job) => job.mode === "workflow").map((job) => [job.asyncId, job]));
2777
+ const childrenByParent = new Map<string, AsyncJobState[]>();
2778
+ const roots: AsyncJobState[] = [];
2779
+ for (const job of jobs) {
2780
+ const parent = job.parentWorkflowRunId ? parents.get(job.parentWorkflowRunId) : undefined;
2781
+ // Root-only adaptive summaries cannot advertise live work under a non-running parent.
2782
+ const keepLiveRoot = isProgressiveActiveJob(job) && parent?.status !== "running";
2783
+ if (parent && parent.asyncId !== job.asyncId && !keepLiveRoot) {
2784
+ const children = childrenByParent.get(parent.asyncId) ?? [];
2785
+ children.push(job);
2786
+ childrenByParent.set(parent.asyncId, children);
2787
+ } else roots.push(job);
2788
+ }
2789
+ return { roots, projectionFor: workflowWidgetProjectionLookup(now, childrenByParent) };
2790
+ }
2791
+
2792
+ function widgetChecklistWithoutMaterializedChildren(projection: WorkflowWidgetProjection): WorkflowChecklistProjection | undefined {
2793
+ const { checklist, materializedKeys } = projection;
2794
+ if (!checklist || !materializedKeys) return checklist;
2795
+ return { ...checklist, phases: checklist.phases.map((phase) => ({ ...phase, items: phase.items.filter((item) => !materializedKeys.has(item.key)) })) };
2796
+ }
2797
+
2798
+ function materializedWidgetChildLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame: number | undefined, projectionFor: WorkflowWidgetProjectionLookup): string[] {
2799
+ const { children, inlineFleetCovered } = projectionFor(job);
2800
+ if (inlineFleetCovered || !children?.length) return [];
2801
+ const lines: string[] = [];
2802
+ const shown = orderedWidgetJobs(children).slice(0, MAX_WIDGET_JOBS);
2803
+ for (const [index, child] of shown.entries()) {
2804
+ const projection = projectionFor(child);
2805
+ const last = index === children.length - 1;
2806
+ const identity = child.workflowKey ?? child.asyncId;
2807
+ const stats = widgetStats(child, theme, projection, !expanded);
2808
+ lines.push(` ${last ? "└─" : "├─"} ${theme.bold(identity)} ${widgetStatusGlyph(child, theme, frame)} ${themeBold(theme, widgetJobName(child))}${contextModeBadge(theme, child.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`);
2809
+ for (const detail of foregroundStyleWidgetDetails(child, theme, expanded, Math.max(0, width - 6), frame, projection)) lines.push(` ${last ? " " : "│ "}${detail}`);
2810
+ for (const detail of materializedWidgetChildLines(child, theme, Math.max(0, width - 4), expanded, frame, projectionFor)) lines.push(` ${detail}`);
2811
+ }
2812
+ if (shown.length < children.length) lines.push(` +${children.length - shown.length} more workflow children`);
2813
+ return lines;
2814
+ }
2726
2815
 
2727
2816
  function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"]): (tui: { requestRender(): void }, theme: Theme) => Component {
2728
2817
  return (tui, theme) => {
@@ -2731,6 +2820,14 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
2731
2820
  let cachedFrame: number | undefined;
2732
2821
  let cachedExpanded: boolean | undefined;
2733
2822
  let cachedLines: string[] | undefined;
2823
+ let cachedCoverage = "[]";
2824
+ let collapsed = false;
2825
+ const invalidate = (): void => {
2826
+ cachedLines = undefined;
2827
+ resetWidgetLayoutSession();
2828
+ tui.requestRender();
2829
+ };
2830
+ asyncWidgetInvalidations.set(ui, invalidate);
2734
2831
  const update = (nextJobs: AsyncJobState[]): void => {
2735
2832
  jobs = nextJobs;
2736
2833
  cachedLines = undefined;
@@ -2738,25 +2835,60 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
2738
2835
  };
2739
2836
  asyncWidgetUpdates.set(ui, update);
2740
2837
  const component = Object.assign(container, {
2838
+ // Only the mouse fields used here, without requiring newer Pi type exports.
2839
+ handleMouse(event: { type: string; button: string; y: number; shift: boolean; alt: boolean; ctrl: boolean }) {
2840
+ if (event.type !== "click" || event.button !== "left" || event.y !== 0) return undefined;
2841
+ if (event.shift || event.alt || event.ctrl) return undefined;
2842
+ collapsed = !collapsed;
2843
+ invalidate();
2844
+ return { handled: true };
2845
+ },
2741
2846
  dispose(): void {
2742
2847
  if (asyncWidgetUpdates.get(ui) === update) asyncWidgetUpdates.delete(ui);
2848
+ if (asyncWidgetInvalidations.get(ui) === invalidate) asyncWidgetInvalidations.delete(ui);
2743
2849
  },
2744
2850
  });
2745
2851
  container.render = (renderWidth: number): string[] => {
2746
- const frame = Math.floor(Date.now() / WIDGET_ANIMATION_INTERVAL_MS);
2852
+ const now = Date.now();
2853
+ const frame = Math.floor(now / WIDGET_ANIMATION_INTERVAL_MS);
2747
2854
  const expanded = ui.getToolsExpanded?.() ?? false;
2748
- if (cachedLines && cachedRenderWidth === renderWidth && cachedFrame === frame && cachedExpanded === expanded) return cachedLines;
2855
+ const coverage = inlineWorkflowCoverage.get(ui);
2856
+ const covered = new Set<string>();
2857
+ if (coverage?.size) {
2858
+ const childrenByParent = new Map<string, AsyncJobState[]>();
2859
+ for (const child of jobs) {
2860
+ if (!child.parentWorkflowRunId) continue;
2861
+ const children = childrenByParent.get(child.parentWorkflowRunId) ?? [];
2862
+ children.push(child);
2863
+ childrenByParent.set(child.parentWorkflowRunId, children);
2864
+ }
2865
+ for (const job of jobs) {
2866
+ const snapshot = coverage.get(job.asyncId);
2867
+ if (snapshot === undefined) continue;
2868
+ const children = childrenByParent.get(job.asyncId) ?? [];
2869
+ if (!children.some((child) => childrenByParent.has(child.asyncId))
2870
+ && snapshot === inlineWorkflowRenderKey(job, children)) covered.add(job.asyncId);
2871
+ }
2872
+ }
2873
+ const coverageKey = JSON.stringify([...covered]);
2874
+ if (cachedLines && cachedRenderWidth === renderWidth && cachedFrame === frame && cachedExpanded === expanded && cachedCoverage === coverageKey) return cachedLines;
2875
+ if (cachedCoverage !== coverageKey) resetWidgetLayoutSession();
2876
+ cachedCoverage = coverageKey;
2749
2877
  const width = Math.max(0, renderWidth - 2);
2750
- const projectionFor = workflowWidgetProjectionLookup();
2878
+ const { roots, projectionFor } = widgetJobTree(jobs, now);
2879
+ for (const job of roots) projectionFor(job).inlineFleetCovered = covered.has(job.asyncId);
2751
2880
  const buildLines = (): string[] => expanded
2752
- ? buildWidgetLinesWithProjection(jobs, theme, width, true, frame, projectionFor)
2753
- : jobs.length === 1
2754
- ? compactSingleWidgetLines(jobs[0]!, theme, width, frame, projectionFor(jobs[0]!))
2755
- : buildWidgetLinesWithProjection(jobs, theme, width, false, frame, projectionFor);
2881
+ ? buildWidgetLinesWithProjection(roots, theme, width, true, frame, projectionFor)
2882
+ : roots.length === 1 && !projectionFor(roots[0]!).children?.length
2883
+ ? compactSingleWidgetLines(roots[0]!, theme, width, frame, projectionFor(roots[0]!))
2884
+ : buildWidgetLinesWithProjection(roots, theme, width, false, frame, projectionFor);
2756
2885
  cachedRenderWidth = renderWidth;
2757
2886
  cachedFrame = frame;
2758
2887
  cachedExpanded = expanded;
2759
- cachedLines = fitAdaptiveWidgetLines(jobs, buildLines, theme, width, expanded, frame, projectionFor).map((line) => paddedWidgetLine(line, renderWidth));
2888
+ cachedLines = (collapsed
2889
+ ? buildSingleLineWidgetLines(jobs, theme, width, frame)
2890
+ : fitAdaptiveWidgetLines(roots, buildLines, theme, width, expanded, frame, projectionFor)
2891
+ ).map((line) => paddedWidgetLine(line, renderWidth));
2760
2892
  return cachedLines;
2761
2893
  };
2762
2894
  return component;
@@ -2765,7 +2897,10 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
2765
2897
 
2766
2898
  function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number, projectionFor: WorkflowWidgetProjectionLookup = workflowWidgetProjectionLookup()): string[] {
2767
2899
  if (jobs.length === 0) return [];
2768
- if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame, projectionFor(jobs[0]!));
2900
+ if (jobs.length === 1) return [
2901
+ ...buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame, projectionFor(jobs[0]!)),
2902
+ ...materializedWidgetChildLines(jobs[0]!, theme, width, expanded, frame, projectionFor).map((line) => truncLine(line, width)),
2903
+ ];
2769
2904
  const running = jobs.filter((job) => job.status === "running");
2770
2905
  const queued = jobs.filter((job) => job.status === "queued");
2771
2906
  const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
@@ -2782,7 +2917,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2782
2917
  let slots = MAX_WIDGET_JOBS;
2783
2918
  const appendJob = (job: AsyncJobState): void => {
2784
2919
  const projection = projectionFor(job);
2785
- const compactWorkflow = !expanded && job.mode === "workflow";
2920
+ const compactWorkflow = (!expanded || projection.inlineFleetCovered) && job.mode === "workflow";
2786
2921
  const stats = compactWorkflow ? "" : widgetStats(job, theme, projection, !expanded);
2787
2922
  const details = compactWorkflow
2788
2923
  ? [
@@ -2791,7 +2926,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2791
2926
  : [
2792
2927
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
2793
2928
  ...widgetLaneDetailLines(job, theme, projection),
2794
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded || !job.steps?.length }),
2929
+ ...workflowChecklistWidgetLines(widgetChecklistWithoutMaterializedChildren(projection), theme, " ", expanded, frame, { includeItemErrors: !expanded || !job.steps?.length }),
2795
2930
  ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
2796
2931
  ];
2797
2932
  items.push([
@@ -2799,6 +2934,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2799
2934
  ? compactWorkflowHeaderLine(job, theme, width)
2800
2935
  : `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
2801
2936
  ...details,
2937
+ ...materializedWidgetChildLines(job, theme, width, expanded, frame, projectionFor),
2802
2938
  ]);
2803
2939
  };
2804
2940
 
@@ -2845,7 +2981,8 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2845
2981
  }
2846
2982
 
2847
2983
  export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number): string[] {
2848
- return buildWidgetLinesWithProjection(jobs, theme, width, expanded, frame);
2984
+ const { roots, projectionFor } = widgetJobTree(jobs, Date.now());
2985
+ return buildWidgetLinesWithProjection(roots, theme, width, expanded, frame, projectionFor);
2849
2986
  }
2850
2987
 
2851
2988
  /**
@@ -3347,9 +3484,6 @@ export function renderSubagentResult(
3347
3484
  if (r.skillsWarning) {
3348
3485
  c.addChild(new Text(fit(theme.fg("warning", `Warning: ${r.skillsWarning}`)), 0, 0));
3349
3486
  }
3350
- if (r.attemptedModels && r.attemptedModels.length > 1) {
3351
- c.addChild(new Text(fit(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
3352
- }
3353
3487
  c.addChild(new Text(fit(theme.fg("dim", formatUsage(r.usage, r.model))), 0, 0));
3354
3488
  if (r.sessionFile) {
3355
3489
  c.addChild(new Text(fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)), 0, 0));
@@ -3526,9 +3660,6 @@ export function renderSubagentResult(
3526
3660
  if (r.skillsWarning) {
3527
3661
  c.addChild(new Text(fit(theme.fg("warning", ` Warning: ${r.skillsWarning}`)), 0, 0));
3528
3662
  }
3529
- if (r.attemptedModels && r.attemptedModels.length > 1) {
3530
- c.addChild(new Text(fit(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
3531
- }
3532
3663
 
3533
3664
  if (rRunning && rProg) {
3534
3665
  if (rProg.skills?.length) {
@@ -1,5 +1,5 @@
1
1
  import type { ChildWatchdogProgress, ChildWatchdogWarningSummary } from "../shared/types.ts";
2
- import { SUBAGENT_WATCHDOG_WARNING_TYPE, type ResolvedWatchdogConfig, type WatchdogCadenceConfig, type WatchdogCategory, type WatchdogLspConfig } from "./types.ts";
2
+ import { WATCHDOG_WARNING_CATEGORIES, WATCHDOG_WARNING_IMPORTANCES, type ResolvedWatchdogConfig, type WatchdogCadenceConfig, type WatchdogLspConfig } from "./types.ts";
3
3
 
4
4
  export const CHILD_WATCHDOG_WARNING_LIMIT = 20;
5
5
 
@@ -33,6 +33,7 @@ export interface ChildWatchdogStatusEvent {
33
33
  phase: ChildWatchdogPhase;
34
34
  ts: number;
35
35
  reason?: string;
36
+ warning?: ChildWatchdogWarningSummary;
36
37
  }
37
38
 
38
39
  export type ChildWatchdogStateSnapshot = ChildWatchdogProgress;
@@ -127,6 +128,9 @@ function childConfigLsp(value: unknown): WatchdogLspConfig {
127
128
  export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdogConfig | undefined {
128
129
  if (!raw) return undefined;
129
130
  const parsed = childConfigObject(JSON.parse(raw), "root");
131
+ if (Object.hasOwn(parsed, "fallbackModels")) {
132
+ throw new Error("Invalid child watchdog config: fallbackModels was removed; configure one model instead.");
133
+ }
130
134
  if (parsed.enabled === false) return undefined;
131
135
  if ("enabled" in parsed && parsed.enabled !== true) throw new Error("Invalid child watchdog config: enabled must be true or false.");
132
136
  const thinking = parsed.thinking;
@@ -155,6 +159,15 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
155
159
  export function isChildWatchdogStatusEvent(value: unknown): value is ChildWatchdogStatusEvent {
156
160
  if (!value || typeof value !== "object") return false;
157
161
  const event = value as Partial<ChildWatchdogStatusEvent>;
162
+ const warningValue = event.warning;
163
+ const warning = warningValue as Partial<ChildWatchdogWarningSummary>;
164
+ const validWarning = warningValue === undefined || (warningValue !== null && typeof warningValue === "object" && !Array.isArray(warningValue)
165
+ && (warning.severity === "concern" || warning.severity === "blocker")
166
+ && typeof warning.importance === "string" && (WATCHDOG_WARNING_IMPORTANCES as readonly string[]).includes(warning.importance)
167
+ && typeof warning.category === "string" && (WATCHDOG_WARNING_CATEGORIES as readonly string[]).includes(warning.category)
168
+ && typeof warning.summary === "string" && typeof warning.evidence === "string" && typeof warning.recommendedAction === "string"
169
+ && typeof warning.addressed === "boolean" && typeof warning.stalemate === "boolean"
170
+ && (warning.displayedAt === undefined || typeof warning.displayedAt === "string"));
158
171
  return event.type === CHILD_WATCHDOG_STATUS_EVENT
159
172
  && typeof event.seq === "number"
160
173
  && Number.isInteger(event.seq)
@@ -162,7 +175,8 @@ export function isChildWatchdogStatusEvent(value: unknown): value is ChildWatchd
162
175
  && typeof event.ts === "number"
163
176
  && Number.isFinite(event.ts)
164
177
  && typeof event.phase === "string"
165
- && (CHILD_WATCHDOG_PHASES as readonly string[]).includes(event.phase);
178
+ && (CHILD_WATCHDOG_PHASES as readonly string[]).includes(event.phase)
179
+ && validWarning;
166
180
  }
167
181
 
168
182
  export function childWatchdogIsActive(snapshot: ChildWatchdogStateSnapshot | undefined): boolean {
@@ -182,40 +196,20 @@ export function acceptChildWatchdogEvent(input: {
182
196
  const eventIndex = input.event.childIndex ?? input.event.stepIndex;
183
197
  if (input.childIndex !== undefined && eventIndex !== input.childIndex) return undefined;
184
198
  if (input.current && input.event.seq <= input.current.seq) return undefined;
199
+ const warnings = input.event.warning
200
+ ? [...(input.current?.warnings ?? []), input.event.warning].slice(-CHILD_WATCHDOG_WARNING_LIMIT)
201
+ : input.current?.warnings;
185
202
  return {
186
203
  phase: input.event.phase,
187
204
  seq: input.event.seq,
188
205
  lastUpdate: input.event.ts,
189
206
  ...(input.event.reason ? { reason: input.event.reason } : {}),
190
- ...(input.current?.warnings?.length ? { warnings: input.current.warnings } : {}),
191
- };
192
- }
193
-
194
- function childWatchdogWarningFromMessage(message: unknown): ChildWatchdogWarningSummary | undefined {
195
- const candidate = message as { role?: unknown; customType?: unknown; details?: Record<string, unknown> } | undefined;
196
- if (candidate?.role !== "custom" || candidate.customType !== SUBAGENT_WATCHDOG_WARNING_TYPE) return undefined;
197
- const details = candidate.details ?? {};
198
- if (details.severity !== "concern" && details.severity !== "blocker") return undefined;
199
- if (typeof details.summary !== "string" || typeof details.evidence !== "string" || typeof details.recommendedAction !== "string") return undefined;
200
- return {
201
- severity: details.severity,
202
- category: details.category as WatchdogCategory,
203
- summary: details.summary,
204
- evidence: details.evidence,
205
- recommendedAction: details.recommendedAction,
206
- ...(typeof details.displayedAt === "string" ? { displayedAt: details.displayedAt } : {}),
207
- addressed: false,
208
- stalemate: details.state === "stalemate",
207
+ ...(warnings?.length ? { warnings } : {}),
209
208
  };
210
209
  }
211
210
 
212
- /** A watchdog warning message is appended; an assistant turn marks earlier warnings addressed. Undefined when unchanged. */
213
- export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot | undefined, message: unknown, now = Date.now()): ChildWatchdogStateSnapshot | undefined {
214
- const warning = childWatchdogWarningFromMessage(message);
215
- if (warning) {
216
- const warnings = [...(current?.warnings ?? []), warning].slice(-CHILD_WATCHDOG_WARNING_LIMIT);
217
- return current ? { ...current, warnings } : { phase: "idle", seq: 0, lastUpdate: now, warnings };
218
- }
211
+ /** An assistant turn marks earlier warnings addressed. Undefined when unchanged. */
212
+ export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot | undefined, message: unknown): ChildWatchdogStateSnapshot | undefined {
219
213
  if ((message as { role?: unknown } | undefined)?.role !== "assistant" || !current?.warnings?.some((entry) => !entry.addressed)) return undefined;
220
214
  return { ...current, warnings: current.warnings.map((entry) => entry.addressed ? entry : { ...entry, addressed: true }) };
221
215
  }
@@ -223,3 +217,9 @@ export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot |
223
217
  export function unresolvedChildWatchdogBlockers(progress: Pick<ChildWatchdogProgress, "warnings"> | undefined): ChildWatchdogWarningSummary[] {
224
218
  return (progress?.warnings ?? []).filter((warning) => warning.severity === "blocker" && (!warning.addressed || warning.stalemate));
225
219
  }
220
+
221
+ export function childWatchdogProgressForModel(progress: ChildWatchdogProgress | undefined): ChildWatchdogProgress | undefined {
222
+ if (!progress) return undefined;
223
+ const warnings = (progress.warnings ?? []).filter((warning) => warning.importance === "high");
224
+ return { ...progress, warnings: warnings.length ? warnings : undefined };
225
+ }
@@ -216,7 +216,7 @@ export function watchdogWarningFromLspDiagnostics(result: WatchdogLspResult): Wa
216
216
  return {
217
217
  severity,
218
218
  category: "correctness",
219
- confidence: "high",
219
+ importance: "high",
220
220
  source: "lsp",
221
221
  summary: `LSP found ${count} ${kind}${count === 1 ? "" : "s"} in changed ${count === 1 ? "file" : "files"}.`,
222
222
  evidence: evidence || formatDiagnostic(primary),
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { normalizeModelSegment, resolveModelCandidate } from "../runs/shared/model-fallback.ts";
2
+ import { normalizeModelSegment, resolveModelCandidate } from "../runs/shared/model-resolution.ts";
3
+ import type { WatchdogEndpointConfig } from "./types.ts";
3
4
  import {
4
5
  getSupportedThinkingLevels,
5
6
  splitKnownThinkingSuffix,
@@ -43,6 +44,7 @@ export interface ResolvedWatchdogModelInput {
43
44
  }
44
45
 
45
46
  export interface WatchdogModelRecommendation {
47
+ source: "configured" | "suggested";
46
48
  model: string;
47
49
  thinking: ThinkingLevel;
48
50
  label: string;
@@ -147,6 +149,7 @@ function resolveStrongCandidate(ctx: ExtensionContext, family: StrongWatchdogFam
147
149
  if (!getSupportedThinkingLevels(modelInfo).includes(STRONG_WATCHDOG_THINKING)) continue;
148
150
  const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "no current session model";
149
151
  return {
152
+ source: "suggested",
150
153
  model: resolved.model,
151
154
  thinking: STRONG_WATCHDOG_THINKING,
152
155
  label: preference.label,
@@ -165,3 +168,20 @@ export function recommendStrongWatchdogModel(ctx: ExtensionContext): WatchdogMod
165
168
  const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "the current session";
166
169
  throw new Error(`No authenticated strong complementary watchdog model was found for ${current}. Configure access to Opus 4.8 or GPT 5.5, then run the recommendation again.`);
167
170
  }
171
+
172
+ export function recommendWatchdogModel(ctx: ExtensionContext, configured: WatchdogEndpointConfig): WatchdogModelRecommendation {
173
+ if (!configured.model) return recommendStrongWatchdogModel(ctx);
174
+ const resolved = resolveWatchdogModelInput(ctx, configured.model);
175
+ const thinking = resolved.thinking ?? parseWatchdogThinkingInput(configured.thinking) ?? "off";
176
+ return {
177
+ ...resolved,
178
+ source: "configured",
179
+ thinking: thinking === false ? "off" : thinking,
180
+ label: "configured watchdog",
181
+ reason: "Keep the configured watchdog model and thinking; no replacement recommendation is needed. This is not a strength or independence assessment.",
182
+ };
183
+ }
184
+
185
+ export function formatWatchdogRecommendation(recommendation: WatchdogModelRecommendation): string {
186
+ return `${recommendation.source === "configured" ? "Keep configured watchdog" : "Recommended strong watchdog"}: ${recommendation.model}:${recommendation.thinking}`;
187
+ }
@@ -4,6 +4,7 @@ import { streamSimple } from "@earendil-works/pi-ai/compat";
4
4
  import { Type, type Static } from "typebox";
5
5
  import { appendPermissionAudit, permissionArgsPreview } from "../runs/shared/permissions.ts";
6
6
  import { agentStreamOptions } from "../shared/agent-stream-options.ts";
7
+ import { opencodeSessionHeaders } from "../shared/opencode-session-headers.ts";
7
8
  import { decodeChildWatchdogConfig } from "./child-status.ts";
8
9
  import { childResolvedConfig } from "./register-child.ts";
9
10
  import { resolveWatchdogReviewModel } from "./review.ts";
@@ -94,6 +95,7 @@ export function createWatchdogPermissionArbiter(options: WatchdogPermissionArbit
94
95
  const config = childResolvedConfig(childConfig);
95
96
  const selection = await resolveWatchdogReviewModel(request.ctx, config);
96
97
  const auth = selection.auth;
98
+ const sessionId = request.ctx.sessionManager.getSessionId();
97
99
  const registeredProvider = (request.ctx.modelRegistry as {
98
100
  getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
99
101
  }).getRegisteredProviderConfig?.(selection.model.provider);
@@ -104,7 +106,7 @@ export function createWatchdogPermissionArbiter(options: WatchdogPermissionArbit
104
106
  ...streamOptions,
105
107
  ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
106
108
  env: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,
107
- headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
109
+ headers: { ...opencodeSessionHeaders(model, sessionId), ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
108
110
  });
109
111
  agent = new Agent({
110
112
  initialState: {
@@ -10,7 +10,8 @@ import {
10
10
  type ChildWatchdogPhase,
11
11
  type ChildWatchdogStatusEvent,
12
12
  } from "./child-status.ts";
13
- import type { ResolvedWatchdogConfig, WatchdogWarningDetails } from "./types.ts";
13
+ import type { ChildWatchdogWarningSummary } from "../shared/types.ts";
14
+ import { SUBAGENT_WATCHDOG_WARNING_TYPE, type ResolvedWatchdogConfig, type WatchdogWarningDetails } from "./types.ts";
14
15
 
15
16
  export function childResolvedConfig(config: ChildWatchdogConfig): ResolvedWatchdogConfig {
16
17
  return {
@@ -57,7 +58,7 @@ export function registerChildWatchdog(
57
58
  let currentContext: ExtensionContext | undefined;
58
59
  let diffBaseline: WatchdogDiffBaseline | undefined;
59
60
  let seq = 0;
60
- const emitStatus = (phase: ChildWatchdogPhase, reason?: string): void => {
61
+ const emitStatus = (phase: ChildWatchdogPhase, reason?: string, warning?: ChildWatchdogWarningSummary): void => {
61
62
  writeStatus({
62
63
  type: CHILD_WATCHDOG_STATUS_EVENT,
63
64
  ...(childConfig.runId ? { runId: childConfig.runId } : {}),
@@ -67,6 +68,7 @@ export function registerChildWatchdog(
67
68
  phase,
68
69
  ts: Date.now(),
69
70
  ...(reason ? { reason } : {}),
71
+ ...(warning ? { warning } : {}),
70
72
  });
71
73
  };
72
74
  const resolved = childResolvedConfig(childConfig);
@@ -77,8 +79,14 @@ export function registerChildWatchdog(
77
79
  reviewChangesOnly: true,
78
80
  displayWarning: (details, options) => {
79
81
  const childDetails = childWarningDetails(details, childConfig);
82
+ emitStatus("reviewing", undefined, { severity: childDetails.severity, importance: childDetails.importance, category: childDetails.category, summary: childDetails.summary, evidence: childDetails.evidence, recommendedAction: childDetails.recommendedAction, ...(childDetails.displayedAt ? { displayedAt: childDetails.displayedAt } : {}), addressed: false, stalemate: childDetails.state === "stalemate" });
80
83
  pi.sendMessage(createWatchdogWarningMessage(childDetails, { display: true, details: childDetails }), options);
81
84
  },
85
+ displayUserWarning: (details) => {
86
+ const childDetails = childWarningDetails(details, childConfig);
87
+ emitStatus("reviewing", undefined, { severity: childDetails.severity, importance: childDetails.importance, category: childDetails.category, summary: childDetails.summary, evidence: childDetails.evidence, recommendedAction: childDetails.recommendedAction, ...(childDetails.displayedAt ? { displayedAt: childDetails.displayedAt } : {}), addressed: false, stalemate: childDetails.state === "stalemate" });
88
+ pi.appendEntry(SUBAGENT_WATCHDOG_WARNING_TYPE, childDetails);
89
+ },
82
90
  });
83
91
  const rememberContext = (ctx: ExtensionContext) => {
84
92
  currentContext = ctx;