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
@@ -12,8 +12,9 @@ import {
12
12
  import { ruleViolationWarning, type WatchdogRuleViolation } from "./rules.ts";
13
13
  import { WatchdogScopeArtifact } from "./scope.ts";
14
14
  import { resolveWatchdogConfig } from "./settings.ts";
15
- import { formatWatchdogTurnDelta } from "./turn-delta.ts";
15
+ import { formatWatchdogOrchestrationActivity, formatWatchdogTurnDelta } from "./turn-delta.ts";
16
16
  import {
17
+ WATCHDOG_WARNING_IMPORTANCES,
17
18
  type ResolvedWatchdogConfig,
18
19
  type WatchdogEndpointConfig,
19
20
  type WatchdogLspRuntimeSnapshot,
@@ -31,6 +32,9 @@ type ReviewStopReason = "stop" | "error" | "aborted" | "length";
31
32
  export interface WatchdogReviewResult {
32
33
  warnings?: WatchdogWarning[];
33
34
  stopReason?: ReviewStopReason;
35
+ /** Provider error text for a failed review, surfaced in status `Last error`. */
36
+ errorMessage?: string;
37
+ clarification?: { question: string; evidence: string };
34
38
  }
35
39
 
36
40
  export interface WatchdogReviewRequest {
@@ -40,6 +44,7 @@ export interface WatchdogReviewRequest {
40
44
  reviewId: number;
41
45
  config: ResolvedWatchdogConfig;
42
46
  emitWarning(warning: WatchdogWarning): boolean;
47
+ allowClarification?: boolean;
43
48
  signal?: AbortSignal;
44
49
  }
45
50
 
@@ -81,6 +86,9 @@ interface MainWatchdogRuntimeOptions {
81
86
  review?: WatchdogReviewFunction;
82
87
  reviewDescription?: string;
83
88
  displayWarning?: (warning: WatchdogWarningDetails, options?: WatchdogWarningSendOptions) => void;
89
+ displayUserWarning?: (warning: WatchdogWarningDetails) => void;
90
+ /** Supplying this main-session delivery capability gates clarification (children omit it). */
91
+ displayClarification?: (content: string) => void;
84
92
  reviewChangesOnly?: boolean;
85
93
  lspDiagnostics?: WatchdogLspDiagnosticsFunction;
86
94
  repoChangeSignature?: typeof computeWatchdogRepoChangeSignature;
@@ -88,8 +96,8 @@ interface MainWatchdogRuntimeOptions {
88
96
 
89
97
  export type WatchdogWarningSendOptions = { deliverAs: "steer" } | { triggerTurn: false };
90
98
 
91
- type ContextLike = Pick<ExtensionContext, "cwd">;
92
- type ReviewDeltaOutcome = "completed" | "timeout" | "stale";
99
+ type ContextLike = Pick<ExtensionContext, "cwd"> & { signal?: AbortSignal };
100
+ type ReviewDeltaOutcome = "completed" | "timeout" | "stale" | NonNullable<WatchdogReviewResult["clarification"]>;
93
101
 
94
102
  const DEFAULT_REVIEW: WatchdogReviewFunction = () => ({ warnings: [] });
95
103
  const MAX_REVIEW_INPUT_CHARS = 24_000;
@@ -126,6 +134,7 @@ export class MainWatchdogRuntime {
126
134
  private readonly reviewConnected: boolean;
127
135
  private readonly reviewDescription: string;
128
136
  private readonly displayWarning: ((warning: WatchdogWarningDetails, options?: WatchdogWarningSendOptions) => void) | undefined;
137
+ private readonly displayUserWarning: ((warning: WatchdogWarningDetails) => void) | undefined;
129
138
  private readonly reviewChangesOnly: boolean;
130
139
  private readonly lspDiagnostics: WatchdogLspDiagnosticsFunction;
131
140
  private readonly repoChangeSignature: typeof computeWatchdogRepoChangeSignature;
@@ -170,6 +179,11 @@ export class MainWatchdogRuntime {
170
179
  private activeReviewAbortController: AbortController | undefined;
171
180
  private failedReviews = 0;
172
181
  private staleReviews = 0;
182
+ private readonly displayClarification: MainWatchdogRuntimeOptions["displayClarification"];
183
+ private askedThisPrompt = false;
184
+ private activityTail = "";
185
+ private activityPending = false;
186
+ private activityReviewUsed = false;
173
187
 
174
188
  constructor(options: MainWatchdogRuntimeOptions = {}) {
175
189
  this.cwd = options.cwd ?? process.cwd();
@@ -178,6 +192,8 @@ export class MainWatchdogRuntime {
178
192
  this.reviewConnected = Boolean(options.review);
179
193
  this.reviewDescription = options.reviewDescription ?? (options.review ? "injected seam" : "not wired");
180
194
  this.displayWarning = options.displayWarning;
195
+ this.displayUserWarning = options.displayUserWarning;
196
+ this.displayClarification = options.displayClarification;
181
197
  this.reviewChangesOnly = options.reviewChangesOnly === true;
182
198
  this.lspDiagnostics = options.lspDiagnostics ?? collectWatchdogLspDiagnostics;
183
199
  this.repoChangeSignature = options.repoChangeSignature ?? computeWatchdogRepoChangeSignature;
@@ -200,6 +216,8 @@ export class MainWatchdogRuntime {
200
216
  refreshConfig(cwd = this.cwd): WatchdogSettingsResult {
201
217
  this.cwd = cwd;
202
218
  const wasEnabled = this.isEnabled();
219
+ const hadClarification = this.configResult.config.clarification;
220
+ const previousMain = this.configResult.config.main;
203
221
  const session = this.sessionOverrideEnabled === undefined && this.sessionModelOverride === undefined
204
222
  ? undefined
205
223
  : {
@@ -215,6 +233,10 @@ export class MainWatchdogRuntime {
215
233
  this.guard = new WatchdogEmissionGuard({ maxWarnings: this.guardMaxWarnings });
216
234
  }
217
235
  if (wasEnabled && !this.isEnabled()) this.invalidateActiveReview("watchdog disabled");
236
+ if (!this.isEnabled() || !this.configResult.config.clarification) this.clearActivity();
237
+ if (this.displayClarification && (hadClarification || this.configResult.config.clarification) && (this.reviewing || this.waitingAtAgentEnd)) {
238
+ if (!this.configResult.config.clarification || previousMain.model !== this.configResult.config.main.model || previousMain.thinking !== this.configResult.config.main.thinking) this.invalidateActiveReview("clarification configuration changed");
239
+ }
218
240
  return this.configResult;
219
241
  }
220
242
 
@@ -252,7 +274,8 @@ export class MainWatchdogRuntime {
252
274
  return this.getSnapshot();
253
275
  }
254
276
 
255
- reset(_reason = "reset", options: { clearReviewInputSignature?: boolean; resetChangeSignature?: boolean; clearLspLedger?: boolean; clearScope?: boolean } = {}): void {
277
+ reset(_reason = "reset", options: { clearReviewInputSignature?: boolean; resetChangeSignature?: boolean; clearLspLedger?: boolean; clearScope?: boolean; clearActivity?: boolean } = {}): void {
278
+ this.activeReviewAbortController?.abort();
256
279
  this.abortActiveAgentEnd();
257
280
  this.epoch++;
258
281
  this.status = "idle";
@@ -274,6 +297,7 @@ export class MainWatchdogRuntime {
274
297
  this.lastLspSnapshot = undefined;
275
298
  }
276
299
  if (options.clearScope) this.scope.reset();
300
+ if (options.clearScope || options.clearActivity) this.clearActivity();
277
301
  if (options.clearReviewInputSignature) this.lastReviewInputSignature = undefined;
278
302
  if (options.resetChangeSignature) this.resetRepoChangeBaseline({ reviewed: true });
279
303
  this.guard.reset();
@@ -281,6 +305,8 @@ export class MainWatchdogRuntime {
281
305
  }
282
306
 
283
307
  dispose(): void {
308
+ this.clearActivity();
309
+ this.activeReviewAbortController?.abort();
284
310
  this.disposed = true;
285
311
  this.abortActiveAgentEnd();
286
312
  this.epoch++;
@@ -305,6 +331,8 @@ export class MainWatchdogRuntime {
305
331
  if (this.disposed) return;
306
332
  const incomingPrompt = promptFromBeforeAgentStart(event);
307
333
  this.reset("before_agent_start");
334
+ this.askedThisPrompt = false;
335
+ this.activityReviewUsed = false;
308
336
  this.resetBoundaryRepeats();
309
337
  this.refreshConfig(ctx.cwd);
310
338
  this.userPrompt = incomingPrompt;
@@ -322,6 +350,13 @@ export class MainWatchdogRuntime {
322
350
  this.refreshConfig(ctx.cwd);
323
351
  if (!this.isEnabled()) return;
324
352
  try {
353
+ if (this.displayClarification && this.configResult.config.clarification && this.boundaryRepeats === 0) {
354
+ const activity = formatWatchdogOrchestrationActivity(event);
355
+ if (activity) {
356
+ this.activityTail = [this.activityTail, boundWatchdogReviewText(activity, 3_000)].filter(Boolean).join(REVIEW_DELTA_SEPARATOR).slice(-6_000);
357
+ this.activityPending = true;
358
+ }
359
+ }
325
360
  this.observedRepoEditThisTurn ||= eventIndicatesRepoEdit(event);
326
361
  const delta = formatWatchdogTurnDelta({
327
362
  includeUserPrompt: this.includeUserPromptInNextDelta,
@@ -359,20 +394,19 @@ export class MainWatchdogRuntime {
359
394
  if (this.disposed) return;
360
395
  this.refreshConfig(ctx.cwd);
361
396
  if (!this.isEnabled()) return;
362
- const changeSignature = this.resolveReviewChangeSignature(ctx.cwd);
363
- if (this.reviewChangesOnly && !changeSignature) {
364
- this.clearPendingDeltas();
365
- if (this.status === "queued") this.status = "idle";
366
- this.resolveWaiters(true);
367
- return;
368
- }
369
- if (changeSignature && changeSignature.key === this.lastReviewedChangeSignature) {
397
+ if (ctx.signal?.aborted) return;
398
+ const changeSignature = this.resolveReviewChangeSignature(this.currentRepoChangeSignature(ctx.cwd), ctx.cwd);
399
+ const activityReview = Boolean(this.displayClarification && this.configResult.config.clarification && !this.activityReviewUsed && this.boundaryRepeats === 0 && this.activityPending);
400
+ const knownEvidence = changeSignature && changeSignature.key === this.lastReviewedChangeSignature;
401
+ if (!activityReview && (knownEvidence || (this.reviewChangesOnly && !changeSignature))) {
370
402
  this.clearPendingDeltas();
371
- this.status = "idle";
403
+ if (knownEvidence || this.status === "queued") this.status = "idle";
372
404
  this.resolveWaiters(true);
373
405
  return;
374
406
  }
375
407
  this.cancelMidRunReview();
408
+ if (activityReview && (!changeSignature || knownEvidence)) this.activityReviewUsed = true;
409
+ this.activityPending = false;
376
410
  this.waitingAtAgentEnd = true;
377
411
  const agentEndEpoch = this.epoch;
378
412
  const agentEndId = ++this.agentEndIdCounter;
@@ -403,7 +437,8 @@ export class MainWatchdogRuntime {
403
437
  this.resolveWaiters(true);
404
438
  return;
405
439
  }
406
- const outcome = await this.reviewDelta(delta, this.configResult.config.agentEndTimeoutMs);
440
+ const outcome = await this.reviewDelta(delta, this.configResult.config.agentEndTimeoutMs, { allowClarification: true });
441
+ if (!this.isAgentEndCurrent(agentEndEpoch, agentEndId)) return;
407
442
  this.waitingAtAgentEnd = false;
408
443
  if (outcome === "timeout") {
409
444
  this.staleReviews++;
@@ -413,11 +448,16 @@ export class MainWatchdogRuntime {
413
448
  this.resolveWaiters(true);
414
449
  return;
415
450
  }
416
- if (outcome === "completed" && this.status !== "failed" && this.status !== "stale") {
451
+ if ((outcome === "completed" || typeof outcome === "object") && this.status !== "failed" && this.status !== "stale") {
452
+ // A question consumes this evidence just like a completed review, not a pending exchange.
417
453
  this.lastReviewInputSignature = signature;
418
454
  if (changeSignature) this.lastReviewedChangeSignature = changeSignature.key;
419
455
  this.currentChangedPaths = changeSignature?.changedPaths;
420
456
  this.status = "idle";
457
+ if (typeof outcome === "object" && !ctx.signal?.aborted) this.displayClarification?.([
458
+ "Main watchdog clarification:", outcome.question, `Evidence: ${outcome.evidence}`,
459
+ "Consider this missing context as you continue the task. This is not approval, permission, or a warning. The reviewer has yielded; no reply or follow-up review is required.",
460
+ ].join("\n"));
421
461
  }
422
462
  this.resolveWaiters(true);
423
463
  } finally {
@@ -431,13 +471,13 @@ export class MainWatchdogRuntime {
431
471
  this.ruleWarningsThisRun.add(violation.summary);
432
472
  const details = normalizeWatchdogWarningDetails(ruleViolationWarning(violation), { state: "displayed", displayedAt: new Date().toISOString() });
433
473
  this.lastWarning = details;
434
- this.displayWarning?.(details, { deliverAs: "steer" });
474
+ this.routeWarning(details, { deliverAs: "steer" });
435
475
  }
436
476
 
437
- recordDisplayedWarning(warning: WatchdogWarning): WatchdogWarningDetails {
477
+ displayRecordedWarning(warning: WatchdogWarning): void {
438
478
  const details = normalizeWatchdogWarningDetails(warning, { state: "displayed", source: warning.source ?? "main" });
439
479
  this.lastWarning = details;
440
- return details;
480
+ this.routeWarning(details);
441
481
  }
442
482
 
443
483
  getSnapshot(cwd?: string): WatchdogRuntimeSnapshot {
@@ -468,6 +508,15 @@ export class MainWatchdogRuntime {
468
508
  };
469
509
  }
470
510
 
511
+ /** A real mid-stream user input can steer without emitting before_agent_start. */
512
+ handleUserInput(): void {
513
+ if (this.displayClarification && this.configResult.config.clarification && (this.reviewing || this.waitingAtAgentEnd)) this.reset("new user input");
514
+ }
515
+
516
+ handleModelChange(): void {
517
+ if (this.displayClarification && this.configResult.config.clarification && (this.reviewing || this.waitingAtAgentEnd)) this.reset("model changed");
518
+ }
519
+
471
520
  async waitForIdle(timeoutMs = 1_000): Promise<boolean> {
472
521
  return this.waitForSettled(timeoutMs);
473
522
  }
@@ -491,7 +540,13 @@ export class MainWatchdogRuntime {
491
540
  }
492
541
 
493
542
  private warningMeetsThreshold(warning: WatchdogWarning): boolean {
494
- return this.configResult.config.severityThreshold === "concern" || warning.severity === "blocker";
543
+ return (WATCHDOG_WARNING_IMPORTANCES as readonly string[]).includes(warning.importance)
544
+ && (this.configResult.config.severityThreshold === "concern" || warning.severity === "blocker");
545
+ }
546
+
547
+ private routeWarning(details: WatchdogWarningDetails, options?: WatchdogWarningSendOptions): void {
548
+ if (details.importance === "high") this.displayWarning?.(details, options);
549
+ else this.displayUserWarning?.(details);
495
550
  }
496
551
 
497
552
  private acceptWarning(epoch: number, reviewId: number, warning: WatchdogWarning): boolean {
@@ -523,6 +578,7 @@ export class MainWatchdogRuntime {
523
578
  }
524
579
 
525
580
  private invalidateActiveReview(_reason: string): void {
581
+ this.activeReviewAbortController?.abort();
526
582
  this.abortActiveAgentEnd();
527
583
  this.epoch++;
528
584
  this.status = "idle";
@@ -567,7 +623,7 @@ export class MainWatchdogRuntime {
567
623
  this.activeReviewWarning = undefined;
568
624
  }
569
625
 
570
- private async reviewDelta(delta: string, timeoutMs: number, options: { correction?: boolean } = {}): Promise<ReviewDeltaOutcome> {
626
+ private async reviewDelta(delta: string, timeoutMs: number, options: { correction?: boolean; allowClarification?: boolean } = {}): Promise<ReviewDeltaOutcome> {
571
627
  if (this.reviewing || this.disposed) return "stale";
572
628
  this.reviewing = true;
573
629
  const reviewEpoch = this.epoch;
@@ -578,6 +634,7 @@ export class MainWatchdogRuntime {
578
634
  let timeout: ReturnType<typeof setTimeout> | undefined;
579
635
  const abortController = new AbortController();
580
636
  this.activeReviewAbortController = abortController;
637
+ const allowClarification = Boolean(options.allowClarification && this.displayClarification && this.configResult.config.clarification && !this.askedThisPrompt && !this.stalemate);
581
638
  const reviewPromise = Promise.resolve().then(() => this.review({
582
639
  delta,
583
640
  epoch: reviewEpoch,
@@ -586,6 +643,7 @@ export class MainWatchdogRuntime {
586
643
  hasScope: this.scopeBlock().trim().length > 0,
587
644
  signal: abortController.signal,
588
645
  emitWarning: (warning) => this.acceptWarning(reviewEpoch, reviewId, warning),
646
+ ...(allowClarification ? { allowClarification: true } : {}),
589
647
  }));
590
648
  try {
591
649
  const result = await Promise.race([
@@ -600,9 +658,18 @@ export class MainWatchdogRuntime {
600
658
  }
601
659
  if (!this.isCurrent(reviewEpoch, reviewId)) return "stale";
602
660
  if (!result) return "stale";
661
+ if (allowClarification && this.configResult.config.clarification && result.clarification && (!result.stopReason || result.stopReason === "stop") && !this.activeReviewWarning && !result.warnings?.length && !abortController.signal.aborted) {
662
+ const question = result.clarification.question.trim();
663
+ const evidence = result.clarification.evidence.trim();
664
+ if (question && evidence) {
665
+ this.askedThisPrompt = true;
666
+ return { question: boundWatchdogReviewText(question, 1_000), evidence: boundWatchdogReviewText(evidence, 2_000) };
667
+ }
668
+ }
603
669
  for (const warning of result.warnings ?? []) this.acceptWarning(reviewEpoch, reviewId, warning);
604
670
  if (result.stopReason && result.stopReason !== "stop") {
605
- this.fail(`Watchdog review ended with stop reason '${result.stopReason}'.`);
671
+ const detail = result.errorMessage?.trim() ? ` ${boundWatchdogReviewText(result.errorMessage.trim(), 600)}` : "";
672
+ this.fail(`Watchdog review ended with stop reason '${result.stopReason}'.${detail}`);
606
673
  return "completed";
607
674
  }
608
675
  this.displayAcceptedReviewWarning(options.correction);
@@ -634,7 +701,7 @@ export class MainWatchdogRuntime {
634
701
  };
635
702
  if (correction) {
636
703
  this.lastWarning = details;
637
- this.displayWarning?.(details, { deliverAs: "steer" });
704
+ this.routeWarning(details, { deliverAs: "steer" });
638
705
  return;
639
706
  }
640
707
  this.deliverBoundaryWarning(details);
@@ -655,7 +722,7 @@ export class MainWatchdogRuntime {
655
722
  : details;
656
723
  this.stalemate = stalemate;
657
724
  this.lastWarning = delivered;
658
- this.displayWarning?.(delivered, stalemate ? { triggerTurn: false } : undefined);
725
+ this.routeWarning(delivered, stalemate ? { triggerTurn: false } : undefined);
659
726
  }
660
727
 
661
728
  // The previous boundary finding is a repeat to count, not a duplicate, until stalemate.
@@ -681,9 +748,8 @@ export class MainWatchdogRuntime {
681
748
  this.observedRepoEditThisTurn = false;
682
749
  }
683
750
 
684
- private resolveReviewChangeSignature(cwd = this.cwd): WatchdogRepoChangeSignature | undefined {
751
+ private resolveReviewChangeSignature(current: WatchdogRepoChangeSignature | undefined, cwd: string): WatchdogRepoChangeSignature | undefined {
685
752
  if (!this.reviewChangesOnly) return undefined;
686
- const current = this.currentRepoChangeSignature(cwd);
687
753
  if (current) {
688
754
  this.currentChangedPaths = current.changedPaths;
689
755
  if (current.key === this.turnStartChangeSignature?.key) return undefined;
@@ -785,13 +851,14 @@ export class MainWatchdogRuntime {
785
851
  const changes = changeSignature?.changedPaths.length
786
852
  ? ["Changed repo paths:", ...changeSignature.changedPaths.slice(0, 200).map((file) => `- ${file}`)].join("\n")
787
853
  : "";
788
- const contextPieces = [scopeBlock, changes, lspBlock].filter(Boolean);
854
+ const activity = this.activityTail ? `Recent delivered orchestration activity (oldest first; bounded observations, not a task board; waits/holds are not evidence of neglect):\n${this.activityTail}` : "";
855
+ const contextPieces = [scopeBlock, changes, lspBlock, activity].filter(Boolean);
789
856
  if (!contextPieces.length) return boundWatchdogReviewText(input);
790
857
 
791
858
  const maxContextLength = Math.floor(MAX_REVIEW_INPUT_CHARS / 2);
792
859
  const maxPieceLength = Math.max(1_000, Math.floor(maxContextLength / contextPieces.length));
793
860
  const boundedContext = contextPieces.map((piece) => piece.length > maxPieceLength
794
- ? `${piece.slice(0, maxPieceLength - 6)}\n- ...`
861
+ ? piece === activity ? boundWatchdogReviewText(piece, maxPieceLength) : `${piece.slice(0, maxPieceLength - 6)}\n- ...`
795
862
  : piece).join(REVIEW_DELTA_SEPARATOR);
796
863
  const separatorLength = input ? REVIEW_DELTA_SEPARATOR.length : 0;
797
864
  const inputBudget = MAX_REVIEW_INPUT_CHARS - boundedContext.length - separatorLength;
@@ -808,6 +875,12 @@ export class MainWatchdogRuntime {
808
875
  this.pendingDeltaChars = 0;
809
876
  }
810
877
 
878
+ private clearActivity(): void {
879
+ if (!this.activityTail) return;
880
+ this.activityTail = "";
881
+ this.activityPending = false;
882
+ }
883
+
811
884
  private fail(message: string): void {
812
885
  this.failedReviews++;
813
886
  this.lastError = message;
@@ -32,7 +32,7 @@ export class WatchdogScopeArtifact {
32
32
  if (!this.entries.length) return "";
33
33
  return [
34
34
  "Current scope:",
35
- "The following real user prompts are the current scope record, newest last. Newer prompts supersede and mutate older prompts: they may add, modify, or remove requirements. Flag work that serves no current scope item as category 'scope-drift'.",
35
+ "The following real user prompts are the current scope record, newest last. Side questions are additive, not scope drift or cancellation of older objectives; only explicit changes supersede requirements. Use watchdog_warn for evidence-backed reminders of forgotten authorized work, not dependencies still pending or explicit holds. The orchestrator owns task tracking. Flag unauthorized work as category 'scope-drift'.",
36
36
  ...this.entries.map((entry, index) => [
37
37
  `Scope prompt ${index + 1} (${entry.createdAt}):`,
38
38
  entry.prompt,
@@ -61,6 +61,7 @@ interface ParseMeta {
61
61
 
62
62
  export const DEFAULT_WATCHDOG_CONFIG: ResolvedWatchdogConfig = {
63
63
  enabled: false,
64
+ clarification: false,
64
65
  agentEndTimeoutMs: 30_000,
65
66
  severityThreshold: "concern",
66
67
  maxWarnings: null,
@@ -92,6 +93,7 @@ export const DEFAULT_WATCHDOG_CONFIG: ResolvedWatchdogConfig = {
92
93
 
93
94
  const WATCHDOG_FIELDS = new Set([
94
95
  "enabled",
96
+ "clarification",
95
97
  "agentEndTimeoutMs",
96
98
  "severityThreshold",
97
99
  "maxWarnings",
@@ -303,6 +305,7 @@ function parseWatchdogPatch(value: unknown, field: string, meta: ParseMeta): Wat
303
305
  assertKnownFields(input, WATCHDOG_FIELDS, field, meta);
304
306
  const patch: WatchdogConfigPatch = {};
305
307
  if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
308
+ if ("clarification" in input) patch.clarification = parseBoolean(input.clarification, `${field}.clarification`, meta);
306
309
  if ("agentEndTimeoutMs" in input) {
307
310
  patch.agentEndTimeoutMs = parseInteger(input.agentEndTimeoutMs, `${field}.agentEndTimeoutMs`, meta, "a positive integer", (candidate) => candidate >= 1);
308
311
  }
@@ -4,8 +4,8 @@ import { THINKING_LEVELS, type ThinkingLevel } from "../shared/model-info.ts";
4
4
  import type { Details } from "../shared/types.ts";
5
5
  import { buildWatchdogStatus } from "./register-main.ts";
6
6
  import type { MainWatchdogRuntime } from "./runtime.ts";
7
- import { parseWatchdogThinkingInput, recommendStrongWatchdogModel, resolveWatchdogModelInput } from "./model-selection.ts";
8
- import { writeWatchdogModelSettings, type WatchdogModelSettingsTarget, type WatchdogSettingsWriteScope } from "./settings.ts";
7
+ import { formatWatchdogRecommendation, parseWatchdogThinkingInput, recommendStrongWatchdogModel, recommendWatchdogModel, resolveWatchdogModelInput } from "./model-selection.ts";
8
+ import { resolveWatchdogConfig, writeWatchdogModelSettings, type WatchdogModelSettingsTarget, type WatchdogSettingsWriteScope } from "./settings.ts";
9
9
 
10
10
  interface WatchdogToolParams {
11
11
  action?: string;
@@ -52,7 +52,7 @@ function parseThinking(raw: string | false | undefined): ThinkingLevel | false |
52
52
  return parseWatchdogThinkingInput(raw, "watchdog.configure thinking") ?? undefined;
53
53
  }
54
54
 
55
- function resolveConfiguredValue(ctx: ExtensionContext, params: WatchdogToolParams): { model?: string | null; thinking?: ThinkingLevel | false | null; description: string } {
55
+ function resolveConfiguredValue(ctx: ExtensionContext, params: WatchdogToolParams, runtime?: MainWatchdogRuntime): { model?: string | null; thinking?: ThinkingLevel | false | null; description: string } {
56
56
  const thinking = parseThinking(params.thinking);
57
57
  const rawModel = params.model?.trim();
58
58
  if (!rawModel) {
@@ -61,7 +61,9 @@ function resolveConfiguredValue(ctx: ExtensionContext, params: WatchdogToolParam
61
61
  }
62
62
  if (rawModel === "inherit") return { model: null, thinking: thinking ?? null, description: "inherit" };
63
63
  if (rawModel === "recommended") {
64
- const recommendation = recommendStrongWatchdogModel(ctx);
64
+ const recommendation = (params.target ?? "main") === "main"
65
+ ? recommendWatchdogModel(ctx, (runtime?.getSnapshot(ctx.cwd).config ?? resolveWatchdogConfig(ctx.cwd).config).main)
66
+ : recommendStrongWatchdogModel(ctx);
65
67
  return {
66
68
  model: recommendation.model,
67
69
  thinking: recommendation.thinking,
@@ -76,11 +78,11 @@ function resolveConfiguredValue(ctx: ExtensionContext, params: WatchdogToolParam
76
78
  };
77
79
  }
78
80
 
79
- function buildRecommendationText(ctx: ExtensionContext): string {
80
- const recommendation = recommendStrongWatchdogModel(ctx);
81
+ function buildRecommendationText(ctx: ExtensionContext, runtime?: MainWatchdogRuntime): string {
82
+ const recommendation = recommendWatchdogModel(ctx, (runtime?.getSnapshot(ctx.cwd).config ?? resolveWatchdogConfig(ctx.cwd).config).main);
81
83
  return [
82
84
  "Subagent watchdog recommended model",
83
- `Recommended: ${recommendation.model}:${recommendation.thinking}`,
85
+ recommendation.source === "configured" ? formatWatchdogRecommendation(recommendation) : `Recommended: ${recommendation.model}:${recommendation.thinking}`,
84
86
  `Reason: ${recommendation.reason}`,
85
87
  "Apply temporarily with subagent({ action: \"watchdog.configure\", scope: \"session\", model: \"recommended\" }).",
86
88
  "Persist with scope: \"project\" or scope: \"user\" only when the user asks for that scope.",
@@ -100,10 +102,9 @@ function buildCheckText(runtime: MainWatchdogRuntime | undefined, ctx: Extension
100
102
  }
101
103
  lines.push(`LSP diagnostics: ${snapshot.lsp.enabled ? "on" : "off"} · ${snapshot.lsp.status}`);
102
104
  try {
103
- const recommendation = recommendStrongWatchdogModel(ctx);
104
- lines.push(`Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking}`);
105
+ lines.push(formatWatchdogRecommendation(recommendWatchdogModel(ctx, snapshot.config.main)));
105
106
  } catch (error) {
106
- lines.push(`Recommended strong watchdog unavailable: ${messageFromError(error)}`);
107
+ lines.push(`Watchdog recommendation unavailable: ${messageFromError(error)}`);
107
108
  }
108
109
  return lines.join("\n");
109
110
  }
@@ -114,13 +115,13 @@ export function handleWatchdogToolAction(action: string, params: WatchdogToolPar
114
115
  if (!runtime) return result("Subagent watchdog runtime is unavailable.", true);
115
116
  return result(buildWatchdogStatus(runtime.getSnapshot(ctx.cwd), ctx));
116
117
  }
117
- if (action === "watchdog.recommend-model") return result(buildRecommendationText(ctx));
118
+ if (action === "watchdog.recommend-model") return result(buildRecommendationText(ctx, runtime));
118
119
  if (action === "watchdog.check") return result(buildCheckText(runtime, ctx));
119
120
  if (action !== "watchdog.configure") return result(`Unknown watchdog action: ${action}`, true);
120
121
 
121
122
  const scope = parseScope(params.scope);
122
123
  const target = parseTarget(params);
123
- const value = resolveConfiguredValue(ctx, params);
124
+ const value = resolveConfiguredValue(ctx, params, runtime);
124
125
  if (scope === "session") {
125
126
  if (!runtime) return result("Subagent watchdog runtime is unavailable.", true);
126
127
  if (target.kind !== "main") return result("Session-scoped watchdog.configure currently supports target='main' only.", true);
@@ -159,3 +159,26 @@ export function formatWatchdogTurnDelta(input: WatchdogTurnDeltaInput): string {
159
159
  if (input.finalAssistantStop) sections.push("Final assistant stop: stop without tool call");
160
160
  return sections.join("\n\n---\n\n");
161
161
  }
162
+
163
+ /** Only completed, exactly paired native orchestration calls are activity evidence. */
164
+ export function formatWatchdogOrchestrationActivity(event: unknown): string {
165
+ const turn = event as { type?: string; message?: { content?: unknown }; toolResults?: unknown[] } | undefined;
166
+ if (turn?.type !== "turn_end" || !Array.isArray(turn.message?.content) || !Array.isArray(turn.toolResults)) return "";
167
+ const sections: string[] = [];
168
+ for (const call of turn.message.content) {
169
+ if (call?.type !== "toolCall" || typeof call.id !== "string") continue;
170
+ const args = call.arguments;
171
+ if (!args || typeof args !== "object") continue;
172
+ const eligible = call.name === "bg_wait"
173
+ || (call.name === "subagent_supervisor" && ["pending", "list", "reply"].includes(args.action))
174
+ || (call.name === "subagent" && (["status", "resume", "interrupt", "steer", "stop"].includes(args.action)
175
+ || (args.action === undefined && (typeof args.agent === "string" || typeof args.workflowScript === "string" || typeof args.workflowScriptPath === "string"))));
176
+ if (!eligible) continue;
177
+ const result = turn.toolResults.find((value) => {
178
+ const result = value as { role?: string; toolCallId?: string; toolName?: string };
179
+ return result?.role === "toolResult" && result.toolCallId === call.id && result.toolName === call.name;
180
+ });
181
+ if (result) sections.push(formatToolCall(call.name, args), formatWatchdogReviewMessage(result) ?? "");
182
+ }
183
+ return sections.join("\n\n");
184
+ }
@@ -15,8 +15,8 @@ export const WATCHDOG_WARNING_CATEGORIES = [
15
15
  ] as const;
16
16
  export type WatchdogCategory = typeof WATCHDOG_WARNING_CATEGORIES[number];
17
17
 
18
- export const WATCHDOG_WARNING_CONFIDENCES = ["medium", "high"] as const;
19
- export type WatchdogConfidence = typeof WATCHDOG_WARNING_CONFIDENCES[number];
18
+ export const WATCHDOG_WARNING_IMPORTANCES = ["low", "medium", "high"] as const;
19
+ export type WatchdogImportance = typeof WATCHDOG_WARNING_IMPORTANCES[number];
20
20
 
21
21
  export const WATCHDOG_WARNING_SOURCES = ["main", "child", "lsp"] as const;
22
22
  export type WatchdogWarningSource = typeof WATCHDOG_WARNING_SOURCES[number];
@@ -44,11 +44,11 @@ export type WatchdogWarningState = typeof WATCHDOG_WARNING_STATES[number];
44
44
 
45
45
  export interface WatchdogWarning {
46
46
  severity: WatchdogSeverity;
47
+ importance: WatchdogImportance;
47
48
  summary: string;
48
49
  evidence: string;
49
50
  recommendedAction: string;
50
51
  category?: WatchdogCategory;
51
- confidence?: WatchdogConfidence;
52
52
  source?: WatchdogWarningSource;
53
53
  agent?: string;
54
54
  runId?: string;
@@ -150,6 +150,8 @@ export interface WatchdogRulesConfig {
150
150
 
151
151
  export interface ResolvedWatchdogConfig {
152
152
  enabled: boolean;
153
+ /** Main-only, bounded boundary clarification; never enabled for child reviews. */
154
+ clarification: boolean;
153
155
  agentEndTimeoutMs: number;
154
156
  severityThreshold: WatchdogSeverity;
155
157
  maxWarnings: number | null;
@@ -34,12 +34,12 @@ export function formatWatchdogWarningContent(warning: WatchdogWarning): string {
34
34
  const details = normalizeWatchdogWarningDetails(warning);
35
35
  const attrs = [
36
36
  `severity="${escapeXmlAttribute(details.severity)}"`,
37
+ `importance="${escapeXmlAttribute(details.importance)}"`,
37
38
  `category="${escapeXmlAttribute(details.category)}"`,
38
39
  `source="${escapeXmlAttribute(details.source)}"`,
39
40
  `guidance="weigh, don't blindly obey"`,
40
41
  ];
41
42
  const optionalTags = [
42
- tag("confidence", details.confidence),
43
43
  tag("agent", details.agent),
44
44
  tag("run_id", details.runId),
45
45
  tag("state", details.state),