@selesai/code 0.13.12 → 0.13.14

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 (203) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/extensions/pi-subagents/CHANGELOG.md +77 -1
  3. package/dist/extensions/pi-subagents/VISION.md +12 -1
  4. package/dist/extensions/pi-subagents/docs/agents.md +13 -6
  5. package/dist/extensions/pi-subagents/docs/configuration.md +35 -9
  6. package/dist/extensions/pi-subagents/docs/extension-api.md +1 -1
  7. package/dist/extensions/pi-subagents/docs/missions.md +1 -1
  8. package/dist/extensions/pi-subagents/docs/models.md +6 -6
  9. package/dist/extensions/pi-subagents/docs/observability.md +6 -3
  10. package/dist/extensions/pi-subagents/docs/tool-reference.md +3 -3
  11. package/dist/extensions/pi-subagents/docs/watchdog.md +92 -114
  12. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  13. package/dist/extensions/pi-subagents/install.mjs +1 -2
  14. package/dist/extensions/pi-subagents/package.json +1 -1
  15. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  16. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +6 -6
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  20. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +1 -1
  21. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -4
  22. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +3 -0
  23. package/dist/extensions/pi-subagents/src/agents/agents.ts +120 -124
  24. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +5 -1
  25. package/dist/extensions/pi-subagents/src/api/preflight.ts +4 -0
  26. package/dist/extensions/pi-subagents/src/api/shared-types.ts +3 -0
  27. package/dist/extensions/pi-subagents/src/extension/config.ts +20 -0
  28. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  29. package/dist/extensions/pi-subagents/src/extension/schemas.ts +6 -2
  30. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
  31. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +19 -13
  32. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +26 -8
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +103 -23
  34. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +6 -2
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +54 -3
  37. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +16 -0
  38. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +22 -2
  39. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +63 -6
  40. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -1
  41. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -12
  42. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +13 -0
  43. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +3 -1
  44. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +21 -8
  45. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +59 -6
  46. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +95 -18
  47. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +11 -43
  48. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -0
  49. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +24 -3
  51. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +4 -0
  52. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -6
  53. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +1 -0
  54. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +33 -18
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +73 -37
  56. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +33 -6
  57. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +4 -2
  58. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  59. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -7
  60. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +467 -63
  62. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +3 -1
  63. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +0 -12
  64. package/dist/extensions/pi-subagents/src/shared/fork-session-cwd.ts +27 -0
  65. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +3 -0
  66. package/dist/extensions/pi-subagents/src/shared/types.ts +42 -4
  67. package/dist/extensions/pi-subagents/src/shared/utils.ts +18 -7
  68. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  69. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +26 -12
  70. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +61 -2
  71. package/dist/extensions/pi-subagents/src/tui/fleet.ts +12 -7
  72. package/dist/extensions/pi-subagents/src/tui/render.ts +227 -14
  73. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +56 -34
  74. package/dist/extensions/pi-subagents/src/watchdog/diff-tool.ts +77 -0
  75. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +5 -3
  76. package/dist/extensions/pi-subagents/src/watchdog/guidance.ts +20 -0
  77. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +16 -13
  78. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +10 -9
  79. package/dist/extensions/pi-subagents/src/watchdog/render.ts +4 -5
  80. package/dist/extensions/pi-subagents/src/watchdog/review.ts +15 -4
  81. package/dist/extensions/pi-subagents/src/watchdog/rules.ts +70 -0
  82. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +75 -92
  83. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +2 -1
  84. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +48 -104
  85. package/dist/extensions/pi-subagents/src/watchdog/types.ts +18 -32
  86. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +0 -1
  87. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -2
  88. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +439 -0
  89. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +28 -1
  90. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +335 -21
  91. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +4 -1
  92. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +1 -1
  93. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +22 -18
  94. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +0 -1
  95. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +21 -18
  96. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +307 -22
  97. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -0
  98. package/dist/extensions/pi-subagents/test/support/helpers.ts +25 -0
  99. package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +15 -4
  100. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -2
  101. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +96 -15
  102. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +53 -5
  103. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +20 -0
  104. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +221 -8
  105. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +45 -24
  106. package/dist/extensions/pi-subagents/test/unit/agent-scan-dirs.test.ts +133 -0
  107. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +28 -2
  108. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +1 -1
  109. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  110. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +4 -1
  111. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +18 -4
  112. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +16 -0
  113. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +38 -0
  114. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
  115. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +21 -0
  116. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +19 -6
  117. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +26 -0
  118. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +22 -1
  119. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +2 -6
  120. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +66 -1
  121. package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +1 -1
  122. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -10
  123. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +136 -18
  124. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +20 -0
  125. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +22 -0
  126. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +1 -0
  127. package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +8 -0
  128. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +127 -48
  129. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -0
  130. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +17 -0
  131. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +88 -0
  132. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -2
  133. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +9 -9
  134. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -2
  135. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +9 -3
  136. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +22 -9
  137. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +91 -8
  138. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +27 -0
  139. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +50 -0
  140. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +0 -1
  141. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +2 -2
  142. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +34 -0
  143. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -2
  144. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +73 -13
  145. package/dist/extensions/pi-subagents/test/unit/watchdog-diff-tool.test.ts +79 -0
  146. package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +2 -4
  147. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +7 -8
  148. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +48 -3
  149. package/dist/extensions/pi-subagents/test/unit/watchdog-rules.test.ts +37 -0
  150. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +36 -118
  151. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +1 -1
  152. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +37 -14
  153. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +45 -26
  154. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +223 -0
  155. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +50 -1
  156. package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +22 -0
  157. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +136 -4
  158. package/dist/extensions/pi-zentui/README.md +19 -5
  159. package/dist/extensions/pi-zentui/docs/configuration.md +37 -9
  160. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +29 -0
  161. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +31 -2
  162. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +55 -11
  163. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +115 -3
  164. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +119 -2
  165. package/dist/extensions/pi-zentui/extensions/zentui/thinking-experimental.ts +1768 -0
  166. package/dist/extensions/pi-zentui/extensions/zentui/thinking-status.ts +69 -0
  167. package/dist/extensions/pi-zentui/extensions/zentui/thinking-steps.ts +155 -0
  168. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +2 -1
  169. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +16 -2
  170. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
  171. package/dist/extensions/pi-zentui/test/config-load-lifecycle.test.ts +124 -0
  172. package/dist/extensions/pi-zentui/test/config.test.ts +82 -2
  173. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +19 -4
  174. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +58 -9
  175. package/dist/extensions/pi-zentui/test/package-contents.mjs +33 -0
  176. package/dist/extensions/pi-zentui/test/prototype-patch-registry.test.ts +131 -0
  177. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +4 -4
  178. package/dist/extensions/pi-zentui/test/settings-command.test.ts +289 -7
  179. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +106 -0
  180. package/dist/extensions/pi-zentui/test/thinking-experimental-docs.test.ts +51 -0
  181. package/dist/extensions/pi-zentui/test/thinking-experimental-loader-diagnostics.mjs +54 -0
  182. package/dist/extensions/pi-zentui/test/thinking-experimental-missing-export.test.ts +44 -0
  183. package/dist/extensions/pi-zentui/test/thinking-experimental-rows.test.ts +297 -0
  184. package/dist/extensions/pi-zentui/test/thinking-experimental-tui.mjs +2329 -0
  185. package/dist/extensions/pi-zentui/test/thinking-experimental.test.ts +1668 -0
  186. package/dist/extensions/pi-zentui/test/thinking-status.test.ts +85 -0
  187. package/dist/extensions/pi-zentui/test/thinking-steps.test.ts +122 -0
  188. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +58 -0
  189. package/dist/skills/brandkit/SKILL.md +0 -2
  190. package/dist/skills/{industrial-brutalist-ui → brutalist}/SKILL.md +1 -3
  191. package/dist/skills/gpt-taste/SKILL.md +0 -2
  192. package/dist/skills/image-to-code/SKILL.md +0 -2
  193. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -2
  194. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -2
  195. package/dist/skills/{minimalist-ui → minimalist}/SKILL.md +1 -3
  196. package/dist/skills/{full-output-enforcement → output}/SKILL.md +1 -3
  197. package/dist/skills/{redesign-existing-projects → redesign}/SKILL.md +1 -3
  198. package/dist/skills/{high-end-visual-design → soft}/SKILL.md +1 -3
  199. package/dist/skills/stitch/DESIGN.md +121 -0
  200. package/dist/skills/{stitch-design-taste → stitch}/SKILL.md +1 -3
  201. package/dist/skills/{design-taste-frontend → taste}/SKILL.md +1 -3
  202. package/dist/skills/{design-taste-frontend-v1 → taste-v1}/SKILL.md +2 -4
  203. package/package.json +1 -1
