pi-subagents 0.67.0 → 0.69.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 (122) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +41 -12
  4. package/docs/configuration.md +61 -19
  5. package/docs/extension-api.md +5 -1
  6. package/docs/missions.md +2 -2
  7. package/docs/models.md +11 -79
  8. package/docs/observability.md +18 -8
  9. package/docs/standalone-background.md +13 -3
  10. package/docs/tool-reference.md +38 -14
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +59 -1
  13. package/index.ts +5 -2
  14. package/package.json +4 -2
  15. package/runner-peer-loader.mjs +24 -0
  16. package/runner-peer-preload.mjs +25 -11
  17. package/skills/pi-subagents/SKILL.md +18 -21
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  19. package/skills/pi-subagents/references/execution-controls.md +6 -4
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  21. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  22. package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
  23. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  24. package/src/agents/agent-management.ts +57 -58
  25. package/src/agents/agent-serializer.ts +4 -3
  26. package/src/agents/agents.ts +185 -72
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/agents/skills.ts +1 -1
  30. package/src/api/preflight.ts +20 -16
  31. package/src/api/required-child-extensions.ts +6 -0
  32. package/src/extension/config.ts +10 -37
  33. package/src/extension/fanout-child.ts +3 -0
  34. package/src/extension/herdr-pi-bridge.ts +160 -0
  35. package/src/extension/index.ts +42 -31
  36. package/src/extension/public-execution.ts +3 -3
  37. package/src/extension/schemas.ts +23 -6
  38. package/src/extension/tool-description.ts +8 -7
  39. package/src/inspectors/ghostty/plugin.ts +13 -1
  40. package/src/intercom/native-supervisor-channel.ts +22 -18
  41. package/src/policy/authority.ts +4 -0
  42. package/src/profiles/profiles.ts +12 -6
  43. package/src/runs/background/active-run-index.ts +17 -1
  44. package/src/runs/background/async-execution.ts +309 -126
  45. package/src/runs/background/async-job-tracker.ts +8 -6
  46. package/src/runs/background/async-resume.ts +13 -4
  47. package/src/runs/background/async-status.ts +15 -4
  48. package/src/runs/background/auto-drain.ts +20 -10
  49. package/src/runs/background/binary-bootstrap.ts +5 -0
  50. package/src/runs/background/chain-append.ts +1 -1
  51. package/src/runs/background/chain-root-attachment.ts +14 -33
  52. package/src/runs/background/notify.ts +74 -6
  53. package/src/runs/background/result-files.ts +8 -4
  54. package/src/runs/background/result-watcher.ts +19 -2
  55. package/src/runs/background/run-child-session.ts +20 -29
  56. package/src/runs/background/runner-aliases.ts +4 -33
  57. package/src/runs/background/runner-child-launch.ts +4 -1
  58. package/src/runs/background/runner-child-sessions.ts +2 -2
  59. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  60. package/src/runs/background/scheduled-runs.ts +11 -5
  61. package/src/runs/background/stale-run-reconciler.ts +35 -11
  62. package/src/runs/background/subagent-runner.ts +413 -276
  63. package/src/runs/background/subagent-wait.ts +128 -23
  64. package/src/runs/background/wait-completions.ts +75 -27
  65. package/src/runs/background/wait-subscriptions.ts +9 -3
  66. package/src/runs/background/wait-tool.ts +4 -2
  67. package/src/runs/foreground/async-stop-action.ts +93 -3
  68. package/src/runs/foreground/execution.ts +115 -219
  69. package/src/runs/foreground/foreground-history.ts +2 -1
  70. package/src/runs/foreground/subagent-executor.ts +281 -80
  71. package/src/runs/shared/acceptance.ts +194 -37
  72. package/src/runs/shared/async-status-projection.ts +123 -33
  73. package/src/runs/shared/child-launch-plan.ts +15 -3
  74. package/src/runs/shared/child-launch.ts +19 -6
  75. package/src/runs/shared/child-runtime-config.ts +5 -0
  76. package/src/runs/shared/child-session.ts +94 -50
  77. package/src/runs/shared/child-tool-plan.ts +28 -16
  78. package/src/runs/shared/dynamic-fanout.ts +2 -2
  79. package/src/runs/shared/external-cli-contract.ts +11 -1
  80. package/src/runs/shared/external-cli-preflight.ts +6 -2
  81. package/src/runs/shared/herdr-connection.ts +134 -0
  82. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  83. package/src/runs/shared/herdr-machine.ts +279 -0
  84. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  85. package/src/runs/shared/herdr-placed-run.ts +263 -0
  86. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  87. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  88. package/src/runs/shared/model-scope.ts +1 -1
  89. package/src/runs/shared/nested-events.ts +11 -2
  90. package/src/runs/shared/parallel-utils.ts +7 -2
  91. package/src/runs/shared/pi-spawn.ts +1 -1
  92. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  93. package/src/runs/shared/worktree-setup-command.ts +27 -4
  94. package/src/runs/shared/worktree.ts +30 -8
  95. package/src/shared/child-cache-retention.ts +43 -0
  96. package/src/shared/launch-contract.ts +6 -9
  97. package/src/shared/pruned-fork.ts +1 -1
  98. package/src/shared/required-child-extensions.ts +81 -0
  99. package/src/shared/settings.ts +5 -2
  100. package/src/shared/shortcuts.ts +0 -4
  101. package/src/shared/types.ts +81 -29
  102. package/src/slash/slash-commands.ts +0 -6
  103. package/src/slash/subagents-admin.ts +13 -9
  104. package/src/tui/render.ts +20 -10
  105. package/src/watchdog/child-status.ts +28 -36
  106. package/src/watchdog/lsp-diagnostics.ts +1 -1
  107. package/src/watchdog/model-selection.ts +1 -1
  108. package/src/watchdog/register-child.ts +10 -3
  109. package/src/watchdog/register-main.ts +20 -20
  110. package/src/watchdog/render.ts +1 -1
  111. package/src/watchdog/review.ts +14 -30
  112. package/src/watchdog/rules.ts +1 -1
  113. package/src/watchdog/runtime.ts +23 -12
  114. package/src/watchdog/settings.ts +3 -6
  115. package/src/watchdog/types.ts +3 -5
  116. package/src/watchdog/warning-format.ts +1 -1
  117. package/src/workflows/scripted-workflow.ts +68 -7
  118. package/src/workflows/workflow-receipt.ts +21 -3
  119. package/src/workflows/workflow-resources.ts +13 -2
  120. package/src/runs/shared/model-exclusions.ts +0 -374
  121. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  122. package/src/runs/shared/readonly-session-evidence.ts +0 -307
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";
@@ -95,11 +95,11 @@ function liveDetailKeyText(): string {
95
95
  }
