pi-subagents 0.66.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -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";
@@ -87,12 +87,11 @@ function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>):
87
87
  return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model} · thinking ${thinking}${overrideText}`;
88
88
  }
89
89
 
90
- function recommendationLine(ctx: ExtensionContext): string {
90
+ function recommendationLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
91
91
  try {
92
- const recommendation = recommendStrongWatchdogModel(ctx);
93
- return `Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking} (${recommendation.label}, complementary reviewer)`;
92
+ return formatWatchdogRecommendation(recommendWatchdogModel(ctx, snapshot.config.main));
94
93
  } catch (error) {
95
- return `Recommended strong watchdog: unavailable (${messageFromError(error)})`;
94
+ return `Watchdog recommendation unavailable (${messageFromError(error)})`;
96
95
  }
97
96
  }
98
97
 
@@ -111,7 +110,8 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
111
110
  "Subagent watchdog",
112
111
  `Main: ${boolLabel(snapshot.enabled)}${!snapshot.config.enabled && snapshot.sessionOverride === undefined ? " (default off)" : ""}`,
113
112
  `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"}`,
113
+ `Review trigger: ${snapshot.reviewTrigger === "repo-edits" ? snapshot.config.clarification ? "repo edits + bounded main orchestration activity" : "repo edits only" : "every non-empty turn delta"}`,
114
+ `Main clarification: ${snapshot.config.clarification ? "on" : "off"}`,
115
115
  `Scope context: ${snapshot.config.scope.enabled ? "on" : "off"}`,
116
116
  `Cadence: ${snapshot.config.cadence.everyNTools === null ? "boundary only" : `every ${snapshot.config.cadence.everyNTools} tools + boundary`}`,
117
117
  lspLine(snapshot),
@@ -119,7 +119,7 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
119
119
  mainModelLine(snapshot, ctx),
120
120
  `Main thinking: ${mainThinkingLine(snapshot, ctx)}`,
121
121
  childrenLine(snapshot),
122
- recommendationLine(ctx),
122
+ recommendationLine(snapshot, ctx),
123
123
  `Agent-end timeout: ${snapshot.config.agentEndTimeoutMs}ms`,
124
124
  `Stalemate: ${snapshot.boundaryRepeats}/${snapshot.config.stalemateRepeats}${snapshot.stalemate ? " · stopped" : ""}`,
125
125
  `Rules: ${snapshot.config.rules ? `${Object.keys(snapshot.config.rules.roleModels).length} role models · ${snapshot.config.rules.action}` : "none"}`,
@@ -145,7 +145,7 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
145
145
  "",
146
146
  "Model commands:",
147
147
  "- /subagents-watchdog recommend-model",
148
- "- /subagents-watchdog model recommended",
148
+ "- /subagents-watchdog model recommended (saves to user settings, not project settings)",
149
149
  "- /subagents-watchdog model <provider/model[:thinking]>",
150
150
  "- /subagents-watchdog model inherit",
151
151
  "- /subagents-watchdog session model recommended",
@@ -154,10 +154,10 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
154
154
  return lines.join("\n");
155
155
  }
156
156
 
157
- function parseTestCommand(input: string): { severity: "concern" | "blocker"; text: string } | undefined {
158
- 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]+)$/);
159
159
  if (!match) return undefined;
160
- 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() };
161
161
  }
162
162
 