@@ -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
 
@@ -59,4 +59,5 @@ export function isWatchdogAutoFollowPromptEvent(event: unknown): boolean {
59
59
  export function markWatchdogAutoFollowPromptEvent<T extends object>(event: T): T {
60
60
  Object.defineProperty(event, WATCHDOG_AUTO_FOLLOW_PROMPT_MARKER, { value: true });
61
61
  return event;
62
- }
62
+ }
63
+
@@ -3,39 +3,31 @@ import * as path from "node:path";
3
3
  import { THINKING_LEVELS, type ThinkingLevel } from "../shared/model-info.ts";
4
4
  import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
5
5
  import {
6
- WATCHDOG_DELIVERY_MODES,
7
- WATCHDOG_LATE_WARNING_POLICIES,
8
6
  WATCHDOG_WARNING_SEVERITIES,
9
7
  type ResolvedWatchdogConfig,
10
- type WatchdogAsyncCompletionConfig,
11
- type WatchdogAutoFollowConfig,
12
8
  type WatchdogChildOverrideConfig,
13
9
  type WatchdogChildrenConfig,
14
- type WatchdogDeliveryMode,
15
10
  type WatchdogEndpointConfig,
16
11
  type WatchdogCadenceConfig,
17
12
  type WatchdogGuidanceConfig,
18
- type WatchdogLateWarningPolicy,
19
13
  type WatchdogScopeConfig,
20
14
  type WatchdogLspConfig,
15
+ type WatchdogRoleModelRule,
16
+ type WatchdogRulesConfig,
21
17
  type WatchdogSettingsError,
22
18
  type WatchdogSettingsResult,
23
19
  type WatchdogSettingsSource,
24
20
  type WatchdogSeverity,
25
- type WatchdogSyncBacklog,
26
21
  } from "./types.ts";