96
96
 
97
97
  export function liveDetailHintText(): string {
98
- return `Press ${liveDetailKeyText()} for live detail · ${formatShortcutLabel(FLEET_OPEN_SHORTCUT)} Fleet`;
98
+ return `Press ${liveDetailKeyText()} for live detail`;
99
99
  }
100
100
 
101
101
  function workflowDetailHintText(): string {
102
- return `Press ${liveDetailKeyText()} for details · ${formatShortcutLabel(FLEET_OPEN_SHORTCUT)} Fleet`;
102
+ return `Press ${liveDetailKeyText()} for details`;
103
103
  }
104
104
 
105
105
  function foregroundSingleHintText(shortcut?: string): string {
@@ -2545,6 +2545,10 @@ function buildSingleLineWidgetLines(jobs: AsyncJobState[], theme: Theme, width:
2545
2545
  if (counts.failed.length > 0) parts.push(`${counts.failed.length} failed`);
2546
2546
  if (counts.stopped.length > 0) parts.push(`${counts.stopped.length} stopped`);
2547
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
+ }
2548
2552
  if (!hasActive && counts.complete.length > 0) parts.push(`${counts.complete.length}/${jobs.length} done`);
2549
2553
  return [truncLine(`${theme.fg(hasActive ? "accent" : "dim", glyph)} ${theme.fg(hasActive ? "accent" : "dim", "subagents")} (${parts.join(", ") || `${jobs.length} total`})`, width)];
2550
2554
  }
@@ -2817,6 +2821,7 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
2817
2821
  let cachedExpanded: boolean | undefined;
2818
2822
  let cachedLines: string[] | undefined;
2819
2823
  let cachedCoverage = "[]";
2824
+ let collapsed = false;
2820
2825
  const invalidate = (): void => {
2821
2826
  cachedLines = undefined;
2822
2827
  resetWidgetLayoutSession();
@@ -2830,6 +2835,14 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
2830
2835
  };
2831
2836
  asyncWidgetUpdates.set(ui, update);
2832
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
+ },
2833
2846
  dispose(): void {
2834
2847
  if (asyncWidgetUpdates.get(ui) === update) asyncWidgetUpdates.delete(ui);
2835
2848
  if (asyncWidgetInvalidations.get(ui) === invalidate) asyncWidgetInvalidations.delete(ui);
@@ -2872,7 +2885,10 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
2872
2885
  cachedRenderWidth = renderWidth;
2873
2886
  cachedFrame = frame;
2874
2887
  cachedExpanded = expanded;
2875
- cachedLines = fitAdaptiveWidgetLines(roots, 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));
2876
2892
  return cachedLines;
