@selesai/code 0.5.20 → 0.5.21

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 (263) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  4. package/dist/extensions/pi-subagents/README.md +602 -220
  5. package/dist/extensions/pi-subagents/banner.png +0 -0
  6. package/dist/extensions/pi-subagents/index.ts +1 -0
  7. package/dist/extensions/pi-subagents/install.mjs +3 -3
  8. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  9. package/dist/extensions/pi-subagents/package.json +26 -14
  10. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  11. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  12. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  13. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  14. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  15. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  16. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  18. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  19. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  20. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  21. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  22. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  23. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  24. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  25. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  26. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  27. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  28. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  29. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  30. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  31. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  32. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  33. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  34. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  35. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  36. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  37. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  38. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  39. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  40. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  41. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  42. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  43. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  44. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  45. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  46. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  48. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  49. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  50. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  51. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  53. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  54. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  55. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  56. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  57. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  58. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  59. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  60. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  61. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  63. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  65. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  66. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  67. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  68. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  69. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  70. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  71. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  72. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  73. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  74. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  76. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  78. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  80. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  82. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  83. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  87. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  88. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  89. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  90. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  94. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  97. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  98. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  99. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  100. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  101. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  102. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  103. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  104. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  105. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  106. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  107. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  108. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  109. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  110. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  111. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  112. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  113. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  114. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  115. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  116. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  117. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  120. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  121. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  122. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  124. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  125. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  126. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  127. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  140. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  141. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  142. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  143. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  144. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  145. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  146. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  147. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  148. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  149. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  150. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  151. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  152. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  153. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  154. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  155. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  156. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  157. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  158. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  159. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  160. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  161. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  162. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  163. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  164. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  165. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  166. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  167. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  168. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  169. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  170. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  171. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  172. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  173. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  174. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  175. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  176. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  177. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  178. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  179. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  180. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  181. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  182. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  183. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  184. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  185. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  186. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  187. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  188. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  189. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  190. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  191. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  192. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  194. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  195. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  196. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  197. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  198. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  199. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  200. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  201. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  202. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  203. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  204. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  205. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  206. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  207. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  208. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  209. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  210. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  211. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  212. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  213. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  214. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  215. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  216. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  217. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  218. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  222. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  223. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  224. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  225. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  226. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  227. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  228. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  229. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  230. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  232. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  233. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  234. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  235. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  236. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  237. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  238. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  239. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  240. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  250. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  251. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  252. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  253. package/dist/extensions/question/batch.ts +39 -0
  254. package/dist/extensions/question/dialog-adapter.ts +23 -1
  255. package/dist/extensions/question/index.ts +237 -7
  256. package/dist/extensions/question/schemas.ts +28 -15
  257. package/dist/extensions/question/tests/batch.test.ts +26 -0
  258. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  259. package/dist/extensions/question/tui-adapter.ts +2 -3
  260. package/dist/extensions/question/types.ts +37 -0
  261. package/dist/extensions/question/ui-protocol.ts +3 -3
  262. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  263. package/package.json +1 -1