27
22
 
28
- type WatchdogAutoFollowPatch = Partial<WatchdogAutoFollowConfig>;
29
23
  type WatchdogGuidancePatch = Partial<WatchdogGuidanceConfig>;
30
24
  type WatchdogScopePatch = Partial<WatchdogScopeConfig>;
31
25
  type WatchdogCadencePatch = Partial<WatchdogCadenceConfig>;
32
26
  type WatchdogEndpointPatch = Partial<WatchdogEndpointConfig>;
33
27
  type WatchdogChildOverridePatch = Partial<WatchdogChildOverrideConfig>;
34
- type WatchdogChildrenPatch = Partial<Omit<WatchdogChildrenConfig, "autoFollow" | "overrides">> & {
35
- autoFollow?: WatchdogAutoFollowPatch;
28
+ type WatchdogChildrenPatch = Partial<Omit<WatchdogChildrenConfig, "overrides">> & {
36
29
  overrides?: Record<string, WatchdogChildOverridePatch>;
37
30
  };
38
- type WatchdogAsyncCompletionPatch = Partial<WatchdogAsyncCompletionConfig>;
39
31
  type WatchdogLspPatch = Partial<WatchdogLspConfig>;
40
32
 
41
33
  export type WatchdogSettingsWriteScope = "user" | "project";
@@ -52,14 +44,13 @@ export interface WatchdogModelSettingsWrite {
52
44
  thinking?: ThinkingLevel | false | null;
53
45
  }
54
46
 
55
- type WatchdogConfigPatch = Partial<Omit<ResolvedWatchdogConfig, "guidance" | "autoFollow" | "scope" | "cadence" | "main" | "children" | "asyncCompletion" | "lsp">> & {
47
+ type WatchdogConfigPatch = Partial<Omit<ResolvedWatchdogConfig, "guidance" | "scope" | "cadence" | "main" | "children" | "lsp" | "rules">> & {
48
+ rules?: WatchdogRulesConfig;
56
49
  guidance?: WatchdogGuidancePatch;
57
- autoFollow?: WatchdogAutoFollowPatch;
58
50
  scope?: WatchdogScopePatch;
59
51
  cadence?: WatchdogCadencePatch;
60
52
  main?: WatchdogEndpointPatch;
61
53
  children?: WatchdogChildrenPatch;
62
- asyncCompletion?: WatchdogAsyncCompletionPatch;
63
54
  lsp?: WatchdogLspPatch;
64
55
  };
65
56
 
@@ -70,22 +61,13 @@ interface ParseMeta {
70
61
 
71
62
  export const DEFAULT_WATCHDOG_CONFIG: ResolvedWatchdogConfig = {
72
63
  enabled: false,
73
- delivery: "held",
74
- showDuringRun: false,
75
- syncBacklog: "off",
76
64
  agentEndTimeoutMs: 30_000,
77
- lateWarningPolicy: "show-stale-no-autofollow",
78
65
  severityThreshold: "concern",
79
66
  maxWarnings: null,
80
67
  guidance: {
81
68
  watchdogMd: true,
82
- systemPromptPath: null,
83
- },
84
- autoFollow: {
85
- blockers: true,
86
- maxAttempts: 3,
87
- stalemateRepeats: 3,
88
69
  },
70
+ stalemateRepeats: 3,
89
71
  scope: {
90
72
  enabled: true,
91
73
  },
@@ -98,73 +80,51 @@ export const DEFAULT_WATCHDOG_CONFIG: ResolvedWatchdogConfig = {
98
80
  children: {
99
81
  enabled: false,
100
82
  watchdogTailTimeoutMs: 120_000,
101
- autoFollow: {
102
- blockers: true,
103
- maxAttempts: 3,
104
- stalemateRepeats: 3,
105
- },
106
83
  overrides: {},
107
84
  },
108
- asyncCompletion: {
109
- enabled: false,
110
- autoFollowBlockers: false,
111
- },
112
85
  lsp: {
113
86
  enabled: true,
114
87
  timeoutMs: 3_000,
115
88
  maxFiles: 20,
116
89
  maxDiagnostics: 50,
117
90
  },
118
- compactAtPercent: 80,
119
- reviewRetryDelayMs: 1_000,
120
- maxReviewFailures: 3,
121
91
  };
122
92
 
123
93
  const WATCHDOG_FIELDS = new Set([
124
94
  "enabled",
125
- "delivery",
126
- "showDuringRun",
127
- "syncBacklog",
128
95
  "agentEndTimeoutMs",
129
- "lateWarningPolicy",
130
96
  "severityThreshold",
131
97
  "maxWarnings",
132
98
  "guidance",
133
- "autoFollow",
99
+ "stalemateRepeats",
134
100
  "scope",
135
101
  "cadence",
136
102
  "main",
137
103
  "children",
138
- "asyncCompletion",
139
104
  "lsp",
140
- "compactAtPercent",
141
- "reviewRetryDelayMs",
142
- "maxReviewFailures",
105
+ "rules",
143
106
  ]);
144
- const GUIDANCE_FIELDS = new Set(["watchdogMd", "systemPromptPath"]);
145
- const AUTO_FOLLOW_FIELDS = new Set(["blockers", "maxAttempts", "stalemateRepeats"]);
107
+ const RULES_FIELDS = new Set(["action", "roleModels"]);
108
+ const ROLE_MODEL_RULE_FIELDS = new Set(["allow", "deny", "note"]);
109
+ const GUIDANCE_FIELDS = new Set(["watchdogMd"]);
146
110
  const SCOPE_FIELDS = new Set(["enabled"]);
147
111
  const CADENCE_FIELDS = new Set(["everyNTools"]);
148
112
  const ENDPOINT_FIELDS = new Set(["enabled", "model", "thinking"]);
149
- const CHILDREN_FIELDS = new Set(["enabled", "model", "thinking", "watchdogTailTimeoutMs", "autoFollow", "overrides"]);
150
- const CHILD_OVERRIDE_FIELDS = new Set(["enabled", "model", "thinking"]);
151
- const ASYNC_COMPLETION_FIELDS = new Set(["enabled", "autoFollowBlockers"]);
113
+ const CHILDREN_FIELDS = new Set(["enabled", "model", "thinking", "watchdogTailTimeoutMs", "cadence", "overrides"]);
114
+ const CHILD_OVERRIDE_FIELDS = new Set(["enabled", "model", "thinking", "cadence"]);
152
115
  const LSP_FIELDS = new Set(["enabled", "timeoutMs", "maxFiles", "maxDiagnostics"]);
153
116
 
154
117
  function cloneDefaultConfig(): ResolvedWatchdogConfig {
155
118
  return {
156
119
  ...DEFAULT_WATCHDOG_CONFIG,
157
120
  guidance: { ...DEFAULT_WATCHDOG_CONFIG.guidance },
158
- autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.autoFollow },
159
121
  scope: { ...DEFAULT_WATCHDOG_CONFIG.scope },
160
122
  cadence: { ...DEFAULT_WATCHDOG_CONFIG.cadence },
161
123
  main: { ...DEFAULT_WATCHDOG_CONFIG.main },
162
124
  children: {
163
125
  ...DEFAULT_WATCHDOG_CONFIG.children,
164
- autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.children.autoFollow },
165
126
  overrides: {},
166
127
  },
167
- asyncCompletion: { ...DEFAULT_WATCHDOG_CONFIG.asyncCompletion },
168
128
  lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp },
169
129
  };
170
130
  }
@@ -227,35 +187,11 @@ function parseEnum<T extends string>(value: unknown, field: string, meta: ParseM
227
187
  throw invalid(meta, field, values.map((item) => `'${item}'`).join(" or "));
228
188
  }
229
189
 
230
- function parseSyncBacklog(value: unknown, field: string, meta: ParseMeta): WatchdogSyncBacklog {
231
- if (value === "off") return "off";
232
- return parseInteger(value, field, meta, "'off' or a positive integer", (candidate) => candidate >= 1);
233
- }
234
-
235
190
  function parseGuidancePatch(value: unknown, field: string, meta: ParseMeta): WatchdogGuidancePatch {
236
191
  const input = parseObject(value, field, meta);
237
192
  assertKnownFields(input, GUIDANCE_FIELDS, field, meta);
238
193
  const patch: WatchdogGuidancePatch = {};
239
194
  if ("watchdogMd" in input) patch.watchdogMd = parseBoolean(input.watchdogMd, `${field}.watchdogMd`, meta);
240
- if ("systemPromptPath" in input) {
241
- patch.systemPromptPath = input.systemPromptPath === null
242
- ? null
243
- : parseNonEmptyString(input.systemPromptPath, `${field}.systemPromptPath`, meta);
244
- }
245
- return patch;
246
- }
247
-
248
- function parseAutoFollowPatch(value: unknown, field: string, meta: ParseMeta): WatchdogAutoFollowPatch {
249
- const input = parseObject(value, field, meta);
250
- assertKnownFields(input, AUTO_FOLLOW_FIELDS, field, meta);
251
- const patch: WatchdogAutoFollowPatch = {};
252
- if ("blockers" in input) patch.blockers = parseBoolean(input.blockers, `${field}.blockers`, meta);
253
- if ("maxAttempts" in input) {
254
- patch.maxAttempts = parseNullableInteger(input.maxAttempts, `${field}.maxAttempts`, meta, "null or a positive integer", (candidate) => candidate >= 1);
255
- }
256
- if ("stalemateRepeats" in input) {
257
- patch.stalemateRepeats = parseInteger(input.stalemateRepeats, `${field}.stalemateRepeats`, meta, "a positive integer", (candidate) => candidate >= 1);
258
- }
259
195
  return patch;
260
196
  }
261
197
 
@@ -296,6 +232,7 @@ function parseChildOverridePatch(value: unknown, field: string, meta: ParseMeta)
296
232
  if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
297
233
  if ("model" in input) patch.model = parseNonEmptyString(input.model, `${field}.model`, meta);
298
234
  if ("thinking" in input) patch.thinking = parseThinking(input.thinking, `${field}.thinking`, meta);
235
+ if ("cadence" in input) patch.cadence = parseCadencePatch(input.cadence, `${field}.cadence`, meta);
299
236
  return patch;
300
237
  }