163
163
  function formatThinking(value: ThinkingLevel | false | undefined): string {
@@ -171,12 +171,12 @@ function parseThinkingCommand(raw: string): ThinkingLevel | false | null {
171
171
  return parseWatchdogThinkingInput(value, "/subagents-watchdog thinking") ?? null;
172
172
  }
173
173
 
174
- function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): { model: string | null; thinking: ThinkingLevel | false | null; description: string } {
174
+ function resolveModelCommandValue(runtime: MainWatchdogRuntime, ctx: ExtensionCommandContext, raw: string): { model: string | null; thinking: ThinkingLevel | false | null; description: string } {
175
175
  const value = raw.trim();
176
176
  if (!value) throw new Error("Expected a model, 'recommended', or 'inherit'.");
177
177
  if (value === "inherit") return { model: null, thinking: null, description: "current session model and thinking" };
178
178
  if (value === "recommended") {
179
- const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
179
+ const recommendation = recommendWatchdogModel(ctx, runtime.getSnapshot(ctx.cwd).config.main);
180
180
  return {
181
181
  model: recommendation.model,
182
182
  thinking: recommendation.thinking,
@@ -191,18 +191,18 @@ function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): {
191
191
  };
192
192
  }
193
193
 
194
- function buildRecommendationText(ctx: ExtensionCommandContext): string {
195
- const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
194
+ function buildRecommendationText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandContext): string {
195
+ const recommendation = recommendWatchdogModel(ctx, runtime.getSnapshot(ctx.cwd).config.main);
196
196
  return [
197
197
  "Subagent watchdog recommended model",
198
198
  `Current session: ${currentSessionModelLine(ctx as ExtensionContext)}`,
199
- `Recommended: ${recommendation.model}:${recommendation.thinking}`,
199
+ recommendation.source === "configured" ? formatWatchdogRecommendation(recommendation) : `Recommended: ${recommendation.model}:${recommendation.thinking}`,
200
200
  `Reason: ${recommendation.reason}`,
201
201
  "",
202
202
  "Apply for this session:",
203
203
  "/subagents-watchdog session model recommended",
204
204
  "",
205
- "Save as your user default:",
205
+ "Save as your user default (affects other projects without overrides; does not change project settings):",
206
206
  "/subagents-watchdog model recommended",
207
207
  ].join("\n");
208
208
  }
@@ -221,20 +221,15 @@ function buildCheckText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandConte
221
221
  }
222
222
  lines.push(`Main thinking: ${mainThinkingLine(snapshot, ctx as ExtensionContext)}`);
223
223
  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
- }
224
+ lines.push(recommendationLine(snapshot, ctx));
230
225
  return lines.join("\n");
231
226
  }
232
227
 
