pi-subagents 0.66.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +4 -3
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +6 -3
  6. package/docs/configuration.md +7 -5
  7. package/docs/extension-api.md +33 -18
  8. package/docs/missions.md +8 -0
  9. package/docs/models.md +1 -1
  10. package/docs/observability.md +4 -4
  11. package/docs/standalone-background.md +49 -0
  12. package/docs/tool-reference.md +18 -8
  13. package/docs/watchdog.md +35 -4
  14. package/docs/workflows.md +26 -12
  15. package/inspector-runner.mjs +2 -2
  16. package/package.json +1 -1
  17. package/prompts/parallel-review.md +1 -1
  18. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  19. package/skills/pi-subagents/SKILL.md +14 -0
  20. package/skills/pi-subagents/references/execution-controls.md +7 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  22. package/src/agents/advertised-agent-prompt.ts +34 -3
  23. package/src/agents/agents.ts +6 -0
  24. package/src/agents/builtin-names.ts +1 -0
  25. package/src/api/delegation.ts +4 -0
  26. package/src/api/preflight.ts +76 -45
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +63 -4
  29. package/src/extension/index.ts +20 -8
  30. package/src/extension/public-execution.ts +4 -2
  31. package/src/extension/rpc.ts +4 -0
  32. package/src/extension/schemas.ts +67 -78
  33. package/src/extension/tool-description.ts +29 -82
  34. package/src/inspectors/actions.ts +148 -0
  35. package/src/inspectors/ghostty/actions.ts +74 -0
  36. package/src/inspectors/ghostty/plugin.ts +17 -0
  37. package/src/inspectors/herdr/actions.ts +99 -179
  38. package/src/inspectors/herdr/plugin.ts +20 -0
  39. package/src/inspectors/herdr/project-panes.ts +1 -1
  40. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  41. package/src/inspectors/plugins.ts +8 -0
  42. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  43. package/src/inspectors/types.ts +51 -0
  44. package/src/intercom/intercom-bridge.ts +50 -8
  45. package/src/intercom/native-supervisor-channel.ts +22 -13
  46. package/src/runs/background/active-async-capacity.ts +4 -0
  47. package/src/runs/background/async-execution.ts +45 -56
  48. package/src/runs/background/async-resume.ts +5 -9
  49. package/src/runs/background/auto-drain.ts +6 -3
  50. package/src/runs/background/binary-bootstrap.ts +33 -0
  51. package/src/runs/background/fleet-view.ts +30 -2
  52. package/src/runs/background/notify.ts +31 -1
  53. package/src/runs/background/owned-process-tree.ts +29 -2
  54. package/src/runs/background/run-child-session.ts +61 -4
  55. package/src/runs/background/run-status.ts +3 -0
  56. package/src/runs/background/runner-aliases.ts +11 -3
  57. package/src/runs/background/runner-child-launch.ts +2 -0
  58. package/src/runs/background/runner-child-sessions.ts +5 -4
  59. package/src/runs/background/scheduled-runs.ts +40 -13
  60. package/src/runs/background/steering.ts +20 -2
  61. package/src/runs/background/subagent-runner.ts +47 -31
  62. package/src/runs/background/subagent-wait.ts +51 -8
  63. package/src/runs/background/wait-tool.ts +1 -1
  64. package/src/runs/foreground/async-steering-action.ts +18 -7
  65. package/src/runs/foreground/execution.ts +44 -31
  66. package/src/runs/foreground/prompt-audit.ts +3 -1
  67. package/src/runs/foreground/subagent-executor.ts +110 -100
  68. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  69. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  70. package/src/runs/shared/acceptance.ts +5 -2
  71. package/src/runs/shared/async-status-projection.ts +4 -0
  72. package/src/runs/shared/capability-ceiling.ts +2 -0
  73. package/src/runs/shared/child-hooks.ts +25 -10
  74. package/src/runs/shared/child-launch.ts +12 -2
  75. package/src/runs/shared/child-lifecycle.ts +6 -3
  76. package/src/runs/shared/child-runtime-config.ts +3 -1
  77. package/src/runs/shared/child-session.ts +33 -2
  78. package/src/runs/shared/child-tool-plan.ts +122 -3
  79. package/src/runs/shared/completion-guard.ts +5 -3
  80. package/src/runs/shared/effective-system-prompt.ts +33 -0
  81. package/src/runs/shared/external-cli-runner.ts +9 -7
  82. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  83. package/src/runs/shared/model-fallback.ts +2 -0
  84. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  85. package/src/runs/shared/pi-spawn.ts +10 -0
  86. package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
  87. package/src/runs/shared/task-intent.ts +46 -13
  88. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  89. package/src/runs/shared/worktree.ts +42 -12
  90. package/src/shared/fork-context.ts +15 -72
  91. package/src/shared/launch-contract.ts +65 -2
  92. package/src/shared/opencode-session-headers.ts +30 -0
  93. package/src/shared/types.ts +4 -1
  94. package/src/slash/delegation-adapters.ts +3 -1
  95. package/src/slash/delegation-request.ts +14 -0
  96. package/src/slash/slash-commands.ts +2 -1
  97. package/src/slash/subagents-admin.ts +11 -4
  98. package/src/tui/fleet-status.ts +164 -19
  99. package/src/tui/fleet.ts +16 -14
  100. package/src/tui/render.ts +149 -28
  101. package/src/watchdog/child-status.ts +8 -0
  102. package/src/watchdog/model-selection.ts +20 -0
  103. package/src/watchdog/permission-arbiter.ts +3 -1
  104. package/src/watchdog/register-child.ts +1 -0
  105. package/src/watchdog/register-main.ts +31 -27
  106. package/src/watchdog/review.ts +132 -67
  107. package/src/watchdog/runtime.ts +82 -20
  108. package/src/watchdog/scope.ts +1 -1
  109. package/src/watchdog/settings.ts +9 -3
  110. package/src/watchdog/tool-actions.ts +13 -12
  111. package/src/watchdog/turn-delta.ts +23 -0
  112. package/src/watchdog/types.ts +4 -0
  113. package/src/workflows/scripted-workflow.ts +237 -7
  114. package/src/workflows/workflow-checklist.ts +2 -2
  115. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
