@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
@@ -17,7 +17,6 @@ describe("watchdog warning formatting and rendering", () => {
17
17
  agent: "main",
18
18
  runId: "run-1",
19
19
  stale: true,
20
- autoFollowAttempt: 2,
21
20
  });
22
21
 
23
22
  assert.match(content, /^<subagent_watchdog severity="blocker" category="correctness" source="main" guidance="weigh, don't blindly obey">/);
@@ -28,7 +27,7 @@ describe("watchdog warning formatting and rendering", () => {
28
27
  assert.match(content, /<agent>main<\/agent>/);
29
28
  assert.match(content, /<run_id>run-1<\/run_id>/);
30
29
  assert.match(content, /<stale>true<\/stale>/);
31
- assert.match(content, /<auto_follow_attempt>2<\/auto_follow_attempt>/);
30
+ assert.doesNotMatch(content, /auto_follow/);
32
31
  assert.match(content, /<blocker_guidance>/);
33
32
  });
34
33
 
@@ -54,8 +53,8 @@ describe("watchdog warning formatting and rendering", () => {
54
53
  category: "loop-risk",
55
54
  source: "main",
56
55
  summary: "Repeated blocker",
57
- evidence: "The same issue survived another auto-follow.",
58
- recommendedAction: "Stop auto-follow and ask for user review.",
56
+ evidence: "The same issue survived another continuation.",
57
+ recommendedAction: "Ask for user review.",
59
58
  state: "stalemate",
60
59
  stalemateRepeats: 3,
61
60
  };
@@ -65,10 +64,10 @@ describe("watchdog warning formatting and rendering", () => {
65
64
  const failed = formatWatchdogWarningRenderText({ ...base, state: "failed", error: "provider aborted" });
66
65
  const displayed = formatWatchdogWarningRenderText({ ...base, state: "displayed" });
67
66
 
68
- assert.match(stalemate, /Subagent watchdog Blocker \(stalemate · auto-follow stopped\): Repeated blocker/);
69
- assert.match(stalemate, /Auto-follow stopped after 3 repeated blocker warnings\./);
70
- assert.match(stale, /Subagent watchdog Concern \(stale · no auto-follow\): Repeated blocker/);
71
- assert.match(stale, /must not auto-follow/);
67
+ assert.match(stalemate, /Subagent watchdog Blocker \(stalemate\): Repeated blocker/);
68
+ assert.match(stalemate, /Same warning 3 times in a row; the watchdog stopped continuing the run\./);
69
+ assert.match(stale, /Subagent watchdog Concern \(stale\): Repeated blocker/);
70
+ assert.match(stale, /arrived after the watchdog catch-up timeout\./);
72
71
  assert.match(failed, /failed review/);
73
72
  assert.match(failed, /Failure: provider aborted/);
74
73
  assert.match(displayed, /Subagent watchdog Blocker \(displayed\): Repeated blocker/);
@@ -1,6 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
2
5
  import { describe, it } from "node:test";
3
6
  import type { StreamFn } from "@earendil-works/pi-agent-core";
7
+ import { WATCHDOG_GUIDANCE_MAX_CHARS } from "../../src/watchdog/guidance.ts";
4
8
  import {
5
9
  createAssistantMessageEventStream,
6
10
  fauxAssistantMessage,
@@ -35,14 +39,11 @@ function cloneConfig(): ResolvedWatchdogConfig {
35
39
  return {
36
40
  ...DEFAULT_WATCHDOG_CONFIG,
37
41
  guidance: { ...DEFAULT_WATCHDOG_CONFIG.guidance },
38
- autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.autoFollow },
39
42
  main: { ...DEFAULT_WATCHDOG_CONFIG.main },
40
43
  children: {
41
44
  ...DEFAULT_WATCHDOG_CONFIG.children,
42
- autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.children.autoFollow },
43
45
  overrides: { ...DEFAULT_WATCHDOG_CONFIG.children.overrides },
44
46
  },
45
- asyncCompletion: { ...DEFAULT_WATCHDOG_CONFIG.asyncCompletion },
46
47
  lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp },
47
48
  };
48
49
  }
@@ -240,6 +241,50 @@ describe("main watchdog review adapter", () => {
240
241
  assert.equal(result?.stopReason, "aborted");
241
242
  });
242
243
 
244
+ it("includes WATCHDOG.md guidance in the system prompt", async () => {
245
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "watchdog-review-guidance-"));
246
+ const originalAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
247
+ try {
248
+ process.env.SELESAI_CODING_AGENT_DIR = path.join(dir, "agent");
249
+ fs.mkdirSync(path.join(dir, "project", ".selesai"), { recursive: true });
250
+ fs.writeFileSync(path.join(dir, "project", ".selesai", "WATCHDOG.md"), "Never accept skipped tests.\n", "utf-8");
251
+ fs.mkdirSync(path.join(dir, "agent"), { recursive: true });
252
+ fs.writeFileSync(path.join(dir, "agent", "WATCHDOG.md"), "u".repeat(WATCHDOG_GUIDANCE_MAX_CHARS), "utf-8");
253
+ const current = model("openai", "gpt-guidance");
254
+ const ctx = { ...(createCtx({ current }) as object), cwd: path.join(dir, "project") } as never;
255
+ const { streamFn, calls } = createStreamFn([fauxAssistantMessage("done", { stopReason: "stop" })]);
256
+
257
+ await createMainWatchdogReview(ctx, { streamFn })(request(enabledConfig(), []));
258
+ const prompt = String(calls[0]?.context.systemPrompt ?? "");
259
+ assert.match(prompt, /Standing instructions from WATCHDOG\.md \(project first, then user\):\nNever accept skipped tests\.\n\nu+$/);
260
+ assert.equal(prompt.split("(project first, then user):\n")[1]?.length, WATCHDOG_GUIDANCE_MAX_CHARS, "combined guidance is capped from the head");
261
+
262
+ const disabled = enabledConfig();
263
+ disabled.guidance = { watchdogMd: false };
264
+ const second = createStreamFn([fauxAssistantMessage("done", { stopReason: "stop" })]);
265
+ await createMainWatchdogReview(ctx, { streamFn: second.streamFn })(request(disabled, []));
266
+ assert.doesNotMatch(String(second.calls[0]?.context.systemPrompt ?? ""), /Standing instructions/);
267
+ } finally {
268
+ if (originalAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
269
+ else process.env.SELESAI_CODING_AGENT_DIR = originalAgentDir;
270
+ fs.rmSync(dir, { recursive: true, force: true });
271
+ }
272
+ });
273
+
274
+ it("offers watchdog_diff only when a repo baseline is available", async () => {
275
+ const current = model("openai", "gpt-diff");
276
+ const ctx = createCtx({ current });
277
+ const withBaseline = createStreamFn([fauxAssistantMessage("done", { stopReason: "stop" })]);
278
+ await createMainWatchdogReview(ctx, { streamFn: withBaseline.streamFn, diffBaseline: () => ({ root: "/tmp/watchdog-review", ref: "abc123" }) })(request(enabledConfig(), []));
279
+ assert.deepEqual(withBaseline.calls[0]?.context.tools?.map((tool) => tool.name).sort(), ["find", "grep", "ls", "read", "watchdog_diff", "watchdog_warn"]);
280
+ assert.match(String(withBaseline.calls[0]?.context.systemPrompt ?? ""), /watchdog_diff/);
281
+
282
+ const without = createStreamFn([fauxAssistantMessage("done", { stopReason: "stop" })]);
283
+ await createMainWatchdogReview(ctx, { streamFn: without.streamFn, diffBaseline: () => undefined })(request(enabledConfig(), []));
284
+ assert.deepEqual(without.calls[0]?.context.tools?.map((tool) => tool.name).sort(), ["find", "grep", "ls", "read", "watchdog_warn"]);
285
+ assert.doesNotMatch(String(without.calls[0]?.context.systemPrompt ?? ""), /watchdog_diff/);
286
+ });
287
+
243
288
  it("does not expose mutating tools to the watchdog agent", async () => {
244
289
  const current = model("openai", "gpt-readonly");
245
290
  const ctx = createCtx({ current });
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { evaluateLaunchRule, ruleViolationWarning, watchdogGlobMatch } from "../../src/watchdog/rules.ts";
4
+ import type { WatchdogRulesConfig } from "../../src/watchdog/types.ts";
5
+
6
+ describe("watchdog launch rules", () => {
7
+ it("matches globs anchored and case-sensitively", () => {
8
+ assert.equal(watchdogGlobMatch("openai-codex/gpt-5.6-*:max", "openai-codex/gpt-5.6-luna:max"), true);
9
+ assert.equal(watchdogGlobMatch("openai-codex/gpt-5.6-*", "openai-codex/gpt-5.6-luna:max"), true);
10
+ assert.equal(watchdogGlobMatch("gpt-5.6-*", "openai-codex/gpt-5.6-luna"), false, "patterns are anchored at the start");
11
+ assert.equal(watchdogGlobMatch("*", "anything/at-all"), true);
12
+ assert.equal(watchdogGlobMatch("anthropic/claude-opus-4.?", "anthropic/claude-opus-4.8"), true);
13
+ assert.equal(watchdogGlobMatch("anthropic/claude-opus-4.8", "anthropic/claude-opus-4x8"), false);
14
+ assert.equal(watchdogGlobMatch("Anthropic/*", "anthropic/claude"), false);
15
+ });
16
+
17
+ it("applies deny before allow, matches the base model when the suffix differs, and shapes a concern", () => {
18
+ const config: WatchdogRulesConfig = { action: "warn", roleModels: { scout: { allow: ["openai-codex/gpt-5.6-luna"], deny: ["openai-codex/gpt-5.6-sol*"], note: "scout is cheap" } } };
19
+ assert.equal(evaluateLaunchRule(config, "scout", "openai-codex/gpt-5.6-luna:max"), undefined);
20
+ const denied = evaluateLaunchRule(config, "scout", "openai-codex/gpt-5.6-sol:high");
21
+ assert.match(denied?.summary ?? "", /denied model 'openai-codex\/gpt-5\.6-sol:high'/);
22
+ assert.match(denied?.evidence ?? "", /scout is cheap/);
23
+ const outside = evaluateLaunchRule(config, "scout", "anthropic/claude-opus-4-8");
24
+ assert.match(outside?.summary ?? "", /not in its allowed list/);
25
+ assert.match(outside?.recommendedAction ?? "", /openai-codex\/gpt-5\.6-luna/);
26
+ assert.equal(evaluateLaunchRule(config, "worker", "anthropic/claude-opus-4-8"), undefined, "other roles are unaffected");
27
+ assert.equal(evaluateLaunchRule(config, "scout", undefined), undefined, "an unknown model cannot be judged");
28
+ assert.equal(evaluateLaunchRule(undefined, "scout", "x/y"), undefined);
29
+
30
+ const warning = ruleViolationWarning(denied!);
31
+ assert.equal(warning.severity, "concern");
32
+ assert.equal(warning.category, "missed-constraint");
33
+ assert.equal(warning.confidence, "high");
34
+ assert.equal(warning.source, "main");
35
+ assert.equal(warning.agent, "scout");
36
+ });
37
+ });
@@ -6,22 +6,19 @@ import * as path from "node:path";
6
6
  import { describe, it } from "node:test";
7
7
  import { DEFAULT_WATCHDOG_CONFIG } from "../../src/watchdog/settings.ts";
8
8
  import { MainWatchdogRuntime, type WatchdogReviewFunction } from "../../src/watchdog/runtime.ts";
9
- import type { ResolvedWatchdogConfig, WatchdogLspResult, WatchdogSettingsResult, WatchdogWarning } from "../../src/watchdog/types.ts";
9
+ import type { ResolvedWatchdogConfig, WatchdogLspResult, WatchdogSettingsResult, WatchdogWarning, WatchdogWarningDetails } from "../../src/watchdog/types.ts";
10
10
 
11
11
  function cloneConfig(): ResolvedWatchdogConfig {
12
12
  return {
13
13
  ...DEFAULT_WATCHDOG_CONFIG,
14
14
  guidance: { ...DEFAULT_WATCHDOG_CONFIG.guidance },
15
- autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.autoFollow },
16
15
  scope: { ...DEFAULT_WATCHDOG_CONFIG.scope },
17
16
  cadence: { ...DEFAULT_WATCHDOG_CONFIG.cadence },
18
17
  main: { ...DEFAULT_WATCHDOG_CONFIG.main },
19
18
  children: {
20
19
  ...DEFAULT_WATCHDOG_CONFIG.children,
21
- autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.children.autoFollow },
22
20
  overrides: { ...DEFAULT_WATCHDOG_CONFIG.children.overrides },
23
21
  },
24
- asyncCompletion: { ...DEFAULT_WATCHDOG_CONFIG.asyncCompletion },
25
22
  lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp },
26
23
  };