301
238
 
@@ -309,7 +246,7 @@ function parseChildrenPatch(value: unknown, field: string, meta: ParseMeta): Wat
309
246
  if ("watchdogTailTimeoutMs" in input) {
310
247
  patch.watchdogTailTimeoutMs = parseInteger(input.watchdogTailTimeoutMs, `${field}.watchdogTailTimeoutMs`, meta, "a positive integer", (candidate) => candidate >= 1);
311
248
  }
312
- if ("autoFollow" in input) patch.autoFollow = parseAutoFollowPatch(input.autoFollow, `${field}.autoFollow`, meta);
249
+ if ("cadence" in input) patch.cadence = parseCadencePatch(input.cadence, `${field}.cadence`, meta);
313
250
  if ("overrides" in input) {
314
251
  const overrides = parseObject(input.overrides, `${field}.overrides`, meta);
315
252
  patch.overrides = {};
@@ -321,15 +258,6 @@ function parseChildrenPatch(value: unknown, field: string, meta: ParseMeta): Wat
321
258
  return patch;
322
259
  }
323
260
 
324
- function parseAsyncCompletionPatch(value: unknown, field: string, meta: ParseMeta): WatchdogAsyncCompletionPatch {
325
- const input = parseObject(value, field, meta);
326
- assertKnownFields(input, ASYNC_COMPLETION_FIELDS, field, meta);
327
- const patch: WatchdogAsyncCompletionPatch = {};
328
- if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
329
- if ("autoFollowBlockers" in input) patch.autoFollowBlockers = parseBoolean(input.autoFollowBlockers, `${field}.autoFollowBlockers`, meta);
330
- return patch;
331
- }
332
-
333
261
  function parseLspPatch(value: unknown, field: string, meta: ParseMeta): WatchdogLspPatch {
334
262
  const input = parseObject(value, field, meta);
335
263
  assertKnownFields(input, LSP_FIELDS, field, meta);
@@ -341,20 +269,43 @@ function parseLspPatch(value: unknown, field: string, meta: ParseMeta): Watchdog
341
269
  return patch;
342
270
  }