package/src/tui/render.ts CHANGED
@@ -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 };
@@ -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
@@ -2723,6 +2746,68 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
2723
2746
  }
2724
2747
 
2725
2748
  const asyncWidgetUpdates = new WeakMap<ExtensionContext["ui"], (jobs: AsyncJobState[]) => void>();
2749
+ const inlineWorkflowCoverage = new WeakMap<ExtensionContext["ui"], ReadonlyMap<string, string>>();
2750
+ const asyncWidgetInvalidations = new WeakMap<ExtensionContext["ui"], () => void>();
2751
+
2752
+ /** Include child identity and freshness in the roster's existing snapshot. */
2753
+ export function inlineWorkflowRenderKey(job: AsyncJobState, children: AsyncJobState[]): string {
2754
+ if (!children.length) return widgetRenderKey(job);
2755
+ return JSON.stringify([widgetRenderKey(job), children.map((child) => [
2756
+ child.asyncId, widgetRenderKey(child, true), child.context,
2757
+ child.steps?.map((step) => [Boolean(step.runner), step.tokens?.window]), Boolean(child.workflowGraph),
2758
+ ])]);
2759
+ }
2760
+
2761
+ /** Presentation-only coverage from the mounted inline Fleet roster, never configuration. */
2762
+ export function setInlineWorkflowCoverage(ui: ExtensionContext["ui"], coverage: ReadonlyMap<string, string>): void {
2763
+ const previous = inlineWorkflowCoverage.get(ui);
2764
+ if ((previous?.size ?? 0) === coverage.size && [...coverage].every(([id, key]) => previous?.get(id) === key)) return;
2765
+ if (coverage.size) inlineWorkflowCoverage.set(ui, coverage);
2766
+ else inlineWorkflowCoverage.delete(ui);
2767
+ asyncWidgetInvalidations.get(ui)?.();
2768
+ }
2769
+
2770
+ /** Attach only to loaded workflow parents; orphan jobs remain top-level. */
2771
+ function widgetJobTree(jobs: AsyncJobState[], now: number): { roots: AsyncJobState[]; projectionFor: WorkflowWidgetProjectionLookup } {
2772
+ const parents = new Map(jobs.filter((job) => job.mode === "workflow").map((job) => [job.asyncId, job]));
2773
+ const childrenByParent = new Map<string, AsyncJobState[]>();
2774
+ const roots: AsyncJobState[] = [];
2775
+ for (const job of jobs) {
2776
+ const parent = job.parentWorkflowRunId ? parents.get(job.parentWorkflowRunId) : undefined;
2777
+ // Root-only adaptive summaries cannot advertise live work under a non-running parent.
2778
+ const keepLiveRoot = isProgressiveActiveJob(job) && parent?.status !== "running";
2779
+ if (parent && parent.asyncId !== job.asyncId && !keepLiveRoot) {
2780
+ const children = childrenByParent.get(parent.asyncId) ?? [];
2781
+ children.push(job);
2782
+ childrenByParent.set(parent.asyncId, children);
2783
+ } else roots.push(job);
2784
+ }
2785
+ return { roots, projectionFor: workflowWidgetProjectionLookup(now, childrenByParent) };
2786
+ }
2787
+
2788
+ function widgetChecklistWithoutMaterializedChildren(projection: WorkflowWidgetProjection): WorkflowChecklistProjection | undefined {
2789
+ const { checklist, materializedKeys } = projection;
2790
+ if (!checklist || !materializedKeys) return checklist;
2791
+ return { ...checklist, phases: checklist.phases.map((phase) => ({ ...phase, items: phase.items.filter((item) => !materializedKeys.has(item.key)) })) };
2792
+ }
2793
+
2794
+ function materializedWidgetChildLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame: number | undefined, projectionFor: WorkflowWidgetProjectionLookup): string[] {
2795
+ const { children, inlineFleetCovered } = projectionFor(job);
2796
+ if (inlineFleetCovered || !children?.length) return [];
2797
+ const lines: string[] = [];
2798
+ const shown = orderedWidgetJobs(children).slice(0, MAX_WIDGET_JOBS);
2799
+ for (const [index, child] of shown.entries()) {
2800
+ const projection = projectionFor(child);
2801
+ const last = index === children.length - 1;
2802
+ const identity = child.workflowKey ?? child.asyncId;
2803
+ const stats = widgetStats(child, theme, projection, !expanded);
2804
+ lines.push(` ${last ? "└─" : "├─"} ${theme.bold(identity)} ${widgetStatusGlyph(child, theme, frame)} ${themeBold(theme, widgetJobName(child))}${contextModeBadge(theme, child.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`);
2805
+ for (const detail of foregroundStyleWidgetDetails(child, theme, expanded, Math.max(0, width - 6), frame, projection)) lines.push(` ${last ? " " : "│ "}${detail}`);
2806
+ for (const detail of materializedWidgetChildLines(child, theme, Math.max(0, width - 4), expanded, frame, projectionFor)) lines.push(` ${detail}`);
2807
+ }
2808
+ if (shown.length < children.length) lines.push(` +${children.length - shown.length} more workflow children`);
2809
+ return lines;
2810
+ }
2726
2811
 