2877
2893
  };
2878
2894
  return component;
@@ -3468,9 +3484,6 @@ export function renderSubagentResult(
3468
3484
  if (r.skillsWarning) {
3469
3485
  c.addChild(new Text(fit(theme.fg("warning", `Warning: ${r.skillsWarning}`)), 0, 0));
3470
3486
  }
3471
- if (r.attemptedModels && r.attemptedModels.length > 1) {
3472
- c.addChild(new Text(fit(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
3473
- }
3474
3487
  c.addChild(new Text(fit(theme.fg("dim", formatUsage(r.usage, r.model))), 0, 0));
3475
3488
  if (r.sessionFile) {
3476
3489
  c.addChild(new Text(fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)), 0, 0));
@@ -3647,9 +3660,6 @@ export function renderSubagentResult(
3647
3660
  if (r.skillsWarning) {
3648
3661
  c.addChild(new Text(fit(theme.fg("warning", ` Warning: ${r.skillsWarning}`)), 0, 0));
3649
3662
  }
3650
- if (r.attemptedModels && r.attemptedModels.length > 1) {
3651
- c.addChild(new Text(fit(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
3652
- }
3653
3663
 
3654
3664
  if (rRunning && rProg) {
3655
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
 
@@ -16,7 +16,6 @@ export interface ChildWatchdogConfig {
16
16
  agentEndTimeoutMs: number;
17
17
  maxWarnings: number | null;
18
18
  model?: string;
19
- fallbackModels?: string[];
20
19
  thinking?: string | false;
21
20
  lsp: WatchdogLspConfig;
22
21
  stalemateRepeats: number;
@@ -34,6 +33,7 @@ export interface ChildWatchdogStatusEvent {
34
33
  phase: ChildWatchdogPhase;
35
34
  ts: number;
36
35
  reason?: string;
36
+ warning?: ChildWatchdogWarningSummary;
37
37
  }
38
38
 
39
39
  export type ChildWatchdogStateSnapshot = ChildWatchdogProgress;
@@ -48,7 +48,6 @@ export function resolveChildWatchdogConfig(input: {
48
48
  const enabled = input.config.enabled && (override?.enabled ?? input.config.children.enabled);
49
49
  if (!enabled) return undefined;
50
50
  const model = override?.model ?? input.config.children.model;
51
- const fallbackModels = override?.fallbackModels ?? input.config.children.fallbackModels;
52
51
  const thinking = override?.thinking ?? input.config.children.thinking;
53
52
  const cadence = override?.cadence ?? input.config.children.cadence ?? input.config.cadence;
54
53
  return {
@@ -59,7 +58,6 @@ export function resolveChildWatchdogConfig(input: {
59
58
  agentEndTimeoutMs: input.config.agentEndTimeoutMs,
60
59
  maxWarnings: input.config.maxWarnings,
61
60
  ...(model ? { model } : {}),
62
- ...(fallbackModels !== undefined ? { fallbackModels: [...fallbackModels] } : {}),
63
61
  ...(thinking !== undefined ? { thinking } : {}),
64
62
  lsp: { ...input.config.lsp },
65
63
  stalemateRepeats: input.config.stalemateRepeats,
@@ -130,6 +128,9 @@ function childConfigLsp(value: unknown): WatchdogLspConfig {
130
128
  export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdogConfig | undefined {
131
129
  if (!raw) return undefined;
132
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
+ }
133
134
  if (parsed.enabled === false) return undefined;
134
135
  if ("enabled" in parsed && parsed.enabled !== true) throw new Error("Invalid child watchdog config: enabled must be true or false.");
135
136
  const thinking = parsed.thinking;
@@ -140,10 +141,6 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
140
141
  const agent = childConfigOptionalString(parsed, "agent");
141
142
  const childIndex = childConfigOptionalIndex(parsed, "childIndex");
142
143
  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
- }
147
144
  return {
148
145
  ...(runId ? { runId } : {}),
149
146
  ...(agent ? { agent } : {}),
@@ -152,7 +149,6 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
152
149
  agentEndTimeoutMs: childConfigPositiveInteger(parsed, "agentEndTimeoutMs"),
153
150
  maxWarnings: childConfigNullableNonNegativeInteger(parsed, "maxWarnings"),
154
151
  ...(model ? { model } : {}),
155
- ...(fallbackModels !== undefined ? { fallbackModels: (fallbackModels as string[]).map((value) => value.trim()) } : {}),
156
152
  ...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
157
153
  lsp: childConfigLsp(parsed.lsp),
158
154
  stalemateRepeats: childConfigPositiveInteger(parsed, "stalemateRepeats"),
@@ -163,6 +159,15 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
163
159
  export function isChildWatchdogStatusEvent(value: unknown): value is ChildWatchdogStatusEvent {
164
160
  if (!value || typeof value !== "object") return false;
165
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"));
166
171
  return event.type === CHILD_WATCHDOG_STATUS_EVENT
167
172
  && typeof event.seq === "number"
168
173
  && Number.isInteger(event.seq)
@@ -170,7 +175,8 @@ export function isChildWatchdogStatusEvent(value: unknown): value is ChildWatchd
170
175
  && typeof event.ts === "number"
171
176
  && Number.isFinite(event.ts)
172
177
  && typeof event.phase === "string"
173
- && (CHILD_WATCHDOG_PHASES as readonly string[]).includes(event.phase);
178
+ && (CHILD_WATCHDOG_PHASES as readonly string[]).includes(event.phase)
179
+ && validWarning;
174
180
  }
175
181
 
176
182
  export function childWatchdogIsActive(snapshot: ChildWatchdogStateSnapshot | undefined): boolean {
@@ -190,40 +196,20 @@ export function acceptChildWatchdogEvent(input: {
190
196
  const eventIndex = input.event.childIndex ?? input.event.stepIndex;
191
197
  if (input.childIndex !== undefined && eventIndex !== input.childIndex) return undefined;
192
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;
193
202
  return {
194
203
  phase: input.event.phase,
195
204
  seq: input.event.seq,
196
205
  lastUpdate: input.event.ts,
197
206
  ...(input.event.reason ? { reason: input.event.reason } : {}),
198
- ...(input.current?.warnings?.length ? { warnings: input.current.warnings } : {}),
207
+ ...(warnings?.length ? { warnings } : {}),
199
208
  };
200
209
  }
201
210
 
202
- function childWatchdogWarningFromMessage(message: unknown): ChildWatchdogWarningSummary | undefined {
203
- const candidate = message as { role?: unknown; customType?: unknown; details?: Record<string, unknown> } | undefined;
204
- if (candidate?.role !== "custom" || candidate.customType !== SUBAGENT_WATCHDOG_WARNING_TYPE) return undefined;
205
- const details = candidate.details ?? {};
206
- if (details.severity !== "concern" && details.severity !== "blocker") return undefined;
207
- if (typeof details.summary !== "string" || typeof details.evidence !== "string" || typeof details.recommendedAction !== "string") return undefined;
208
- return {
209
- severity: details.severity,
210
- category: details.category as WatchdogCategory,
211
- summary: details.summary,
212
- evidence: details.evidence,
213
- recommendedAction: details.recommendedAction,
214
- ...(typeof details.displayedAt === "string" ? { displayedAt: details.displayedAt } : {}),
215
- addressed: false,
216
- stalemate: details.state === "stalemate",
217
- };
218
- }
219
-
220
- /** A watchdog warning message is appended; an assistant turn marks earlier warnings addressed. Undefined when unchanged. */
221
- export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot | undefined, message: unknown, now = Date.now()): ChildWatchdogStateSnapshot | undefined {
222
- const warning = childWatchdogWarningFromMessage(message);
223
- if (warning) {
224
- const warnings = [...(current?.warnings ?? []), warning].slice(-CHILD_WATCHDOG_WARNING_LIMIT);
225
- return current ? { ...current, warnings } : { phase: "idle", seq: 0, lastUpdate: now, warnings };
226
- }
211
+ /** An assistant turn marks earlier warnings addressed. Undefined when unchanged. */
212
+ export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot | undefined, message: unknown): ChildWatchdogStateSnapshot | undefined {
227
213
  if ((message as { role?: unknown } | undefined)?.role !== "assistant" || !current?.warnings?.some((entry) => !entry.addressed)) return undefined;
228
214
  return { ...current, warnings: current.warnings.map((entry) => entry.addressed ? entry : { ...entry, addressed: true }) };
229
215
  }
@@ -231,3 +217,9 @@ export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot |
231
217
  export function unresolvedChildWatchdogBlockers(progress: Pick<ChildWatchdogProgress, "warnings"> | undefined): ChildWatchdogWarningSummary[] {
232
218
  return (progress?.warnings ?? []).filter((warning) => warning.severity === "blocker" && (!warning.addressed || warning.stalemate));
233
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,5 @@
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
3
  import type { WatchdogEndpointConfig } from "./types.ts";
4
4
  import {
5
5
  getSupportedThinkingLevels,
@@ -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 {
@@ -21,7 +22,6 @@ export function childResolvedConfig(config: ChildWatchdogConfig): ResolvedWatchd
21
22
  main: {
22
23
  enabled: true,
23
24
  ...(config.model ? { model: config.model } : {}),
24
- ...(config.fallbackModels !== undefined ? { fallbackModels: [...config.fallbackModels] } : {}),
25
25
  ...(config.thinking !== undefined ? { thinking: config.thinking } : {}),
26
26
  },
27
27
  stalemateRepeats: config.stalemateRepeats,
@@ -58,7 +58,7 @@ export function registerChildWatchdog(
58
58
  let currentContext: ExtensionContext | undefined;
59
59
  let diffBaseline: WatchdogDiffBaseline | undefined;
60
60
  let seq = 0;
61
- const emitStatus = (phase: ChildWatchdogPhase, reason?: string): void => {
61
+ const emitStatus = (phase: ChildWatchdogPhase, reason?: string, warning?: ChildWatchdogWarningSummary): void => {
62
62
  writeStatus({
63
63
  type: CHILD_WATCHDOG_STATUS_EVENT,
64
64
  ...(childConfig.runId ? { runId: childConfig.runId } : {}),
@@ -68,6 +68,7 @@ export function registerChildWatchdog(
68
68
  phase,
69
69
  ts: Date.now(),
70
70
  ...(reason ? { reason } : {}),
71
+ ...(warning ? { warning } : {}),
71
72
  });
72
73
  };
73
74
  const resolved = childResolvedConfig(childConfig);
@@ -78,8 +79,14 @@ export function registerChildWatchdog(
78
79
  reviewChangesOnly: true,
79
80
  displayWarning: (details, options) => {
80
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" });
81
83
  pi.sendMessage(createWatchdogWarningMessage(childDetails, { display: true, details: childDetails }), options);
82
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
+ },
83
90
  });
84
91
  const rememberContext = (ctx: ExtensionContext) => {
85
92
  currentContext = ctx;
@@ -63,16 +63,11 @@ 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);
67
66
  if (snapshot.config.main.model) {
68
67
  const source = snapshot.sessionModelOverride?.model ? "session override" : "configured";
69
- return `Main model: ${splitKnownThinkingSuffix(snapshot.config.main.model).baseModel} (${source})${fallbacks}`;
68
+ return `Main model: ${splitKnownThinkingSuffix(snapshot.config.main.model).baseModel} (${source})`;
70
69
  }
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"}`;
70
+ return `Main model: ${currentSessionModelLine(ctx)}`;
76
71
  }
77
72
 
78
73
  function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>): string {
@@ -85,12 +80,11 @@ function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>):
85
80
  const bits = [agent];
86
81
  if (override.enabled !== undefined) bits.push(boolLabel(override.enabled));
87
82
  if (override.model) bits.push(splitKnownThinkingSuffix(override.model).baseModel);
88
- if (override.fallbackModels !== undefined) bits.push(fallbackLine(override.fallbackModels));
89
83
  if (override.thinking !== undefined) bits.push(`thinking ${override.thinking === false ? "off" : override.thinking}`);
90
84
  return bits.join(" ");
91
85
  }).join("; ")}`
92
86
  : "";
93
- return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model}${fallbackLine(children.fallbackModels)} · thinking ${thinking}${overrideText}`;
87
+ return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model} · thinking ${thinking}${overrideText}`;
94
88
  }
95
89
 
96
90
  function recommendationLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
@@ -160,10 +154,10 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
160
154
  return lines.join("\n");
161
155
  }
162
156
 
163
- function parseTestCommand(input: string): { severity: "concern" | "blocker"; text: string } | undefined {
164
- const match = input.match(/^test\s+(concern|blocker)\s+([\s\S]+)$/);
157
+ function parseTestCommand(input: string): { severity: "concern" | "blocker"; importance: "low" | "medium" | "high"; text: string } | undefined {
158
+ const match = input.match(/^test\s+(concern|blocker)\s+(low|medium|high)\s+([\s\S]+)$/);
165
159
  if (!match) return undefined;
166
- return { severity: match[1] as "concern" | "blocker", text: match[2]!.trim() };
160
+ return { severity: match[1] as "concern" | "blocker", importance: match[2] as "low" | "medium" | "high", text: match[3]!.trim() };
167
161
  }
168
162
 
169
163
  function formatThinking(value: ThinkingLevel | false | undefined): string {
@@ -231,11 +225,11 @@ function buildCheckText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandConte
231
225
  return lines.join("\n");
232
226
  }
233
227
 
234
- function createTestWarning(severity: "concern" | "blocker", text: string): WatchdogWarning {
228
+ function createTestWarning(severity: "concern" | "blocker", importance: "low" | "medium" | "high", text: string): WatchdogWarning {
235
229
  return {
236
230
  severity,
237
231
  category: "other",
238
- confidence: "high",
232
+ importance,
239
233
  source: "main",
240
234
  state: "displayed",
241
235
  summary: text,
@@ -366,15 +360,14 @@ async function handleWatchdogCommand(
366
360
  const test = parseTestCommand(input);
367
361
  if (test) {
368
362
  if (!test.text) {
369
- ctx.ui.notify("Usage: /subagents-watchdog test concern|blocker <text>", "error");
363
+ ctx.ui.notify("Usage: /subagents-watchdog test concern|blocker low|medium|high <text>", "error");
370
364
  return;
371
365
  }
372
- const warning = createTestWarning(test.severity, test.text);
373
- const details = runtime.recordDisplayedWarning(warning);
374
- pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }));
366
+ const warning = createTestWarning(test.severity, test.importance, test.text);
367
+ runtime.displayRecordedWarning(warning);
375
368
  return;
376
369
  }
377
- ctx.ui.notify(`Usage: /subagents-watchdog [status|on|off|session on|session off|recommend-model|model recommended|model <provider/model[:thinking]>|model inherit|thinking ${THINKING_LEVELS.join("|")}|thinking inherit|session model recommended|check|test concern <text>|test blocker <text>]`, "error");
370
+ ctx.ui.notify(`Usage: /subagents-watchdog [status|on|off|session on|session off|recommend-model|model recommended|model <provider/model[:thinking]>|model inherit|thinking ${THINKING_LEVELS.join("|")}|thinking inherit|session model recommended|check|test concern|blocker low|medium|high <text>]`, "error");
378
371
  }
379
372
 
380
373
  export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatchdogOptions = {}): MainWatchdogRuntime {
@@ -388,6 +381,7 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
388
381
  reviewDescription: options.review ? "injected seam" : "real model review",
389
382
  reviewChangesOnly: true,
390
383
  displayWarning: (details, options) => pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }), options),
384
+ displayUserWarning: (details) => pi.appendEntry(SUBAGENT_WATCHDOG_WARNING_TYPE, details),
391
385
  displayClarification: (content) => pi.sendMessage({ customType: "subagent_watchdog_clarification", content, display: true }, { deliverAs: "steer", triggerTurn: true }),
392
386
  });
393
387
 
@@ -401,6 +395,12 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
401
395
  }
402
396
  return renderWatchdogWarning(details, renderOptions, theme);
403
397
  });
398
+ pi.registerEntryRenderer<WatchdogWarningDetails>(SUBAGENT_WATCHDOG_WARNING_TYPE, (entry, renderOptions, theme) => {
399
+ const details = entry.data as WatchdogWarningDetails | undefined;
400
+ return details?.summary && details.evidence && details.recommendedAction
401
+ ? renderWatchdogWarning(details, renderOptions, theme)
402
+ : undefined;
403
+ });
404
404
 
405
405
  pi.registerCommand("subagents-watchdog", {
406
406
  description: "Show or toggle the default-off subagent watchdog",
@@ -435,7 +435,7 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
435
435
  });
436
436
  pi.on("session_before_switch", () => runtime.reset("session switch", { clearReviewInputSignature: true, clearLspLedger: true, clearScope: true }));
437
437
  pi.on("session_before_fork", () => runtime.reset("session fork", { clearReviewInputSignature: true, clearLspLedger: true, clearScope: true }));
438
- pi.on("session_compact", () => runtime.reset("session compact", { clearScope: true }));
438
+ pi.on("session_compact", () => runtime.reset("session compact", { clearActivity: true }));
439
439
  pi.on("session_shutdown", () => {
440
440
  currentContext = undefined;
441
441
  runtime.dispose();
@@ -26,7 +26,7 @@ export function formatWatchdogWarningRenderText(warning: WatchdogWarningDetails)
26
26
  `Subagent watchdog ${subject}${labels.length ? ` (${labels.join(", ")})` : ""}: ${warning.summary}`,
27
27
  `Evidence: ${warning.evidence}`,
28
28
  `Recommended action: ${warning.recommendedAction}`,
29
- `Category: ${titleCase(warning.category)} · Source: ${warning.source}${warning.agent ? ` · Agent: ${warning.agent}` : ""}${warning.runId ? ` · Run: ${warning.runId}` : ""}`,
29
+ `Importance: ${titleCase(warning.importance)} · Category: ${titleCase(warning.category)} · Source: ${warning.source}${warning.agent ? ` · Agent: ${warning.agent}` : ""}${warning.runId ? ` · Run: ${warning.runId}` : ""}`,
30
30
  ];
31
31
  if (warning.state === "failed" && warning.error) lines.push(`Failure: ${warning.error}`);
32
32
  if (warning.state === "stalemate" && warning.stalemateRepeats !== undefined) {
@@ -3,7 +3,7 @@ import { createReadOnlyTools, convertToLlm, type ExtensionContext } from "@earen
3
3
  import { streamSimple } from "@earendil-works/pi-ai/compat";
4
4
  import type { Model, ProviderHeaders } from "@earendil-works/pi-ai";
5
5
  import { Type, type Static } from "typebox";
6
- import { buildModelCandidates, isContextOverflow, isRetryableModelFailureAttempt, resolveModelCandidate } from "../runs/shared/model-fallback.ts";
6
+ import { resolveModelCandidate } from "../runs/shared/model-resolution.ts";
7
7
  import { agentStreamOptions } from "../shared/agent-stream-options.ts";
8
8
  import { opencodeSessionHeaders } from "../shared/opencode-session-headers.ts";
9
9
  import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from "../shared/model-info.ts";
@@ -12,11 +12,11 @@ import { loadWatchdogGuidance } from "./guidance.ts";
12
12
  import { boundWatchdogReviewText, type WatchdogReviewFunction, type WatchdogReviewRequest } from "./runtime.ts";
13
13
  import {
14
14
  WATCHDOG_WARNING_CATEGORIES,
15
- WATCHDOG_WARNING_CONFIDENCES,
15
+ WATCHDOG_WARNING_IMPORTANCES,
16
16
  WATCHDOG_WARNING_SEVERITIES,
17
17
  type ResolvedWatchdogConfig,
18
18
  type WatchdogCategory,
19
- type WatchdogConfidence,
19
+ type WatchdogImportance,
20
20
  type WatchdogSeverity,
21
21
  type WatchdogWarning,
22
22
  } from "./types.ts";
@@ -27,11 +27,11 @@ const WatchdogAskParams = Type.Object({ question: Type.String(), evidence: Type.
27
27
 
28
28
  const WatchdogWarnParams = Type.Object({
29
29
  severity: Type.String({ enum: WATCHDOG_WARNING_SEVERITIES, description: "concern for actionable risk, blocker for a likely wrong or unsafe outcome" }),
30
+ importance: Type.String({ enum: WATCHDOG_WARNING_IMPORTANCES, description: "low or medium for user-only visibility; high when the parent model must receive the finding" }),
30
31
  summary: Type.String({ description: "One concise sentence naming the issue." }),
31
32
  evidence: Type.String({ description: "Specific evidence from the turn delta or inspected files." }),
32
33
  recommendedAction: Type.String({ description: "Specific action the parent should take before accepting or continuing." }),
33
34
  category: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CATEGORIES })),
34
- confidence: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CONFIDENCES })),
35
35
  }, { additionalProperties: false });
36
36
 
37
37
  type WatchdogWarnParams = Static<typeof WatchdogWarnParams>;
@@ -182,8 +182,8 @@ function nonEmptyString(value: string, field: string): string {
182
182
  function toWatchdogWarning(params: WatchdogWarnParams): WatchdogWarning {
183
183
  return {
184
184
  severity: params.severity as WatchdogSeverity,
185
+ importance: params.importance as WatchdogImportance,
185
186
  category: (params.category ?? "other") as WatchdogCategory,
186
- confidence: (params.confidence ?? "medium") as WatchdogConfidence,
187
187
  source: "main",
188
188
  summary: nonEmptyString(params.summary, "summary"),
189
189
  evidence: nonEmptyString(params.evidence, "evidence"),
@@ -197,7 +197,7 @@ function createWatchdogWarnTool(request: WatchdogReviewRequest): AgentTool<typeo
197
197
  label: "Watchdog warning",
198
198
  description: [
199
199
  "Emit one actionable main-session watchdog warning.",
200
- "Use only for medium/high confidence concerns or blockers that the parent should consider before accepting the work.",
200
+ "Set importance explicitly: low or medium for user-only visibility, high only when the parent model must receive the finding.",
201
201
  "Do not use for nits, praise, informational notes, or clean reviews.",
202
202
  ].join(" "),
203
203
  parameters: WatchdogWarnParams,
@@ -227,8 +227,8 @@ export function buildWatchdogSystemPrompt(ctx: Pick<ExtensionContext, "cwd">, op
227
227
  options.hasScope ? "Use the Current scope record alongside supplied activity evidence; an unrelated user question does not cancel older authorized work." : undefined,
228
228
  `You are read-only. You may use ${options.hasDiff ? "read, grep, find, ls, and watchdog_diff (the full repo diff since the session baseline; pass a path to narrow it)" : "read, grep, find, and ls"}. Do not edit files, run shell commands, spawn agents, or mutate state.`,
229
229
  "Emit warnings only by calling watchdog_warn. Freeform assistant text is ignored and must not be used to report warnings.",
230
- "Emit only medium/high confidence actionable concerns or blockers: missed user constraints, correctness risks, test gaps that matter, unsafe changes, stale facts, loop risks, or scope drift.",
231
- "Do not emit nits, style preferences, low-confidence guesses, informational notes, praise, or summaries.",
230
+ "Emit only actionable concerns or blockers: missed user constraints, correctness risks, test gaps that matter, unsafe changes, stale facts, loop risks, or scope drift.",
231
+ "Do not emit nits, style preferences, unsupported guesses, informational notes, praise, or summaries.",
232
232
  "If the turn is clean, call no tools and end normally.",
233
233
  "Use severity='blocker' only when the issue should stop acceptance until addressed; otherwise use severity='concern'.",
234
234
  guidance ? `\nStanding instructions from WATCHDOG.md (project first, then user):\n${guidance}` : undefined,
@@ -256,32 +256,17 @@ export function createMainWatchdogReview(provider: WatchdogContextProvider, opti
256
256
  if (!ctx) throw new Error("Main watchdog review cannot run without an active Pi extension context.");
257
257
  const aborted = () => ctx.signal?.aborted || request.signal?.aborted;
258
258
  if (aborted()) return { stopReason: "aborted" };
259
- const inherited = !request.config.main.model && ctx.model ? fullModelId(ctx.model) : undefined;
260
- const candidates = request.config.main.fallbackModels?.length
261
- ? buildModelCandidates(request.config.main.model ?? inherited, request.config.main.fallbackModels,
262
- ctx.modelRegistry.getAvailable().map(toModelInfo), ctx.model?.provider, { primaryModelFromParent: Boolean(inherited) })
263
- : [request.config.main.model];
264
- for (let index = 0; index < candidates.length; index++) {
265
- if (aborted()) return { stopReason: "aborted" };
266
- const candidate = candidates[index];
267
- const config = { ...request.config, main: { ...request.config.main, model: candidate === inherited ? undefined : candidate } };
268
- try {
269
- const attempt = await runWatchdogAttempt(ctx, { ...request, config }, options);
270
- if (aborted()) return { stopReason: "aborted" };
271
- if (!attempt.retryable || index === candidates.length - 1) return attempt.result;
272
- } catch (error) {
273
- if (aborted()) return { stopReason: "aborted" };
274
- if (!(error instanceof WatchdogAuthError)) throw error;
275
- if (isContextOverflow(error.message) || !isRetryableModelFailureAttempt({ error: error.message }) || index === candidates.length - 1) throw error.cause ?? error;
276
- }
259
+ try {
260
+ return (await runWatchdogAttempt(ctx, request, options)).result;
261
+ } catch (error) {
262
+ if (error instanceof WatchdogAuthError) throw error.cause ?? error;
263
+ throw error;
277
264
  }
278
- throw new Error("No usable watchdog model candidates.");
279
265
  };
280
266
  }
281
267
 
282
268
  async function runWatchdogAttempt(ctx: ExtensionContext, request: WatchdogReviewRequest, options: CreateMainWatchdogReviewOptions): Promise<{
283
269
  result: Awaited<ReturnType<WatchdogReviewFunction>>;
284
- retryable?: boolean;
285
270
  }> {
286
271
  const selection = await resolveWatchdogReviewModel(ctx, request.config, {
287
272
  currentThinkingLevel: options.getThinkingLevel?.(),
@@ -373,7 +358,6 @@ async function runWatchdogAttempt(ctx: ExtensionContext, request: WatchdogReview
373
358
  const stopReason = reason === "error" || reason === "aborted" || reason === "length" ? reason : "stop";
374
359
  const error = terminal && "errorMessage" in terminal && typeof terminal.errorMessage === "string" ? terminal.errorMessage : undefined;
375
360
  return {
376
- result: clarification ? { clarification } : { stopReason },
377
- retryable: !clarification && stopReason === "error" && !isContextOverflow(error) && isRetryableModelFailureAttempt({ error, messages: agent.state.messages, toolCount }),
361
+ result: clarification ? { clarification } : error ? { stopReason, errorMessage: error } : { stopReason },
378
362
  };
379
363
  }
@@ -51,7 +51,7 @@ export function evaluateLaunchRule(rules: WatchdogRulesConfig | undefined, agent
51
51
  }
52
52
 
53
53
  export function ruleViolationWarning(violation: WatchdogRuleViolation): WatchdogWarning {
54
- return { severity: "concern", category: "missed-constraint", confidence: "high", source: "main", ...violation };
54
+ return { severity: "concern", category: "missed-constraint", importance: "high", source: "main", ...violation };
55
55
  }
56
56
 
57
57
  export function applyWatchdogLaunchRules(input: { cwd: string; agent: string; model?: string; warn?: (violation: WatchdogRuleViolation) => void }): string | undefined {