pi-subagents 0.63.0 → 0.64.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.
@@ -9,7 +9,8 @@ import {
9
9
  watchdogWarningFromLspDiagnostics,
10
10
  type WatchdogLspDiagnosticsFunction,
11
11
  } from "./lsp-diagnostics.ts";
12
- import { WatchdogScopeArtifact, isWatchdogAutoFollowPromptEvent } from "./scope.ts";
12
+ import { ruleViolationWarning, type WatchdogRuleViolation } from "./rules.ts";
13
+ import { WatchdogScopeArtifact } from "./scope.ts";
13
14
  import { resolveWatchdogConfig } from "./settings.ts";
14
15
  import { formatWatchdogTurnDelta } from "./turn-delta.ts";
15
16
  import {
@@ -62,9 +63,8 @@ export interface WatchdogRuntimeSnapshot {
62
63
  staleReviews: number;
63
64
  reviewConnected: boolean;
64
65
  reviewDescription: string;
65
- autoFollowQueued: boolean;
66
- autoFollowAttempts: number;
67
- autoFollowStalemate: boolean;
66
+ boundaryRepeats: number;
67
+ stalemate: boolean;
68
68
  reviewTrigger: "turn-delta" | "repo-edits";
69
69
  changedPaths?: string[];
70
70
  lsp: WatchdogLspRuntimeSnapshot;
@@ -80,20 +80,29 @@ interface MainWatchdogRuntimeOptions {
80
80
  resolveConfig?: (cwd: string, options?: { session?: Record<string, unknown> }) => WatchdogSettingsResult;
81
81
  review?: WatchdogReviewFunction;
82
82
  reviewDescription?: string;
83
- displayWarning?: (warning: WatchdogWarningDetails, options?: { deliverAs?: "steer" }) => void;
84
- sendUserMessage?: (message: string) => void | Promise<void>;
83
+ displayWarning?: (warning: WatchdogWarningDetails, options?: WatchdogWarningSendOptions) => void;
85
84
  reviewChangesOnly?: boolean;
86
85
  lspDiagnostics?: WatchdogLspDiagnosticsFunction;
87
86
  repoChangeSignature?: typeof computeWatchdogRepoChangeSignature;
88
87
  }
89
88
 
89
+ export type WatchdogWarningSendOptions = { deliverAs: "steer" } | { triggerTurn: false };
90
+
90
91
  type ContextLike = Pick<ExtensionContext, "cwd">;
91
92
  type ReviewDeltaOutcome = "completed" | "timeout" | "stale";
92
93
 
93
94
  const DEFAULT_REVIEW: WatchdogReviewFunction = () => ({ warnings: [] });
94
95
  const MAX_REVIEW_INPUT_CHARS = 24_000;
96
+ const REVIEW_INPUT_HEAD_CHARS = 6_000;
95
97
  const REVIEW_DELTA_SEPARATOR = "\n\n---\n\n";
96
98
 
99
+ export function boundWatchdogReviewText(text: string, cap = MAX_REVIEW_INPUT_CHARS): string {
100
+ if (text.length <= cap) return text;
101
+ const head = Math.min(REVIEW_INPUT_HEAD_CHARS, Math.floor(cap / 4));
102
+ const marker = `\n\n[... about ${text.length - cap} characters omitted ...]\n\n`;
103
+ return `${text.slice(0, head)}${marker}${text.slice(text.length - (cap - head - marker.length))}`;
104
+ }
105
+
97
106
  function errorMessage(error: unknown): string {
98
107
  return error instanceof Error ? error.message : String(error);
99
108
  }
@@ -116,8 +125,7 @@ export class MainWatchdogRuntime {
116
125
  private readonly review: WatchdogReviewFunction;
117
126
  private readonly reviewConnected: boolean;
118
127
  private readonly reviewDescription: string;
119
- private readonly displayWarning: ((warning: WatchdogWarningDetails, options?: { deliverAs?: "steer" }) => void) | undefined;
120
- private readonly sendUserMessage: ((message: string) => void | Promise<void>) | undefined;
128
+ private readonly displayWarning: ((warning: WatchdogWarningDetails, options?: WatchdogWarningSendOptions) => void) | undefined;
121
129
  private readonly reviewChangesOnly: boolean;
122
130
  private readonly lspDiagnostics: WatchdogLspDiagnosticsFunction;
123
131
  private readonly repoChangeSignature: typeof computeWatchdogRepoChangeSignature;
@@ -145,7 +153,6 @@ export class MainWatchdogRuntime {
145
153
  private userPrompt: string | undefined;
146
154
  private waiters: Waiter[] = [];
147
155
  private lastWarning: WatchdogWarningDetails | undefined;
148
- private displayedWarningSequence = 0;
149
156
  private lastError: string | undefined;
150
157
  private lastReviewInputSignature: string | undefined;
151
158
  private turnStartChangeSignature: WatchdogRepoChangeSignature | undefined;
@@ -155,12 +162,10 @@ export class MainWatchdogRuntime {
155
162
  private observedRepoEditThisTurn = false;
156
163
  private toolResultsThisRun = 0;
157
164
  private midRunReviewing = false;
158
- private autoFollowQueued = false;
159
- private autoFollowAttempts = 0;
160
- private consecutiveAutoFollowIdentity: string | undefined;
161
- private consecutiveAutoFollowRepeats = 0;
162
- private autoFollowStalemate = false;
163
- private pendingAutoFollowPrompts: string[] = [];
165
+ private lastBoundaryIdentity: string | undefined;
166
+ private boundaryRepeats = 0;
167
+ private stalemate = false;
168
+ private ruleWarningsThisRun = new Set<string>();
164
169
  private midRunGeneration = 0;
165
170
  private activeReviewAbortController: AbortController | undefined;
166
171
  private failedReviews = 0;
@@ -173,7 +178,6 @@ export class MainWatchdogRuntime {
173
178
  this.reviewConnected = Boolean(options.review);
174
179
  this.reviewDescription = options.reviewDescription ?? (options.review ? "injected seam" : "not wired");
175
180
  this.displayWarning = options.displayWarning;
176
- this.sendUserMessage = options.sendUserMessage;
177
181
  this.reviewChangesOnly = options.reviewChangesOnly === true;
178
182
  this.lspDiagnostics = options.lspDiagnostics ?? collectWatchdogLspDiagnostics;
179
183
  this.repoChangeSignature = options.repoChangeSignature ?? computeWatchdogRepoChangeSignature;
@@ -189,7 +193,8 @@ export class MainWatchdogRuntime {
189
193
  this.sessionOverrideEnabled = undefined;
190
194
  this.sessionModelOverride = undefined;
191
195
  this.refreshConfig(ctx.cwd);
192
- this.reset("session_start", { clearReviewInputSignature: true, resetChangeSignature: true, clearLspLedger: true, clearScope: true, resetAutoFollow: true });
196
+ this.reset("session_start", { clearReviewInputSignature: true, resetChangeSignature: true, clearLspLedger: true, clearScope: true });
197
+ this.resetBoundaryRepeats();
193
198
  }
194
199
 
195
200
  refreshConfig(cwd = this.cwd): WatchdogSettingsResult {
@@ -247,7 +252,7 @@ export class MainWatchdogRuntime {
247
252
  return this.getSnapshot();
248
253
  }
249
254
 
250
- reset(_reason = "reset", options: { clearReviewInputSignature?: boolean; resetChangeSignature?: boolean; clearLspLedger?: boolean; clearScope?: boolean; resetAutoFollow?: boolean } = {}): void {
255
+ reset(_reason = "reset", options: { clearReviewInputSignature?: boolean; resetChangeSignature?: boolean; clearLspLedger?: boolean; clearScope?: boolean } = {}): void {
251
256
  this.abortActiveAgentEnd();
252
257
  this.epoch++;
253
258
  this.status = "idle";
@@ -263,16 +268,12 @@ export class MainWatchdogRuntime {
263
268
  this.observedRepoEditThisTurn = false;
264
269
  this.toolResultsThisRun = 0;
265
270
  this.midRunReviewing = false;
266
- this.autoFollowQueued = false;
271
+ this.ruleWarningsThisRun.clear();
267
272
  if (options.clearLspLedger) {
268
273
  this.lspLedger.reset();
269
274
  this.lastLspSnapshot = undefined;
270
275
  }
271
- if (options.clearScope) {
272
- this.scope.reset();
273
- this.pendingAutoFollowPrompts = [];
274
- }
275
- if (options.resetAutoFollow) this.resetAutoFollowState();
276
+ if (options.clearScope) this.scope.reset();
276
277
  if (options.clearReviewInputSignature) this.lastReviewInputSignature = undefined;
277
278
  if (options.resetChangeSignature) this.resetRepoChangeBaseline({ reviewed: true });
278
279
  this.guard.reset();
@@ -297,22 +298,17 @@ export class MainWatchdogRuntime {
297
298
  this.observedRepoEditThisTurn = false;
298
299
  this.toolResultsThisRun = 0;
299
300
  this.midRunReviewing = false;
300
- this.autoFollowQueued = false;
301
301
  this.resolveWaiters(false);
302
302
  }
303
303
 
304
304
  handleBeforeAgentStart(event: unknown, ctx: ContextLike): void {
305
305
  if (this.disposed) return;
306
306
  const incomingPrompt = promptFromBeforeAgentStart(event);
307
- // Only exact queued auto-follow text is treated as an auto-follow turn; a real user
308
- // prompt racing in ahead of one stays real and the pending matches survive for later.
309
- const pendingIndex = incomingPrompt === undefined ? -1 : this.pendingAutoFollowPrompts.indexOf(incomingPrompt);
310
- const autoFollowPrompt = pendingIndex >= 0 || isWatchdogAutoFollowPromptEvent(event);
311
- if (pendingIndex >= 0) this.pendingAutoFollowPrompts.splice(pendingIndex, 1);
312
- this.reset("before_agent_start", { resetAutoFollow: !autoFollowPrompt });
307
+ this.reset("before_agent_start");
308
+ this.resetBoundaryRepeats();
313
309
  this.refreshConfig(ctx.cwd);
314
310
  this.userPrompt = incomingPrompt;
315
- if (!autoFollowPrompt && this.userPrompt?.trim()) {
311
+ if (this.userPrompt?.trim()) {
316
312
  this.includeUserPromptInNextDelta = true;
317
313
  this.scope.addPrompt(this.userPrompt);
318
314
  } else {
@@ -383,8 +379,6 @@ export class MainWatchdogRuntime {
383
379
  const lspAbortController = new AbortController();
384
380
  this.activeAgentEndId = agentEndId;
385
381
  this.activeAgentEndAbortController = lspAbortController;
386
- let displayedDuringAgentEnd: WatchdogWarningDetails | undefined;
387
- const previousDisplayedSequence = this.displayedWarningSequence;
388
382
  try {
389
383
  this.guard.startModelUpdate();
390
384
  const lspBlock = await this.collectLspDiagnostics(changeSignature, {
@@ -425,8 +419,6 @@ export class MainWatchdogRuntime {
425
419
  this.currentChangedPaths = changeSignature?.changedPaths;
426
420
  this.status = "idle";
427
421
  }
428
- displayedDuringAgentEnd = this.displayedWarningSequence !== previousDisplayedSequence ? this.lastWarning : undefined;
429
- this.queueAutoFollowIfNeeded(displayedDuringAgentEnd);
430
422
  this.resolveWaiters(true);
431
423
  } finally {
432
424
  if (this.activeAgentEndAbortController === lspAbortController) this.activeAgentEndAbortController = undefined;
@@ -434,6 +426,14 @@ export class MainWatchdogRuntime {
434
426
  }
435
427
  }
436
428
 
429
+ displayRuleWarning(violation: WatchdogRuleViolation): void {
430
+ if (this.disposed || this.ruleWarningsThisRun.has(violation.summary)) return;
431
+ this.ruleWarningsThisRun.add(violation.summary);
432
+ const details = normalizeWatchdogWarningDetails(ruleViolationWarning(violation), { state: "displayed", displayedAt: new Date().toISOString() });
433
+ this.lastWarning = details;
434
+ this.displayWarning?.(details, { deliverAs: "steer" });
435
+ }
436
+
437
437
  recordDisplayedWarning(warning: WatchdogWarning): WatchdogWarningDetails {
438
438
  const details = normalizeWatchdogWarningDetails(warning, { state: "displayed", source: warning.source ?? "main" });
439
439
  this.lastWarning = details;
@@ -460,9 +460,8 @@ export class MainWatchdogRuntime {
460
460
  staleReviews: this.staleReviews,
461
461
  reviewConnected: this.reviewConnected,
462
462
  reviewDescription: this.reviewDescription,
463
- autoFollowQueued: this.autoFollowQueued,
464
- autoFollowAttempts: this.autoFollowAttempts,
465
- autoFollowStalemate: this.autoFollowStalemate,
463
+ boundaryRepeats: this.boundaryRepeats,
464
+ stalemate: this.stalemate,
466
465
  reviewTrigger: this.reviewChangesOnly ? "repo-edits" : "turn-delta",
467
466
  ...(this.currentChangedPaths?.length ? { changedPaths: [...this.currentChangedPaths] } : {}),
468
467
  lsp: this.lspSnapshot(),
@@ -497,7 +496,7 @@ export class MainWatchdogRuntime {
497
496
 
498
497
  private acceptWarning(epoch: number, reviewId: number, warning: WatchdogWarning): boolean {
499
498
  if (!this.isCurrent(epoch, reviewId) || !this.isEnabled() || !this.warningMeetsThreshold(warning)) return false;
500
- const decision = this.guard.evaluate(warning);
499
+ const decision = this.guard.evaluate(warning, { allowRepeatOf: this.repeatableBoundaryIdentity() });
501
500
  if (!decision.accepted) return false;
502
501
  const details = normalizeWatchdogWarningDetails(warning, {
503
502
  state: "candidate",
@@ -511,7 +510,7 @@ export class MainWatchdogRuntime {
511
510
 
512
511
  private displayBoundaryWarning(warning: WatchdogWarning): boolean {
513
512
  if (!this.isEnabled() || !this.warningMeetsThreshold(warning)) return false;
514
- const decision = this.guard.evaluate(warning);
513
+ const decision = this.guard.evaluate(warning, { allowRepeatOf: this.repeatableBoundaryIdentity() });
515
514
  if (!decision.accepted) return false;
516
515
  const details = normalizeWatchdogWarningDetails(warning, {
517
516
  state: "displayed",
@@ -519,9 +518,7 @@ export class MainWatchdogRuntime {
519
518
  identity: decision.identity,
520
519
  displayedAt: new Date().toISOString(),
521
520
  });
522
- this.lastWarning = details;
523
- this.displayedWarningSequence++;
524
- this.displayWarning?.(details);
521
+ this.deliverBoundaryWarning(details);
525
522
  return true;
526
523
  }
527
524
 
@@ -635,51 +632,41 @@ export class MainWatchdogRuntime {
635
632
  state: "displayed",
636
633
  displayedAt: new Date().toISOString(),
637
634
  };
638
- this.lastWarning = details;
639
- this.displayedWarningSequence++;
640
- this.displayWarning?.(details, correction ? { deliverAs: "steer" } : undefined);
641
- }
642
-
643
- private resetAutoFollowState(): void {
644
- this.autoFollowQueued = false;
645
- this.autoFollowAttempts = 0;
646
- this.consecutiveAutoFollowIdentity = undefined;
647
- this.consecutiveAutoFollowRepeats = 0;
648
- this.autoFollowStalemate = false;
635
+ if (correction) {
636
+ this.lastWarning = details;
637
+ this.displayWarning?.(details, { deliverAs: "steer" });
638
+ return;
639
+ }
640
+ this.deliverBoundaryWarning(details);
649
641
  }
650
642
 
651
- private queueAutoFollowIfNeeded(warning: WatchdogWarningDetails | undefined): void {
652
- if (!warning || warning.severity !== "blocker" || warning.stale || !this.configResult.config.autoFollow.blockers || !this.isEnabled()) return;
653
- const identity = warning.identity ?? reviewInputSignature([warning.severity, warning.summary, warning.evidence].join("\n"));
654
- if (this.consecutiveAutoFollowIdentity === identity) this.consecutiveAutoFollowRepeats++;
643
+ // A displayed boundary warning continues the run; the same identity back from consecutive
644
+ // boundaries means no progress, so it is shown held instead of continuing again.
645
+ private deliverBoundaryWarning(details: WatchdogWarningDetails): void {
646
+ const identity = details.identity ?? reviewInputSignature([details.severity, details.summary, details.evidence].join("\n"));
647
+ if (this.lastBoundaryIdentity === identity) this.boundaryRepeats++;
655
648
  else {
656
- this.consecutiveAutoFollowIdentity = identity;
657
- this.consecutiveAutoFollowRepeats = 1;
658
- }
659
- if (this.consecutiveAutoFollowRepeats >= this.configResult.config.autoFollow.stalemateRepeats) {
660
- this.autoFollowStalemate = true;
661
- this.lastWarning = { ...warning, state: "stalemate", stalemateRepeats: this.consecutiveAutoFollowRepeats };
662
- return;
649
+ this.lastBoundaryIdentity = identity;
650
+ this.boundaryRepeats = 1;
663
651
  }
664
- const maxAttempts = this.configResult.config.autoFollow.maxAttempts;
665
- if (maxAttempts !== null && this.autoFollowAttempts >= maxAttempts) return;
666
- if (!this.sendUserMessage) return;
667
- this.autoFollowAttempts++;
668
- this.autoFollowQueued = true;
669
- const prompt = [
670
- "Watchdog auto-follow: address this blocker before continuing.",
671
- `Summary: ${warning.summary}`,
672
- `Evidence: ${warning.evidence}`,
673
- `Recommended action: ${warning.recommendedAction}`,
674
- ].join("\n");
675
- this.pendingAutoFollowPrompts.push(prompt);
676
- if (this.pendingAutoFollowPrompts.length > 8) this.pendingAutoFollowPrompts.shift();
677
- void Promise.resolve(this.sendUserMessage(prompt)).catch((error) => {
678
- this.autoFollowQueued = false;
679
- const index = this.pendingAutoFollowPrompts.indexOf(prompt);
680
- if (index >= 0) this.pendingAutoFollowPrompts.splice(index, 1);
681
- this.lastError = `Watchdog auto-follow failed: ${errorMessage(error)}`;
682
- });
652
+ const stalemate = this.boundaryRepeats >= this.configResult.config.stalemateRepeats;
653
+ const delivered: WatchdogWarningDetails = stalemate
654
+ ? { ...details, state: "stalemate", stalemateRepeats: this.boundaryRepeats }
655
+ : details;
656
+ this.stalemate = stalemate;
657
+ this.lastWarning = delivered;
658
+ this.displayWarning?.(delivered, stalemate ? { triggerTurn: false } : undefined);
659
+ }
660
+
661
+ // The previous boundary finding is a repeat to count, not a duplicate, until stalemate.
662
+ private repeatableBoundaryIdentity(): string | undefined {
663
+ return this.waitingAtAgentEnd && !this.stalemate ? this.lastBoundaryIdentity : undefined;
664
+ }
665
+
666
+ private resetBoundaryRepeats(): void {
667
+ this.lastBoundaryIdentity = undefined;
668
+ this.boundaryRepeats = 0;
669
+ this.stalemate = false;
683
670
  }
684
671
 
685
672
  private currentRepoChangeSignature(cwd = this.cwd): WatchdogRepoChangeSignature | undefined {
@@ -783,7 +770,7 @@ export class MainWatchdogRuntime {
783
770
  private appendBoundedDelta(delta: string): void {
784
771
  let entry = delta.trim();
785
772
  if (!entry) return;
786
- if (entry.length > MAX_REVIEW_INPUT_CHARS) entry = entry.slice(-MAX_REVIEW_INPUT_CHARS);
773
+ if (entry.length > MAX_REVIEW_INPUT_CHARS) entry = boundWatchdogReviewText(entry);
787
774
  this.pendingDeltas.push(entry);
788
775
  this.pendingDeltaChars += entry.length;
789
776
  while (this.pendingDeltas.length > 1 && this.pendingDeltaChars + (this.pendingDeltas.length - 1) * REVIEW_DELTA_SEPARATOR.length > MAX_REVIEW_INPUT_CHARS) {
@@ -799,7 +786,7 @@ export class MainWatchdogRuntime {
799
786
  ? ["Changed repo paths:", ...changeSignature.changedPaths.slice(0, 200).map((file) => `- ${file}`)].join("\n")
800
787
  : "";
801
788
  const contextPieces = [scopeBlock, changes, lspBlock].filter(Boolean);
802
- if (!contextPieces.length) return input.length > MAX_REVIEW_INPUT_CHARS ? input.slice(-MAX_REVIEW_INPUT_CHARS) : input;
789
+ if (!contextPieces.length) return boundWatchdogReviewText(input);
803
790
 
804
791
  const maxContextLength = Math.floor(MAX_REVIEW_INPUT_CHARS / 2);
805
792
  const maxPieceLength = Math.max(1_000, Math.floor(maxContextLength / contextPieces.length));
@@ -808,11 +795,7 @@ export class MainWatchdogRuntime {
808
795
  : piece).join(REVIEW_DELTA_SEPARATOR);
809
796
  const separatorLength = input ? REVIEW_DELTA_SEPARATOR.length : 0;
810
797
  const inputBudget = MAX_REVIEW_INPUT_CHARS - boundedContext.length - separatorLength;
811
- const boundedInput = inputBudget <= 0
812
- ? ""
813
- : input.length > inputBudget
814
- ? input.slice(-inputBudget)
815
- : input;
798
+ const boundedInput = inputBudget <= 0 ? "" : boundWatchdogReviewText(input, inputBudget);
816
799
  return [boundedContext, boundedInput].filter(Boolean).join(REVIEW_DELTA_SEPARATOR);
817
800
  }
818
801
 
@@ -1,5 +1,3 @@
1
- export const WATCHDOG_AUTO_FOLLOW_PROMPT_MARKER = Symbol("subagent-watchdog-auto-follow-prompt");
2
-
3
1
  const MAX_SCOPE_ENTRIES = 8;
4
2
  const MAX_SCOPE_ENTRY_CHARS = 2_000;
5
3
  const MAX_SCOPE_TOTAL_CHARS = 16_000;
@@ -51,12 +49,3 @@ export class WatchdogScopeArtifact {
51
49
  }
52
50
  }
53
51
  }
54
-
55
- export function isWatchdogAutoFollowPromptEvent(event: unknown): boolean {
56
- return Boolean(event && typeof event === "object" && (event as { [WATCHDOG_AUTO_FOLLOW_PROMPT_MARKER]?: unknown })[WATCHDOG_AUTO_FOLLOW_PROMPT_MARKER]);
57
- }
58
-
59
- export function markWatchdogAutoFollowPromptEvent<T extends object>(event: T): T {
60
- Object.defineProperty(event, WATCHDOG_AUTO_FOLLOW_PROMPT_MARKER, { value: true });
61
- return event;
62
- }