2727
2812
  function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"]): (tui: { requestRender(): void }, theme: Theme) => Component {
2728
2813
  return (tui, theme) => {
@@ -2731,6 +2816,13 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
2731
2816
  let cachedFrame: number | undefined;
2732
2817
  let cachedExpanded: boolean | undefined;
2733
2818
  let cachedLines: string[] | undefined;
2819
+ let cachedCoverage = "[]";
2820
+ const invalidate = (): void => {
2821
+ cachedLines = undefined;
2822
+ resetWidgetLayoutSession();
2823
+ tui.requestRender();
2824
+ };
2825
+ asyncWidgetInvalidations.set(ui, invalidate);
2734
2826
  const update = (nextJobs: AsyncJobState[]): void => {
2735
2827
  jobs = nextJobs;
2736
2828
  cachedLines = undefined;
@@ -2740,23 +2832,47 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
2740
2832
  const component = Object.assign(container, {
2741
2833
  dispose(): void {
2742
2834
  if (asyncWidgetUpdates.get(ui) === update) asyncWidgetUpdates.delete(ui);
2835
+ if (asyncWidgetInvalidations.get(ui) === invalidate) asyncWidgetInvalidations.delete(ui);
2743
2836
  },
2744
2837
  });
2745
2838
  container.render = (renderWidth: number): string[] => {
2746
- const frame = Math.floor(Date.now() / WIDGET_ANIMATION_INTERVAL_MS);
2839
+ const now = Date.now();
2840
+ const frame = Math.floor(now / WIDGET_ANIMATION_INTERVAL_MS);
2747
2841
  const expanded = ui.getToolsExpanded?.() ?? false;
2748
- if (cachedLines && cachedRenderWidth === renderWidth && cachedFrame === frame && cachedExpanded === expanded) return cachedLines;
2842
+ const coverage = inlineWorkflowCoverage.get(ui);
2843
+ const covered = new Set<string>();
2844
+ if (coverage?.size) {
2845
+ const childrenByParent = new Map<string, AsyncJobState[]>();
2846
+ for (const child of jobs) {
2847
+ if (!child.parentWorkflowRunId) continue;
2848
+ const children = childrenByParent.get(child.parentWorkflowRunId) ?? [];
2849
+ children.push(child);
2850
+ childrenByParent.set(child.parentWorkflowRunId, children);
2851
+ }
2852
+ for (const job of jobs) {
2853
+ const snapshot = coverage.get(job.asyncId);
2854
+ if (snapshot === undefined) continue;
2855
+ const children = childrenByParent.get(job.asyncId) ?? [];
2856
+ if (!children.some((child) => childrenByParent.has(child.asyncId))
2857
+ && snapshot === inlineWorkflowRenderKey(job, children)) covered.add(job.asyncId);
2858
+ }
2859
+ }
2860
+ const coverageKey = JSON.stringify([...covered]);
2861
+ if (cachedLines && cachedRenderWidth === renderWidth && cachedFrame === frame && cachedExpanded === expanded && cachedCoverage === coverageKey) return cachedLines;
2862
+ if (cachedCoverage !== coverageKey) resetWidgetLayoutSession();
2863
+ cachedCoverage = coverageKey;
2749
2864
  const width = Math.max(0, renderWidth - 2);
2750
- const projectionFor = workflowWidgetProjectionLookup();
2865
+ const { roots, projectionFor } = widgetJobTree(jobs, now);
2866
+ for (const job of roots) projectionFor(job).inlineFleetCovered = covered.has(job.asyncId);
2751
2867
  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);
2868
+ ? buildWidgetLinesWithProjection(roots, theme, width, true, frame, projectionFor)
2869
+ : roots.length === 1 && !projectionFor(roots[0]!).children?.length
2870
+ ? compactSingleWidgetLines(roots[0]!, theme, width, frame, projectionFor(roots[0]!))
2871
+ : buildWidgetLinesWithProjection(roots, theme, width, false, frame, projectionFor);
2756
2872
  cachedRenderWidth = renderWidth;
2757
2873
  cachedFrame = frame;
2758
2874
  cachedExpanded = expanded;
2759
- cachedLines = fitAdaptiveWidgetLines(jobs, buildLines, theme, width, expanded, frame, projectionFor).map((line) => paddedWidgetLine(line, renderWidth));
2875
+ cachedLines = fitAdaptiveWidgetLines(roots, buildLines, theme, width, expanded, frame, projectionFor).map((line) => paddedWidgetLine(line, renderWidth));
2760
2876
  return cachedLines;
2761
2877
  };
2762
2878
  return component;
@@ -2765,7 +2881,10 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
2765
2881
 
2766
2882
  function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number, projectionFor: WorkflowWidgetProjectionLookup = workflowWidgetProjectionLookup()): string[] {
2767
2883
  if (jobs.length === 0) return [];
2768
- if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame, projectionFor(jobs[0]!));
2884
+ if (jobs.length === 1) return [
2885
+ ...buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame, projectionFor(jobs[0]!)),
2886
+ ...materializedWidgetChildLines(jobs[0]!, theme, width, expanded, frame, projectionFor).map((line) => truncLine(line, width)),
2887
+ ];
2769
2888
  const running = jobs.filter((job) => job.status === "running");