27
24
  }
@@ -40,7 +37,6 @@ function enabledConfig(overrides: Partial<ResolvedWatchdogConfig> = {}): Resolve
40
37
  if (overrides.lsp) config.lsp = { ...config.lsp, ...overrides.lsp };
41
38
  if (overrides.scope) config.scope = { ...config.scope, ...overrides.scope };
42
39
  if (overrides.cadence) config.cadence = { ...config.cadence, ...overrides.cadence };
43
- if (overrides.autoFollow) config.autoFollow = { ...config.autoFollow, ...overrides.autoFollow };
44
40
  return config;
45
41
  }
46
42
 
@@ -173,7 +169,6 @@ describe("main watchdog runtime", () => {
173
169
  assert.equal(snapshot.lastWarning?.state, "displayed");
174
170
  assert.equal(displayed.length, 1);
175
171
  assert.equal((displayed[0] as { state?: string }).state, "displayed");
176
- assert.equal(snapshot.autoFollowQueued, false);
177
172
  });
178
173
 
179
174
  it("drops stale async warning callbacks after reset", async () => {
@@ -205,7 +200,7 @@ describe("main watchdog runtime", () => {
205
200
  assert.equal(snapshot.lastWarning, undefined);
206
201
  });
207
202
 
208
- it("treats failed review stop reasons as failed and never auto-follows", async () => {
203
+ it("treats failed review stop reasons as failed", async () => {
209
204
  const runtime = new MainWatchdogRuntime({
210
205
  resolveConfig: () => configResult(enabledConfig()),
211
206
  review: () => ({ stopReason: "length" }),
@@ -218,7 +213,6 @@ describe("main watchdog runtime", () => {
218
213
  assert.equal(snapshot.status, "failed");
219
214
  assert.match(snapshot.lastError ?? "", /stop reason 'length'/);
220
215
  assert.equal(snapshot.failedReviews, 1);
221
- assert.equal(snapshot.autoFollowQueued, false);
222
216
  });
223
217
 
224
218
  it("marks unresolved agent-end review work stale on timeout", async () => {
@@ -240,7 +234,6 @@ describe("main watchdog runtime", () => {
240
234
  const snapshot = runtime.getSnapshot();
241
235
  assert.equal(snapshot.status, "stale");
242
236
  assert.equal(snapshot.staleReviews, 1);
243
- assert.equal(snapshot.autoFollowQueued, false);
244
237
  runtime.reset("cleanup");
245
238
  });
246
239
 
@@ -303,7 +296,6 @@ describe("main watchdog runtime", () => {
303
296
  const snapshot = runtime.getSnapshot();
304
297
  assert.equal(snapshot.status, "stale");
305
298
  assert.equal(snapshot.lastWarning, undefined);
306
- assert.equal(snapshot.autoFollowQueued, false);
307
299
  });
308
300
 
309
301
  it("invalidates an in-flight review when refreshed config disables the watchdog", async () => {
@@ -350,8 +342,9 @@ describe("main watchdog runtime", () => {
350
342
  runtime.enqueueDelta(`Assistant:\n${"x".repeat(30_000)}`);
351
343
  await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
352
344
 
353
- assert.equal(reviewedDelta.length, 24_000);
354
- assert.match(reviewedDelta, /^x+$/);
345
+ assert.equal(reviewedDelta.length, 24_000, "head + marker + tail fill the cap exactly");
346
+ assert.match(reviewedDelta, /^Assistant:\nx+\n\n\[\.\.\. about \d+ characters omitted \.\.\.\]\n\nx+$/);
347
+ assert.equal(reviewedDelta.indexOf("\n\n[..."), 6_000, "the first 6,000 characters are kept as the head");
355
348
  });
356
349
 
357
350
  it("does not record duplicate signatures for stale invalidated reviews", async () => {
@@ -879,73 +872,6 @@ describe("main watchdog runtime", () => {
879
872
  assert.deepEqual((delivered[0] as { options?: unknown }).options, { deliverAs: "steer" });
880
873
  });
881
874
 
882
- it("auto-follows agent-end blockers, respects max attempts, and real prompts reset attempts", async () => {
883
- const sent: string[] = [];
884
- const runtime = new MainWatchdogRuntime({
885
- resolveConfig: () => configResult(enabledConfig({ autoFollow: { blockers: true, maxAttempts: 1, stalemateRepeats: 3 } })),
886
- sendUserMessage: (message) => sent.push(message),
887
- review: (request) => {
888
- assert.equal(request.emitWarning({ ...warning(), severity: "blocker", summary: "Needs fixing" }), true);
889
- return { stopReason: "stop" };
890
- },
891
- });
892
-
893
- runtime.handleBeforeAgentStart({ prompt: "Patch carefully." }, { cwd: "/tmp/project" });
894
- runtime.enqueueDelta("Assistant:\nBad patch.");
895
- await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
896
- assert.equal(sent.length, 1);
897
- assert.match(sent[0] ?? "", /Watchdog auto-follow/);
898
- assert.equal(runtime.getSnapshot().autoFollowAttempts, 1);
899
-
900
- runtime.handleBeforeAgentStart({ prompt: sent[0] }, { cwd: "/tmp/project" });
901
- runtime.enqueueDelta("Assistant:\nStill bad.");
902
- await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
903
- assert.equal(sent.length, 1);
904
-
905
- runtime.handleBeforeAgentStart({ prompt: "Human says try again with a different edit." }, { cwd: "/tmp/project" });
906
- runtime.enqueueDelta("Assistant:\nBad again in a different way.");
907
- await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
908
- assert.equal(sent.length, 2);
909
- assert.equal(runtime.getSnapshot().autoFollowAttempts, 1);
910
- });
911
-
912
- it("keeps a racing real prompt authoritative while a queued auto-follow is pending", async () => {
913
- const sent: string[] = [];
914
- let reviewedDelta = "";
915
- let blockerSummary = "First blocker";
916
- const runtime = new MainWatchdogRuntime({
917
- resolveConfig: () => configResult(enabledConfig({ autoFollow: { blockers: true, maxAttempts: 1, stalemateRepeats: 5 } })),
918
- sendUserMessage: (message) => sent.push(message),
919
- review: (request) => {
920
- reviewedDelta = request.delta;
921
- assert.equal(request.emitWarning({ ...warning(), severity: "blocker", summary: blockerSummary }), true);
922
- return { stopReason: "stop" };
923
- },
924
- });
925
-
926
- runtime.handleBeforeAgentStart({ prompt: "Patch carefully." }, { cwd: "/tmp/project" });
927
- runtime.enqueueDelta("Assistant:\nBad patch.");
928
- await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
929
- assert.equal(sent.length, 1);
930
- assert.equal(runtime.getSnapshot().autoFollowAttempts, 1);
931
-
932
- // A real user prompt arrives before the queued auto-follow turn starts.
933
- blockerSummary = "Second blocker";
934
- runtime.handleBeforeAgentStart({ prompt: "Human: also update the docs." }, { cwd: "/tmp/project" });
935
- runtime.enqueueDelta("Assistant:\nStill bad.");
936
- await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
937
- assert.match(reviewedDelta, /also update the docs\./);
938
- assert.equal(sent.length, 2, "real prompt resets attempts so a new blocker auto-follows again");
939
-
940
- // The originally queued auto-follow prompt then arrives and stays excluded from scope.
941
- blockerSummary = "Third blocker";
942
- runtime.handleBeforeAgentStart({ prompt: sent[0] }, { cwd: "/tmp/project" });
943
- runtime.enqueueDelta("Assistant:\nStill bad again.");
944
- await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
945
- assert.doesNotMatch(reviewedDelta, /Watchdog auto-follow: address this blocker/);
946
- assert.equal(sent.length, 2, "auto-follow turn must not reset the attempt counter");
947
- });
948
-
949
875
  it("cancels an in-flight cadence review so the agent-end boundary review still runs", async () => {
950
876
  let releaseFirstReview!: () => void;
951
877
  let reviewCalls = 0;
@@ -979,56 +905,48 @@ describe("main watchdog runtime", () => {
979
905
  assert.equal(snapshot.status, "idle");
980
906
  });
981
907
 
982
- it("stops auto-following repeated blocker identities at stalemate threshold", async () => {
983
- const sent: string[] = [];
908
+ it("marks the third consecutive identical boundary warning as stalemate and delivers it held", async () => {
909
+ const delivered: Array<{ warning: WatchdogWarningDetails; options?: unknown }> = [];
984
910
  const runtime = new MainWatchdogRuntime({
985
- resolveConfig: () => configResult(enabledConfig({ autoFollow: { blockers: true, maxAttempts: 5, stalemateRepeats: 2 } })),
986
- sendUserMessage: (message) => sent.push(message),
911
+ resolveConfig: () => configResult(enabledConfig({ stalemateRepeats: 3 })),
912
+ displayWarning: (details, options) => { delivered.push({ warning: details, options }); },
987
913
  review: (request) => {
988
- assert.equal(request.emitWarning({ ...warning(), severity: "blocker", summary: "Same blocker" }), true);
914
+ request.emitWarning({ ...warning(), severity: "blocker", summary: "Same blocker" });
989
915
  return { stopReason: "stop" };
990
916
  },
991
917
  });
992
918
 
993
919
  runtime.handleBeforeAgentStart({ prompt: "Patch carefully." }, { cwd: "/tmp/project" });
994
- runtime.enqueueDelta("Assistant:\nBad patch.");
995
- await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
996
- assert.equal(sent.length, 1);
997
-
998
- runtime.handleBeforeAgentStart({ prompt: sent[0] }, { cwd: "/tmp/project" });
999
- runtime.enqueueDelta("Assistant:\nStill bad but not identical input.");
1000
- await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
920
+ for (const delta of ["Assistant:\nBad patch.", "Assistant:\nStill bad.", "Assistant:\nStill bad again."]) {
921
+ runtime.enqueueDelta(delta);
922
+ await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
923
+ }
1001
924
 
1002
- const snapshot = runtime.getSnapshot();
1003
- assert.equal(sent.length, 1);
1004
- assert.equal(snapshot.autoFollowStalemate, true);
925
+ assert.equal(delivered.length, 3, "repeats keep nudging until the stalemate threshold");
926
+ assert.equal(delivered[0]?.options, undefined);
927
+ assert.equal(delivered[1]?.options, undefined);
928
+ assert.deepEqual(delivered[2]?.options, { triggerTurn: false });
929
+ assert.equal(delivered[2]?.warning.state, "stalemate");
930
+ assert.equal(delivered[2]?.warning.stalemateRepeats, 3);
931
+ let snapshot = runtime.getSnapshot();
932
+ assert.equal(snapshot.stalemate, true);
933
+ assert.equal(snapshot.boundaryRepeats, 3);
1005
934
  assert.equal(snapshot.lastWarning?.state, "stalemate");
1006
- });
1007
935
 
1008
- it("does not auto-follow stale blocker warnings", async () => {
1009
- let emitWarning!: (candidate: WatchdogWarning) => boolean;
1010
- let finishReview!: () => void;
1011
- let started!: () => void;
1012
- const reviewStarted = new Promise<void>((resolve) => { started = resolve; });
1013
- const sent: string[] = [];
1014
- const runtime = new MainWatchdogRuntime({
1015
- resolveConfig: () => configResult(enabledConfig({ agentEndTimeoutMs: 5 })),
1016
- sendUserMessage: (message) => sent.push(message),
1017
- review: async (request) => {
1018
- emitWarning = request.emitWarning;
1019
- started();
1020
- await new Promise<void>((resolve) => { finishReview = resolve; });
1021
- },
1022
- });
936
+ // After stalemate the same finding is a plain duplicate again: no further delivery.
937
+ runtime.enqueueDelta("Assistant:\nStill bad, fourth time.");
938
+ await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
939
+ assert.equal(delivered.length, 3);
1023
940
 
1024
- runtime.enqueueDelta("Assistant:\nStill working");
1025
- const end = runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
1026
- await reviewStarted;
1027
- await end;
1028
- assert.equal(emitWarning({ ...warning(), severity: "blocker" }), false);
1029
- finishReview();
1030
- await tick();
1031
- assert.equal(sent.length, 0);
941
+ // A real prompt resets the counter.
942
+ runtime.handleBeforeAgentStart({ prompt: "Human: try a different approach." }, { cwd: "/tmp/project" });
943
+ runtime.enqueueDelta("Assistant:\nNew attempt.");
944
+ await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
945
+ assert.equal(delivered.length, 4);
946
+ assert.equal(delivered[3]?.options, undefined);
947
+ snapshot = runtime.getSnapshot();
948
+ assert.equal(snapshot.boundaryRepeats, 1);
949
+ assert.equal(snapshot.stalemate, false);
1032
950
  });
1033
951
 
1034
952
  it("session on/off overrides explicit main enabled settings", () => {
@@ -30,4 +30,4 @@ describe("watchdog scope artifact", () => {
30
30
  assert.equal(isWatchdogAutoFollowPromptEvent(event), true);
31
31
  assert.equal(isWatchdogAutoFollowPromptEvent({ prompt: "Watchdog auto-follow: address this blocker" }), false);
32
32
  });
33
- });
33
+ });
@@ -51,12 +51,10 @@ describe("watchdog settings", () => {
51
51
  assert.equal(result.config.enabled, false);
52
52
  assert.equal(result.config.main.enabled, false);
53
53
  assert.equal(result.config.children.enabled, false);
54
- assert.equal(result.config.asyncCompletion.enabled, false);
55
- assert.equal(result.config.delivery, "held");
56
- assert.equal(result.config.showDuringRun, false);
54
+ assert.equal(result.config.guidance.watchdogMd, true);
57
55
  assert.equal(result.config.agentEndTimeoutMs, 30_000);
58
56
  assert.equal(result.config.children.watchdogTailTimeoutMs, 120_000);
59
- assert.equal(result.config.autoFollow.maxAttempts, 3);
57
+ assert.equal(result.config.stalemateRepeats, 3);
60
58
  assert.deepEqual(result.config.scope, { enabled: true });
61
59
  assert.deepEqual(result.config.cadence, { everyNTools: null });
62
60
  assert.deepEqual(result.config.lsp, { enabled: true, timeoutMs: 3000, maxFiles: 20, maxDiagnostics: 50 });
@@ -83,14 +81,14 @@ describe("watchdog settings", () => {
83
81
  subagents: {
84
82
  watchdog: {
85
83
  enabled: true,
86
- autoFollow: { blockers: true, maxAttempts: 5 },
84
+ stalemateRepeats: 5,
87
85
  children: {
88
- autoFollow: { maxAttempts: 4 },
86
+ cadence: { everyNTools: 10 },
89
87
  overrides: {
90
- worker: { enabled: true, model: "anthropic/claude-test" },
88
+ worker: { enabled: true, model: "anthropic/claude-test", cadence: { everyNTools: 5 } },
91
89
  },
92
90
  },
93
- guidance: { systemPromptPath: "/tmp/user-watchdog.md" },
91
+ guidance: { watchdogMd: false },
94
92
  scope: { enabled: false },
95
93
  cadence: { everyNTools: 10 },
96
94
  },
@@ -99,11 +97,10 @@ describe("watchdog settings", () => {
99
97
  writeJson(projectSettingsPath(), {
100
98
  subagents: {
101
99
  watchdog: {
102
- autoFollow: { stalemateRepeats: 2 },
100
+ stalemateRepeats: 2,
103
101
  lsp: { enabled: false, timeoutMs: 1500, maxFiles: 4, maxDiagnostics: 7 },
104
102
  main: { model: "openai/gpt-test" },
105
103
  children: {
106
- autoFollow: { blockers: false },
107
104
  overrides: {
108
105
  worker: { thinking: "medium" },
109
106
  reviewer: { enabled: false },
@@ -115,7 +112,6 @@ describe("watchdog settings", () => {
115
112
 
116
113
  const result = resolveWatchdogConfig(tempProject, {
117
114
  session: {
118
- autoFollow: { maxAttempts: null },
119
115
  children: {
120
116
  overrides: {
121
117
  worker: { enabled: false },
@@ -127,16 +123,17 @@ describe("watchdog settings", () => {
127
123
  assert.equal(result.ok, true);
128
124
  assert.equal(result.config.main.enabled, true);
129
125
  assert.equal(result.config.main.model, "openai/gpt-test");
130
- assert.deepEqual(result.config.autoFollow, { blockers: true, maxAttempts: null, stalemateRepeats: 2 });
131
- assert.equal(result.config.guidance.systemPromptPath, "/tmp/user-watchdog.md");
126
+ assert.equal(result.config.stalemateRepeats, 2);
127
+ assert.equal(result.config.guidance.watchdogMd, false);
132
128
  assert.deepEqual(result.config.scope, { enabled: false });
133
129
  assert.deepEqual(result.config.cadence, { everyNTools: 10 });
130
+ assert.deepEqual(result.config.children.cadence, { everyNTools: 10 });
134
131
  assert.deepEqual(result.config.lsp, { enabled: false, timeoutMs: 1500, maxFiles: 4, maxDiagnostics: 7 });
135
- assert.deepEqual(result.config.children.autoFollow, { blockers: false, maxAttempts: 4, stalemateRepeats: 3 });
136
132
  assert.deepEqual(result.config.children.overrides.worker, {
137
133
  enabled: false,
138
134
  model: "anthropic/claude-test",
139
135
  thinking: "medium",
136
+ cadence: { everyNTools: 5 },
140
137
  });
141
138
  assert.deepEqual(result.config.children.overrides.reviewer, { enabled: false });
142
139
  });
@@ -178,6 +175,29 @@ describe("watchdog settings", () => {
178
175
  );
179
176
  });
180
177
 
178
+ it("parses launch rules and rejects malformed rule fields", () => {
179
+ const rules = {
180
+ action: "block",
181
+ roleModels: { scout: { allow: ["openai-codex/gpt-5.6-luna:max"] }, oracle: { deny: ["*"], note: "ask first" } },
182
+ } as const;
183
+ writeJson(userSettingsPath(), {
184
+ subagents: {
185
+ watchdog: { rules },
186
+ },
187
+ });
188
+ const result = resolveWatchdogConfig(tempProject);
189
+ assert.equal(result.ok, true, result.errors[0]?.message);
190
+ assert.deepEqual(result.config.rules, rules);
191
+ assert.equal(resolveWatchdogConfig(tempProject, { session: { enabled: true } }).config.rules?.action, "block");
192
+
193
+ writeJson(userSettingsPath(), { subagents: { watchdog: { rules: { action: "shout" } } } });
194
+ assert.match(resolveWatchdogConfig(tempProject).errors[0]?.message ?? "", /invalid 'subagents\.watchdog\.rules\.action'; expected 'warn' or 'block'/);
195
+ writeJson(userSettingsPath(), { subagents: { watchdog: { rules: { roleModels: { scout: { allowed: ["x"] } } } } } });
196
+ assert.match(resolveWatchdogConfig(tempProject).errors[0]?.message ?? "", /unknown field 'subagents\.watchdog\.rules\.roleModels\.scout\.allowed'/);
197
+ writeJson(userSettingsPath(), { subagents: { watchdog: {} } });
198
+ assert.equal(resolveWatchdogConfig(tempProject).config.rules, undefined);
199
+ });
200
+
181
201
  it("rejects invalid scope and cadence config at settings load", () => {
182
202
  writeJson(userSettingsPath(), {
183
203
  subagents: {
@@ -202,6 +222,9 @@ describe("watchdog settings", () => {
202
222
  result = resolveWatchdogConfig(tempProject);
203
223
  assert.equal(result.ok, false);
204
224
  assert.match(result.errors[0]?.message ?? "", /invalid 'subagents\.watchdog\.cadence\.everyNTools'/);
225
+
226
+ writeJson(userSettingsPath(), { subagents: { watchdog: { children: { cadence: { everyNTools: 4 } } } } });
227
+ assert.match(resolveWatchdogConfig(tempProject).errors[0]?.message ?? "", /invalid 'subagents\.watchdog\.children\.cadence\.everyNTools'/);
205
228
  });
206
229
 
207
230
  it("rejects invalid LSP config at settings load", () => {
@@ -297,7 +297,7 @@ describe("workflow chat progress rendering", () => {
297
297
  assert.doesNotMatch(text, /failed\s+detaches/);
298
298
  });
299
299
 
300
- it("seeds declared preflight lanes as planned before workflow trace entries", () => {
300
+ it("keeps advisory preflight lanes in plan metadata instead of runtime rows", () => {
301
301
  const preflight = {
302
302
  version: 1 as const,
303
303
  coverage: "partial" as const,
@@ -306,40 +306,57 @@ describe("workflow chat progress rendering", () => {
306
306
  { key: "review", mode: "review" as const, expectedOutput: "review.md" },
307
307
  ],
308
308
  };
309
- assert.deepEqual(buildWorkflowChatProgressRows([], preflight).map((row) => ({ key: row.key, state: row.state })), [
310
- { key: "writer", state: "planned" },
311
- { key: "review", state: "planned" },
312
- ]);
313
- const text = componentText(renderSubagentResult({
314
- content: [{ type: "text", text: "Workflow running." }],
309
+ assert.deepEqual(buildWorkflowChatProgressRows([], preflight), []);
310
+ const result = {
311
+ content: [{ type: "text" as const, text: "Workflow running." }],
315
312
  details: {
316
- mode: "workflow",
313
+ mode: "workflow" as const,
317
314
  runId: "wf_planned",
318
315
  results: [],
319
316
  preflight,
320
- chatProgress: { mode: "live-card", repoRelation: "same", repoLabel: "pi-subagents" },
317
+ chatProgress: { mode: "live-card" as const, repoRelation: "same" as const, repoLabel: "pi-subagents" },
321
318
  workflow: { trace: [], emits: [], console: [] },
322
319
  },
323
- }, { expanded: false }, theme as any));
324
- assert.match(text, /planned\s+writer/);
325
- assert.match(text, /planned\s+review/);
320
+ };
321
+ const text = componentText(renderSubagentResult(result, { expanded: false }, theme as any));
322
+ assert.match(text, /Plan: 2 lanes · writer, review/);
323
+ assert.match(text, /waiting for workflow child launches/);
324
+ assert.doesNotMatch(text, /planned\s+writer/);
325
+ assert.doesNotMatch(text, /planned\s+review/);
326
326
  assert.doesNotMatch(text, /mode:mutation/);
327
327
  assert.doesNotMatch(text, /expected:review\.md/);
328
- assert.doesNotMatch(text, /waiting for workflow child launches/);
329
328
 
330
- const expanded = componentText(renderSubagentResult({
331
- content: [{ type: "text", text: "Workflow running." }],
332
- details: {
333
- mode: "workflow",
334
- runId: "wf_planned",
335
- results: [],
336
- preflight,
337
- chatProgress: { mode: "live-card", repoRelation: "same", repoLabel: "pi-subagents" },
338
- workflow: { trace: [], emits: [], console: [] },
339
- },
340
- }, { expanded: true }, theme as any));
341
- assert.match(expanded, /mode:mutation/);
342
- assert.match(expanded, /expected:review\.md/);
329
+ const expanded = componentText(renderSubagentResult(result, { expanded: true }, theme as any));
330
+ assert.match(expanded, /Plan: 2 lanes · writer, review/);
331
+ assert.doesNotMatch(expanded, /planned\s+writer/);
332
+ assert.doesNotMatch(expanded, /planned\s+review/);
333
+ });
334
+
335
+ it("annotates an authoritative dotted child without adding its declared root", () => {
336
+ const preflight = { version: 1 as const, coverage: "partial" as const, lanes: [{ key: "pr14", mode: "review" as const }] };
337
+ const rows = buildWorkflowChatProgressRows([
338
+ { operation: "run", key: "pr14.quality", generatedLaneKey: "pr14", state: "started", agent: "reviewer" },
339
+ ], preflight);
340
+
341
+ assert.deepEqual(rows.map((row) => ({ key: row.key, state: row.state, mode: row.preflight?.mode })), [
342
+ { key: "pr14.quality", state: "running", mode: "review" },
343
+ ]);
344
+ });
345
+
346
+ it("prefers a specific preflight lane over an earlier broad generated alias", () => {
347
+ const preflight = {
348
+ version: 1 as const,
349
+ coverage: "partial" as const,
350
+ lanes: [
351
+ { key: "writer", mode: "mutation" as const },
352
+ { key: "writer.quality", mode: "review" as const },
353
+ ],
354
+ };
355
+ const rows = buildWorkflowChatProgressRows([
356
+ { operation: "run", key: "writer.quality.deep", generatedLaneKey: "writer", state: "started", agent: "reviewer" },
357
+ ], preflight);
358
+
359
+ assert.equal(rows[0]?.preflight?.mode, "review");
343
360
  });
344
361
 
345
362
  it("uses the compact plan preview for collapsed workflow launch output", () => {
@@ -434,6 +451,7 @@ describe("workflow chat progress rendering", () => {
434
451
  key: `step-${index}`,
435
452
  state: "started" as const,
436
453
  label: `review ${index}`,
454
+ phase: `phase-${index}`,
437
455
  }));
438
456
  const result = {
439
457
  content: [{ type: "text" as const, text: "Workflow running." }],
@@ -454,6 +472,7 @@ describe("workflow chat progress rendering", () => {
454
472
  const expanded = renderSubagentResult(result, { expanded: true }, theme as any, undefined, { horizontalSpacing: 0, compactResultMaxLines: 3 }).render(120);
455
473
  assert.ok(expanded.length > 3);
456
474
  assert.match(expanded[1]!, /^ Repo pi-subagents\s*$/);
475
+ assert.match(expanded.join("\n"), /phase-9 1 active/);
457
476
  assert.doesNotMatch(expanded.join("\n"), /rows hidden · .* expands/);
458
477
  });
459
478