@@ -0,0 +1,144 @@
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";
5
+ import { describe, it } from "node:test";
6
+ import {
7
+ WatchdogLspDiagnosticsLedger,
8
+ collectWatchdogLspDiagnostics,
9
+ formatWatchdogLspDiagnosticsBlock,
10
+ watchdogWarningFromLspDiagnostics,
11
+ } from "../../src/watchdog/lsp-diagnostics.ts";
12
+ import type { WatchdogLspResult } from "../../src/watchdog/types.ts";
13
+
14
+ function result(diagnostics: WatchdogLspResult["diagnostics"]): WatchdogLspResult {
15
+ return {
16
+ status: "ok",
17
+ provider: "stub-lsp",
18
+ checkedPaths: ["src/file.ts"],
19
+ skippedPaths: [],
20
+ diagnostics,
21
+ };
22
+ }
23
+
24
+ describe("watchdog LSP diagnostics", () => {
25
+ it("formats diagnostics for watchdog review input", () => {
26
+ const block = formatWatchdogLspDiagnosticsBlock(result([{
27
+ path: "src/file.ts",
28
+ line: 2,
29
+ column: 3,
30
+ severity: "error",
31
+ source: "typescript",
32
+ code: "TS2322",
33
+ message: "Type mismatch.",
34
+ }]));
35
+
36
+ assert.match(block, /^LSP diagnostics:/);
37
+ assert.match(block, /src\/file\.ts:2:3 error TS2322 typescript: Type mismatch\./);
38
+ });
39
+
40
+ it("omits info and hints from watchdog review input", () => {
41
+ const block = formatWatchdogLspDiagnosticsBlock(result([{
42
+ path: "src/file.ts",
43
+ line: 2,
44
+ column: 3,
45
+ severity: "info",
46
+ source: "typescript",
47
+ message: "Helpful note.",
48
+ }, {
49
+ path: "src/file.ts",
50
+ line: 3,
51
+ column: 4,
52
+ severity: "hint",
53
+ source: "typescript",
54
+ message: "Suggestion.",
55
+ }]));
56
+
57
+ assert.equal(block, "");
58
+ });
59
+
60
+ it("maps errors to blockers and warnings to concerns", () => {
61
+ const blocker = watchdogWarningFromLspDiagnostics(result([{
62
+ path: "src/file.ts",
63
+ line: 1,
64
+ column: 1,
65
+ severity: "error",
66
+ source: "typescript",
67
+ message: "Cannot find name 'x'.",
68
+ }]));
69
+ assert.equal(blocker?.severity, "blocker");
70
+ assert.equal(blocker?.source, "lsp");
71
+
72
+ const concern = watchdogWarningFromLspDiagnostics(result([{
73
+ path: "src/file.ts",
74
+ line: 1,
75
+ column: 1,
76
+ severity: "warning",
77
+ source: "typescript",
78
+ message: "Unused value.",
79
+ }]));
80
+ assert.equal(concern?.severity, "concern");
81
+
82
+ const info = watchdogWarningFromLspDiagnostics(result([{
83
+ path: "src/file.ts",
84
+ line: 1,
85
+ column: 1,
86
+ severity: "info",
87
+ source: "typescript",
88
+ message: "Helpful note.",
89
+ }]));
90
+ assert.equal(info, undefined);
91
+ });
92
+
93
+ it("returns a failed result for malformed language-server JSON", async () => {
94
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "pi-watchdog-lsp-"));
95
+ try {
96
+ const binDir = path.join(temp, "node_modules", ".bin");
97
+ fs.mkdirSync(path.join(temp, "src"), { recursive: true });
98
+ fs.mkdirSync(binDir, { recursive: true });
99
+ fs.writeFileSync(path.join(temp, "src", "file.ts"), "export const value = 1;\n", "utf-8");
100
+ const scriptPath = path.join(binDir, "tls-malformed.js");
101
+ fs.writeFileSync(scriptPath, "process.stdout.write('Content-Length: 8\\r\\n\\r\\nnot-json'); setTimeout(() => process.exit(0), 50);\n", "utf-8");
102
+ if (process.platform === "win32") {
103
+ fs.writeFileSync(path.join(binDir, "typescript-language-server.cmd"), `@echo off\r\n"${process.execPath}" "%~dp0\\tls-malformed.js" %*\r\n`, "utf-8");
104
+ } else {
105
+ const commandPath = path.join(binDir, "typescript-language-server");
106
+ fs.writeFileSync(commandPath, `#!/bin/sh\nexec "${process.execPath}" "$(dirname "$0")/tls-malformed.js" "$@"\n`, { encoding: "utf-8", mode: 0o755 });
107
+ }
108
+
109
+ const diagnostics = await collectWatchdogLspDiagnostics({
110
+ cwd: temp,
111
+ root: temp,
112
+ changedPaths: ["src/file.ts"],
113
+ config: { enabled: true, timeoutMs: 500, maxFiles: 10, maxDiagnostics: 10 },
114
+ });
115
+
116
+ assert.equal(diagnostics.status, "failed");
117
+ assert.match(diagnostics.message ?? "", /Invalid LSP JSON-RPC response/);
118
+ } finally {
119
+ try {
120
+ fs.rmSync(temp, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
121
+ } catch (error) {
122
+ if (process.platform !== "win32" || (error as NodeJS.ErrnoException).code !== "EPERM") throw error;
123
+ }
124
+ }
125
+ });
126
+
127
+ it("suppresses repeated diagnostic identities until the file clears", () => {
128
+ const ledger = new WatchdogLspDiagnosticsLedger();
129
+ const diagnostic = {
130
+ path: "src/file.ts",
131
+ line: 1,
132
+ column: 1,
133
+ severity: "warning" as const,
134
+ source: "typescript",
135
+ code: "TS6133",
136
+ message: "Unused value.",
137
+ };
138
+
139
+ assert.equal(ledger.reduce(result([diagnostic])).diagnostics.length, 1);
140
+ assert.equal(ledger.reduce(result([{ ...diagnostic, line: 4, column: 9 }])).diagnostics.length, 0);
141
+ assert.equal(ledger.reduce(result([])).diagnostics.length, 0);
142
+ assert.equal(ledger.reduce(result([{ ...diagnostic, line: 8 }])).diagnostics.length, 1);
143
+ });
144
+ });
@@ -0,0 +1,121 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { recommendStrongWatchdogModel, resolveWatchdogModelInput } from "../../src/watchdog/model-selection.ts";
4
+
5
+ function createCtx(
6
+ current: { provider: string; id: string },
7
+ authenticated: string[] = ["openai-codex/gpt-5.5", "anthropic/claude-opus-4-8"],
8
+ models: Array<{ provider: string; id: string; reasoning: boolean; thinkingLevelMap?: Record<string, string | null> }> = [
9
+ { provider: "openai-codex", id: "gpt-5.5", reasoning: true },
10
+ { provider: "anthropic", id: "claude-opus-4-8", reasoning: true },
11
+ ],
12
+ ) {
13
+ return {
14
+ cwd: "/tmp/watchdog-model-selection",
15
+ model: current,
16
+ modelRegistry: {
17
+ getAvailable: () => models,
18
+ find: (provider: string, id: string) => models.find((model) => model.provider === provider && model.id === id),
19
+ hasConfiguredAuth: (model: { provider: string; id: string }) => authenticated.includes(`${model.provider}/${model.id}`),
20
+ },
21
+ } as never;
22
+ }
23
+
24
+ describe("watchdog model selection", () => {
25
+ it("recommends Opus 4.8 high when the main session is GPT 5.5", () => {
26
+ const recommendation = recommendStrongWatchdogModel(createCtx({ provider: "openai-codex", id: "gpt-5.5" }));
27
+
28
+ assert.equal(recommendation.model, "anthropic/claude-opus-4-8");
29
+ assert.equal(recommendation.thinking, "high");
30
+ });
31
+
32
+ it("recommends GPT 5.5 high when the main session is Opus 4.8", () => {
33
+ const recommendation = recommendStrongWatchdogModel(createCtx({ provider: "anthropic", id: "claude-opus-4-8" }));
34
+
35
+ assert.equal(recommendation.model, "openai-codex/gpt-5.5");
36
+ assert.equal(recommendation.thinking, "high");
37
+ });
38
+
39
+ it("requires the complementary strong model to be authenticated", () => {
40
+ assert.throws(
41
+ () => recommendStrongWatchdogModel(createCtx({ provider: "openai-codex", id: "gpt-5.5" }, ["openai-codex/gpt-5.5"])),
42
+ /No authenticated strong complementary watchdog model/,
43
+ );
44
+ });
45
+
46
+ it("does not treat GPT 5.5 mini variants as the strong GPT 5.5 watchdog", () => {
47
+ const models = [
48
+ { provider: "openai-codex", id: "gpt-5.5-mini", reasoning: true },
49
+ { provider: "openai-codex", id: "gpt-5.5-20250101-mini", reasoning: true },
50
+ ];
51
+ const ctx = {
52
+ cwd: "/tmp/watchdog-model-selection",
53
+ model: { provider: "anthropic", id: "claude-opus-4-8" },
54
+ modelRegistry: {
55
+ getAvailable: () => models,
56
+ find: (provider: string, id: string) => models.find((model) => model.provider === provider && model.id === id),
57
+ hasConfiguredAuth: (model: { provider: string; id: string }) => `${model.provider}/${model.id}` === "openai-codex/gpt-5.5-mini" || `${model.provider}/${model.id}` === "openai-codex/gpt-5.5-20250101-mini",
58
+ },
59
+ } as never;
60
+
61
+ assert.throws(
62
+ () => recommendStrongWatchdogModel(ctx),
63
+ /No authenticated strong complementary watchdog model/,
64
+ );
65
+ });
66
+
67
+ it("does not treat dated Opus mini variants as the strong Opus 4.8 watchdog", () => {
68
+ const models = [{ provider: "anthropic", id: "claude-opus-4-8-20250101-mini", reasoning: true }];
69
+ const ctx = {
70
+ cwd: "/tmp/watchdog-model-selection",
71
+ model: { provider: "openai-codex", id: "gpt-5.5" },
72
+ modelRegistry: {
73
+ getAvailable: () => models,
74
+ find: (provider: string, id: string) => models.find((model) => model.provider === provider && model.id === id),
75
+ hasConfiguredAuth: (model: { provider: string; id: string }) => `${model.provider}/${model.id}` === "anthropic/claude-opus-4-8-20250101-mini",
76
+ },
77
+ } as never;
78
+
79
+ assert.throws(
80
+ () => recommendStrongWatchdogModel(ctx),
81
+ /No authenticated strong complementary watchdog model/,
82
+ );
83
+ });
84
+
85
+ it("does not treat cheap-prefixed variants as strong watchdog families", () => {
86
+ const models = [
87
+ { provider: "openai-codex", id: "cheap-gpt-5.5", reasoning: true },
88
+ { provider: "anthropic", id: "cheap-claude-opus-4-8", reasoning: true },
89
+ ];
90
+ const ctx = {
91
+ cwd: "/tmp/watchdog-model-selection",
92
+ model: { provider: "anthropic", id: "claude-opus-4-8" },
93
+ modelRegistry: {
94
+ getAvailable: () => models,
95
+ find: (provider: string, id: string) => models.find((model) => model.provider === provider && model.id === id),
96
+ hasConfiguredAuth: (model: { provider: string; id: string }) => `${model.provider}/${model.id}` === "openai-codex/cheap-gpt-5.5" || `${model.provider}/${model.id}` === "anthropic/cheap-claude-opus-4-8",
97
+ },
98
+ } as never;
99
+
100
+ assert.throws(
101
+ () => recommendStrongWatchdogModel(ctx),
102
+ /No authenticated strong complementary watchdog model/,
103
+ );
104
+ });
105
+
106
+ it("canonicalizes explicit models and preserves thinking suffixes", () => {
107
+ const resolved = resolveWatchdogModelInput(createCtx({ provider: "anthropic", id: "claude-opus-4-8" }), "openai-codex/gpt-5-5:high");
108
+
109
+ assert.equal(resolved.model, "openai-codex/gpt-5.5");
110
+ assert.equal(resolved.thinking, "high");
111
+ });
112
+
113
+ it("accepts a max suffix for an authenticated watchdog model", () => {
114
+ const ctx = createCtx(
115
+ { provider: "openai", id: "gpt-5" },
116
+ ["openai/gpt-5"],
117
+ [{ provider: "openai", id: "gpt-5", reasoning: true, thinkingLevelMap: { max: "max" } }],
118
+ );
119
+ assert.equal(resolveWatchdogModelInput(ctx, "openai/gpt-5:max").thinking, "max");
120
+ });
121
+ });
@@ -0,0 +1,76 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { formatWatchdogWarningRenderText } from "../../src/watchdog/render.ts";
4
+ import { createWatchdogWarningMessage, formatWatchdogWarningContent } from "../../src/watchdog/warning-format.ts";
5
+ import { SUBAGENT_WATCHDOG_WARNING_TYPE, type WatchdogWarningDetails } from "../../src/watchdog/types.ts";
6
+
7
+ describe("watchdog warning formatting and rendering", () => {
8
+ it("puts all LLM-needed warning fields in custom message content", () => {
9
+ const content = formatWatchdogWarningContent({
10
+ severity: "blocker",
11
+ category: "correctness",
12
+ source: "main",
13
+ summary: "Fix <bug>",
14
+ evidence: "The failing assertion says A & B differ.",
15
+ recommendedAction: "Update the parser before finalizing.",
16
+ confidence: "high",
17
+ agent: "main",
18
+ runId: "run-1",
19
+ stale: true,
20
+ autoFollowAttempt: 2,
21
+ });
22
+
23
+ assert.match(content, /^<subagent_watchdog severity="blocker" category="correctness" source="main" guidance="weigh, don't blindly obey">/);
24
+ assert.match(content, /<summary>Fix &lt;bug&gt;<\/summary>/);
25
+ assert.match(content, /<evidence>The failing assertion says A &amp; B differ\.<\/evidence>/);
26
+ assert.match(content, /<recommended_action>Update the parser before finalizing\.<\/recommended_action>/);
27
+ assert.match(content, /<confidence>high<\/confidence>/);
28
+ assert.match(content, /<agent>main<\/agent>/);
29
+ assert.match(content, /<run_id>run-1<\/run_id>/);
30
+ assert.match(content, /<stale>true<\/stale>/);
31
+ assert.match(content, /<auto_follow_attempt>2<\/auto_follow_attempt>/);
32
+ assert.match(content, /<blocker_guidance>/);
33
+ });
34
+
35
+ it("keeps structured details alongside the LLM-visible content", () => {
36
+ const message = createWatchdogWarningMessage({
37
+ severity: "concern",
38
+ summary: "Missing focused test",
39
+ evidence: "The parser changed without a unit test.",
40
+ recommendedAction: "Add a parser regression test.",
41
+ });
42
+
43
+ assert.equal(message.customType, SUBAGENT_WATCHDOG_WARNING_TYPE);
44
+ assert.equal(message.display, true);
45
+ assert.equal(message.details.category, "other");
46
+ assert.equal(message.details.source, "main");
47
+ assert.match(message.content, /<summary>Missing focused test<\/summary>/);
48
+ assert.match(message.content, /<recommended_action>Add a parser regression test\.<\/recommended_action>/);
49
+ });
50
+
51
+ it("renders concern, blocker, stale, failed, and stalemate states in text", () => {
52
+ const base: WatchdogWarningDetails = {
53
+ severity: "blocker",
54
+ category: "loop-risk",
55
+ source: "main",
56
+ summary: "Repeated blocker",
57
+ evidence: "The same issue survived another auto-follow.",
58
+ recommendedAction: "Stop auto-follow and ask for user review.",
59
+ state: "stalemate",
60
+ stalemateRepeats: 3,
61
+ };
62
+
63
+ const stalemate = formatWatchdogWarningRenderText(base);
64
+ const stale = formatWatchdogWarningRenderText({ ...base, severity: "concern", state: "stale", stale: true });
65
+ const failed = formatWatchdogWarningRenderText({ ...base, state: "failed", error: "provider aborted" });
66
+ const displayed = formatWatchdogWarningRenderText({ ...base, state: "displayed" });
67
+
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/);
72
+ assert.match(failed, /failed review/);
73
+ assert.match(failed, /Failure: provider aborted/);
74
+ assert.match(displayed, /Subagent watchdog Blocker \(displayed\): Repeated blocker/);
75
+ });
76
+ });
@@ -0,0 +1,327 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { StreamFn } from "@earendil-works/pi-agent-core";
4
+ import {
5
+ createAssistantMessageEventStream,
6
+ fauxAssistantMessage,
7
+ fauxToolCall,
8
+ type AssistantMessage,
9
+ type Context,
10
+ type Model,
11
+ type SimpleStreamOptions,
12
+ } from "@earendil-works/pi-ai";
13
+ import { DEFAULT_WATCHDOG_CONFIG } from "../../src/watchdog/settings.ts";
14
+ import { createMainWatchdogReview, resolveWatchdogReviewModel } from "../../src/watchdog/review.ts";
15
+ import type { WatchdogReviewRequest } from "../../src/watchdog/runtime.ts";
16
+ import type { ResolvedWatchdogConfig, WatchdogWarning } from "../../src/watchdog/types.ts";
17
+
18
+ function model(provider: string, id: string, overrides: Partial<Model<any>> = {}): Model<any> {
19
+ return {
20
+ id,
21
+ name: id,
22
+ api: "faux",
23
+ provider,
24
+ baseUrl: "https://example.invalid",
25
+ reasoning: true,
26
+ input: ["text"],
27
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
28
+ contextWindow: 100_000,
29
+ maxTokens: 4_096,
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ function cloneConfig(): ResolvedWatchdogConfig {
35
+ return {
36
+ ...DEFAULT_WATCHDOG_CONFIG,
37
+ guidance: { ...DEFAULT_WATCHDOG_CONFIG.guidance },
38
+ autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.autoFollow },
39
+ main: { ...DEFAULT_WATCHDOG_CONFIG.main },
40
+ children: {
41
+ ...DEFAULT_WATCHDOG_CONFIG.children,
42
+ autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.children.autoFollow },
43
+ overrides: { ...DEFAULT_WATCHDOG_CONFIG.children.overrides },
44
+ },
45
+ asyncCompletion: { ...DEFAULT_WATCHDOG_CONFIG.asyncCompletion },
46
+ lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp },
47
+ };
48
+ }
49
+
50
+ function enabledConfig(main: Partial<ResolvedWatchdogConfig["main"]> = {}): ResolvedWatchdogConfig {
51
+ const config = cloneConfig();
52
+ config.enabled = true;
53
+ config.main = { ...config.main, enabled: true, ...main };
54
+ return config;
55
+ }
56
+
57
+ function createCtx(input: {
58
+ current?: Model<any>;
59
+ models?: Model<any>[];
60
+ authenticated?: string[];
61
+ thinkingLevel?: string;
62
+ providerConfig?: { provider: string; api: string; streamSimple: StreamFn };
63
+ }) {
64
+ const allModels = input.models ?? (input.current ? [input.current] : []);
65
+ const authenticated = new Set(input.authenticated ?? allModels.map((entry) => `${entry.provider}/${entry.id}`));
66
+ return {
67
+ cwd: "/tmp/watchdog-review",
68
+ model: input.current,
69
+ ...(input.thinkingLevel ? { thinkingLevel: input.thinkingLevel } : {}),
70
+ signal: undefined,
71
+ getSystemPrompt: () => "Parent system prompt",
72
+ modelRegistry: {
73
+ getAvailable: () => allModels.filter((entry) => authenticated.has(`${entry.provider}/${entry.id}`)),
74
+ find: (provider: string, id: string) => allModels.find((entry) => entry.provider === provider && entry.id === id),
75
+ hasConfiguredAuth: (entry: Model<any>) => authenticated.has(`${entry.provider}/${entry.id}`),
76
+ getApiKeyAndHeaders: async (entry: Model<any>) => authenticated.has(`${entry.provider}/${entry.id}`)
77
+ ? { ok: true as const, apiKey: `key-${entry.provider}-${entry.id}`, headers: { "x-model": entry.id }, env: { WATCHDOG_PROVIDER: entry.provider } }
78
+ : { ok: false as const, error: `No auth for ${entry.provider}/${entry.id}` },
79
+ getRegisteredProviderConfig: (provider: string) => input.providerConfig?.provider === provider ? input.providerConfig : undefined,
80
+ },
81
+ } as never;
82
+ }
83
+
84
+ function responseStream(message: AssistantMessage) {
85
+ const stream = createAssistantMessageEventStream();
86
+ queueMicrotask(() => {
87
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
88
+ stream.push({ type: "error", reason: message.stopReason, error: message });
89
+ } else {
90
+ stream.push({ type: "done", reason: message.stopReason, message });
91
+ }
92
+ });
93
+ return stream;
94
+ }
95
+
96
+ function createStreamFn(responses: AssistantMessage[]) {
97
+ const calls: Array<{ model: Model<any>; context: Context; options?: SimpleStreamOptions }> = [];
98
+ const streamFn: StreamFn = (nextModel, context, options) => {
99
+ calls.push({ model: nextModel, context, options });
100
+ return responseStream(responses.shift() ?? fauxAssistantMessage("done", { stopReason: "stop" }));
101
+ };
102
+ return { streamFn, calls };
103
+ }
104
+
105
+ function request(config: ResolvedWatchdogConfig, warnings: WatchdogWarning[]): WatchdogReviewRequest {
106
+ return {
107
+ delta: "Assistant changed src/example.ts and said tests passed.",
108
+ epoch: 1,
109
+ reviewId: 7,
110
+ config,
111
+ emitWarning(warning) {
112
+ warnings.push(warning);
113
+ return true;
114
+ },
115
+ };
116
+ }
117
+
118
+ describe("main watchdog review adapter", () => {
119
+ it("ignores clean freeform review text and emits no warnings", async () => {
120
+ const current = model("openai", "gpt-clean");
121
+ const ctx = createCtx({ current });
122
+ const { streamFn } = createStreamFn([fauxAssistantMessage("No concerns.", { stopReason: "stop" })]);
123
+ const warnings: WatchdogWarning[] = [];
124
+
125
+ const result = await createMainWatchdogReview(ctx, { streamFn })(request(enabledConfig(), warnings));
126
+
127
+ assert.deepEqual(warnings, []);
128
+ assert.equal(result?.stopReason, "stop");
129
+ });
130
+
131
+ it("records watchdog_warn emissions through the runtime seam", async () => {
132
+ const current = model("openai", "gpt-warning");
133
+ const ctx = createCtx({ current });
134
+ const { streamFn } = createStreamFn([
135
+ fauxAssistantMessage(fauxToolCall("watchdog_warn", {
136
+ severity: "blocker",
137
+ category: "correctness",
138
+ confidence: "high",
139
+ summary: "The test claim is unverified",
140
+ evidence: "The delta says tests passed but no test command appears.",
141
+ recommendedAction: "Run the focused test before accepting the result.",
142
+ }), { stopReason: "toolUse" }),
143
+ fauxAssistantMessage("done", { stopReason: "stop" }),
144
+ ]);
145
+ const warnings: WatchdogWarning[] = [];
146
+
147
+ await createMainWatchdogReview(ctx, { streamFn })(request(enabledConfig(), warnings));
148
+
149
+ assert.equal(warnings.length, 1);
150
+ assert.deepEqual(warnings[0], {
151
+ severity: "blocker",
152
+ category: "correctness",
153
+ confidence: "high",
154
+ source: "main",
155
+ summary: "The test claim is unverified",
156
+ evidence: "The delta says tests passed but no test command appears.",
157
+ recommendedAction: "Run the focused test before accepting the result.",
158
+ });
159
+ });
160
+
161
+ it("does not start the agent stream when the review request signal is already aborted", async () => {
162
+ const current = model("openai", "gpt-pre-abort");
163
+ const ctx = createCtx({ current });
164
+ const controller = new AbortController();
165
+ controller.abort();
166
+ let streamStarted = false;
167
+ const streamFn: StreamFn = () => {
168
+ streamStarted = true;
169
+ return responseStream(fauxAssistantMessage("should not start", { stopReason: "stop" }));
170
+ };
171
+ const warnings: WatchdogWarning[] = [];
172
+
173
+ const result = await createMainWatchdogReview(ctx, { streamFn })(
174
+ { ...request(enabledConfig(), warnings), signal: controller.signal },
175
+ );
176
+
177
+ assert.equal(streamStarted, false);
178
+ assert.equal(result?.stopReason, "aborted");
179
+ });
180
+
181
+ it("does not start the agent stream when the review request aborts during model setup", async () => {
182
+ const current = model("openai", "gpt-setup-abort");
183
+ const controller = new AbortController();
184
+ let releaseAuth!: () => void;
185
+ const authStarted = new Promise<void>((resolve) => { releaseAuth = resolve; });
186
+ let streamStarted = false;
187
+ const ctx = {
188
+ ...createCtx({ current }),
189
+ modelRegistry: {
190
+ ...createCtx({ current }).modelRegistry,
191
+ async getApiKeyAndHeaders(entry: Model<any>) {
192
+ await authStarted;
193
+ return { ok: true as const, apiKey: `key-${entry.provider}-${entry.id}` };
194
+ },
195
+ },
196
+ } as never;
197
+ const streamFn: StreamFn = () => {
198
+ streamStarted = true;
199
+ return responseStream(fauxAssistantMessage("should not start", { stopReason: "stop" }));
200
+ };
201
+ const warnings: WatchdogWarning[] = [];
202
+
203
+ const review = createMainWatchdogReview(ctx, { streamFn })(
204
+ { ...request(enabledConfig(), warnings), signal: controller.signal },
205
+ );
206
+ controller.abort();
207
+ releaseAuth();
208
+ const result = await review;
209
+
210
+ assert.equal(streamStarted, false);
211
+ assert.equal(result?.stopReason, "aborted");
212
+ });
213
+
214
+ it("aborts the underlying agent stream when the review request signal aborts", async () => {
215
+ const current = model("openai", "gpt-abort");
216
+ const ctx = createCtx({ current });
217
+ const controller = new AbortController();
218
+ let streamStarted!: () => void;
219
+ let streamAborted = false;
220
+ const started = new Promise<void>((resolve) => { streamStarted = resolve; });
221
+ const streamFn: StreamFn = (_nextModel, _context, options) => {
222
+ const stream = createAssistantMessageEventStream();
223
+ options?.signal?.addEventListener("abort", () => {
224
+ streamAborted = true;
225
+ stream.push({ type: "error", reason: "aborted", error: fauxAssistantMessage("aborted", { stopReason: "aborted" }) });
226
+ }, { once: true });
227
+ streamStarted();
228
+ return stream;
229
+ };
230
+ const warnings: WatchdogWarning[] = [];
231
+
232
+ const review = createMainWatchdogReview(ctx, { streamFn })(
233
+ { ...request(enabledConfig(), warnings), signal: controller.signal },
234
+ );
235
+ await started;
236
+ controller.abort();
237
+ const result = await review;
238
+
239
+ assert.equal(streamAborted, true);
240
+ assert.equal(result?.stopReason, "aborted");
241
+ });
242
+
243
+ it("does not expose mutating tools to the watchdog agent", async () => {
244
+ const current = model("openai", "gpt-readonly");
245
+ const ctx = createCtx({ current });
246
+ const { streamFn, calls } = createStreamFn([
247
+ fauxAssistantMessage(fauxToolCall("bash", { command: "touch should-not-run" }), { stopReason: "toolUse" }),
248
+ fauxAssistantMessage("done", { stopReason: "stop" }),
249
+ ]);
250
+ const warnings: WatchdogWarning[] = [];
251
+
252
+ await createMainWatchdogReview(ctx, { streamFn })(request(enabledConfig(), warnings));
253
+
254
+ assert.equal(warnings.length, 0);
255
+ assert.deepEqual(calls[0]?.context.tools?.map((tool) => tool.name).sort(), ["find", "grep", "ls", "read", "watchdog_warn"]);
256
+ const toolResult = calls[1]?.context.messages.find((message) => message.role === "toolResult" && message.toolName === "bash");
257
+ assert.equal(toolResult?.isError, true);
258
+ });
259
+
260
+ it("fails loudly for explicit unauthenticated watchdog models", async () => {
261
+ const current = model("openai", "gpt-current");
262
+ const watchdog = model("anthropic", "claude-watchdog");
263
+ const ctx = createCtx({ current, models: [current, watchdog], authenticated: [`${current.provider}/${current.id}`] });
264
+
265
+ await assert.rejects(
266
+ () => resolveWatchdogReviewModel(ctx, enabledConfig({ model: "anthropic/claude-watchdog" })),
267
+ /authenticated.*anthropic/s,
268
+ );
269
+ });
270
+
271
+ it("fails loudly for explicit missing watchdog models", async () => {
272
+ const current = model("openai", "gpt-current");
273
+ const ctx = createCtx({ current });
274
+
275
+ await assert.rejects(
276
+ () => resolveWatchdogReviewModel(ctx, enabledConfig({ model: "anthropic/missing-watchdog" })),
277
+ /was not found.*anthropic\/missing-watchdog/s,
278
+ );
279
+ });
280
+
281
+ it("uses the registered stream for matching custom providers", async () => {
282
+ const current = model("custom-provider", "watchdog", { api: "custom-api" });
283
+ const { streamFn, calls } = createStreamFn([fauxAssistantMessage("clean", { stopReason: "stop" })]);
284
+ const ctx = createCtx({ current, providerConfig: { provider: current.provider, api: "custom-api", streamSimple: streamFn } });
285
+ const warnings: WatchdogWarning[] = [];
286
+
287
+ const result = await createMainWatchdogReview(ctx)(request(enabledConfig(), warnings));
288
+
289
+ assert.equal(result?.stopReason, "stop");
290
+ assert.equal(calls.length, 1);
291
+ assert.equal(calls[0]?.model, current);
292
+ assert.equal(calls[0]?.options?.apiKey, "key-custom-provider-watchdog");
293
+ assert.equal(calls[0]?.options?.headers?.["x-model"], "watchdog");
294
+ assert.deepEqual(calls[0]?.options?.env, { WATCHDOG_PROVIDER: "custom-provider" });
295
+ });
296
+
297
+ it("falls back to the current session model and thinking when no watchdog model is configured", async () => {
298
+ const current = model("github-copilot", "gpt-session");
299
+ const ctx = createCtx({ current });
300
+ const { streamFn, calls } = createStreamFn([fauxAssistantMessage("clean", { stopReason: "stop" })]);
301
+ const warnings: WatchdogWarning[] = [];
302
+
303
+ await createMainWatchdogReview(ctx, { streamFn, getThinkingLevel: () => "high" })(request(enabledConfig(), warnings));
304
+
305
+ assert.equal(calls[0]?.model, current);
306
+ assert.equal(calls[0]?.options?.apiKey, "key-github-copilot-gpt-session");
307
+ assert.equal(calls[0]?.options?.reasoning, "high");
308
+ assert.deepEqual(calls[0]?.options?.env, { WATCHDOG_PROVIDER: "github-copilot" });
309
+ });
310
+
311
+ it("resolves configured model suffixes and thinking deterministically", async () => {
312
+ const current = model("openai", "gpt-current");
313
+ const dated = model("openai", "gpt-5-20260707");
314
+ const ctx = createCtx({ current, models: [current, dated] });
315
+
316
+ const suffixWins = await resolveWatchdogReviewModel(ctx, enabledConfig({ model: "openai.gpt_5:high", thinking: "low" }));
317
+ const explicitOff = await resolveWatchdogReviewModel(ctx, enabledConfig({ model: "openai/gpt-5-20260707", thinking: false }));
318
+ const suffixBeatsFalse = await resolveWatchdogReviewModel(ctx, enabledConfig({ model: "openai/gpt-5-20260707:medium", thinking: false }));
319
+
320
+ assert.equal(suffixWins.model, dated);
321
+ assert.equal(suffixWins.thinkingLevel, "high");
322
+ assert.equal(explicitOff.model, dated);
323
+ assert.equal(explicitOff.thinkingLevel, "off");
324
+ assert.equal(suffixBeatsFalse.model, dated);
325
+ assert.equal(suffixBeatsFalse.thinkingLevel, "medium");
326
+ });
327
+ });