2770
2889
  const queued = jobs.filter((job) => job.status === "queued");
2771
2890
  const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
@@ -2782,7 +2901,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2782
2901
  let slots = MAX_WIDGET_JOBS;
2783
2902
  const appendJob = (job: AsyncJobState): void => {
2784
2903
  const projection = projectionFor(job);
2785
- const compactWorkflow = !expanded && job.mode === "workflow";
2904
+ const compactWorkflow = (!expanded || projection.inlineFleetCovered) && job.mode === "workflow";
2786
2905
  const stats = compactWorkflow ? "" : widgetStats(job, theme, projection, !expanded);
2787
2906
  const details = compactWorkflow
2788
2907
  ? [
@@ -2791,7 +2910,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2791
2910
  : [
2792
2911
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
2793
2912
  ...widgetLaneDetailLines(job, theme, projection),
2794
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded || !job.steps?.length }),
2913
+ ...workflowChecklistWidgetLines(widgetChecklistWithoutMaterializedChildren(projection), theme, " ", expanded, frame, { includeItemErrors: !expanded || !job.steps?.length }),
2795
2914
  ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
2796
2915
  ];
2797
2916
  items.push([
@@ -2799,6 +2918,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2799
2918
  ? compactWorkflowHeaderLine(job, theme, width)
2800
2919
  : `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
2801
2920
  ...details,
2921
+ ...materializedWidgetChildLines(job, theme, width, expanded, frame, projectionFor),
2802
2922
  ]);
2803
2923
  };
2804
2924
 
@@ -2845,7 +2965,8 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2845
2965
  }
2846
2966
 
2847
2967
  export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number): string[] {
2848
- return buildWidgetLinesWithProjection(jobs, theme, width, expanded, frame);
2968
+ const { roots, projectionFor } = widgetJobTree(jobs, Date.now());
2969
+ return buildWidgetLinesWithProjection(roots, theme, width, expanded, frame, projectionFor);
2849
2970
  }
2850
2971
 
2851
2972
  /**
@@ -16,6 +16,7 @@ export interface ChildWatchdogConfig {
16
16
  agentEndTimeoutMs: number;
17
17
  maxWarnings: number | null;
18
18
  model?: string;
19
+ fallbackModels?: string[];
19
20
  thinking?: string | false;
20
21
  lsp: WatchdogLspConfig;
21
22
  stalemateRepeats: number;
@@ -47,6 +48,7 @@ export function resolveChildWatchdogConfig(input: {
47
48
  const enabled = input.config.enabled && (override?.enabled ?? input.config.children.enabled);
48
49
  if (!enabled) return undefined;
49
50
  const model = override?.model ?? input.config.children.model;
51
+ const fallbackModels = override?.fallbackModels ?? input.config.children.fallbackModels;
50
52
  const thinking = override?.thinking ?? input.config.children.thinking;
51
53
  const cadence = override?.cadence ?? input.config.children.cadence ?? input.config.cadence;
52
54
  return {
@@ -57,6 +59,7 @@ export function resolveChildWatchdogConfig(input: {
57
59
  agentEndTimeoutMs: input.config.agentEndTimeoutMs,
58
60
  maxWarnings: input.config.maxWarnings,
59
61
  ...(model ? { model } : {}),
62
+ ...(fallbackModels !== undefined ? { fallbackModels: [...fallbackModels] } : {}),
60
63
  ...(thinking !== undefined ? { thinking } : {}),
61
64
  lsp: { ...input.config.lsp },
62
65
  stalemateRepeats: input.config.stalemateRepeats,
@@ -137,6 +140,10 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
137
140
  const agent = childConfigOptionalString(parsed, "agent");
138
141
  const childIndex = childConfigOptionalIndex(parsed, "childIndex");
139
142
  const model = childConfigOptionalString(parsed, "model");
143
+ const fallbackModels = parsed.fallbackModels;
144
+ if (fallbackModels !== undefined && (!Array.isArray(fallbackModels) || fallbackModels.some((value) => typeof value !== "string" || !value.trim()))) {
145
+ throw new Error("Invalid child watchdog config: fallbackModels must be an array of non-empty strings.");
146
+ }
140
147
  return {
141
148
  ...(runId ? { runId } : {}),
142
149
  ...(agent ? { agent } : {}),
@@ -145,6 +152,7 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
145
152
  agentEndTimeoutMs: childConfigPositiveInteger(parsed, "agentEndTimeoutMs"),
146
153
  maxWarnings: childConfigNullableNonNegativeInteger(parsed, "maxWarnings"),
147
154
  ...(model ? { model } : {}),
155
+ ...(fallbackModels !== undefined ? { fallbackModels: (fallbackModels as string[]).map((value) => value.trim()) } : {}),
148
156
  ...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
149
157
  lsp: childConfigLsp(parsed.lsp),
150
158
  stalemateRepeats: childConfigPositiveInteger(parsed, "stalemateRepeats"),
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { normalizeModelSegment, resolveModelCandidate } from "../runs/shared/model-fallback.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: {
@@ -21,6 +21,7 @@ export function childResolvedConfig(config: ChildWatchdogConfig): ResolvedWatchd
21
21
  main: {
22
22
  enabled: true,
23
23
  ...(config.model ? { model: config.model } : {}),
24
+ ...(config.fallbackModels !== undefined ? { fallbackModels: [...config.fallbackModels] } : {}),
24
25
  ...(config.thinking !== undefined ? { thinking: config.thinking } : {}),
25
26
  },
26
27
  stalemateRepeats: config.stalemateRepeats,
@@ -3,7 +3,7 @@ import { Text } from "@earendil-works/pi-tui";
3
3
  import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, type ThinkingLevel } from "../shared/model-info.ts";
4
4
  import { SLASH_TEXT_RESULT_TYPE } from "../shared/types.ts";
5
5
  import { captureWatchdogDiffBaseline, type WatchdogDiffBaseline } from "./diff-tool.ts";
6
- import { recommendStrongWatchdogModel, resolveWatchdogModelInput, parseWatchdogThinkingInput } from "./model-selection.ts";
6
+ import { formatWatchdogRecommendation, recommendWatchdogModel, resolveWatchdogModelInput, parseWatchdogThinkingInput } from "./model-selection.ts";
7
7
  import { renderWatchdogWarning } from "./render.ts";
8
8
  import { createMainWatchdogReview } from "./review.ts";
9
9
  import { MainWatchdogRuntime, type WatchdogReviewFunction } from "./runtime.ts";
@@ -63,11 +63,16 @@ function mainThinkingLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"
63
63
  }
64
64
 
65
65
  function mainModelLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
66
+ const fallbacks = fallbackLine(snapshot.config.main.fallbackModels);
66
67
  if (snapshot.config.main.model) {
67
68
  const source = snapshot.sessionModelOverride?.model ? "session override" : "configured";
68
- return `Main model: ${splitKnownThinkingSuffix(snapshot.config.main.model).baseModel} (${source})`;
69
+ return `Main model: ${splitKnownThinkingSuffix(snapshot.config.main.model).baseModel} (${source})${fallbacks}`;
69
70
  }
70
- return `Main model: ${currentSessionModelLine(ctx)}`;
71
+ return `Main model: ${currentSessionModelLine(ctx)}${fallbacks}`;
72
+ }
73
+
74
+ function fallbackLine(models: string[] | undefined): string {
75
+ return models === undefined ? "" : ` · fallbacks ${models.length ? models.join(" → ") : "none"}`;
71
76
  }
72
77
 
73
78
  function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>): string {
@@ -80,19 +85,19 @@ function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>):
80
85
  const bits = [agent];
81
86
  if (override.enabled !== undefined) bits.push(boolLabel(override.enabled));
82
87
  if (override.model) bits.push(splitKnownThinkingSuffix(override.model).baseModel);
88
+ if (override.fallbackModels !== undefined) bits.push(fallbackLine(override.fallbackModels));
83
89
  if (override.thinking !== undefined) bits.push(`thinking ${override.thinking === false ? "off" : override.thinking}`);
84
90
  return bits.join(" ");
85
91
  }).join("; ")}`
86
92
  : "";
87
- return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model} · thinking ${thinking}${overrideText}`;
93
+ return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model}${fallbackLine(children.fallbackModels)} · thinking ${thinking}${overrideText}`;
88
94
  }