343
271
 
272
+ function parseStringList(value: unknown, field: string, meta: ParseMeta): string[] {
273
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !entry.trim())) throw invalid(meta, field, "an array of non-empty strings");
274
+ return value.map((entry: string) => entry.trim());
275
+ }
276
+
277
+ function parseRoleModelRule(value: unknown, field: string, meta: ParseMeta): WatchdogRoleModelRule {
278
+ const input = parseObject(value, field, meta);
279
+ assertKnownFields(input, ROLE_MODEL_RULE_FIELDS, field, meta);
280
+ const rule: WatchdogRoleModelRule = {};
281
+ if ("allow" in input) rule.allow = parseStringList(input.allow, `${field}.allow`, meta);
282
+ if ("deny" in input) rule.deny = parseStringList(input.deny, `${field}.deny`, meta);
283
+ if ("note" in input) rule.note = parseNonEmptyString(input.note, `${field}.note`, meta);
284
+ return rule;
285
+ }
286
+
287
+ function parseRules(value: unknown, field: string, meta: ParseMeta): WatchdogRulesConfig {
288
+ const input = parseObject(value, field, meta);
289
+ assertKnownFields(input, RULES_FIELDS, field, meta);
290
+ const rules: WatchdogRulesConfig = { action: "warn", roleModels: {} };
291
+ if ("action" in input) rules.action = parseEnum(input.action, `${field}.action`, meta, ["warn", "block"] as const);
292
+ if ("roleModels" in input) {
293
+ for (const [agent, rule] of Object.entries(parseObject(input.roleModels, `${field}.roleModels`, meta))) {
294
+ if (!agent.trim()) throw invalid(meta, `${field}.roleModels`, "agent names to be non-empty");
295
+ rules.roleModels[agent] = parseRoleModelRule(rule, `${field}.roleModels.${agent}`, meta);
296
+ }
297
+ }
298
+ return rules;
299
+ }
300
+
344
301
  function parseWatchdogPatch(value: unknown, field: string, meta: ParseMeta): WatchdogConfigPatch {
345
302
  const input = parseObject(value, field, meta);
346
303
  assertKnownFields(input, WATCHDOG_FIELDS, field, meta);
347
304
  const patch: WatchdogConfigPatch = {};
348
305
  if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
349
- if ("delivery" in input) patch.delivery = parseEnum<WatchdogDeliveryMode>(input.delivery, `${field}.delivery`, meta, WATCHDOG_DELIVERY_MODES);
350
- if ("showDuringRun" in input) patch.showDuringRun = parseBoolean(input.showDuringRun, `${field}.showDuringRun`, meta);
351
- if ("syncBacklog" in input) patch.syncBacklog = parseSyncBacklog(input.syncBacklog, `${field}.syncBacklog`, meta);
352
306
  if ("agentEndTimeoutMs" in input) {
353
307
  patch.agentEndTimeoutMs = parseInteger(input.agentEndTimeoutMs, `${field}.agentEndTimeoutMs`, meta, "a positive integer", (candidate) => candidate >= 1);
354
308
  }
355
- if ("lateWarningPolicy" in input) {
356
- patch.lateWarningPolicy = parseEnum<WatchdogLateWarningPolicy>(input.lateWarningPolicy, `${field}.lateWarningPolicy`, meta, WATCHDOG_LATE_WARNING_POLICIES);
357
- }
358
309
  if ("severityThreshold" in input) {
359
310
  patch.severityThreshold = parseEnum<WatchdogSeverity>(input.severityThreshold, `${field}.severityThreshold`, meta, WATCHDOG_WARNING_SEVERITIES);
360
311
  }
@@ -362,22 +313,15 @@ function parseWatchdogPatch(value: unknown, field: string, meta: ParseMeta): Wat
362
313
  patch.maxWarnings = parseNullableInteger(input.maxWarnings, `${field}.maxWarnings`, meta, "null or a non-negative integer", (candidate) => candidate >= 0);
363
314
  }
