@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,532 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import type {
7
+ WatchdogLspConfig,
8
+ WatchdogLspDiagnostic,
9
+ WatchdogLspDiagnosticSeverity,
10
+ WatchdogLspResult,
11
+ WatchdogWarning,
12
+ } from "./types.ts";
13
+ export interface WatchdogLspRequest {
14
+ cwd: string;
15
+ root: string;
16
+ changedPaths: string[];
17
+ config: WatchdogLspConfig;
18
+ signal?: AbortSignal;
19
+ }
20
+ export type WatchdogLspDiagnosticsFunction = (request: WatchdogLspRequest) => Promise<WatchdogLspResult> | WatchdogLspResult;
21
+ interface TargetFile {
22
+ relPath: string;
23
+ absPath: string;
24
+ uri: string;
25
+ languageId: string;
26
+ }
27
+ interface LspCommand {
28
+ command: string;
29
+ args: string[];
30
+ label: string;
31
+ }
32
+ type JsonRpcId = number | string;
33
+ type JsonRpcMessage = {
34
+ jsonrpc?: string;
35
+ id?: JsonRpcId | null;
36
+ method?: string;
37
+ params?: unknown;
38
+ result?: unknown;
39
+ error?: { message?: string; code?: number };
40
+ };
41
+ type LspDiagnostic = {
42
+ range?: {
43
+ start?: { line?: number; character?: number };
44
+ };
45
+ severity?: number;
46
+ code?: string | number;
47
+ source?: string;
48
+ message?: string;
49
+ };
50
+ const TS_JS_EXTENSIONS = new Map<string, string>([
51
+ [".ts", "typescript"],
52
+ [".tsx", "typescriptreact"],
53
+ [".mts", "typescript"],
54
+ [".cts", "typescript"],
55
+ [".js", "javascript"],
56
+ [".jsx", "javascriptreact"],
57
+ [".mjs", "javascript"],
58
+ [".cjs", "javascript"],
59
+ ]);
60
+ const PROVIDER_NAME = "typescript-language-server";
61
+ const MAX_MESSAGE_LENGTH = 500;
62
+ const MAX_STDERR_LENGTH = 2_000;
63
+ const SHUTDOWN_TIMEOUT_MS = 250;
64
+ function normalizeRelPath(value: string): string {
65
+ return value.replaceAll(path.sep, "/").replace(/^\.\//, "");
66
+ }
67
+ function isPathInsideRoot(absPath: string, root: string): boolean {
68
+ const rel = path.relative(root, absPath);
69
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
70
+ }
71
+ function languageIdForPath(filePath: string): string | undefined {
72
+ return TS_JS_EXTENSIONS.get(path.extname(filePath).toLowerCase());
73
+ }
74
+
75
+ function trimDiagnosticMessage(message: string): string {
76
+ const normalized = message.replace(/\s+/g, " ").trim();
77
+ return normalized.length > MAX_MESSAGE_LENGTH ? `${normalized.slice(0, MAX_MESSAGE_LENGTH - 1)}…` : normalized;
78
+ }
79
+
80
+ function severityFromLsp(value: number | undefined): WatchdogLspDiagnosticSeverity {
81
+ if (value === 1) return "error";
82
+ if (value === 2) return "warning";
83
+ if (value === 3) return "info";
84
+ return "hint";
85
+ }
86
+
87
+ function pathExecutable(filePath: string): boolean {
88
+ try {
89
+ const stat = fs.statSync(filePath);
90
+ if (!stat.isFile()) return false;
91
+ if (process.platform === "win32") return true;
92
+ fs.accessSync(filePath, fs.constants.X_OK);
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function pathExecutableNames(name: string): string[] {
100
+ if (process.platform !== "win32") return [name];
101
+ const extensions = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean);
102
+ return [name, ...extensions.map((ext) => `${name}${ext.toLowerCase()}`), ...extensions.map((ext) => `${name}${ext.toUpperCase()}`)];
103
+ }
104
+
105
+ function resolveTypeScriptLanguageServer(root: string): LspCommand | undefined {
106
+ for (const name of pathExecutableNames(PROVIDER_NAME)) {
107
+ const local = path.join(root, "node_modules", ".bin", name);
108
+ if (pathExecutable(local)) return { command: local, args: ["--stdio"], label: `${PROVIDER_NAME} (project)` };
109
+ }
110
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
111
+ for (const name of pathExecutableNames(PROVIDER_NAME)) {
112
+ const candidate = path.join(dir, name);
113
+ if (pathExecutable(candidate)) return { command: candidate, args: ["--stdio"], label: PROVIDER_NAME };
114
+ }
115
+ }
116
+ return undefined;
117
+ }
118
+
119
+ function collectTargetFiles(root: string, changedPaths: string[], maxFiles: number): { targets: TargetFile[]; skippedPaths: string[] } {
120
+ const targets: TargetFile[] = [];
121
+ const skippedPaths: string[] = [];
122
+ for (const changedPath of changedPaths) {
123
+ const relPath = normalizeRelPath(changedPath);
124
+ const absPath = path.resolve(root, relPath);
125
+ const languageId = languageIdForPath(absPath);
126
+ if (!languageId || !isPathInsideRoot(absPath, root)) {
127
+ skippedPaths.push(relPath);
128
+ continue;
129
+ }
130
+ let stat: fs.Stats;
131
+ try {
132
+ stat = fs.statSync(absPath);
133
+ } catch (error) {
134
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
135
+ skippedPaths.push(relPath);
136
+ continue;
137
+ }
138
+ throw error;
139
+ }
140
+ if (!stat.isFile()) {
141
+ skippedPaths.push(relPath);
142
+ continue;
143
+ }
144
+ if (targets.length >= maxFiles) {
145
+ skippedPaths.push(relPath);
146
+ continue;
147
+ }
148
+ targets.push({ relPath, absPath, uri: pathToFileURL(absPath).href, languageId });
149
+ }
150
+ return { targets, skippedPaths };
151
+ }
152
+
153
+ function diagnosticIdentity(diagnostic: WatchdogLspDiagnostic): string {
154
+ return createHash("sha256")
155
+ .update([
156
+ diagnostic.path,
157
+ diagnostic.severity,
158
+ diagnostic.source,
159
+ diagnostic.code ?? "",
160
+ diagnostic.message,
161
+ ].join("\n"))
162
+ .digest("hex");
163
+ }
164
+
165
+ export class WatchdogLspDiagnosticsLedger {
166
+ private readonly seen = new Map<string, Set<string>>();
167
+
168
+ reset(): void {
169
+ this.seen.clear();
170
+ }
171
+
172
+ reduce(result: WatchdogLspResult): WatchdogLspResult {
173
+ if (result.status === "disabled" || result.status === "unavailable" || result.status === "failed") return result;
174
+ const currentByPath = new Map<string, Set<string>>();
175
+ const fresh: WatchdogLspDiagnostic[] = [];
176
+ for (const diagnostic of result.diagnostics) {
177
+ const identity = diagnosticIdentity(diagnostic);
178
+ const current = currentByPath.get(diagnostic.path) ?? new Set<string>();
179
+ current.add(identity);
180
+ currentByPath.set(diagnostic.path, current);
181
+ if (!this.seen.get(diagnostic.path)?.has(identity)) fresh.push(diagnostic);
182
+ }
183
+ for (const [filePath, identities] of currentByPath) this.seen.set(filePath, identities);
184
+ if (result.status === "ok") {
185
+ for (const checkedPath of result.checkedPaths) {
186
+ if (!currentByPath.has(checkedPath)) this.seen.delete(checkedPath);
187
+ }
188
+ }
189
+ return { ...result, diagnostics: fresh };
190
+ }
191
+ }
192
+
193
+ function formatDiagnostic(diagnostic: WatchdogLspDiagnostic): string {
194
+ const code = diagnostic.code ? ` ${diagnostic.code}` : "";
195
+ return `${diagnostic.path}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity}${code} ${diagnostic.source}: ${diagnostic.message}`;
196
+ }
197
+
198
+ export function formatWatchdogLspDiagnosticsBlock(result: WatchdogLspResult): string {
199
+ const actionable = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error" || diagnostic.severity === "warning");
200
+ if (!actionable.length) return "";
201
+ return [
202
+ "LSP diagnostics:",
203
+ ...actionable.map((diagnostic) => `- ${formatDiagnostic(diagnostic)}`),
204
+ ].join("\n");
205
+ }
206
+
207
+ export function watchdogWarningFromLspDiagnostics(result: WatchdogLspResult): WatchdogWarning | undefined {
208
+ const actionable = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error" || diagnostic.severity === "warning");
209
+ if (!actionable.length) return undefined;
210
+ const errors = actionable.filter((diagnostic) => diagnostic.severity === "error");
211
+ const severity = errors.length ? "blocker" : "concern";
212
+ const primary = errors[0] ?? actionable[0]!;
213
+ const count = errors.length || actionable.length;
214
+ const kind = errors.length ? "error" : "warning";
215
+ const evidence = actionable.slice(0, 5).map(formatDiagnostic).join("\n");
216
+ return {
217
+ severity,
218
+ category: "correctness",
219
+ confidence: "high",
220
+ source: "lsp",
221
+ summary: `LSP found ${count} ${kind}${count === 1 ? "" : "s"} in changed ${count === 1 ? "file" : "files"}.`,
222
+ evidence: evidence || formatDiagnostic(primary),
223
+ recommendedAction: "Fix the reported diagnostics or explain why they are expected before accepting the change.",
224
+ };
225
+ }
226
+
227
+ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string, signal?: AbortSignal): Promise<T> {
228
+ if (signal?.aborted) return Promise.reject(new Error("aborted"));
229
+ let timer: ReturnType<typeof setTimeout> | undefined;
230
+ return new Promise((resolve, reject) => {
231
+ const onAbort = () => {
232
+ if (timer) clearTimeout(timer);
233
+ reject(new Error("aborted"));
234
+ };
235
+ timer = setTimeout(() => {
236
+ signal?.removeEventListener("abort", onAbort);
237
+ reject(new Error(message));
238
+ }, timeoutMs);
239
+ signal?.addEventListener("abort", onAbort, { once: true });
240
+ promise.then(
241
+ (value) => {
242
+ if (timer) clearTimeout(timer);
243
+ signal?.removeEventListener("abort", onAbort);
244
+ resolve(value);
245
+ },
246
+ (error: unknown) => {
247
+ if (timer) clearTimeout(timer);
248
+ signal?.removeEventListener("abort", onAbort);
249
+ reject(error);
250
+ },
251
+ );
252
+ });
253
+ }
254
+
255
+ class JsonRpcLspClient {
256
+ private nextId = 1;
257
+ private stdoutBuffer = Buffer.alloc(0);
258
+ private readonly pending = new Map<JsonRpcId, { resolve(value: unknown): void; reject(error: Error): void }>();
259
+ readonly diagnostics = new Map<string, LspDiagnostic[]>();
260
+ private readonly child: ChildProcessWithoutNullStreams;
261
+ private stderr = "";
262
+ private exited = false;
263
+ private readonly exitWaiters: Array<() => void> = [];
264
+
265
+ constructor(child: ChildProcessWithoutNullStreams) {
266
+ this.child = child;
267
+ child.stdout.on("data", (chunk: Buffer) => this.handleStdout(chunk));
268
+ child.stderr.on("data", (chunk: Buffer) => {
269
+ this.stderr = `${this.stderr}${chunk.toString("utf-8")}`.slice(-MAX_STDERR_LENGTH);
270
+ });
271
+ child.on("error", (error) => {
272
+ this.exited = true;
273
+ this.rejectPending(error);
274
+ this.resolveExitWaiters();
275
+ });
276
+ child.on("exit", (code, signal) => {
277
+ this.exited = true;
278
+ this.rejectPending(new Error(`language server exited${code === null ? "" : ` with code ${code}`}${signal ? ` signal ${signal}` : ""}`));
279
+ this.resolveExitWaiters();
280
+ });
281
+ }
282
+
283
+ request(method: string, params: unknown, timeoutMs: number, signal?: AbortSignal): Promise<unknown> {
284
+ const id = this.nextId++;
285
+ const promise = new Promise<unknown>((resolve, reject) => {
286
+ this.pending.set(id, { resolve, reject });
287
+ this.send({ jsonrpc: "2.0", id, method, params });
288
+ });
289
+ return withTimeout(promise, timeoutMs, `${method} timed out`, signal);
290
+ }
291
+
292
+ notify(method: string, params: unknown): void {
293
+ this.send({ jsonrpc: "2.0", method, params });
294
+ }
295
+
296
+ async shutdown(): Promise<void> {
297
+ if (this.exited) return;
298
+ try {
299
+ await this.request("shutdown", null, SHUTDOWN_TIMEOUT_MS);
300
+ this.notify("exit", null);
301
+ } catch {
302
+ this.child.kill("SIGTERM");
303
+ }
304
+ await this.waitForExit(SHUTDOWN_TIMEOUT_MS);
305
+ }
306
+
307
+ kill(): void {
308
+ if (!this.exited) this.child.kill("SIGTERM");
309
+ }
310
+
311
+ stderrTail(): string {
312
+ return this.stderr.trim();
313
+ }
314
+
315
+ private send(payload: JsonRpcMessage): void {
316
+ if (this.exited) throw new Error("language server already exited");
317
+ const body = JSON.stringify(payload);
318
+ this.child.stdin.write(`Content-Length: ${Buffer.byteLength(body, "utf-8")}\r\n\r\n${body}`);
319
+ }
320
+
321
+ private handleStdout(chunk: Buffer): void {
322
+ this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
323
+ while (true) {
324
+ const headerEnd = this.stdoutBuffer.indexOf("\r\n\r\n");
325
+ if (headerEnd === -1) return;
326
+ const header = this.stdoutBuffer.slice(0, headerEnd).toString("utf-8");
327
+ const match = header.match(/content-length:\s*(\d+)/i);
328
+ if (!match) {
329
+ this.stdoutBuffer = this.stdoutBuffer.slice(headerEnd + 4);
330
+ continue;
331
+ }
332
+ const length = Number(match[1]);
333
+ const bodyStart = headerEnd + 4;
334
+ const bodyEnd = bodyStart + length;
335
+ if (this.stdoutBuffer.length < bodyEnd) return;
336
+ const body = this.stdoutBuffer.slice(bodyStart, bodyEnd).toString("utf-8");
337
+ this.stdoutBuffer = this.stdoutBuffer.slice(bodyEnd);
338
+ try {
339
+ this.handleMessage(JSON.parse(body) as JsonRpcMessage);
340
+ } catch (error) {
341
+ const message = error instanceof Error ? error.message : String(error);
342
+ this.failProtocol(new Error(`Invalid LSP JSON-RPC response: ${message}`));
343
+ return;
344
+ }
345
+ }
346
+ }
347
+
348
+ private handleMessage(message: JsonRpcMessage): void {
349
+ if (message.method === "textDocument/publishDiagnostics") {
350
+ const params = message.params as { uri?: unknown; diagnostics?: unknown } | undefined;
351
+ if (typeof params?.uri === "string" && Array.isArray(params.diagnostics)) {
352
+ this.diagnostics.set(params.uri, params.diagnostics as LspDiagnostic[]);
353
+ }
354
+ return;
355
+ }
356
+ if (message.id === undefined || message.id === null) return;
357
+ const pending = this.pending.get(message.id);
358
+ if (!pending) return;
359
+ this.pending.delete(message.id);
360
+ if (message.error) {
361
+ pending.reject(new Error(message.error.message || `LSP request ${message.id} failed`));
362
+ } else {
363
+ pending.resolve(message.result);
364
+ }
365
+ }
366
+
367
+ private failProtocol(error: Error): void {
368
+ if (this.exited) return;
369
+ this.rejectPending(error);
370
+ this.child.kill("SIGTERM");
371
+ }
372
+
373
+ private waitForExit(timeoutMs: number): Promise<void> {
374
+ if (this.exited) return Promise.resolve();
375
+ return new Promise((resolve) => {
376
+ const timer = setTimeout(resolve, timeoutMs);
377
+ this.exitWaiters.push(() => {
378
+ clearTimeout(timer);
379
+ resolve();
380
+ });
381
+ });
382
+ }
383
+
384
+ private resolveExitWaiters(): void {
385
+ for (const resolve of this.exitWaiters.splice(0)) resolve();
386
+ }
387
+
388
+ private rejectPending(error: Error): void {
389
+ for (const pending of this.pending.values()) pending.reject(error);
390
+ this.pending.clear();
391
+ }
392
+ }
393
+
394
+ function convertDiagnostics(target: TargetFile, diagnostics: LspDiagnostic[]): WatchdogLspDiagnostic[] {
395
+ return diagnostics
396
+ .filter((diagnostic) => typeof diagnostic.message === "string" && diagnostic.range?.start)
397
+ .map((diagnostic) => ({
398
+ path: target.relPath,
399
+ line: Math.max(1, (diagnostic.range?.start?.line ?? 0) + 1),
400
+ column: Math.max(1, (diagnostic.range?.start?.character ?? 0) + 1),
401
+ severity: severityFromLsp(diagnostic.severity),
402
+ source: diagnostic.source || PROVIDER_NAME,
403
+ ...(diagnostic.code !== undefined ? { code: String(diagnostic.code) } : {}),
404
+ message: trimDiagnosticMessage(diagnostic.message ?? ""),
405
+ }));
406
+ }
407
+
408
+ function initializeParams(root: string): unknown {
409
+ const rootUri = pathToFileURL(root).href;
410
+ return {
411
+ processId: process.pid,
412
+ rootUri,
413
+ capabilities: {
414
+ textDocument: {
415
+ publishDiagnostics: { relatedInformation: false, versionSupport: true },
416
+ },
417
+ workspace: { configuration: false, workspaceFolders: true },
418
+ },
419
+ workspaceFolders: [{ uri: rootUri, name: path.basename(root) || "workspace" }],
420
+ };
421
+ }
422
+
423
+ async function waitForDiagnostics(client: JsonRpcLspClient, targets: TargetFile[], timeoutMs: number, signal?: AbortSignal): Promise<boolean> {
424
+ const started = Date.now();
425
+ while (!signal?.aborted && Date.now() - started < timeoutMs) {
426
+ if (targets.every((target) => client.diagnostics.has(target.uri))) return true;
427
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, Math.max(1, timeoutMs - (Date.now() - started)))));
428
+ }
429
+ return targets.every((target) => client.diagnostics.has(target.uri));
430
+ }
431
+
432
+ async function collectWithTypeScriptLanguageServer(input: {
433
+ root: string;
434
+ targets: TargetFile[];
435
+ skippedPaths: string[];
436
+ command: LspCommand;
437
+ config: WatchdogLspConfig;
438
+ signal?: AbortSignal;
439
+ }): Promise<WatchdogLspResult> {
440
+ const started = Date.now();
441
+ const child = spawn(input.command.command, input.command.args, {
442
+ cwd: input.root,
443
+ stdio: "pipe",
444
+ env: { ...process.env, NO_COLOR: "1" },
445
+ shell: process.platform === "win32" && /\.(cmd|bat)$/i.test(input.command.command),
446
+ });
447
+ const client = new JsonRpcLspClient(child);
448
+ const remaining = () => Math.max(1, input.config.timeoutMs - (Date.now() - started));
449
+ const abort = () => client.kill();
450
+ input.signal?.addEventListener("abort", abort, { once: true });
451
+ try {
452
+ await client.request("initialize", initializeParams(input.root), remaining(), input.signal);
453
+ client.notify("initialized", {});
454
+ for (const target of input.targets) {
455
+ const text = fs.readFileSync(target.absPath, "utf-8");
456
+ client.notify("textDocument/didOpen", {
457
+ textDocument: { uri: target.uri, languageId: target.languageId, version: 1, text },
458
+ });
459
+ client.notify("textDocument/didSave", {
460
+ textDocument: { uri: target.uri },
461
+ text,
462
+ });
463
+ }
464
+ const complete = await waitForDiagnostics(client, input.targets, remaining(), input.signal);
465
+ const diagnostics = input.targets
466
+ .flatMap((target) => convertDiagnostics(target, client.diagnostics.get(target.uri) ?? []))
467
+ .slice(0, input.config.maxDiagnostics);
468
+ return {
469
+ status: complete ? "ok" : "timeout",
470
+ provider: input.command.label,
471
+ checkedPaths: input.targets.map((target) => target.relPath),
472
+ skippedPaths: input.skippedPaths,
473
+ diagnostics,
474
+ ...(complete ? {} : { message: `Timed out waiting ${input.config.timeoutMs}ms for fresh LSP diagnostics.` }),
475
+ };
476
+ } catch (error) {
477
+ const message = error instanceof Error ? error.message : String(error);
478
+ const timedOut = message.includes("timed out") || message === "aborted";
479
+ const stderr = client.stderrTail();
480
+ return {
481
+ status: timedOut ? "timeout" : "failed",
482
+ provider: input.command.label,
483
+ checkedPaths: input.targets.map((target) => target.relPath),
484
+ skippedPaths: input.skippedPaths,
485
+ diagnostics: [],
486
+ message: stderr ? `${message}; ${stderr}` : message,
487
+ };
488
+ } finally {
489
+ input.signal?.removeEventListener("abort", abort);
490
+ await client.shutdown();
491
+ }
492
+ }
493
+
494
+ export async function collectWatchdogLspDiagnostics(request: WatchdogLspRequest): Promise<WatchdogLspResult> {
495
+ if (!request.config.enabled) {
496
+ return { status: "disabled", checkedPaths: [], skippedPaths: [], diagnostics: [] };
497
+ }
498
+ const root = path.resolve(request.root || request.cwd);
499
+ const { targets, skippedPaths } = collectTargetFiles(root, request.changedPaths, request.config.maxFiles);
500
+ if (!targets.length) {
501
+ return {
502
+ status: "skipped",
503
+ checkedPaths: [],
504
+ skippedPaths,
505
+ diagnostics: [],
506
+ message: "No changed TypeScript or JavaScript files to check.",
507
+ };
508
+ }
509
+ const command = resolveTypeScriptLanguageServer(root);
510
+ if (!command) {
511
+ return {
512
+ status: "unavailable",
513
+ provider: PROVIDER_NAME,
514
+ checkedPaths: [],
515
+ skippedPaths: [...skippedPaths, ...targets.map((target) => target.relPath)],
516
+ diagnostics: [],
517
+ message: `${PROVIDER_NAME} was not found in project node_modules/.bin or PATH.`,
518
+ };
519
+ }
520
+ try {
521
+ return await collectWithTypeScriptLanguageServer({ root, targets, skippedPaths, command, config: request.config, signal: request.signal });
522
+ } catch (error) {
523
+ return {
524
+ status: "failed",
525
+ provider: command.label,
526
+ checkedPaths: targets.map((target) => target.relPath),
527
+ skippedPaths,
528
+ diagnostics: [],
529
+ message: error instanceof Error ? error.message : String(error),
530
+ };
531
+ }
532
+ }
@@ -0,0 +1,167 @@
1
+ import type { ExtensionContext } from "@selesai/code";
2
+ import { normalizeModelSegment, resolveModelCandidate } from "../runs/shared/model-fallback.ts";
3
+ import {
4
+ getSupportedThinkingLevels,
5
+ splitKnownThinkingSuffix,
6
+ THINKING_LEVELS,
7
+ toModelInfo,
8
+ type ModelInfo,
9
+ type ThinkingLevel,
10
+ } from "../shared/model-info.ts";
11
+
12
+ export const STRONG_WATCHDOG_THINKING: ThinkingLevel = "high";
13
+
14
+ const STRONG_WATCHDOG_MODELS = {
15
+ opus48: {
16
+ label: "Opus 4.8",
17
+ queries: [
18
+ "anthropic/claude-opus-4-8",
19
+ "anthropic/claude-opus-4.8",
20
+ "anthropic/opus-4-8",
21
+ "anthropic/opus-4.8",
22
+ ],
23
+ },
24
+ gpt55: {
25
+ label: "GPT 5.5",
26
+ queries: [
27
+ "openai-codex/gpt-5.5",
28
+ "openai-codex/gpt-5-5",
29
+ "openai/gpt-5.5",
30
+ "openai/gpt-5-5",
31
+ ],
32
+ },
33
+ } as const;
34
+
35
+ type StrongWatchdogFamily = keyof typeof STRONG_WATCHDOG_MODELS;
36
+
37
+ type RegistryModel = ReturnType<ExtensionContext["modelRegistry"]["find"]>;
38
+
39
+ export interface ResolvedWatchdogModelInput {
40
+ model: string;
41
+ thinking?: ThinkingLevel;
42
+ registryModel: NonNullable<RegistryModel>;
43
+ }
44
+
45
+ export interface WatchdogModelRecommendation {
46
+ model: string;
47
+ thinking: ThinkingLevel;
48
+ label: string;
49
+ reason: string;
50
+ registryModel: NonNullable<RegistryModel>;
51
+ }
52
+
53
+ function fullModelId(model: Pick<ModelInfo, "provider" | "id">): string {
54
+ return `${model.provider}/${model.id}`;
55
+ }
56
+
57
+ function modelRegistryEntries(ctx: ExtensionContext): ModelInfo[] {
58
+ return ctx.modelRegistry.getAvailable().map(toModelInfo);
59
+ }
60
+
61
+ function splitProviderModel(value: string): { provider: string; id: string } | undefined {
62
+ const slashIndex = value.indexOf("/");
63
+ if (slashIndex <= 0 || slashIndex === value.length - 1) return undefined;
64
+ return { provider: value.slice(0, slashIndex), id: value.slice(slashIndex + 1) };
65
+ }
66
+
67
+ function assertSupportedThinking(value: string, source: string): ThinkingLevel {
68
+ if ((THINKING_LEVELS as readonly string[]).includes(value)) return value as ThinkingLevel;
69
+ throw new Error(`Unsupported watchdog thinking '${value}' from ${source}; expected ${THINKING_LEVELS.join(", ")}, false, or inherit.`);
70
+ }
71
+
72
+ export function parseWatchdogThinkingInput(value: string | false | undefined, source = "watchdog input"): ThinkingLevel | false | undefined {
73
+ if (value === undefined || value === "") return undefined;
74
+ if (value === false) return false;
75
+ if (value === "false") return false;
76
+ return assertSupportedThinking(value, source);
77
+ }
78
+
79
+ export function resolveWatchdogModelInput(ctx: ExtensionContext, rawModel: string): ResolvedWatchdogModelInput {
80
+ const trimmed = rawModel.trim();
81
+ if (!trimmed) throw new Error("Watchdog model must be a non-empty provider/model value.");
82
+ const availableModels = modelRegistryEntries(ctx);
83
+ const preferredProvider = typeof ctx.model?.provider === "string" ? ctx.model.provider : undefined;
84
+ const resolved = resolveModelCandidate(trimmed, availableModels, preferredProvider) ?? trimmed;
85
+ const { baseModel, thinkingSuffix } = splitKnownThinkingSuffix(resolved);
86
+ const named = splitProviderModel(baseModel);
87
+ if (!named) throw new Error(`Watchdog model '${rawModel}' did not resolve to provider/model. Use a provider-qualified model such as openai-codex/gpt-5.5:high or anthropic/claude-opus-4-8:high.`);
88
+ const registryModel = ctx.modelRegistry.find(named.provider, named.id);
89
+ if (!registryModel) throw new Error(`Watchdog model '${rawModel}' was not found as '${baseModel}'.`);
90
+ if (!ctx.modelRegistry.hasConfiguredAuth(registryModel)) {
91
+ throw new Error(`Watchdog model '${baseModel}' is not authenticated. Configure credentials for provider '${named.provider}' or choose an authenticated model.`);
92
+ }
93
+ return {
94
+ model: `${named.provider}/${named.id}`,
95
+ ...(thinkingSuffix ? { thinking: assertSupportedThinking(thinkingSuffix.slice(1), "watchdog model suffix") } : {}),
96
+ registryModel,
97
+ };
98
+ }
99
+
100
+ function familyForModel(model: Pick<ModelInfo, "provider" | "id"> | undefined): StrongWatchdogFamily | undefined {
101
+ if (!model) return undefined;
102
+ const provider = normalizeModelSegment(model.provider);
103
+ const id = normalizeModelSegment(model.id);
104
+ if (provider.includes("openai") && /^gpt-5-5(-\d{8}|-\d{4}-\d{2}-\d{2})?$/.test(id)) return "gpt55";
105
+ if (provider.includes("anthropic") && /^(claude-opus-4-8|opus-4-8)(-\d{8}|-\d{4}-\d{2}-\d{2})?$/.test(id)) return "opus48";
106
+ return undefined;
107
+ }
108
+
109
+ function currentProviderFamily(ctx: ExtensionContext): "openai" | "anthropic" | undefined {
110
+ const provider = typeof ctx.model?.provider === "string" ? normalizeModelSegment(ctx.model.provider) : "";
111
+ if (provider.includes("openai")) return "openai";
112
+ if (provider.includes("anthropic")) return "anthropic";
113
+ return undefined;
114
+ }
115
+
116
+ function strongFamilyOrder(ctx: ExtensionContext): StrongWatchdogFamily[] {
117
+ const current = ctx.model ? familyForModel(toModelInfo(ctx.model)) : undefined;
118
+ if (current === "gpt55") return ["opus48"];
119
+ if (current === "opus48") return ["gpt55"];
120
+ const providerFamily = currentProviderFamily(ctx);
121
+ if (providerFamily === "openai") return ["opus48", "gpt55"];
122
+ if (providerFamily === "anthropic") return ["gpt55", "opus48"];
123
+ return ["gpt55", "opus48"];
124
+ }
125
+
126
+ function findFamilyMatch(family: StrongWatchdogFamily, availableModels: ModelInfo[]): string | undefined {
127
+ const matches = availableModels.filter((entry) => familyForModel(entry) === family);
128
+ if (matches.length === 1) return matches[0]!.fullId;
129
+ return undefined;
130
+ }
131
+
132
+ function resolveStrongCandidate(ctx: ExtensionContext, family: StrongWatchdogFamily): WatchdogModelRecommendation | undefined {
133
+ const availableModels = modelRegistryEntries(ctx);
134
+ const preference = STRONG_WATCHDOG_MODELS[family];
135
+ const queries = [...preference.queries];
136
+ const familyMatch = findFamilyMatch(family, availableModels);
137
+ if (familyMatch) queries.push(familyMatch);
138
+ for (const query of queries) {
139
+ let resolved: ResolvedWatchdogModelInput;
140
+ try {
141
+ resolved = resolveWatchdogModelInput(ctx, query);
142
+ } catch {
143
+ continue;
144
+ }
145
+ const modelInfo = toModelInfo(resolved.registryModel);
146
+ if (familyForModel(modelInfo) !== family) continue;
147
+ if (!getSupportedThinkingLevels(modelInfo).includes(STRONG_WATCHDOG_THINKING)) continue;
148
+ const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "no current session model";
149
+ return {
150
+ model: resolved.model,
151
+ thinking: STRONG_WATCHDOG_THINKING,
152
+ label: preference.label,
153
+ reason: `Use ${preference.label} with thinking high as a strong independent watchdog for ${current}.`,
154
+ registryModel: resolved.registryModel,
155
+ };
156
+ }
157
+ return undefined;
158
+ }
159
+
160
+ export function recommendStrongWatchdogModel(ctx: ExtensionContext): WatchdogModelRecommendation {
161
+ for (const family of strongFamilyOrder(ctx)) {
162
+ const recommendation = resolveStrongCandidate(ctx, family);
163
+ if (recommendation) return recommendation;
164
+ }
165
+ const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "the current session";
166
+ throw new Error(`No authenticated strong complementary watchdog model was found for ${current}. Configure access to Opus 4.8 or GPT 5.5, then run the recommendation again.`);
167
+ }