89
95
 
90
- function recommendationLine(ctx: ExtensionContext): string {
96
+ function recommendationLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
91
97
  try {
92
- const recommendation = recommendStrongWatchdogModel(ctx);
93
- return `Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking} (${recommendation.label}, complementary reviewer)`;
98
+ return formatWatchdogRecommendation(recommendWatchdogModel(ctx, snapshot.config.main));
94
99
  } catch (error) {
95
- return `Recommended strong watchdog: unavailable (${messageFromError(error)})`;
100
+ return `Watchdog recommendation unavailable (${messageFromError(error)})`;
96
101
  }
97
102
  }
98
103
 
@@ -111,7 +116,8 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
111
116
  "Subagent watchdog",
112
117
  `Main: ${boolLabel(snapshot.enabled)}${!snapshot.config.enabled && snapshot.sessionOverride === undefined ? " (default off)" : ""}`,
113
118
  `Runtime: ${statusLabel(snapshot.status)}${snapshot.bufferedDeltas > 0 ? ` · buffered deltas ${snapshot.bufferedDeltas}` : ""}`,
114
- `Review trigger: ${snapshot.reviewTrigger === "repo-edits" ? "repo edits only" : "every non-empty turn delta"}`,
119
+ `Review trigger: ${snapshot.reviewTrigger === "repo-edits" ? snapshot.config.clarification ? "repo edits + bounded main orchestration activity" : "repo edits only" : "every non-empty turn delta"}`,
120
+ `Main clarification: ${snapshot.config.clarification ? "on" : "off"}`,
115
121
  `Scope context: ${snapshot.config.scope.enabled ? "on" : "off"}`,