364
315
  if ("guidance" in input) patch.guidance = parseGuidancePatch(input.guidance, `${field}.guidance`, meta);
365
- if ("autoFollow" in input) patch.autoFollow = parseAutoFollowPatch(input.autoFollow, `${field}.autoFollow`, meta);
316
+ if ("stalemateRepeats" in input) {
317
+ patch.stalemateRepeats = parseInteger(input.stalemateRepeats, `${field}.stalemateRepeats`, meta, "a positive integer", (candidate) => candidate >= 1);
318
+ }
366
319
  if ("scope" in input) patch.scope = parseScopePatch(input.scope, `${field}.scope`, meta);
367
320
  if ("cadence" in input) patch.cadence = parseCadencePatch(input.cadence, `${field}.cadence`, meta);
368
321
  if ("main" in input) patch.main = parseEndpointPatch(input.main, `${field}.main`, meta);
369
322
  if ("children" in input) patch.children = parseChildrenPatch(input.children, `${field}.children`, meta);
370
- if ("asyncCompletion" in input) patch.asyncCompletion = parseAsyncCompletionPatch(input.asyncCompletion, `${field}.asyncCompletion`, meta);
371
323
  if ("lsp" in input) patch.lsp = parseLspPatch(input.lsp, `${field}.lsp`, meta);
372
- if ("compactAtPercent" in input) {
373
- patch.compactAtPercent = parseInteger(input.compactAtPercent, `${field}.compactAtPercent`, meta, "an integer from 50 through 95", (candidate) => candidate >= 50 && candidate <= 95);
374
- }
375
- if ("reviewRetryDelayMs" in input) {
376
- patch.reviewRetryDelayMs = parseInteger(input.reviewRetryDelayMs, `${field}.reviewRetryDelayMs`, meta, "a positive integer", (candidate) => candidate >= 1);
377
- }
378
- if ("maxReviewFailures" in input) {
379
- patch.maxReviewFailures = parseInteger(input.maxReviewFailures, `${field}.maxReviewFailures`, meta, "a positive integer", (candidate) => candidate >= 1);
380
- }
324
+ if ("rules" in input) patch.rules = parseRules(input.rules, `${field}.rules`, meta);
381
325
  return patch;
382
326
  }
383
327