233
- function createTestWarning(severity: "concern" | "blocker", text: string): WatchdogWarning {
228
+ function createTestWarning(severity: "concern" | "blocker", importance: "low" | "medium" | "high", text: string): WatchdogWarning {
234
229
  return {
235
230
  severity,
236
231
  category: "other",
237
- confidence: "high",
232
+ importance,
238
233
  source: "main",
239
234
  state: "displayed",
240
235
  summary: text,
@@ -258,7 +253,7 @@ async function handleWatchdogCommand(
258
253
  }
259
254
  if (input === "recommend-model") {
260
255
  try {
261
- sendSlashText(pi, buildRecommendationText(ctx));
256
+ sendSlashText(pi, buildRecommendationText(runtime, ctx));
262
257
  } catch (error) {
263
258
  sendSlashText(pi, `Subagent watchdog recommended model\n\n${messageFromError(error)}`);
264
259
  }
@@ -301,7 +296,7 @@ async function handleWatchdogCommand(
301
296
  if (input.startsWith("session model ")) {
302
297
  const rawModel = input.slice("session model ".length);
303
298
  try {
304
- const value = resolveModelCommandValue(ctx, rawModel);
299
+ const value = resolveModelCommandValue(runtime, ctx, rawModel);
305
300
  const snapshot = value.model === null
306
301
  ? runtime.clearSessionModel(ctx.cwd)
307
302
  : runtime.setSessionModel({ model: value.model, thinking: value.thinking ?? null }, ctx.cwd);
@@ -319,7 +314,7 @@ async function handleWatchdogCommand(
319
314
  if (input.startsWith("model ")) {
320
315
  const rawModel = input.slice("model ".length);
321
316
  try {
322
- const value = resolveModelCommandValue(ctx, rawModel);
317
+ const value = resolveModelCommandValue(runtime, ctx, rawModel);
323
318
  const settingsPath = writeWatchdogModelSettings({
324
319
  scope: "user",
325
320
  target: { kind: "main" },
@@ -329,7 +324,7 @@ async function handleWatchdogCommand(
329
324
  runtime.refreshConfig(ctx.cwd);
330
325
  const snapshot = runtime.getSnapshot(ctx.cwd);
331
326
  sendSlashText(pi, [
332
- `Subagent watchdog model saved: ${value.description}.`,
327
+ `Subagent watchdog model saved to user settings: ${value.description}. Project and session overrides still take precedence.`,
333
328
  `Updated: ${settingsPath}`,
334
329
  `Main now: ${boolLabel(snapshot.enabled)}`,
335
330
  value.model === null ? "The watchdog now inherits the current session model and thinking." : "Run /subagents-watchdog on if the watchdog is still off.",
@@ -365,15 +360,14 @@ async function handleWatchdogCommand(
365
360
  const test = parseTestCommand(input);
366
361
  if (test) {
367
362
  if (!test.text) {
368
- 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");
369
364
  return;
370
365
  }
371
- const warning = createTestWarning(test.severity, test.text);
372
- const details = runtime.recordDisplayedWarning(warning);
373
- pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }));
366
+ const warning = createTestWarning(test.severity, test.importance, test.text);
367
+ runtime.displayRecordedWarning(warning);
374
368
  return;
375
369
  }
376
- 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");
377
371
  }
378
372
 
379
373
  export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatchdogOptions = {}): MainWatchdogRuntime {
@@ -387,6 +381,8 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
387
381
  reviewDescription: options.review ? "injected seam" : "real model review",
388
382
  reviewChangesOnly: true,
389
383
  displayWarning: (details, options) => pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }), options),
384
+ displayUserWarning: (details) => pi.appendEntry(SUBAGENT_WATCHDOG_WARNING_TYPE, details),
385
+ displayClarification: (content) => pi.sendMessage({ customType: "subagent_watchdog_clarification", content, display: true }, { deliverAs: "steer", triggerTurn: true }),
390
386
  });
391
387
 
392
388
  pi.registerMessageRenderer<WatchdogWarningDetails>(SUBAGENT_WATCHDOG_WARNING_TYPE, (message, renderOptions, theme) => {
@@ -399,6 +395,12 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
399
395
  }
400
396
  return renderWatchdogWarning(details, renderOptions, theme);
401
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
+ });
402
404
 
403
405
  pi.registerCommand("subagents-watchdog", {
404
406
  description: "Show or toggle the default-off subagent watchdog",
@@ -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);
@@ -431,7 +435,7 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
431
435
  });
432
436
  pi.on("session_before_switch", () => runtime.reset("session switch", { clearReviewInputSignature: true, clearLspLedger: true, clearScope: true }));
433
437
  pi.on("session_before_fork", () => runtime.reset("session fork", { clearReviewInputSignature: true, clearLspLedger: true, clearScope: true }));
434
- pi.on("session_compact", () => runtime.reset("session compact", { clearScope: true }));
438
+ pi.on("session_compact", () => runtime.reset("session compact", { clearActivity: true }));
435
439
  pi.on("session_shutdown", () => {
436
440
  currentContext = undefined;
437
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,32 +3,35 @@ 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 { 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
+ import { opencodeSessionHeaders } from "../shared/opencode-session-headers.ts";
8
9
  import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from "../shared/model-info.ts";
9
10
  import { createWatchdogDiffTool, WATCHDOG_DIFF_TOOL_NAME, type WatchdogDiffBaseline } from "./diff-tool.ts";
10
11
  import { loadWatchdogGuidance } from "./guidance.ts";
11
- import type { WatchdogReviewFunction, WatchdogReviewRequest } from "./runtime.ts";
12
+ import { boundWatchdogReviewText, type WatchdogReviewFunction, type WatchdogReviewRequest } from "./runtime.ts";
12
13
  import {
13
14
  WATCHDOG_WARNING_CATEGORIES,
14
- WATCHDOG_WARNING_CONFIDENCES,
15
+ WATCHDOG_WARNING_IMPORTANCES,
15
16
  WATCHDOG_WARNING_SEVERITIES,
16
17
  type ResolvedWatchdogConfig,
17
18
  type WatchdogCategory,
18
- type WatchdogConfidence,
19
+ type WatchdogImportance,
19
20
  type WatchdogSeverity,
20
21
  type WatchdogWarning,
21
22
  } from "./types.ts";
22
23
 
23
24
  const WATCHDOG_ALLOWED_TOOL_NAMES = new Set(["read", "grep", "find", "ls", "watchdog_warn", WATCHDOG_DIFF_TOOL_NAME]);
24
25
 
26
+ const WatchdogAskParams = Type.Object({ question: Type.String(), evidence: Type.String() }, { additionalProperties: false });
27
+
25
28
  const WatchdogWarnParams = Type.Object({
26
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" }),
27
31
  summary: Type.String({ description: "One concise sentence naming the issue." }),
28
32
  evidence: Type.String({ description: "Specific evidence from the turn delta or inspected files." }),
29
33
  recommendedAction: Type.String({ description: "Specific action the parent should take before accepting or continuing." }),
30
34
  category: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CATEGORIES })),
31
- confidence: Type.Optional(Type.String({ enum: WATCHDOG_WARNING_CONFIDENCES })),
32
35
  }, { additionalProperties: false });
33
36
 
34
37
  type WatchdogWarnParams = Static<typeof WatchdogWarnParams>;
@@ -114,9 +117,16 @@ function resolveConfiguredModel(ctx: ExtensionContext, rawModel: string): { mode
114
117
  return { model, modelString: resolved };
115
118
  }
116
119
 
120
+ class WatchdogAuthError extends Error {}
121
+
117
122
  async function resolveReviewAuth(ctx: ExtensionContext, model: RegistryModel): Promise<WatchdogReviewAuth> {
118
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
119
- if (auth.ok === false) throw new Error(`Watchdog model auth failed for ${fullModelId(model)}: ${auth.error}`);
123
+ let auth;
124
+ try {
125
+ auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
126
+ } catch (error) {
127
+ throw new WatchdogAuthError(`Watchdog model auth failed for ${fullModelId(model)}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
128
+ }
129
+ if (auth.ok === false) throw new WatchdogAuthError(`Watchdog model auth failed for ${fullModelId(model)}: ${auth.error}`);
120
130
  return {
121
131
  ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
122
132
  ...(auth.headers ? { headers: auth.headers } : {}),
@@ -172,8 +182,8 @@ function nonEmptyString(value: string, field: string): string {
172
182
  function toWatchdogWarning(params: WatchdogWarnParams): WatchdogWarning {
173
183
  return {
174
184
  severity: params.severity as WatchdogSeverity,
185
+ importance: params.importance as WatchdogImportance,
175
186
  category: (params.category ?? "other") as WatchdogCategory,
176
- confidence: (params.confidence ?? "medium") as WatchdogConfidence,
177
187
  source: "main",
178
188
  summary: nonEmptyString(params.summary, "summary"),
179
189
  evidence: nonEmptyString(params.evidence, "evidence"),
@@ -187,7 +197,7 @@ function createWatchdogWarnTool(request: WatchdogReviewRequest): AgentTool<typeo
187
197
  label: "Watchdog warning",
188
198
  description: [
189
199
  "Emit one actionable main-session watchdog warning.",
190
- "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.",
191
201
  "Do not use for nits, praise, informational notes, or clean reviews.",
192
202
  ].join(" "),
193
203
  parameters: WatchdogWarnParams,
@@ -214,11 +224,11 @@ export function buildWatchdogSystemPrompt(ctx: Pick<ExtensionContext, "cwd">, op
214
224
  "You are the main-session subagent watchdog for Pi.",
215
225
  `Working directory: ${ctx.cwd}`,
216
226
  "Review only the supplied parent turn delta. Inspect repository files only when needed to verify a concrete concern.",
217
- options.hasScope ? "When the review input includes a Current scope block, treat newer scope prompts as superseding/mutating older prompts and use category='scope-drift' for work that serves no current scope item." : undefined,
227
+ options.hasScope ? "Use the Current scope record alongside supplied activity evidence; an unrelated user question does not cancel older authorized work." : undefined,
218
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.`,
219
229
  "Emit warnings only by calling watchdog_warn. Freeform assistant text is ignored and must not be used to report warnings.",
220
- "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.",
221
- "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.",
222
232
  "If the turn is clean, call no tools and end normally.",
223
233
  "Use severity='blocker' only when the issue should stop acceptance until addressed; otherwise use severity='concern'.",
224
234
  guidance ? `\nStanding instructions from WATCHDOG.md (project first, then user):\n${guidance}` : undefined,
@@ -236,18 +246,6 @@ function buildReviewPrompt(request: WatchdogReviewRequest, selection: WatchdogRe
236
246
  ].join("\n\n");
237
247
  }
238
248
 
239
- function finalStopReason(agent: Agent): "stop" | "error" | "aborted" | "length" {
240
- for (let index = agent.state.messages.length - 1; index >= 0; index--) {
241
- const message = agent.state.messages[index];
242
- if (message && typeof message === "object" && (message as { role?: unknown }).role === "assistant") {
243
- const stopReason = (message as { stopReason?: unknown }).stopReason;
244
- if (stopReason === "error" || stopReason === "aborted" || stopReason === "length") return stopReason;
245
- return "stop";
246
- }
247
- }
248
- return "stop";
249
- }
250
-
251
249
  function resolveContext(provider: WatchdogContextProvider): ExtensionContext | undefined {
252
250
  return typeof provider === "function" ? provider() : provider;
253
251
  }
@@ -256,59 +254,110 @@ export function createMainWatchdogReview(provider: WatchdogContextProvider, opti
256
254
  return async (request) => {
257
255
  const ctx = resolveContext(provider);
258
256
  if (!ctx) throw new Error("Main watchdog review cannot run without an active Pi extension context.");
259
- if (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: "aborted" };
260
- const selection = await resolveWatchdogReviewModel(ctx, request.config, {
261
- currentThinkingLevel: options.getThinkingLevel?.(),
262
- });
263
- if (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: "aborted" };
264
- const auth = selection.auth;
265
- const registeredProvider = (ctx.modelRegistry as {
266
- getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
267
- }).getRegisteredProviderConfig?.(selection.model.provider);
268
- const baseStreamFn = options.streamFn ?? (registeredProvider?.streamSimple && registeredProvider.api === selection.model.api
269
- ? registeredProvider.streamSimple
270
- : streamSimple);
271
- const streamFn: StreamFn = (model, context, streamOptions) => baseStreamFn(model, context, {
257
+ const aborted = () => ctx.signal?.aborted || request.signal?.aborted;
258
+ if (aborted()) return { stopReason: "aborted" };
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;
264
+ }
265
+ };
266
+ }
267
+
268
+ async function runWatchdogAttempt(ctx: ExtensionContext, request: WatchdogReviewRequest, options: CreateMainWatchdogReviewOptions): Promise<{
269
+ result: Awaited<ReturnType<WatchdogReviewFunction>>;
270
+ }> {
271
+ const selection = await resolveWatchdogReviewModel(ctx, request.config, {
272
+ currentThinkingLevel: options.getThinkingLevel?.(),
273
+ });
274
+ if (ctx.signal?.aborted || request.signal?.aborted) return { result: { stopReason: "aborted" } };
275
+ const auth = selection.auth;
276
+ const registeredProvider = (ctx.modelRegistry as {
277
+ getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
278
+ }).getRegisteredProviderConfig?.(selection.model.provider);
279
+ const baseStreamFn = options.streamFn ?? (registeredProvider?.streamSimple && registeredProvider.api === selection.model.api
280
+ ? registeredProvider.streamSimple
281
+ : streamSimple);
282
+ const sessionId = ctx.sessionManager.getSessionId();
283
+ const streamFn: StreamFn = (model, context, streamOptions) => {
284
+ // Agent may enter one final loop iteration after an aborted mixed tool batch.
285
+ // Never send that iteration to the provider after an intentional yield.
286
+ if (clarification) throw new Error("Watchdog review yielded for clarification.");
287
+ return baseStreamFn(model, context, {
272
288
  ...streamOptions,
273
289
  ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
274
290
  env: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,
275
- headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
276
- });
277
- const diffBaseline = options.diffBaseline?.();
278
- const tools = [
279
- ...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter((tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== "watchdog_warn"),
280
- createWatchdogWarnTool(request),
281
- ...(diffBaseline ? [createWatchdogDiffTool(diffBaseline)] : []),
282
- ];
283
- const agent = new Agent({
284
- initialState: {
285
- systemPrompt: buildWatchdogSystemPrompt(ctx, {
286
- hasScope: request.hasScope,
287
- guidance: loadWatchdogGuidance(ctx.cwd, request.config.guidance.watchdogMd),
288
- hasDiff: diffBaseline !== undefined,
289
- }),
290
- model: selection.model,
291
- thinkingLevel: selection.thinkingLevel,
292
- tools,
293
- },
294
- convertToLlm,
295
- ...agentStreamOptions(streamFn),
296
- getApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,
297
- beforeToolCall: async ({ toolCall }) => WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name)
298
- ? undefined
299
- : { block: true, reason: `Watchdog reviews are read-only; tool '${toolCall.name}' is not allowed.` },
300
- toolExecution: "sequential",
291
+ headers: { ...opencodeSessionHeaders(model, sessionId), ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
301
292
  });
302
- const abort = () => agent.abort();
303
- ctx.signal?.addEventListener("abort", abort, { once: true });
304
- request.signal?.addEventListener("abort", abort, { once: true });
305
- try {
306
- if (ctx.signal?.aborted || request.signal?.aborted) return { stopReason: "aborted" };
307
- await agent.prompt(buildReviewPrompt(request, selection));
308
- } finally {
309
- ctx.signal?.removeEventListener("abort", abort);
310
- request.signal?.removeEventListener("abort", abort);
311
- }
312
- return { stopReason: finalStopReason(agent) };
293
+ };
294
+ const diffBaseline = options.diffBaseline?.();
295
+ let clarification: { question: string; evidence: string } | undefined;
296
+ let warned = false;
297
+ let toolCount = 0;
298
+ const warnRequest = request.allowClarification ? { ...request, emitWarning: (warning: WatchdogWarning) => {
299
+ if (clarification) return false;
300
+ const accepted = request.emitWarning(warning);
301
+ warned ||= accepted;
302
+ return accepted;
303
+ } } : request;
304
+ const tools = [
305
+ ...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter((tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== "watchdog_warn"),
306
+ createWatchdogWarnTool(warnRequest),
307
+ ...(diffBaseline ? [createWatchdogDiffTool(diffBaseline)] : []),
308
+ ];
309
+ if (request.allowClarification) tools.push({
310
+ name: "watchdog_ask",
311
+ label: "Watchdog clarification",
312
+ description: "Send one focused question when missing task status, intent or other orchestrator context prevents a concrete review judgment. Yields and ends this review so the orchestrator can handle the message and continue; no answer or follow-up review is required. Never ask for approval or permission, or after recording a warning. Question and evidence are capped at 1000 and 2000 characters.",
313
+ parameters: WatchdogAskParams,
314
+ executionMode: "sequential",
315
+ async execute(_id, rawParams) {
316
+ const params = rawParams as Static<typeof WatchdogAskParams>;
317
+ if (warned || clarification || ctx.signal?.aborted || request.signal?.aborted) throw new Error("Clarification unavailable after warning, yield, or cancellation.");
318
+ if (!params.question.trim() || !params.evidence.trim()) throw new Error("A focused question and concrete evidence are required.");
319
+ clarification = { question: boundWatchdogReviewText(params.question.trim(), 1_000), evidence: boundWatchdogReviewText(params.evidence.trim(), 2_000) };
320
+ agent.abort(); // Intentional yield also stops mixed tool batches; terminate alone does not.
321
+ return { content: [{ type: "text", text: "Review yielded for clarification." }], details: {} };
322
+ },
323
+ });
324
+ const agent = new Agent({
325
+ initialState: {
326
+ systemPrompt: buildWatchdogSystemPrompt(ctx, {
327
+ hasScope: request.hasScope,
328
+ guidance: loadWatchdogGuidance(ctx.cwd, request.config.guidance.watchdogMd),
329
+ hasDiff: diffBaseline !== undefined,
330
+ }),
331
+ model: selection.model,
332
+ thinkingLevel: selection.thinkingLevel,
333
+ tools,
334
+ },
335
+ convertToLlm,
336
+ ...agentStreamOptions(streamFn),
337
+ getApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,
338
+ beforeToolCall: async ({ toolCall }) => !clarification && (WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name) || (request.allowClarification && toolCall.name === "watchdog_ask"))
339
+ ? undefined
340
+ : { block: true, reason: `Watchdog reviews are read-only; tool '${toolCall.name}' is not allowed.` },
341
+ toolExecution: "sequential",
342
+ });
343
+ // Include rejected/invalid calls as well as read-only work and findings.
344
+ agent.subscribe((event) => { if (event.type === "tool_execution_start") toolCount++; });
345
+ const abort = () => agent.abort();
346
+ ctx.signal?.addEventListener("abort", abort, { once: true });
347
+ request.signal?.addEventListener("abort", abort, { once: true });
348
+ try {
349
+ if (ctx.signal?.aborted || request.signal?.aborted) return { result: { stopReason: "aborted" } };
350
+ await agent.prompt(buildReviewPrompt(request, selection));
351
+ } finally {
352
+ ctx.signal?.removeEventListener("abort", abort);
353
+ request.signal?.removeEventListener("abort", abort);
354
+ }
355
+ if (ctx.signal?.aborted || request.signal?.aborted) return { result: { stopReason: "aborted" } };
356
+ const terminal = agent.state.messages.findLast((message) => message.role === "assistant");
357
+ const reason = terminal && "stopReason" in terminal ? terminal.stopReason : undefined;
358
+ const stopReason = reason === "error" || reason === "aborted" || reason === "length" ? reason : "stop";
359
+ const error = terminal && "errorMessage" in terminal && typeof terminal.errorMessage === "string" ? terminal.errorMessage : undefined;
360
+ return {
361
+ result: clarification ? { clarification } : error ? { stopReason, errorMessage: error } : { stopReason },
313
362
  };
314
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 {