116
122
  `Cadence: ${snapshot.config.cadence.everyNTools === null ? "boundary only" : `every ${snapshot.config.cadence.everyNTools} tools + boundary`}`,
117
123
  lspLine(snapshot),
@@ -119,7 +125,7 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
119
125
  mainModelLine(snapshot, ctx),
120
126
  `Main thinking: ${mainThinkingLine(snapshot, ctx)}`,
121
127
  childrenLine(snapshot),
122
- recommendationLine(ctx),
128
+ recommendationLine(snapshot, ctx),
123
129
  `Agent-end timeout: ${snapshot.config.agentEndTimeoutMs}ms`,
124
130
  `Stalemate: ${snapshot.boundaryRepeats}/${snapshot.config.stalemateRepeats}${snapshot.stalemate ? " · stopped" : ""}`,
125
131
  `Rules: ${snapshot.config.rules ? `${Object.keys(snapshot.config.rules.roleModels).length} role models · ${snapshot.config.rules.action}` : "none"}`,
@@ -145,7 +151,7 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
145
151
  "",
146
152
  "Model commands:",
147
153
  "- /subagents-watchdog recommend-model",
148
- "- /subagents-watchdog model recommended",
154
+ "- /subagents-watchdog model recommended (saves to user settings, not project settings)",
149
155
  "- /subagents-watchdog model <provider/model[:thinking]>",
