@selesai/code 0.5.19 → 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 (265) 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-intercom/broker/spawn.test.ts +31 -0
  4. package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
  5. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  6. package/dist/extensions/pi-subagents/README.md +602 -220
  7. package/dist/extensions/pi-subagents/banner.png +0 -0
  8. package/dist/extensions/pi-subagents/index.ts +1 -0
  9. package/dist/extensions/pi-subagents/install.mjs +3 -3
  10. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  11. package/dist/extensions/pi-subagents/package.json +26 -14
  12. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  13. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  14. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  15. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  16. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  17. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  18. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  20. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  21. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  22. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  23. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  24. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  25. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  26. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  27. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  28. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  29. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  30. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  31. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  32. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  33. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  34. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  35. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  36. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  37. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  38. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  39. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  40. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  41. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  42. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  43. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  44. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  45. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  46. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  47. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  48. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  49. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  50. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  51. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  52. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  53. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  54. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  55. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  56. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  57. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  58. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  59. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  61. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  63. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  65. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  66. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  67. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  68. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  69. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  70. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  71. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  72. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  73. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  74. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  76. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  78. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  80. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  84. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  86. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  88. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  89. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  90. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  92. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  95. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  96. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  97. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  98. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  99. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  100. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  101. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  102. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  103. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  104. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  105. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  106. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  107. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  108. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  109. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  110. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  111. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  112. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  113. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  114. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  115. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  116. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  117. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  118. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  119. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  120. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  121. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  122. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  127. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  140. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  141. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  142. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  143. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  144. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  145. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  146. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  147. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  148. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  149. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  150. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  151. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  152. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  153. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  154. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  155. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  156. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  157. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  158. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  159. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  160. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  161. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  162. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  163. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  164. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  165. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  166. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  167. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  168. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  169. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  170. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  171. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  172. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  173. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  175. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  176. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  177. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  178. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  179. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  180. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  181. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  182. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  183. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  184. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  185. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  186. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  187. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  188. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  189. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  190. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  191. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  192. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  193. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  194. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  195. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  196. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  197. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  198. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  199. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  200. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  201. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  202. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  203. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  204. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  205. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  206. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  207. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  208. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  209. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  210. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  211. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  212. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  213. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  214. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  215. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  216. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  217. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  218. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  219. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  220. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  221. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  222. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  223. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  224. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  225. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  226. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  227. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  228. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  229. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  230. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  232. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  233. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  234. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  235. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  236. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  237. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  238. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  239. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  240. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  250. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  251. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  252. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  253. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  254. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  255. package/dist/extensions/question/batch.ts +39 -0
  256. package/dist/extensions/question/dialog-adapter.ts +23 -1
  257. package/dist/extensions/question/index.ts +237 -7
  258. package/dist/extensions/question/schemas.ts +28 -15
  259. package/dist/extensions/question/tests/batch.test.ts +26 -0
  260. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  261. package/dist/extensions/question/tui-adapter.ts +2 -3
  262. package/dist/extensions/question/types.ts +37 -0
  263. package/dist/extensions/question/ui-protocol.ts +3 -3
  264. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  265. package/package.json +2 -1