150
156
  "- /subagents-watchdog model inherit",
151
157
  "- /subagents-watchdog session model recommended",
@@ -171,12 +177,12 @@ function parseThinkingCommand(raw: string): ThinkingLevel | false | null {
171
177
  return parseWatchdogThinkingInput(value, "/subagents-watchdog thinking") ?? null;
172
178
  }
173
179
 
174
- function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): { model: string | null; thinking: ThinkingLevel | false | null; description: string } {
180
+ function resolveModelCommandValue(runtime: MainWatchdogRuntime, ctx: ExtensionCommandContext, raw: string): { model: string | null; thinking: ThinkingLevel | false | null; description: string } {
175
181
  const value = raw.trim();
176
182
  if (!value) throw new Error("Expected a model, 'recommended', or 'inherit'.");
177
183
  if (value === "inherit") return { model: null, thinking: null, description: "current session model and thinking" };
178
184
  if (value === "recommended") {
179
- const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
185
+ const recommendation = recommendWatchdogModel(ctx, runtime.getSnapshot(ctx.cwd).config.main);
180
186
  return {
181
187
  model: recommendation.model,
182
188
  thinking: recommendation.thinking,
@@ -191,18 +197,18 @@ function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): {
191
197
  };
192
198
  }
193
199
 
194
- function buildRecommendationText(ctx: ExtensionCommandContext): string {
195
- const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
200
+ function buildRecommendationText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandContext): string {
201
+ const recommendation = recommendWatchdogModel(ctx, runtime.getSnapshot(ctx.cwd).config.main);
196
202
  return [
197
203
  "Subagent watchdog recommended model",
198
204
  `Current session: ${currentSessionModelLine(ctx as ExtensionContext)}`,
199
- `Recommended: ${recommendation.model}:${recommendation.thinking}`,
205
+ recommendation.source === "configured" ? formatWatchdogRecommendation(recommendation) : `Recommended: ${recommendation.model}:${recommendation.thinking}`,
200
206
  `Reason: ${recommendation.reason}`,
201
207
  "",
202
208
  "Apply for this session:",
203
209
  "/subagents-watchdog session model recommended",
204
210
  "",
205
- "Save as your user default:",
211
+ "Save as your user default (affects other projects without overrides; does not change project settings):",
206
212
  "/subagents-watchdog model recommended",
207
213
  ].join("\n");
208
214
  }
@@ -221,12 +227,7 @@ function buildCheckText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandConte
221
227
  }
222
228
  lines.push(`Main thinking: ${mainThinkingLine(snapshot, ctx as ExtensionContext)}`);
223
229
  lines.push(lspLine(snapshot));
224
- try {
225
- const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
226
- lines.push(`Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking}`);
227
- } catch (error) {
228
- lines.push(`Recommended strong watchdog: unavailable (${messageFromError(error)})`);
229
- }
230
+ lines.push(recommendationLine(snapshot, ctx));
230
231
  return lines.join("\n");
231
232
  }
232
233
 
@@ -258,7 +259,7 @@ async function handleWatchdogCommand(
258
259
  }
259
260
  if (input === "recommend-model") {
260
261
  try {
261
- sendSlashText(pi, buildRecommendationText(ctx));
262
+ sendSlashText(pi, buildRecommendationText(runtime, ctx));
262
263
  } catch (error) {
263
264
  sendSlashText(pi, `Subagent watchdog recommended model\n\n${messageFromError(error)}`);
264
265
  }
@@ -301,7 +302,7 @@ async function handleWatchdogCommand(
301
302
  if (input.startsWith("session model ")) {
302
303
  const rawModel = input.slice("session model ".length);
303
304
  try {
304
- const value = resolveModelCommandValue(ctx, rawModel);
305
+ const value = resolveModelCommandValue(runtime, ctx, rawModel);
305
306
  const snapshot = value.model === null
306
307
  ? runtime.clearSessionModel(ctx.cwd)
307
308
  : runtime.setSessionModel({ model: value.model, thinking: value.thinking ?? null }, ctx.cwd);
@@ -319,7 +320,7 @@ async function handleWatchdogCommand(
319
320
  if (input.startsWith("model ")) {
320
321
  const rawModel = input.slice("model ".length);
321
322
  try {
322
- const value = resolveModelCommandValue(ctx, rawModel);
323
+ const value = resolveModelCommandValue(runtime, ctx, rawModel);
323
324
  const settingsPath = writeWatchdogModelSettings({
324
325
  scope: "user",
325
326
  target: { kind: "main" },
@@ -329,7 +330,7 @@ async function handleWatchdogCommand(
329
330
  runtime.refreshConfig(ctx.cwd);
330
331
  const snapshot = runtime.getSnapshot(ctx.cwd);
331
332
  sendSlashText(pi, [
332
- `Subagent watchdog model saved: ${value.description}.`,
333
+ `Subagent watchdog model saved to user settings: ${value.description}. Project and session overrides still take precedence.`,
333
334
  `Updated: ${settingsPath}`,
334
335
  `Main now: ${boolLabel(snapshot.enabled)}`,
335
336
  value.model === null ? "The watchdog now inherits the current session model and thinking." : "Run /subagents-watchdog on if the watchdog is still off.",
@@ -387,6 +388,7 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
387
388
  reviewDescription: options.review ? "injected seam" : "real model review",
388
389
  reviewChangesOnly: true,
389
390
  displayWarning: (details, options) => pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }), options),
391
+ displayClarification: (content) => pi.sendMessage({ customType: "subagent_watchdog_clarification", content, display: true }, { deliverAs: "steer", triggerTurn: true }),
390
392
  });
391
393
 
392
394
  pi.registerMessageRenderer<WatchdogWarningDetails>(SUBAGENT_WATCHDOG_WARNING_TYPE, (message, renderOptions, theme) => {
@@ -421,6 +423,8 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
421
423
  rememberContext(ctx);
422
424
  runtime.handleTurnEnd(event, ctx);
423
425
  });
426
+ pi.on("input", (event) => { if (event.source !== "extension") runtime.handleUserInput(); });
427
+ pi.on("model_select", () => runtime.handleModelChange());
424
428
  pi.on("tool_result", (_event, ctx) => {
425
429
  rememberContext(ctx);
426
430
  runtime.handleToolResult(ctx);