@@ -0,0 +1,714 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { ExtensionContext } from "@selesai/code";
3
+ import { computeWatchdogRepoChangeSignature, eventIndicatesRepoEdit, type WatchdogRepoChangeSignature } from "./change-signature.ts";
4
+ import { WatchdogEmissionGuard } from "./emission-guard.ts";
5
+ import {
6
+ collectWatchdogLspDiagnostics,
7
+ formatWatchdogLspDiagnosticsBlock,
8
+ WatchdogLspDiagnosticsLedger,
9
+ watchdogWarningFromLspDiagnostics,
10
+ type WatchdogLspDiagnosticsFunction,
11
+ } from "./lsp-diagnostics.ts";
12
+ import { resolveWatchdogConfig } from "./settings.ts";
13
+ import { formatWatchdogTurnDelta } from "./turn-delta.ts";
14
+ import {
15
+ type ResolvedWatchdogConfig,
16
+ type WatchdogEndpointConfig,
17
+ type WatchdogLspRuntimeSnapshot,
18
+ type WatchdogRuntimeStatus,
19
+ type WatchdogSettingsError,
20
+ type WatchdogSettingsResult,
21
+ type WatchdogSettingsSource,
22
+ type WatchdogWarning,
23
+ type WatchdogWarningDetails,
24
+ } from "./types.ts";
25
+ import { normalizeWatchdogWarningDetails } from "./warning-format.ts";
26
+
27
+ type ReviewStopReason = "stop" | "error" | "aborted" | "length";
28
+
29
+ export interface WatchdogReviewResult {
30
+ warnings?: WatchdogWarning[];
31
+ stopReason?: ReviewStopReason;
32
+ }
33
+
34
+ export interface WatchdogReviewRequest {
35
+ delta: string;
36
+ epoch: number;
37
+ reviewId: number;
38
+ config: ResolvedWatchdogConfig;
39
+ emitWarning(warning: WatchdogWarning): boolean;
40
+ signal?: AbortSignal;
41
+ }
42
+
43
+ export type WatchdogReviewFunction = (request: WatchdogReviewRequest) => Promise<WatchdogReviewResult | void> | WatchdogReviewResult | void;
44
+
45
+ export interface WatchdogRuntimeSnapshot {
46
+ status: WatchdogRuntimeStatus;
47
+ enabled: boolean;
48
+ config: ResolvedWatchdogConfig;
49
+ configOk: boolean;
50
+ errors: WatchdogSettingsError[];
51
+ sources: WatchdogSettingsSource[];
52
+ bufferedDeltas: number;
53
+ epoch: number;
54
+ activeReviewId?: number;
55
+ sessionOverride?: boolean;
56
+ sessionModelOverride?: Partial<Pick<WatchdogEndpointConfig, "model" | "thinking">>;
57
+ lastWarning?: WatchdogWarningDetails;
58
+ lastError?: string;
59
+ failedReviews: number;
60
+ staleReviews: number;
61
+ reviewConnected: boolean;
62
+ reviewDescription: string;
63
+ autoFollowQueued: false;
64
+ reviewTrigger: "turn-delta" | "repo-edits";
65
+ changedPaths?: string[];
66
+ lsp: WatchdogLspRuntimeSnapshot;
67
+ }
68
+
69
+ interface Waiter {
70
+ resolve(settled: boolean): void;
71
+ timer: ReturnType<typeof setTimeout>;
72
+ }
73
+
74
+ interface MainWatchdogRuntimeOptions {
75
+ cwd?: string;
76
+ resolveConfig?: (cwd: string, options?: { session?: Record<string, unknown> }) => WatchdogSettingsResult;
77
+ review?: WatchdogReviewFunction;
78
+ reviewDescription?: string;
79
+ displayWarning?: (warning: WatchdogWarningDetails) => void;
80
+ reviewChangesOnly?: boolean;
81
+ lspDiagnostics?: WatchdogLspDiagnosticsFunction;
82
+ repoChangeSignature?: typeof computeWatchdogRepoChangeSignature;
83
+ }
84
+
85
+ type ContextLike = Pick<ExtensionContext, "cwd">;
86
+ type ReviewDeltaOutcome = "completed" | "timeout" | "stale";
87
+
88
+ const DEFAULT_REVIEW: WatchdogReviewFunction = () => ({ warnings: [] });
89
+ const MAX_REVIEW_INPUT_CHARS = 24_000;
90
+ const REVIEW_DELTA_SEPARATOR = "\n\n---\n\n";
91
+
92
+ function errorMessage(error: unknown): string {
93
+ return error instanceof Error ? error.message : String(error);
94
+ }
95
+
96
+ function promptFromBeforeAgentStart(event: unknown): string | undefined {
97
+ if (!event || typeof event !== "object") return undefined;
98
+ const input = event as { prompt?: unknown; systemPrompt?: unknown };
99
+ if (typeof input.prompt === "string") return input.prompt;
100
+ if (typeof input.systemPrompt === "string") return input.systemPrompt;
101
+ return undefined;
102
+ }
103
+
104
+ function reviewInputSignature(input: string): string {
105
+ return createHash("sha256").update(input).digest("hex");
106
+ }
107
+
108
+ export class MainWatchdogRuntime {
109
+ private cwd: string;
110
+ private readonly resolveConfig: (cwd: string, options?: { session?: Record<string, unknown> }) => WatchdogSettingsResult;
111
+ private readonly review: WatchdogReviewFunction;
112
+ private readonly reviewConnected: boolean;
113
+ private readonly reviewDescription: string;
114
+ private readonly displayWarning: ((warning: WatchdogWarningDetails) => void) | undefined;
115
+ private readonly reviewChangesOnly: boolean;
116
+ private readonly lspDiagnostics: WatchdogLspDiagnosticsFunction;
117
+ private readonly repoChangeSignature: typeof computeWatchdogRepoChangeSignature;
118
+ private readonly lspLedger = new WatchdogLspDiagnosticsLedger();
119
+ private configResult: WatchdogSettingsResult;
120
+ private sessionOverrideEnabled: boolean | undefined;
121
+ private sessionModelOverride: Partial<Pick<WatchdogEndpointConfig, "model" | "thinking">> | undefined;
122
+ private status: WatchdogRuntimeStatus = "idle";
123
+ private pendingDeltas: string[] = [];
124
+ private pendingDeltaChars = 0;
125
+ private guard = new WatchdogEmissionGuard();
126
+ private guardMaxWarnings: number | null = null;
127
+ private epoch = 0;
128
+ private reviewIdCounter = 0;
129
+ private agentEndIdCounter = 0;
130
+ private activeAgentEndId: number | undefined;
131
+ private activeAgentEndAbortController: AbortController | undefined;
132
+ private activeReviewId: number | undefined;
133
+ private activeReviewWarning: WatchdogWarningDetails | undefined;
134
+ private reviewing = false;
135
+ private waitingAtAgentEnd = false;
136
+ private disposed = false;
137
+ private includeUserPromptInNextDelta = false;
138
+ private userPrompt: string | undefined;
139
+ private waiters: Waiter[] = [];
140
+ private lastWarning: WatchdogWarningDetails | undefined;
141
+ private lastError: string | undefined;
142
+ private lastReviewInputSignature: string | undefined;
143
+ private turnStartChangeSignature: WatchdogRepoChangeSignature | undefined;
144
+ private lastReviewedChangeSignature: string | undefined;
145
+ private currentChangedPaths: string[] | undefined;
146
+ private lastLspSnapshot: WatchdogLspRuntimeSnapshot | undefined;
147
+ private observedRepoEditThisTurn = false;
148
+ private failedReviews = 0;
149
+ private staleReviews = 0;
150
+
151
+ constructor(options: MainWatchdogRuntimeOptions = {}) {
152
+ this.cwd = options.cwd ?? process.cwd();
153
+ this.resolveConfig = options.resolveConfig ?? resolveWatchdogConfig;
154
+ this.review = options.review ?? DEFAULT_REVIEW;
155
+ this.reviewConnected = Boolean(options.review);
156
+ this.reviewDescription = options.reviewDescription ?? (options.review ? "injected seam" : "not wired");
157
+ this.displayWarning = options.displayWarning;
158
+ this.reviewChangesOnly = options.reviewChangesOnly === true;
159
+ this.lspDiagnostics = options.lspDiagnostics ?? collectWatchdogLspDiagnostics;
160
+ this.repoChangeSignature = options.repoChangeSignature ?? computeWatchdogRepoChangeSignature;
161
+ this.configResult = this.resolveConfig(this.cwd);
162
+ this.guardMaxWarnings = this.configResult.config.maxWarnings;
163
+ this.guard = new WatchdogEmissionGuard({ maxWarnings: this.guardMaxWarnings });
164
+ this.turnStartChangeSignature = this.currentRepoChangeSignature();
165
+ this.lastReviewedChangeSignature = this.turnStartChangeSignature?.key;
166
+ }
167
+
168
+ bindSession(ctx: ContextLike): void {
169
+ this.cwd = ctx.cwd;
170
+ this.sessionOverrideEnabled = undefined;
171
+ this.sessionModelOverride = undefined;
172
+ this.refreshConfig(ctx.cwd);
173
+ this.reset("session_start", { clearReviewInputSignature: true, resetChangeSignature: true, clearLspLedger: true });
174
+ }
175
+
176
+ refreshConfig(cwd = this.cwd): WatchdogSettingsResult {
177
+ this.cwd = cwd;
178
+ const wasEnabled = this.isEnabled();
179
+ const session = this.sessionOverrideEnabled === undefined && this.sessionModelOverride === undefined
180
+ ? undefined
181
+ : {
182
+ ...(this.sessionOverrideEnabled === undefined ? {} : { enabled: this.sessionOverrideEnabled }),
183
+ main: {
184
+ ...(this.sessionOverrideEnabled === undefined ? {} : { enabled: this.sessionOverrideEnabled }),
185
+ ...(this.sessionModelOverride ?? {}),
186
+ },
187
+ };
188
+ this.configResult = this.resolveConfig(this.cwd, session === undefined ? undefined : { session });
189
+ if (this.configResult.config.maxWarnings !== this.guardMaxWarnings) {
190
+ this.guardMaxWarnings = this.configResult.config.maxWarnings;
191
+ this.guard = new WatchdogEmissionGuard({ maxWarnings: this.guardMaxWarnings });
192
+ }
193
+ if (wasEnabled && !this.isEnabled()) this.invalidateActiveReview("watchdog disabled");
194
+ return this.configResult;
195
+ }
196
+
197
+ setSessionEnabled(enabled: boolean, cwd = this.cwd): WatchdogRuntimeSnapshot {
198
+ this.sessionOverrideEnabled = enabled;
199
+ this.reset("session override");
200
+ this.refreshConfig(cwd);
201
+ return this.getSnapshot();
202
+ }
203
+
204
+ setSessionModel(patch: { model?: string | null; thinking?: WatchdogEndpointConfig["thinking"] | null }, cwd = this.cwd): WatchdogRuntimeSnapshot {
205
+ const next = { ...(this.sessionModelOverride ?? {}) };
206
+ if (patch.model === null) delete next.model;
207
+ else if (patch.model !== undefined) next.model = patch.model;
208
+ if (patch.thinking === null) delete next.thinking;
209
+ else if (patch.thinking !== undefined) next.thinking = patch.thinking;
210
+ this.sessionModelOverride = next.model === undefined && next.thinking === undefined ? undefined : next;
211
+ this.reset("session model override");
212
+ this.refreshConfig(cwd);
213
+ return this.getSnapshot();
214
+ }
215
+
216
+ clearSessionModel(cwd = this.cwd): WatchdogRuntimeSnapshot {
217
+ this.sessionModelOverride = undefined;
218
+ this.reset("session model override cleared");
219
+ this.refreshConfig(cwd);
220
+ return this.getSnapshot();
221
+ }
222
+
223
+ clearSessionOverride(cwd = this.cwd): WatchdogRuntimeSnapshot {
224
+ this.sessionOverrideEnabled = undefined;
225
+ this.sessionModelOverride = undefined;
226
+ this.reset("session override cleared");
227
+ this.refreshConfig(cwd);
228
+ return this.getSnapshot();
229
+ }
230
+
231
+ reset(_reason = "reset", options: { clearReviewInputSignature?: boolean; resetChangeSignature?: boolean; clearLspLedger?: boolean } = {}): void {
232
+ this.abortActiveAgentEnd();
233
+ this.epoch++;
234
+ this.status = "idle";
235
+ this.clearPendingDeltas();
236
+ this.reviewing = false;
237
+ this.waitingAtAgentEnd = false;
238
+ this.activeReviewId = undefined;
239
+ this.activeReviewWarning = undefined;
240
+ this.includeUserPromptInNextDelta = false;
241
+ this.userPrompt = undefined;
242
+ this.lastError = undefined;
243
+ this.currentChangedPaths = undefined;
244
+ this.observedRepoEditThisTurn = false;
245
+ if (options.clearLspLedger) {
246
+ this.lspLedger.reset();
247
+ this.lastLspSnapshot = undefined;
248
+ }
249
+ if (options.clearReviewInputSignature) this.lastReviewInputSignature = undefined;
250
+ if (options.resetChangeSignature) this.resetRepoChangeBaseline({ reviewed: true });
251
+ this.guard.reset();
252
+ this.resolveWaiters(true);
253
+ }
254
+
255
+ dispose(): void {
256
+ this.disposed = true;
257
+ this.abortActiveAgentEnd();
258
+ this.epoch++;
259
+ this.status = "idle";
260
+ this.clearPendingDeltas();
261
+ this.reviewing = false;
262
+ this.waitingAtAgentEnd = false;
263
+ this.activeReviewId = undefined;
264
+ this.activeReviewWarning = undefined;
265
+ this.lastReviewInputSignature = undefined;
266
+ this.currentChangedPaths = undefined;
267
+ this.lastLspSnapshot = undefined;
268
+ this.lspLedger.reset();
269
+ this.observedRepoEditThisTurn = false;
270
+ this.resolveWaiters(false);
271
+ }
272
+
273
+ handleBeforeAgentStart(event: unknown, ctx: ContextLike): void {
274
+ if (this.disposed) return;
275
+ this.reset("before_agent_start");
276
+ this.refreshConfig(ctx.cwd);
277
+ this.userPrompt = promptFromBeforeAgentStart(event);
278
+ this.includeUserPromptInNextDelta = Boolean(this.userPrompt?.trim());
279
+ this.resetRepoChangeBaseline();
280
+ }
281
+
282
+ handleTurnEnd(event: unknown, ctx: ContextLike): void {
283
+ if (this.disposed) return;
284
+ this.refreshConfig(ctx.cwd);
285
+ if (!this.isEnabled()) return;
286
+ try {
287
+ this.observedRepoEditThisTurn ||= eventIndicatesRepoEdit(event);
288
+ const delta = formatWatchdogTurnDelta({
289
+ includeUserPrompt: this.includeUserPromptInNextDelta,
290
+ userPrompt: this.userPrompt,
291
+ events: [event],
292
+ });
293
+ this.includeUserPromptInNextDelta = false;
294
+ this.enqueueDelta(delta);
295
+ } catch (error) {
296
+ this.fail(`Failed to format watchdog turn delta: ${errorMessage(error)}`);
297
+ }
298
+ }
299
+
300
+ enqueueDelta(delta: string): void {
301
+ if (this.disposed || !delta.trim() || !this.isEnabled()) return;
302
+ this.appendBoundedDelta(delta);
303
+ if (!this.reviewing && !this.waitingAtAgentEnd) this.status = "queued";
304
+ }
305
+
306
+ async handleAgentEnd(_event: unknown, ctx: ContextLike): Promise<void> {
307
+ if (this.disposed) return;
308
+ this.refreshConfig(ctx.cwd);
309
+ if (!this.isEnabled()) return;
310
+ const changeSignature = this.resolveReviewChangeSignature(ctx.cwd);
311
+ if (this.reviewChangesOnly && !changeSignature) {
312
+ this.clearPendingDeltas();
313
+ if (this.status === "queued") this.status = "idle";
314
+ this.resolveWaiters(true);
315
+ return;
316
+ }
317
+ if (changeSignature && changeSignature.key === this.lastReviewedChangeSignature) {
318
+ this.clearPendingDeltas();
319
+ this.status = "idle";
320
+ this.resolveWaiters(true);
321
+ return;
322
+ }
323
+ this.waitingAtAgentEnd = true;
324
+ const agentEndEpoch = this.epoch;
325
+ const agentEndId = ++this.agentEndIdCounter;
326
+ const lspAbortController = new AbortController();
327
+ this.activeAgentEndId = agentEndId;
328
+ this.activeAgentEndAbortController = lspAbortController;
329
+ try {
330
+ this.guard.startModelUpdate();
331
+ const lspBlock = await this.collectLspDiagnostics(changeSignature, {
332
+ epoch: agentEndEpoch,
333
+ agentEndId,
334
+ signal: lspAbortController.signal,
335
+ });
336
+ if (this.activeAgentEndAbortController === lspAbortController) this.activeAgentEndAbortController = undefined;
337
+ if (!this.isAgentEndCurrent(agentEndEpoch, agentEndId)) return;
338
+ const delta = this.buildReviewInput(changeSignature, lspBlock);
339
+ this.clearPendingDeltas();
340
+ if (!delta.trim()) {
341
+ this.waitingAtAgentEnd = false;
342
+ if (this.status === "queued") this.status = "idle";
343
+ this.resolveWaiters(true);
344
+ return;
345
+ }
346
+ const signature = reviewInputSignature(delta);
347
+ if (!this.reviewChangesOnly && signature === this.lastReviewInputSignature) {
348
+ this.waitingAtAgentEnd = false;
349
+ this.status = "idle";
350
+ this.resolveWaiters(true);
351
+ return;
352
+ }
353
+ const outcome = await this.reviewDelta(delta, this.configResult.config.agentEndTimeoutMs);
354
+ this.waitingAtAgentEnd = false;
355
+ if (outcome === "timeout") {
356
+ this.staleReviews++;
357
+ this.invalidateActiveReview("agent-end timeout");
358
+ this.status = "stale";
359
+ this.markLastWarningStale();
360
+ this.resolveWaiters(true);
361
+ return;
362
+ }
363
+ if (outcome === "completed" && this.status !== "failed" && this.status !== "stale") {
364
+ this.lastReviewInputSignature = signature;
365
+ if (changeSignature) this.lastReviewedChangeSignature = changeSignature.key;
366
+ this.currentChangedPaths = changeSignature?.changedPaths;
367
+ this.status = "idle";
368
+ }
369
+ this.resolveWaiters(true);
370
+ } finally {
371
+ if (this.activeAgentEndAbortController === lspAbortController) this.activeAgentEndAbortController = undefined;
372
+ if (this.activeAgentEndId === agentEndId) this.activeAgentEndId = undefined;
373
+ }
374
+ }
375
+
376
+ recordDisplayedWarning(warning: WatchdogWarning): WatchdogWarningDetails {
377
+ const details = normalizeWatchdogWarningDetails(warning, { state: "displayed", source: warning.source ?? "main" });
378
+ this.lastWarning = details;
379
+ return details;
380
+ }
381
+
382
+ getSnapshot(cwd?: string): WatchdogRuntimeSnapshot {
383
+ if (cwd) this.refreshConfig(cwd);
384
+ return {
385
+ status: this.status,
386
+ enabled: this.isEnabled(),
387
+ config: this.configResult.config,
388
+ configOk: this.configResult.ok,
389
+ errors: [...this.configResult.errors],
390
+ sources: [...this.configResult.sources],
391
+ bufferedDeltas: this.pendingDeltas.length,
392
+ epoch: this.epoch,
393
+ ...(this.activeReviewId !== undefined ? { activeReviewId: this.activeReviewId } : {}),
394
+ ...(this.sessionOverrideEnabled !== undefined ? { sessionOverride: this.sessionOverrideEnabled } : {}),
395
+ ...(this.sessionModelOverride !== undefined ? { sessionModelOverride: { ...this.sessionModelOverride } } : {}),
396
+ ...(this.lastWarning ? { lastWarning: this.lastWarning } : {}),
397
+ ...(this.lastError ? { lastError: this.lastError } : {}),
398
+ failedReviews: this.failedReviews,
399
+ staleReviews: this.staleReviews,
400
+ reviewConnected: this.reviewConnected,
401
+ reviewDescription: this.reviewDescription,
402
+ autoFollowQueued: false,
403
+ reviewTrigger: this.reviewChangesOnly ? "repo-edits" : "turn-delta",
404
+ ...(this.currentChangedPaths?.length ? { changedPaths: [...this.currentChangedPaths] } : {}),
405
+ lsp: this.lspSnapshot(),
406
+ };
407
+ }
408
+
409
+ async waitForIdle(timeoutMs = 1_000): Promise<boolean> {
410
+ return this.waitForSettled(timeoutMs);
411
+ }
412
+
413
+ private isEnabled(): boolean {
414
+ return this.configResult.ok && this.configResult.config.main.enabled;
415
+ }
416
+
417
+ private abortActiveAgentEnd(): void {
418
+ this.activeAgentEndAbortController?.abort();
419
+ this.activeAgentEndAbortController = undefined;
420
+ this.activeAgentEndId = undefined;
421
+ }
422
+
423
+ private isAgentEndCurrent(epoch: number, agentEndId: number): boolean {
424
+ return !this.disposed && this.epoch === epoch && this.activeAgentEndId === agentEndId && this.waitingAtAgentEnd && this.isEnabled();
425
+ }
426
+
427
+ private isCurrent(epoch: number, reviewId: number): boolean {
428
+ return !this.disposed && this.epoch === epoch && this.activeReviewId === reviewId;
429
+ }
430
+
431
+ private acceptWarning(epoch: number, reviewId: number, warning: WatchdogWarning): boolean {
432
+ if (!this.isCurrent(epoch, reviewId) || !this.isEnabled()) return false;
433
+ const decision = this.guard.evaluate(warning);
434
+ if (!decision.accepted) return false;
435
+ const details = normalizeWatchdogWarningDetails(warning, {
436
+ state: "candidate",
437
+ source: warning.source ?? "main",
438
+ identity: decision.identity,
439
+ });
440
+ this.lastWarning = details;
441
+ this.activeReviewWarning = details;
442
+ return true;
443
+ }
444
+
445
+ private displayBoundaryWarning(warning: WatchdogWarning): boolean {
446
+ if (!this.isEnabled()) return false;
447
+ const decision = this.guard.evaluate(warning);
448
+ if (!decision.accepted) return false;
449
+ const details = normalizeWatchdogWarningDetails(warning, {
450
+ state: "displayed",
451
+ source: warning.source ?? "main",
452
+ identity: decision.identity,
453
+ displayedAt: new Date().toISOString(),
454
+ });
455
+ this.lastWarning = details;
456
+ this.displayWarning?.(details);
457
+ return true;
458
+ }
459
+
460
+ private invalidateActiveReview(_reason: string): void {
461
+ this.abortActiveAgentEnd();
462
+ this.epoch++;
463
+ this.status = "idle";
464
+ this.clearPendingDeltas();
465
+ this.reviewing = false;
466
+ this.waitingAtAgentEnd = false;
467
+ this.activeReviewId = undefined;
468
+ this.activeReviewWarning = undefined;
469
+ }
470
+
471
+ private async reviewDelta(delta: string, timeoutMs: number): Promise<ReviewDeltaOutcome> {
472
+ if (this.reviewing || this.disposed) return "stale";
473
+ this.reviewing = true;
474
+ const reviewEpoch = this.epoch;
475
+ const reviewId = ++this.reviewIdCounter;
476
+ this.activeReviewId = reviewId;
477
+ this.activeReviewWarning = undefined;
478
+ this.status = "reviewing";
479
+ let timeout: ReturnType<typeof setTimeout> | undefined;
480
+ const abortController = new AbortController();
481
+ const reviewPromise = Promise.resolve().then(() => this.review({
482
+ delta,
483
+ epoch: reviewEpoch,
484
+ reviewId,
485
+ config: this.configResult.config,
486
+ signal: abortController.signal,
487
+ emitWarning: (warning) => this.acceptWarning(reviewEpoch, reviewId, warning),
488
+ }));
489
+ try {
490
+ const result = await Promise.race([
491
+ reviewPromise,
492
+ new Promise<"timeout">((resolve) => {
493
+ timeout = setTimeout(() => resolve("timeout"), timeoutMs);
494
+ }),
495
+ ]);
496
+ if (result === "timeout") {
497
+ abortController.abort();
498
+ return "timeout";
499
+ }
500
+ if (!this.isCurrent(reviewEpoch, reviewId)) return "stale";
501
+ for (const warning of result?.warnings ?? []) this.acceptWarning(reviewEpoch, reviewId, warning);
502
+ if (result?.stopReason && result.stopReason !== "stop") {
503
+ this.fail(`Watchdog review ended with stop reason '${result.stopReason}'.`);
504
+ return "completed";
505
+ }
506
+ this.displayAcceptedReviewWarning();
507
+ return "completed";
508
+ } catch (error) {
509
+ if (this.isCurrent(reviewEpoch, reviewId)) {
510
+ this.fail(`Watchdog review failed: ${errorMessage(error)}`);
511
+ return "completed";
512
+ }
513
+ return "stale";
514
+ } finally {
515
+ if (timeout) clearTimeout(timeout);
516
+ if (this.epoch === reviewEpoch && this.activeReviewId === reviewId) {
517
+ this.reviewing = false;
518
+ this.activeReviewId = undefined;
519
+ this.activeReviewWarning = undefined;
520
+ }
521
+ this.resolveWaiters(this.isSettled());
522
+ }
523
+ }
524
+
525
+ private displayAcceptedReviewWarning(): void {
526
+ if (!this.activeReviewWarning) return;
527
+ const details: WatchdogWarningDetails = {
528
+ ...this.activeReviewWarning,
529
+ state: "displayed",
530
+ displayedAt: new Date().toISOString(),
531
+ };
532
+ this.lastWarning = details;
533
+ this.displayWarning?.(details);
534
+ }
535
+
536
+ private currentRepoChangeSignature(cwd = this.cwd): WatchdogRepoChangeSignature | undefined {
537
+ return this.reviewChangesOnly && this.isEnabled() ? this.repoChangeSignature(cwd) : undefined;
538
+ }
539
+
540
+ private resetRepoChangeBaseline(options: { cwd?: string; reviewed?: boolean } = {}): void {
541
+ this.turnStartChangeSignature = this.currentRepoChangeSignature(options.cwd ?? this.cwd);
542
+ if (options.reviewed) this.lastReviewedChangeSignature = this.turnStartChangeSignature?.key;
543
+ else this.lastReviewedChangeSignature ??= this.turnStartChangeSignature?.key;
544
+ this.currentChangedPaths = this.turnStartChangeSignature?.changedPaths;
545
+ this.observedRepoEditThisTurn = false;
546
+ }
547
+
548
+ private resolveReviewChangeSignature(cwd = this.cwd): WatchdogRepoChangeSignature | undefined {
549
+ if (!this.reviewChangesOnly) return undefined;
550
+ const current = this.currentRepoChangeSignature(cwd);
551
+ if (current) {
552
+ this.currentChangedPaths = current.changedPaths;
553
+ if (current.key === this.turnStartChangeSignature?.key) return undefined;
554
+ if (current.changedPaths.length === 0) return undefined;
555
+ return current;
556
+ }
557
+ return this.observedRepoEditThisTurn
558
+ ? { root: cwd, key: `observed-edit:${this.epoch}:${this.reviewIdCounter}:${this.pendingDeltaChars}`, changedPaths: [] }
559
+ : undefined;
560
+ }
561
+
562
+ private async collectLspDiagnostics(changeSignature: WatchdogRepoChangeSignature | undefined, current: { epoch: number; agentEndId: number; signal: AbortSignal }): Promise<string> {
563
+ const config = this.configResult.config.lsp;
564
+ if (!config.enabled || !changeSignature?.changedPaths.length) {
565
+ this.lastLspSnapshot = {
566
+ enabled: config.enabled,
567
+ status: config.enabled ? "skipped" : "disabled",
568
+ checkedPaths: [],
569
+ skippedPaths: [],
570
+ diagnostics: [],
571
+ diagnosticCount: 0,
572
+ freshDiagnosticCount: 0,
573
+ updatedAt: new Date().toISOString(),
574
+ };
575
+ return "";
576
+ }
577
+ try {
578
+ const raw = await this.lspDiagnostics({
579
+ cwd: this.cwd,
580
+ root: changeSignature.root,
581
+ changedPaths: changeSignature.changedPaths,
582
+ config,
583
+ signal: current.signal,
584
+ });
585
+ if (!this.isAgentEndCurrent(current.epoch, current.agentEndId)) return "";
586
+ const diagnosticCount = raw.diagnostics.length;
587
+ const fresh = this.lspLedger.reduce(raw);
588
+ this.lastLspSnapshot = {
589
+ ...fresh,
590
+ enabled: true,
591
+ diagnosticCount,
592
+ freshDiagnosticCount: fresh.diagnostics.length,
593
+ updatedAt: new Date().toISOString(),
594
+ };
595
+ const warning = watchdogWarningFromLspDiagnostics(fresh);
596
+ if (warning) this.displayBoundaryWarning(warning);
597
+ return formatWatchdogLspDiagnosticsBlock(fresh);
598
+ } catch (error) {
599
+ if (!this.isAgentEndCurrent(current.epoch, current.agentEndId)) return "";
600
+ this.lastLspSnapshot = {
601
+ enabled: true,
602
+ status: "failed",
603
+ checkedPaths: [],
604
+ skippedPaths: changeSignature.changedPaths,
605
+ diagnostics: [],
606
+ diagnosticCount: 0,
607
+ freshDiagnosticCount: 0,
608
+ message: `LSP diagnostics failed: ${errorMessage(error)}`,
609
+ updatedAt: new Date().toISOString(),
610
+ };
611
+ return "";
612
+ }
613
+ }
614
+
615
+ private lspSnapshot(): WatchdogLspRuntimeSnapshot {
616
+ if (this.lastLspSnapshot) return {
617
+ ...this.lastLspSnapshot,
618
+ checkedPaths: [...this.lastLspSnapshot.checkedPaths],
619
+ skippedPaths: [...this.lastLspSnapshot.skippedPaths],
620
+ diagnostics: [...this.lastLspSnapshot.diagnostics],
621
+ };
622
+ const enabled = this.isEnabled() && this.configResult.config.lsp.enabled;
623
+ return {
624
+ enabled,
625
+ status: enabled ? "skipped" : "disabled",
626
+ checkedPaths: [],
627
+ skippedPaths: [],
628
+ diagnostics: [],
629
+ diagnosticCount: 0,
630
+ freshDiagnosticCount: 0,
631
+ };
632
+ }
633
+
634
+ private appendBoundedDelta(delta: string): void {
635
+ let entry = delta.trim();
636
+ if (!entry) return;
637
+ if (entry.length > MAX_REVIEW_INPUT_CHARS) entry = entry.slice(-MAX_REVIEW_INPUT_CHARS);
638
+ this.pendingDeltas.push(entry);
639
+ this.pendingDeltaChars += entry.length;
640
+ while (this.pendingDeltas.length > 1 && this.pendingDeltaChars + (this.pendingDeltas.length - 1) * REVIEW_DELTA_SEPARATOR.length > MAX_REVIEW_INPUT_CHARS) {
641
+ const removed = this.pendingDeltas.shift();
642
+ if (removed) this.pendingDeltaChars -= removed.length;
643
+ }
644
+ }
645
+
646
+ private buildReviewInput(changeSignature?: WatchdogRepoChangeSignature, lspBlock = ""): string {
647
+ const input = this.pendingDeltas.join(REVIEW_DELTA_SEPARATOR);
648
+ const changes = changeSignature?.changedPaths.length
649
+ ? ["Changed repo paths:", ...changeSignature.changedPaths.slice(0, 200).map((file) => `- ${file}`)].join("\n")
650
+ : "";
651
+ const contextPieces = [changes, lspBlock].filter(Boolean);
652
+ if (!contextPieces.length) return input.length > MAX_REVIEW_INPUT_CHARS ? input.slice(-MAX_REVIEW_INPUT_CHARS) : input;
653
+
654
+ const maxContextLength = Math.floor(MAX_REVIEW_INPUT_CHARS / 2);
655
+ const maxPieceLength = Math.max(1_000, Math.floor(maxContextLength / contextPieces.length));
656
+ const boundedContext = contextPieces.map((piece) => piece.length > maxPieceLength
657
+ ? `${piece.slice(0, maxPieceLength - 6)}\n- ...`
658
+ : piece).join(REVIEW_DELTA_SEPARATOR);
659
+ const separatorLength = input ? REVIEW_DELTA_SEPARATOR.length : 0;
660
+ const inputBudget = MAX_REVIEW_INPUT_CHARS - boundedContext.length - separatorLength;
661
+ const boundedInput = inputBudget <= 0
662
+ ? ""
663
+ : input.length > inputBudget
664
+ ? input.slice(-inputBudget)
665
+ : input;
666
+ return [boundedContext, boundedInput].filter(Boolean).join(REVIEW_DELTA_SEPARATOR);
667
+ }
668
+
669
+ private clearPendingDeltas(): void {
670
+ this.pendingDeltas = [];
671
+ this.pendingDeltaChars = 0;
672
+ }
673
+
674
+ private fail(message: string): void {
675
+ this.failedReviews++;
676
+ this.lastError = message;
677
+ this.status = "failed";
678
+ this.clearPendingDeltas();
679
+ this.resolveWaiters(true);
680
+ }
681
+
682
+ private markLastWarningStale(): void {
683
+ if (!this.lastWarning || this.lastWarning.state === "displayed") return;
684
+ this.lastWarning = { ...this.lastWarning, stale: true, state: "stale" };
685
+ }
686
+
687
+ private isSettled(): boolean {
688
+ return !this.reviewing && this.pendingDeltas.length === 0;
689
+ }
690
+
691
+ private waitForSettled(timeoutMs: number): Promise<boolean> {
692
+ if (this.isSettled()) return Promise.resolve(true);
693
+ return new Promise((resolve) => {
694
+ const waiter: Waiter = {
695
+ resolve,
696
+ timer: setTimeout(() => {
697
+ this.waiters = this.waiters.filter((entry) => entry !== waiter);
698
+ resolve(false);
699
+ }, timeoutMs),
700
+ };
701
+ this.waiters.push(waiter);
702
+ });
703
+ }
704
+
705
+ private resolveWaiters(settled: boolean): void {
706
+ if (!settled && !this.disposed) return;
707
+ const waiters = this.waiters;
708
+ this.waiters = [];
709
+ for (const waiter of waiters) {
710
+ clearTimeout(waiter.timer);
711
+ waiter.resolve(settled);
712
+ }
713
+ }
714
+ }