airaknit 1.1.2-rc.9

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 (440) hide show
  1. package/LICENSE +84 -0
  2. package/README.md +202 -0
  3. package/bin/airaknit +9 -0
  4. package/bin/airaknit-project +14 -0
  5. package/bin/kanna +9 -0
  6. package/dist/client/assets/CompactSummaryMessage-Yw0BDWEJ.js +1 -0
  7. package/dist/client/assets/ExitPlanModeMessage-DIdkQ4uF.js +1 -0
  8. package/dist/client/assets/LocalFilePreviewDialog-DQx2eiCc.js +3 -0
  9. package/dist/client/assets/LocalProjectsSection-C4xlWkgS.js +1 -0
  10. package/dist/client/assets/TextMessage-B5G39DEJ.js +1 -0
  11. package/dist/client/assets/UserMessage-CIkWk-0L.js +1 -0
  12. package/dist/client/assets/_basePickBy-CVrAFfnZ.js +1 -0
  13. package/dist/client/assets/_baseUniq-JL-aaF4P.js +1 -0
  14. package/dist/client/assets/arc-B07zg7ol.js +1 -0
  15. package/dist/client/assets/architecture-YZFGNWBL-PSLVJL3p.js +1 -0
  16. package/dist/client/assets/architectureDiagram-Q4EWVU46-DfWIF1G_.js +36 -0
  17. package/dist/client/assets/array-BGFCBI0e.js +1 -0
  18. package/dist/client/assets/blockDiagram-DXYQGD6D-CzwIeo_B.js +132 -0
  19. package/dist/client/assets/bundle-mjs-BDE2gWbQ.js +1 -0
  20. package/dist/client/assets/button-DO50qOGv.js +1 -0
  21. package/dist/client/assets/c4Diagram-AHTNJAMY-CR8DCQRE.js +10 -0
  22. package/dist/client/assets/channel-Dj-UUfaF.js +1 -0
  23. package/dist/client/assets/chunk-2KRD3SAO-dznP-cn8.js +1 -0
  24. package/dist/client/assets/chunk-336JU56O-Dqss5Vu6.js +2 -0
  25. package/dist/client/assets/chunk-426QAEUC-CEKis_0_.js +1 -0
  26. package/dist/client/assets/chunk-4BX2VUAB-BYOv0Gm1.js +1 -0
  27. package/dist/client/assets/chunk-4TB4RGXK-BxzubH5S.js +206 -0
  28. package/dist/client/assets/chunk-55IACEB6-BSlTj03a.js +1 -0
  29. package/dist/client/assets/chunk-5FUZZQ4R-9Au93Bi1.js +62 -0
  30. package/dist/client/assets/chunk-5PVQY5BW-BhksHFEZ.js +2 -0
  31. package/dist/client/assets/chunk-67CJDMHE-BFwhz-8t.js +1 -0
  32. package/dist/client/assets/chunk-7N4EOEYR-BDUPds87.js +1 -0
  33. package/dist/client/assets/chunk-AA7GKIK3-CEWTdyXO.js +1 -0
  34. package/dist/client/assets/chunk-BO2N2NFS-D0LvxnhU.js +103 -0
  35. package/dist/client/assets/chunk-BSJP7CBP-BNJnK6sq.js +1 -0
  36. package/dist/client/assets/chunk-Bj-mKKzh.js +1 -0
  37. package/dist/client/assets/chunk-CIAEETIT-CYhfoCeN.js +1 -0
  38. package/dist/client/assets/chunk-EDXVE4YY-C5ovJLc0.js +1 -0
  39. package/dist/client/assets/chunk-ENJZ2VHE-HOhYaeGr.js +10 -0
  40. package/dist/client/assets/chunk-FMBD7UC4-BLCiKcAQ.js +15 -0
  41. package/dist/client/assets/chunk-FOC6F5B3-B6GtY2ek.js +1 -0
  42. package/dist/client/assets/chunk-ICPOFSXX-DPoIZoC5.js +122 -0
  43. package/dist/client/assets/chunk-K5T4RW27-BsKN63rv.js +94 -0
  44. package/dist/client/assets/chunk-KGLVRYIC-BUGn9uuY.js +1 -0
  45. package/dist/client/assets/chunk-LIHQZDEY-DhaZyo03.js +1 -0
  46. package/dist/client/assets/chunk-ORNJ4GCN-DlpeeJyi.js +1 -0
  47. package/dist/client/assets/chunk-OYMX7WX6-Dc9q7aYA.js +231 -0
  48. package/dist/client/assets/chunk-QZHKN3VN-BEdrPoSb.js +1 -0
  49. package/dist/client/assets/chunk-U2HBQHQK-CIB3Bjjd.js +70 -0
  50. package/dist/client/assets/chunk-X2U36JSP-CtB-o8Yp.js +1 -0
  51. package/dist/client/assets/chunk-XPW4576I-C6iHhX_8.js +32 -0
  52. package/dist/client/assets/chunk-YZCP3GAM-CTmKr6ZH.js +1 -0
  53. package/dist/client/assets/chunk-ZZ45TVLE-BgU8A2RF.js +1 -0
  54. package/dist/client/assets/classDiagram-6PBFFD2Q-Bqk5e679.js +1 -0
  55. package/dist/client/assets/classDiagram-v2-HSJHXN6E-6pSaZOkC.js +1 -0
  56. package/dist/client/assets/client-BrKWI4CM.js +1 -0
  57. package/dist/client/assets/client-CGgNRU9w.js +1 -0
  58. package/dist/client/assets/client-DMSLRzg9.js +6 -0
  59. package/dist/client/assets/clone-DWcL7whJ.js +1 -0
  60. package/dist/client/assets/cose-bilkent-S5V4N54A-CrV5wsV_.js +1 -0
  61. package/dist/client/assets/cytoscape.esm--aLzKuep.js +321 -0
  62. package/dist/client/assets/dagre-CuRxWcrj.js +1 -0
  63. package/dist/client/assets/dagre-KV5264BT-BIDiVnkA.js +4 -0
  64. package/dist/client/assets/defaultLocale-CRZydyG6.js +1 -0
  65. package/dist/client/assets/diagram-5BDNPKRD-i1kjKRCB.js +10 -0
  66. package/dist/client/assets/diagram-G4DWMVQ6-9ZSLuhbl.js +24 -0
  67. package/dist/client/assets/diagram-MMDJMWI5-B4_CUjgv.js +43 -0
  68. package/dist/client/assets/diagram-TYMM5635-Ct5eTGS8.js +24 -0
  69. package/dist/client/assets/dist-CuB4kiSK.js +1 -0
  70. package/dist/client/assets/erDiagram-SMLLAGMA-Cy38ercc.js +85 -0
  71. package/dist/client/assets/flowDiagram-DWJPFMVM-CZKuYl0V.js +162 -0
  72. package/dist/client/assets/ganttDiagram-T4ZO3ILL-DLPjCh7a.js +292 -0
  73. package/dist/client/assets/gitGraph-7Q5UKJZL-DqbrtEp9.js +1 -0
  74. package/dist/client/assets/gitGraphDiagram-UUTBAWPF-BoRBkDhQ.js +106 -0
  75. package/dist/client/assets/graphlib-BcQ6qlQh.js +1 -0
  76. package/dist/client/assets/highlighted-body-OFNGDK62-BEpBVDTX.js +1 -0
  77. package/dist/client/assets/index-CetCiuqP.js +105 -0
  78. package/dist/client/assets/index-N29Mip7A.css +1 -0
  79. package/dist/client/assets/info-OMHHGYJF-D98DRBJX.js +1 -0
  80. package/dist/client/assets/infoDiagram-42DDH7IO-BAcdTWbt.js +2 -0
  81. package/dist/client/assets/init-B8gtcn7T.js +1 -0
  82. package/dist/client/assets/isArrayLikeObject-D8SJFmkN.js +1 -0
  83. package/dist/client/assets/isEmpty-BF3YX5Jk.js +1 -0
  84. package/dist/client/assets/ishikawaDiagram-UXIWVN3A-Ynu2VKdC.js +70 -0
  85. package/dist/client/assets/journeyDiagram-VCZTEJTY-BjfhQaN3.js +139 -0
  86. package/dist/client/assets/jsx-runtime-CyI9ICYU.js +1 -0
  87. package/dist/client/assets/kanban-definition-6JOO6SKY-JLXH9zUJ.js +89 -0
  88. package/dist/client/assets/katex-B94qP8b6.js +265 -0
  89. package/dist/client/assets/lib--QVjyxmL.js +29 -0
  90. package/dist/client/assets/lib-B6rgJiZ9.js +1 -0
  91. package/dist/client/assets/line-DCrYfLBn.js +1 -0
  92. package/dist/client/assets/linear-_4upLmeo.js +1 -0
  93. package/dist/client/assets/mermaid-GHXKKRXX-rwJHYUmW.js +1 -0
  94. package/dist/client/assets/mermaid-parser.core-KZinfW8o.js +4 -0
  95. package/dist/client/assets/mermaid.core-QqY9gSNe.js +11 -0
  96. package/dist/client/assets/mindmap-definition-QFDTVHPH-TWgHDAzp.js +96 -0
  97. package/dist/client/assets/ordinal-CCj7PWgZ.js +1 -0
  98. package/dist/client/assets/packet-4T2RLAQJ-DEvfkn3F.js +1 -0
  99. package/dist/client/assets/path-DZF-JdEe.js +1 -0
  100. package/dist/client/assets/pie-ZZUOXDRM-72e6WVjb.js +1 -0
  101. package/dist/client/assets/pieDiagram-DEJITSTG-Cl8PCsoj.js +30 -0
  102. package/dist/client/assets/preload-helper-rov5CBGT.js +1 -0
  103. package/dist/client/assets/pty-client-DZ27IS00.js +1 -0
  104. package/dist/client/assets/ptyInstancesStore-D9ag7SYd.js +1 -0
  105. package/dist/client/assets/quadrantDiagram-34T5L4WZ-CHyVGp9E.js +7 -0
  106. package/dist/client/assets/radar-PYXPWWZC-Cp7xd_EY.js +1 -0
  107. package/dist/client/assets/react-CClhXMB2.js +1 -0
  108. package/dist/client/assets/react-Dd6D81m0.js +1 -0
  109. package/dist/client/assets/react-dom--G6_6fQ_.js +1 -0
  110. package/dist/client/assets/requirementDiagram-MS252O5E-DaanG2iM.js +84 -0
  111. package/dist/client/assets/rough.esm-BsmKo2S5.js +1 -0
  112. package/dist/client/assets/sankeyDiagram-XADWPNL6-B_fhLY36.js +10 -0
  113. package/dist/client/assets/sequenceDiagram-FGHM5R23-C5FNrveI.js +157 -0
  114. package/dist/client/assets/src-DeTlMJAU.js +1 -0
  115. package/dist/client/assets/stateDiagram-FHFEXIEX-nTTcdjjQ.js +1 -0
  116. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dw0632j_.js +1 -0
  117. package/dist/client/assets/timeline-definition-GMOUNBTQ-DkQV1yP8.js +120 -0
  118. package/dist/client/assets/treeView-SZITEDCU-ZLIgC7_K.js +1 -0
  119. package/dist/client/assets/treemap-W4RFUUIX-BqaMbB6N.js +1 -0
  120. package/dist/client/assets/uiIdentityOverlay-Ba7GNj7m.js +1 -0
  121. package/dist/client/assets/vennDiagram-DHZGUBPP-DbZ2xgs6.js +34 -0
  122. package/dist/client/assets/wardley-RL74JXVD-DXQS8zf4.js +1 -0
  123. package/dist/client/assets/wardleyDiagram-NUSXRM2D-BzCJ6MAu.js +20 -0
  124. package/dist/client/assets/xychartDiagram-5P7HB3ND-BSlFecop.js +7 -0
  125. package/dist/client/favicon.png +0 -0
  126. package/dist/client/favicon.svg +15 -0
  127. package/dist/client/fonts/body-medium.woff2 +0 -0
  128. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  129. package/dist/client/fonts/body-regular.woff2 +0 -0
  130. package/dist/client/fonts/body-semibold.woff2 +0 -0
  131. package/dist/client/index.html +31 -0
  132. package/dist/client/manifest.webmanifest +12 -0
  133. package/dist/client/sw.js +32 -0
  134. package/package.json +122 -0
  135. package/src/nats/auth-callout/callout-config.test.ts +93 -0
  136. package/src/nats/auth-callout/callout-config.ts +109 -0
  137. package/src/nats/auth-callout/callout.integration.test.ts +332 -0
  138. package/src/nats/auth-callout/keys.ts +103 -0
  139. package/src/nats/auth-callout/responder.ts +241 -0
  140. package/src/nats/auth-callout/scope-policy.test.ts +159 -0
  141. package/src/nats/auth-callout/scope-policy.ts +210 -0
  142. package/src/nats/auth-callout/token.test.ts +163 -0
  143. package/src/nats/auth-callout/token.ts +157 -0
  144. package/src/nats/nats-daemon-callout.ts +194 -0
  145. package/src/nats/nats-daemon.test.ts +77 -0
  146. package/src/nats/nats-daemon.ts +50 -0
  147. package/src/nats/nats-token.test.ts +61 -0
  148. package/src/nats/nats-token.ts +59 -0
  149. package/src/runner/coordination-mcp-integration.test.ts +134 -0
  150. package/src/runner/nats-coordination-client.test.ts +49 -0
  151. package/src/runner/nats-coordination-client.ts +94 -0
  152. package/src/runner/runner-agent.test.ts +469 -0
  153. package/src/runner/runner-agent.ts +453 -0
  154. package/src/runner/runner-credential.test.ts +93 -0
  155. package/src/runner/runner-credential.ts +82 -0
  156. package/src/runner/runner-nats.test.ts +495 -0
  157. package/src/runner/runner-nats.ts +323 -0
  158. package/src/runner/runner-pair.test.ts +107 -0
  159. package/src/runner/runner-pair.ts +81 -0
  160. package/src/runner/runner.test.ts +135 -0
  161. package/src/runner/runner.ts +212 -0
  162. package/src/runner/turn-factories.test.ts +97 -0
  163. package/src/runner/turn-factories.ts +475 -0
  164. package/src/server/agent-config-journey.test.ts +106 -0
  165. package/src/server/agent.ts +8 -0
  166. package/src/server/auto-continue/auth-error-detector.ts +66 -0
  167. package/src/server/auto-continue/limit-detector.ts +194 -0
  168. package/src/server/bm25.test.ts +92 -0
  169. package/src/server/bm25.ts +101 -0
  170. package/src/server/chat-events-jetstream.test.ts +135 -0
  171. package/src/server/claude-harness.ts +360 -0
  172. package/src/server/claude-pty/agent-normalizers.ts +309 -0
  173. package/src/server/claude-pty/auth.test.ts +38 -0
  174. package/src/server/claude-pty/auth.ts +32 -0
  175. package/src/server/claude-pty/claude-session-registry.adapter.ts +81 -0
  176. package/src/server/claude-pty/claude-session-registry.test.ts +149 -0
  177. package/src/server/claude-pty/driver.test.ts +902 -0
  178. package/src/server/claude-pty/driver.ts +807 -0
  179. package/src/server/claude-pty/jsonl-path.adapter.ts +57 -0
  180. package/src/server/claude-pty/jsonl-path.test.ts +114 -0
  181. package/src/server/claude-pty/jsonl-to-event.test.ts +241 -0
  182. package/src/server/claude-pty/jsonl-to-event.ts +174 -0
  183. package/src/server/claude-pty/output-ring.test.ts +35 -0
  184. package/src/server/claude-pty/output-ring.ts +25 -0
  185. package/src/server/claude-pty/parity-matrix.test.ts +227 -0
  186. package/src/server/claude-pty/pid-registry.adapter.ts +135 -0
  187. package/src/server/claude-pty/pid-registry.test.ts +122 -0
  188. package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +20 -0
  189. package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +32 -0
  190. package/src/server/claude-pty/pty-instance-registry.test.ts +177 -0
  191. package/src/server/claude-pty/pty-instance-registry.ts +166 -0
  192. package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +103 -0
  193. package/src/server/claude-pty/pty-memory-sampler.adapter.ts +85 -0
  194. package/src/server/claude-pty/pty-process.adapter.ts +66 -0
  195. package/src/server/claude-pty/pty-process.test.ts +49 -0
  196. package/src/server/claude-pty/resolve-binary.adapter.ts +106 -0
  197. package/src/server/claude-pty/resolve-binary.test.ts +118 -0
  198. package/src/server/claude-pty/runtime-dir.adapter.ts +19 -0
  199. package/src/server/claude-pty/settings-writer.adapter.ts +27 -0
  200. package/src/server/claude-pty/settings-writer.test.ts +22 -0
  201. package/src/server/claude-pty/smoke-test-io.adapter.ts +28 -0
  202. package/src/server/claude-pty/smoke-test.test.ts +191 -0
  203. package/src/server/claude-pty/smoke-test.ts +185 -0
  204. package/src/server/claude-pty/subagent-orchestrator.ts +887 -0
  205. package/src/server/claude-pty/tool-callback.ts +274 -0
  206. package/src/server/claude-pty/tui-control.test.ts +272 -0
  207. package/src/server/claude-pty/tui-control.ts +182 -0
  208. package/src/server/claude-pty/tui-source.adapter.test.ts +360 -0
  209. package/src/server/claude-pty/tui-source.adapter.ts +343 -0
  210. package/src/server/claude-pty/tunnel-gateway.ts +12 -0
  211. package/src/server/claude-pty-mcp/canonical-args.ts +15 -0
  212. package/src/server/claude-pty-mcp/fs-stat.adapter.ts +8 -0
  213. package/src/server/claude-pty-mcp/history-primer.ts +90 -0
  214. package/src/server/claude-pty-mcp/http-server.adapter.ts +33 -0
  215. package/src/server/claude-pty-mcp/mcp-http.ts +177 -0
  216. package/src/server/claude-pty-mcp/mcp.ts +412 -0
  217. package/src/server/claude-pty-mcp/mention-parser.ts +25 -0
  218. package/src/server/claude-pty-mcp/paths.ts +24 -0
  219. package/src/server/claude-pty-mcp/permission-gate.ts +243 -0
  220. package/src/server/claude-pty-mcp/terminal-pid-registry.adapter.ts +107 -0
  221. package/src/server/claude-pty-mcp/tools/ask-user-question.test.ts +119 -0
  222. package/src/server/claude-pty-mcp/tools/ask-user-question.ts +61 -0
  223. package/src/server/claude-pty-mcp/tools/bash.adapter.ts +76 -0
  224. package/src/server/claude-pty-mcp/tools/bash.test.ts +56 -0
  225. package/src/server/claude-pty-mcp/tools/delegate-subagent.test.ts +155 -0
  226. package/src/server/claude-pty-mcp/tools/delegate-subagent.ts +111 -0
  227. package/src/server/claude-pty-mcp/tools/edit.adapter.ts +95 -0
  228. package/src/server/claude-pty-mcp/tools/edit.test.ts +93 -0
  229. package/src/server/claude-pty-mcp/tools/exit-plan-mode.test.ts +61 -0
  230. package/src/server/claude-pty-mcp/tools/exit-plan-mode.ts +50 -0
  231. package/src/server/claude-pty-mcp/tools/glob.adapter.ts +86 -0
  232. package/src/server/claude-pty-mcp/tools/glob.test.ts +61 -0
  233. package/src/server/claude-pty-mcp/tools/grep.adapter.ts +126 -0
  234. package/src/server/claude-pty-mcp/tools/grep.test.ts +62 -0
  235. package/src/server/claude-pty-mcp/tools/read.adapter.ts +58 -0
  236. package/src/server/claude-pty-mcp/tools/read.test.ts +62 -0
  237. package/src/server/claude-pty-mcp/tools/tool-callback-shim.ts +42 -0
  238. package/src/server/claude-pty-mcp/tools/webfetch.test.ts +81 -0
  239. package/src/server/claude-pty-mcp/tools/webfetch.ts +82 -0
  240. package/src/server/claude-pty-mcp/tools/websearch.test.ts +40 -0
  241. package/src/server/claude-pty-mcp/tools/websearch.ts +42 -0
  242. package/src/server/claude-pty-mcp/tools/write.adapter.ts +60 -0
  243. package/src/server/claude-pty-mcp/tools/write.test.ts +52 -0
  244. package/src/server/claude-pty-mcp/uploads.adapter.ts +98 -0
  245. package/src/server/claude-pty-mcp/uploads.ts +38 -0
  246. package/src/server/claude-turn.test.ts +176 -0
  247. package/src/server/cli-runtime.test.ts +456 -0
  248. package/src/server/cli-runtime.ts +374 -0
  249. package/src/server/cli-supervisor.ts +81 -0
  250. package/src/server/cli.ts +78 -0
  251. package/src/server/client-log-forwarder.test.ts +74 -0
  252. package/src/server/client-log-forwarder.ts +75 -0
  253. package/src/server/codex-app-server-protocol.ts +449 -0
  254. package/src/server/codex-app-server.test.ts +2990 -0
  255. package/src/server/codex-app-server.ts +1713 -0
  256. package/src/server/coordination-integration.test.ts +63 -0
  257. package/src/server/coordination-mcp.test.ts +149 -0
  258. package/src/server/coordination-mcp.ts +197 -0
  259. package/src/server/delegation-coordinator.test.ts +675 -0
  260. package/src/server/delegation-coordinator.ts +454 -0
  261. package/src/server/discovery.test.ts +211 -0
  262. package/src/server/discovery.ts +301 -0
  263. package/src/server/event-store-agent-config.test.ts +124 -0
  264. package/src/server/event-store-coordination.test.ts +149 -0
  265. package/src/server/event-store-profile.test.ts +132 -0
  266. package/src/server/event-store-repo.test.ts +154 -0
  267. package/src/server/event-store-runner-team.test.ts +104 -0
  268. package/src/server/event-store.test.ts +342 -0
  269. package/src/server/event-store.ts +2208 -0
  270. package/src/server/events.ts +379 -0
  271. package/src/server/extension-router.test.ts +183 -0
  272. package/src/server/extension-router.ts +114 -0
  273. package/src/server/extensions/agents/server.test.ts +191 -0
  274. package/src/server/extensions/agents/server.ts +108 -0
  275. package/src/server/extensions/c3/server.test.ts +284 -0
  276. package/src/server/extensions/c3/server.ts +212 -0
  277. package/src/server/extensions/code/server.test.ts +200 -0
  278. package/src/server/extensions/code/server.ts +150 -0
  279. package/src/server/extensions.config.ts +10 -0
  280. package/src/server/external-open.ts +69 -0
  281. package/src/server/generate-fork-context.ts +58 -0
  282. package/src/server/generate-merge-context.test.ts +290 -0
  283. package/src/server/generate-merge-context.ts +141 -0
  284. package/src/server/generate-title.ts +36 -0
  285. package/src/server/git-clone-policy.test.ts +138 -0
  286. package/src/server/git-clone-policy.ts +27 -0
  287. package/src/server/harness-types.ts +1 -0
  288. package/src/server/journey-verification.test.ts +640 -0
  289. package/src/server/journey-verification.ts +195 -0
  290. package/src/server/machine-name.ts +22 -0
  291. package/src/server/nats-auth.test.ts +92 -0
  292. package/src/server/nats-auth.ts +6 -0
  293. package/src/server/nats-bind-guard.test.ts +71 -0
  294. package/src/server/nats-bind-guard.ts +42 -0
  295. package/src/server/nats-bridge.test.ts +141 -0
  296. package/src/server/nats-bridge.ts +111 -0
  297. package/src/server/nats-connector.test.ts +56 -0
  298. package/src/server/nats-connector.ts +71 -0
  299. package/src/server/nats-daemon-manager.test.ts +76 -0
  300. package/src/server/nats-daemon-manager.ts +174 -0
  301. package/src/server/nats-publisher.test.ts +356 -0
  302. package/src/server/nats-publisher.ts +271 -0
  303. package/src/server/nats-responders.test.ts +1018 -0
  304. package/src/server/nats-responders.ts +925 -0
  305. package/src/server/nats-streams.test.ts +112 -0
  306. package/src/server/nats-streams.ts +129 -0
  307. package/src/server/oauth-pool/oauth-responders.ts +185 -0
  308. package/src/server/oauth-pool/oauth-settings-store.ts +173 -0
  309. package/src/server/oauth-pool/oauth-token-pool.ts +303 -0
  310. package/src/server/orchestration.test.ts +1063 -0
  311. package/src/server/orchestration.ts +716 -0
  312. package/src/server/pairing-endpoints.test.ts +171 -0
  313. package/src/server/pairing-store.test.ts +154 -0
  314. package/src/server/pairing-store.ts +124 -0
  315. package/src/server/paths.ts +27 -0
  316. package/src/server/pr3-liveness.test.ts +252 -0
  317. package/src/server/process-utils.ts +10 -0
  318. package/src/server/project-cli.ts +180 -0
  319. package/src/server/provider-catalog.test.ts +177 -0
  320. package/src/server/provider-catalog.ts +146 -0
  321. package/src/server/pty-responders.ts +345 -0
  322. package/src/server/push-notifications.test.ts +234 -0
  323. package/src/server/push-notifications.ts +188 -0
  324. package/src/server/quick-response.test.ts +196 -0
  325. package/src/server/quick-response.ts +154 -0
  326. package/src/server/read-models-agent-config.test.ts +59 -0
  327. package/src/server/read-models-coordination.test.ts +69 -0
  328. package/src/server/read-models.test.ts +332 -0
  329. package/src/server/read-models.ts +258 -0
  330. package/src/server/repo-journey.test.ts +96 -0
  331. package/src/server/repo-manager.test.ts +118 -0
  332. package/src/server/repo-manager.ts +97 -0
  333. package/src/server/repo-status.test.ts +54 -0
  334. package/src/server/repo-status.ts +82 -0
  335. package/src/server/restart.test.ts +27 -0
  336. package/src/server/restart.ts +30 -0
  337. package/src/server/runner-incompatible-gate.test.ts +383 -0
  338. package/src/server/runner-manager.test.ts +72 -0
  339. package/src/server/runner-manager.ts +312 -0
  340. package/src/server/runner-pairing-urls.test.ts +59 -0
  341. package/src/server/runner-pairing-urls.ts +67 -0
  342. package/src/server/runner-proxy.test.ts +456 -0
  343. package/src/server/runner-proxy.ts +494 -0
  344. package/src/server/runner-router.test.ts +429 -0
  345. package/src/server/runner-router.ts +212 -0
  346. package/src/server/runner-routing.test.ts +584 -0
  347. package/src/server/runtime-registry.test.ts +436 -0
  348. package/src/server/runtime-registry.ts +307 -0
  349. package/src/server/sandbox-health.test.ts +127 -0
  350. package/src/server/sandbox-health.ts +94 -0
  351. package/src/server/sandbox-journey.test.ts +232 -0
  352. package/src/server/sandbox-manager.test.ts +146 -0
  353. package/src/server/sandbox-manager.ts +159 -0
  354. package/src/server/server.test.ts +287 -0
  355. package/src/server/server.ts +1108 -0
  356. package/src/server/session-discovery.test.ts +129 -0
  357. package/src/server/session-discovery.ts +475 -0
  358. package/src/server/session-index.test.ts +362 -0
  359. package/src/server/session-index.ts +119 -0
  360. package/src/server/session-seed.ts +288 -0
  361. package/src/server/share.test.ts +108 -0
  362. package/src/server/share.ts +113 -0
  363. package/src/server/skill-discovery.test.ts +201 -0
  364. package/src/server/skill-discovery.ts +77 -0
  365. package/src/server/storage/test-helpers.ts +67 -0
  366. package/src/server/terminal-manager.test.ts +309 -0
  367. package/src/server/terminal-manager.ts +354 -0
  368. package/src/server/transcript-consumer.test.ts +339 -0
  369. package/src/server/transcript-consumer.ts +162 -0
  370. package/src/server/transcript-search.test.ts +193 -0
  371. package/src/server/transcript-search.ts +83 -0
  372. package/src/server/transcript-utils.ts +52 -0
  373. package/src/server/update-manager.test.ts +107 -0
  374. package/src/server/update-manager.ts +230 -0
  375. package/src/server/workflow-engine.test.ts +251 -0
  376. package/src/server/workflow-engine.ts +169 -0
  377. package/src/server/workflow-mcp.ts +49 -0
  378. package/src/server/workflow-store.test.ts +155 -0
  379. package/src/server/workflow-store.ts +139 -0
  380. package/src/server/workspace-agent-integration.test.ts +167 -0
  381. package/src/server/workspace-agent-routes.test.ts +127 -0
  382. package/src/server/workspace-agent-routes.ts +89 -0
  383. package/src/server/workspace-agent.test.ts +103 -0
  384. package/src/server/workspace-agent.ts +102 -0
  385. package/src/server/workspace-cli.test.ts +79 -0
  386. package/src/server/workspace-config-manager.test.ts +109 -0
  387. package/src/server/workspace-config-manager.ts +83 -0
  388. package/src/server/workspace-directory-policy.test.ts +109 -0
  389. package/src/server/workspace-directory-policy.ts +56 -0
  390. package/src/shared/agent-config-types.ts +25 -0
  391. package/src/shared/branding.test.ts +42 -0
  392. package/src/shared/branding.ts +54 -0
  393. package/src/shared/compression.test.ts +85 -0
  394. package/src/shared/compression.ts +42 -0
  395. package/src/shared/coordination-store.test.ts +24 -0
  396. package/src/shared/coordination-store.ts +26 -0
  397. package/src/shared/dev-ports.test.ts +84 -0
  398. package/src/shared/dev-ports.ts +100 -0
  399. package/src/shared/extension-types.ts +45 -0
  400. package/src/shared/fork-presets.ts +54 -0
  401. package/src/shared/harness-types.test.ts +15 -0
  402. package/src/shared/harness-types.ts +21 -0
  403. package/src/shared/log-sink.test.ts +112 -0
  404. package/src/shared/log-sink.ts +185 -0
  405. package/src/shared/mention-pattern.ts +7 -0
  406. package/src/shared/merge-presets.ts +41 -0
  407. package/src/shared/nats-subjects.test.ts +61 -0
  408. package/src/shared/nats-subjects.ts +131 -0
  409. package/src/shared/permission-policy.ts +136 -0
  410. package/src/shared/ports.ts +3 -0
  411. package/src/shared/preset-types.ts +15 -0
  412. package/src/shared/profile-types.ts +49 -0
  413. package/src/shared/projectFileUrl.ts +36 -0
  414. package/src/shared/protocol.ts +153 -0
  415. package/src/shared/pty-instance.ts +43 -0
  416. package/src/shared/puggy/diagnostics/result.ts +18 -0
  417. package/src/shared/puggy/expressions/evaluate.ts +292 -0
  418. package/src/shared/puggy/index.test.ts +101 -0
  419. package/src/shared/puggy/index.ts +69 -0
  420. package/src/shared/puggy/parser/ast.ts +110 -0
  421. package/src/shared/puggy/parser/parser.ts +624 -0
  422. package/src/shared/puggy/renderer/html.ts +447 -0
  423. package/src/shared/runner-protocol.test.ts +277 -0
  424. package/src/shared/runner-protocol.ts +210 -0
  425. package/src/shared/runner-team-types.ts +73 -0
  426. package/src/shared/runtime-types.ts +48 -0
  427. package/src/shared/sandbox-types.ts +53 -0
  428. package/src/shared/tailwind-build.test.ts +12 -0
  429. package/src/shared/tinkaria-system-prompt.ts +101 -0
  430. package/src/shared/tools.test.ts +335 -0
  431. package/src/shared/tools.ts +397 -0
  432. package/src/shared/transcript-entries.ts +27 -0
  433. package/src/shared/transcript-render.test.ts +225 -0
  434. package/src/shared/transcript-render.ts +467 -0
  435. package/src/shared/types.ts +1031 -0
  436. package/src/shared/vite-config.test.ts +47 -0
  437. package/src/shared/web-context.test.ts +110 -0
  438. package/src/shared/web-context.ts +76 -0
  439. package/src/shared/workflow-types.ts +51 -0
  440. package/src/shared/workspace-types.ts +100 -0
@@ -0,0 +1,584 @@
1
+ /**
2
+ * runner-routing.test.ts
3
+ *
4
+ * Tests the per-session dispatch layer added in Stage 2 of PR5.
5
+ * These tests exercise RunnerProxy with a real (embedded) NATS server but a
6
+ * fake RunnerRouter and a fake EventStore, so no disk I/O or live runner is needed.
7
+ *
8
+ * Contract under test:
9
+ * - resolveRunnerForChat selects and persists the runner the first time
10
+ * - sticky pin is honoured on subsequent turns (no re-persist when unchanged)
11
+ * - cancel/respondTool/disposeChat go to the PINNED runner, never re-route
12
+ * - needs_pick (non-empty candidates) throws RunnerPickRequired
13
+ * - needs_pick (empty candidates) throws a plain Error (fail-fast)
14
+ * - unavailable throws a plain Error (fail-fast)
15
+ * - per-runner capability gate is enforced by sendCommand
16
+ */
17
+
18
+ import { describe, test, expect, afterEach } from "bun:test"
19
+ import { NatsServer } from "@lagz0ne/nats-embedded"
20
+ import { connect, type NatsConnection } from "@nats-io/transport-node"
21
+ import { RunnerProxy, RunnerPickRequired } from "./runner-proxy"
22
+ import type { RunnerRouter, RunnerSelection, RunnerDescriptor } from "./runner-router"
23
+ import type { SessionStatus } from "../shared/types"
24
+
25
+ const encoder = new TextEncoder()
26
+ const decoder = new TextDecoder()
27
+
28
+ // ── Fake runner (NATS subscriber) ────────────────────────────────────────────
29
+
30
+ function createMockRunner(nc: NatsConnection, runnerId: string) {
31
+ const received: { subject: string; data: unknown }[] = []
32
+ const sub = nc.subscribe(`runtime.runner.cmd.${runnerId}.>`)
33
+ void (async () => {
34
+ for await (const msg of sub) {
35
+ received.push({
36
+ subject: msg.subject,
37
+ data: JSON.parse(decoder.decode(msg.data)),
38
+ })
39
+ msg.respond(encoder.encode(JSON.stringify({ ok: true })))
40
+ }
41
+ })()
42
+ return { received, dispose: () => sub.unsubscribe() }
43
+ }
44
+
45
+ // ── Minimal store stub ────────────────────────────────────────────────────────
46
+
47
+ interface StubChat {
48
+ id: string
49
+ workspaceId: string
50
+ repoId: null
51
+ title: string
52
+ provider: "claude" | "codex" | null
53
+ model: string | null
54
+ sessionToken: null
55
+ planMode: boolean
56
+ runnerId?: string | null
57
+ }
58
+
59
+ function createMockStore(chatOverrides?: Partial<StubChat>) {
60
+ const calls: Array<{ method: string; args: unknown[] }> = []
61
+ const chat: StubChat = {
62
+ id: "chat-1",
63
+ workspaceId: "ws-1",
64
+ repoId: null,
65
+ title: "Test",
66
+ provider: "claude",
67
+ model: "sonnet",
68
+ sessionToken: null,
69
+ planMode: false,
70
+ runnerId: undefined,
71
+ ...chatOverrides,
72
+ }
73
+
74
+ return {
75
+ requireChat: (_chatId: string) => ({ ...chat }),
76
+ getProject: (_wsId: string) => ({ id: "ws-1", localPath: "/tmp/proj", title: "Proj" }),
77
+ getMessages: async (_chatId: string) => [] as unknown[],
78
+ createChat: async (workspaceId: string) => ({
79
+ id: "new-chat",
80
+ workspaceId,
81
+ repoId: null,
82
+ title: "New Chat",
83
+ provider: null,
84
+ sessionToken: null,
85
+ planMode: false,
86
+ }),
87
+ setChatProvider: async (chatId: string, provider: string) => {
88
+ calls.push({ method: "setChatProvider", args: [chatId, provider] })
89
+ },
90
+ setChatModel: async (chatId: string, model: string | null) => {
91
+ calls.push({ method: "setChatModel", args: [chatId, model] })
92
+ },
93
+ setPlanMode: async (chatId: string, planMode: boolean) => {
94
+ calls.push({ method: "setPlanMode", args: [chatId, planMode] })
95
+ },
96
+ setSessionToken: async (chatId: string, token: string | null) => {
97
+ calls.push({ method: "setSessionToken", args: [chatId, token] })
98
+ },
99
+ setChatRunner: async (chatId: string, runnerId: string | null) => {
100
+ calls.push({ method: "setChatRunner", args: [chatId, runnerId] })
101
+ // Mutate the in-memory chat so subsequent requireChat calls see the pin
102
+ chat.runnerId = runnerId
103
+ },
104
+ enqueueQueuedTurn: async (args: unknown) => {
105
+ calls.push({ method: "enqueueQueuedTurn", args: [args] })
106
+ },
107
+ getQueuedTurn: (_chatId: string) => null,
108
+ clearQueuedTurn: async (_chatId: string) => {},
109
+ state: {
110
+ providerProfiles: new Map(),
111
+ workspaceProfileOverrides: new Map(),
112
+ },
113
+ _calls: calls,
114
+ _chat: chat,
115
+ } as unknown as import("./runner-proxy").RunnerProxyOptions["store"] & { _calls: typeof calls; _chat: StubChat }
116
+ }
117
+
118
+ // ── Fake RunnerRouter ─────────────────────────────────────────────────────────
119
+
120
+ function makeRouter(selectFn: (req: Parameters<RunnerRouter["select"]>[0]) => Promise<RunnerSelection>): RunnerRouter {
121
+ return {
122
+ list: async () => [],
123
+ get: async () => null,
124
+ select: selectFn,
125
+ } as unknown as RunnerRouter
126
+ }
127
+
128
+ function makeDescriptor(runnerId: string, overrides: Partial<RunnerDescriptor> = {}): RunnerDescriptor {
129
+ return {
130
+ runnerId,
131
+ state: "online",
132
+ capabilities: { providers: ["claude", "codex"] },
133
+ protocolVersion: 1,
134
+ incompatible: false,
135
+ lastSeenAt: Date.now(),
136
+ pid: null,
137
+ ownerId: null,
138
+ isShared: false,
139
+ ...overrides,
140
+ }
141
+ }
142
+
143
+ // ── Test suite ────────────────────────────────────────────────────────────────
144
+
145
+ describe("RunnerProxy — per-session routing (Stage 2)", () => {
146
+ let natsServer: NatsServer | null = null
147
+ let clientNc: NatsConnection | null = null
148
+ let runnerNc: NatsConnection | null = null
149
+
150
+ afterEach(async () => {
151
+ if (clientNc && !clientNc.isClosed()) await clientNc.drain()
152
+ clientNc = null
153
+ if (runnerNc && !runnerNc.isClosed()) await runnerNc.drain()
154
+ runnerNc = null
155
+ if (natsServer) await natsServer.stop()
156
+ natsServer = null
157
+ })
158
+
159
+ // ── 1. First turn: no pin → router selects → persisted + dispatched ──────
160
+
161
+ test("first turn with no chat.runnerId: setChatRunner called and command sent to selected runner", async () => {
162
+ const R1 = "runner-R1"
163
+ natsServer = await NatsServer.start({})
164
+ clientNc = await connect({ servers: natsServer.url })
165
+ runnerNc = await connect({ servers: natsServer.url })
166
+
167
+ const r1Mock = createMockRunner(runnerNc, R1)
168
+ await runnerNc.flush()
169
+
170
+ const store = createMockStore({ runnerId: undefined })
171
+
172
+ const router = makeRouter(async () => ({
173
+ kind: "selected",
174
+ runnerId: R1,
175
+ sticky: false,
176
+ }))
177
+
178
+ const proxy = new RunnerProxy({
179
+ nc: clientNc,
180
+ store,
181
+ runnerId: "shared-runner",
182
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
183
+ router,
184
+ sharedRunnerId: () => "shared-runner",
185
+ getRunnerReadiness: (_id) => ({
186
+ incompatible: false,
187
+ protocolVersion: 1,
188
+ capabilities: { providers: ["claude" as const, "codex" as const] },
189
+ }),
190
+ })
191
+
192
+ await proxy.send({ type: "chat.send", chatId: "chat-1", content: "hello", model: "sonnet" })
193
+
194
+ // setChatRunner called with R1
195
+ const persistCall = store._calls.find((c) => c.method === "setChatRunner")
196
+ expect(persistCall).toBeDefined()
197
+ expect(persistCall!.args).toEqual(["chat-1", R1])
198
+
199
+ // Command dispatched to R1's subject
200
+ expect(r1Mock.received).toHaveLength(1)
201
+ expect(r1Mock.received[0]!.subject).toBe(`runtime.runner.cmd.${R1}.start_turn`)
202
+
203
+ r1Mock.dispose()
204
+ })
205
+
206
+ // ── 2. Sticky reused: pin already set, router returns sticky=true ─────────
207
+
208
+ test("sticky pin: if runnerId unchanged setChatRunner not called again", async () => {
209
+ const R1 = "runner-sticky"
210
+ natsServer = await NatsServer.start({})
211
+ clientNc = await connect({ servers: natsServer.url })
212
+ runnerNc = await connect({ servers: natsServer.url })
213
+ const r1Mock = createMockRunner(runnerNc, R1)
214
+ await runnerNc.flush()
215
+
216
+ // Chat already has runnerId=R1 pinned
217
+ const store = createMockStore({ runnerId: R1 })
218
+
219
+ const router = makeRouter(async () => ({
220
+ kind: "selected",
221
+ runnerId: R1,
222
+ sticky: true,
223
+ }))
224
+
225
+ const proxy = new RunnerProxy({
226
+ nc: clientNc,
227
+ store,
228
+ runnerId: "shared-runner",
229
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
230
+ router,
231
+ sharedRunnerId: () => "shared-runner",
232
+ getRunnerReadiness: (_id) => ({
233
+ incompatible: false,
234
+ protocolVersion: 1,
235
+ capabilities: { providers: ["claude" as const, "codex" as const] },
236
+ }),
237
+ })
238
+
239
+ await proxy.send({ type: "chat.send", chatId: "chat-1", content: "hello", model: "sonnet" })
240
+
241
+ // No setChatRunner because pin is unchanged
242
+ const persistCall = store._calls.find((c) => c.method === "setChatRunner")
243
+ expect(persistCall).toBeUndefined()
244
+
245
+ // Command still goes to R1
246
+ expect(r1Mock.received).toHaveLength(1)
247
+ expect(r1Mock.received[0]!.subject).toBe(`runtime.runner.cmd.${R1}.start_turn`)
248
+
249
+ r1Mock.dispose()
250
+ })
251
+
252
+ // ── 3. cancel dispatched to pinned runner, router.select NOT called ───────
253
+
254
+ test("cancel dispatches to pinned chat.runnerId without calling router", async () => {
255
+ const R2 = "runner-pinned"
256
+ const R1 = "runner-other"
257
+ natsServer = await NatsServer.start({})
258
+ clientNc = await connect({ servers: natsServer.url })
259
+ runnerNc = await connect({ servers: natsServer.url })
260
+ const r2Mock = createMockRunner(runnerNc, R2)
261
+ await runnerNc.flush()
262
+
263
+ const store = createMockStore({ runnerId: R2 })
264
+
265
+ let routerSelectCalled = false
266
+ const router = makeRouter(async () => {
267
+ routerSelectCalled = true
268
+ return { kind: "selected", runnerId: R1, sticky: false }
269
+ })
270
+
271
+ const proxy = new RunnerProxy({
272
+ nc: clientNc,
273
+ store,
274
+ runnerId: "shared-runner",
275
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
276
+ router,
277
+ sharedRunnerId: () => "shared-runner",
278
+ getRunnerReadiness: (_id) => ({ incompatible: false, protocolVersion: 1, capabilities: null }),
279
+ })
280
+
281
+ await proxy.cancel("chat-1")
282
+
283
+ expect(routerSelectCalled).toBe(false)
284
+ expect(r2Mock.received).toHaveLength(1)
285
+ expect(r2Mock.received[0]!.subject).toBe(`runtime.runner.cmd.${R2}.cancel_turn`)
286
+
287
+ r2Mock.dispose()
288
+ })
289
+
290
+ // ── 4. respondTool dispatched to pinned runner ────────────────────────────
291
+
292
+ test("respondTool dispatches to pinned runner, router.select NOT called", async () => {
293
+ const R2 = "runner-for-tool"
294
+ natsServer = await NatsServer.start({})
295
+ clientNc = await connect({ servers: natsServer.url })
296
+ runnerNc = await connect({ servers: natsServer.url })
297
+ const r2Mock = createMockRunner(runnerNc, R2)
298
+ await runnerNc.flush()
299
+
300
+ const store = createMockStore({ runnerId: R2 })
301
+
302
+ let routerSelectCalled = false
303
+ const router = makeRouter(async () => {
304
+ routerSelectCalled = true
305
+ return { kind: "selected", runnerId: "other", sticky: false }
306
+ })
307
+
308
+ const proxy = new RunnerProxy({
309
+ nc: clientNc,
310
+ store,
311
+ runnerId: "shared-runner",
312
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
313
+ router,
314
+ sharedRunnerId: () => "shared-runner",
315
+ getRunnerReadiness: (_id) => ({ incompatible: false, protocolVersion: 1, capabilities: null }),
316
+ })
317
+
318
+ await proxy.respondTool({ type: "chat.respondTool", chatId: "chat-1", toolUseId: "tool-1", result: "ok" })
319
+
320
+ expect(routerSelectCalled).toBe(false)
321
+ expect(r2Mock.received).toHaveLength(1)
322
+ expect(r2Mock.received[0]!.subject).toBe(`runtime.runner.cmd.${R2}.respond_tool`)
323
+
324
+ r2Mock.dispose()
325
+ })
326
+
327
+ // ── 5. disposeChat dispatches to pinned runner ────────────────────────────
328
+
329
+ test("disposeChat (cancel + stop_chat_pty) dispatches to pinned runner", async () => {
330
+ const R2 = "runner-dispose"
331
+ natsServer = await NatsServer.start({})
332
+ clientNc = await connect({ servers: natsServer.url })
333
+ runnerNc = await connect({ servers: natsServer.url })
334
+ const r2Mock = createMockRunner(runnerNc, R2)
335
+ await runnerNc.flush()
336
+
337
+ const store = createMockStore({ runnerId: R2 })
338
+
339
+ let routerSelectCalled = false
340
+ const router = makeRouter(async () => {
341
+ routerSelectCalled = true
342
+ return { kind: "selected", runnerId: "other", sticky: false }
343
+ })
344
+
345
+ const proxy = new RunnerProxy({
346
+ nc: clientNc,
347
+ store,
348
+ runnerId: "shared-runner",
349
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
350
+ router,
351
+ sharedRunnerId: () => "shared-runner",
352
+ getRunnerReadiness: (_id) => ({ incompatible: false, protocolVersion: 1, capabilities: null }),
353
+ })
354
+
355
+ await proxy.disposeChat("chat-1")
356
+
357
+ expect(routerSelectCalled).toBe(false)
358
+ const subjects = r2Mock.received.map((m) => m.subject)
359
+ expect(subjects).toContain(`runtime.runner.cmd.${R2}.cancel_turn`)
360
+ expect(subjects).toContain(`runtime.runner.cmd.${R2}.stop_chat_pty`)
361
+
362
+ r2Mock.dispose()
363
+ })
364
+
365
+ // ── 6. needs_pick with non-empty candidates → RunnerPickRequired ──────────
366
+
367
+ test("needs_pick (non-empty candidates) throws RunnerPickRequired, no NATS request", async () => {
368
+ natsServer = await NatsServer.start({})
369
+ clientNc = await connect({ servers: natsServer.url })
370
+ runnerNc = await connect({ servers: natsServer.url })
371
+ await runnerNc.flush()
372
+
373
+ const captured: { subject: string }[] = []
374
+ const sub = runnerNc.subscribe("runtime.runner.cmd.>")
375
+ void (async () => { for await (const m of sub) captured.push({ subject: m.subject }) })()
376
+
377
+ const store = createMockStore({ runnerId: undefined })
378
+ const candidates = [makeDescriptor("runner-A"), makeDescriptor("runner-B")]
379
+
380
+ const router = makeRouter(async () => ({
381
+ kind: "needs_pick",
382
+ candidates,
383
+ reason: "ambiguous" as const,
384
+ }))
385
+
386
+ const proxy = new RunnerProxy({
387
+ nc: clientNc,
388
+ store,
389
+ runnerId: "shared-runner",
390
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
391
+ router,
392
+ sharedRunnerId: () => "shared-runner",
393
+ getRunnerReadiness: (_id) => ({ incompatible: false, protocolVersion: 1, capabilities: null }),
394
+ })
395
+
396
+ await expect(
397
+ proxy.send({ type: "chat.send", chatId: "chat-1", content: "hi", model: "sonnet" }),
398
+ ).rejects.toThrow(RunnerPickRequired)
399
+
400
+ // Verify it is specifically RunnerPickRequired with the right fields
401
+ let caught: RunnerPickRequired | null = null
402
+ try {
403
+ await proxy.send({ type: "chat.send", chatId: "chat-1", content: "hi", model: "sonnet" })
404
+ } catch (err) {
405
+ if (err instanceof RunnerPickRequired) caught = err
406
+ }
407
+ expect(caught).not.toBeNull()
408
+ expect(caught!.chatId).toBe("chat-1")
409
+ expect(caught!.candidates).toHaveLength(2)
410
+ expect(caught!.reason).toBe("ambiguous")
411
+
412
+ // No NATS command dispatched
413
+ await runnerNc.flush()
414
+ expect(captured).toHaveLength(0)
415
+
416
+ sub.unsubscribe()
417
+ })
418
+
419
+ // ── 7. needs_pick with EMPTY candidates → plain Error (fail-fast) ─────────
420
+
421
+ test("needs_pick (empty candidates) throws plain Error, no NATS request", async () => {
422
+ natsServer = await NatsServer.start({})
423
+ clientNc = await connect({ servers: natsServer.url })
424
+ runnerNc = await connect({ servers: natsServer.url })
425
+ await runnerNc.flush()
426
+
427
+ const captured: { subject: string }[] = []
428
+ const sub = runnerNc.subscribe("runtime.runner.cmd.>")
429
+ void (async () => { for await (const m of sub) captured.push({ subject: m.subject }) })()
430
+
431
+ const store = createMockStore({ runnerId: undefined })
432
+
433
+ const router = makeRouter(async () => ({
434
+ kind: "needs_pick",
435
+ candidates: [], // empty — dead-end guard
436
+ reason: "ambiguous" as const,
437
+ }))
438
+
439
+ const proxy = new RunnerProxy({
440
+ nc: clientNc,
441
+ store,
442
+ runnerId: "shared-runner",
443
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
444
+ router,
445
+ sharedRunnerId: () => "shared-runner",
446
+ getRunnerReadiness: (_id) => ({ incompatible: false, protocolVersion: 1, capabilities: null }),
447
+ })
448
+
449
+ await expect(
450
+ proxy.send({ type: "chat.send", chatId: "chat-1", content: "hi", model: "sonnet" }),
451
+ ).rejects.toThrow(Error)
452
+
453
+ // Must NOT be a RunnerPickRequired (plain Error expected)
454
+ let caught: Error | null = null
455
+ try {
456
+ await proxy.send({ type: "chat.send", chatId: "chat-1", content: "hi", model: "sonnet" })
457
+ } catch (err) {
458
+ caught = err as Error
459
+ }
460
+ expect(caught).not.toBeNull()
461
+ expect(caught instanceof RunnerPickRequired).toBe(false)
462
+
463
+ await runnerNc.flush()
464
+ expect(captured).toHaveLength(0)
465
+
466
+ sub.unsubscribe()
467
+ })
468
+
469
+ // ── 8. unavailable → plain Error (fail-fast) ──────────────────────────────
470
+
471
+ test("unavailable throws plain Error, no NATS request", async () => {
472
+ natsServer = await NatsServer.start({})
473
+ clientNc = await connect({ servers: natsServer.url })
474
+ runnerNc = await connect({ servers: natsServer.url })
475
+ await runnerNc.flush()
476
+
477
+ const captured: { subject: string }[] = []
478
+ const sub = runnerNc.subscribe("runtime.runner.cmd.>")
479
+ void (async () => { for await (const m of sub) captured.push({ subject: m.subject }) })()
480
+
481
+ const store = createMockStore({ runnerId: undefined })
482
+
483
+ const router = makeRouter(async () => ({
484
+ kind: "unavailable",
485
+ reason: 'No online runner for "claude" — pair or start a runner.',
486
+ }))
487
+
488
+ const proxy = new RunnerProxy({
489
+ nc: clientNc,
490
+ store,
491
+ runnerId: "shared-runner",
492
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
493
+ router,
494
+ sharedRunnerId: () => "shared-runner",
495
+ getRunnerReadiness: (_id) => ({ incompatible: false, protocolVersion: 1, capabilities: null }),
496
+ })
497
+
498
+ await expect(
499
+ proxy.send({ type: "chat.send", chatId: "chat-1", content: "hi", model: "sonnet" }),
500
+ ).rejects.toThrow('No online runner for "claude"')
501
+
502
+ await runnerNc.flush()
503
+ expect(captured).toHaveLength(0)
504
+
505
+ sub.unsubscribe()
506
+ })
507
+
508
+ // ── 9. Per-runner capability gate: selected runner can't run provider ──────
509
+
510
+ test("capability gate refuses a selected runner that can't run the requested provider", async () => {
511
+ const R1 = "runner-no-codex"
512
+ natsServer = await NatsServer.start({})
513
+ clientNc = await connect({ servers: natsServer.url })
514
+ runnerNc = await connect({ servers: natsServer.url })
515
+ const r1Mock = createMockRunner(runnerNc, R1)
516
+ await runnerNc.flush()
517
+
518
+ const store = createMockStore({ runnerId: undefined, provider: "codex" })
519
+
520
+ const router = makeRouter(async () => ({
521
+ kind: "selected",
522
+ runnerId: R1,
523
+ sticky: false,
524
+ }))
525
+
526
+ const proxy = new RunnerProxy({
527
+ nc: clientNc,
528
+ store,
529
+ runnerId: "shared-runner",
530
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
531
+ router,
532
+ sharedRunnerId: () => "shared-runner",
533
+ // getRunnerReadiness says R1 only supports claude, not codex
534
+ getRunnerReadiness: (_id) => ({
535
+ incompatible: false,
536
+ protocolVersion: 1,
537
+ capabilities: { providers: ["claude" as const] },
538
+ }),
539
+ })
540
+
541
+ await expect(
542
+ proxy.send({ type: "chat.send", chatId: "chat-1", content: "hi", model: "gpt-5" }),
543
+ ).rejects.toThrow(/cannot run codex/)
544
+
545
+ // No command dispatched to R1
546
+ expect(r1Mock.received).toHaveLength(0)
547
+
548
+ r1Mock.dispose()
549
+ })
550
+
551
+ // ── 10. No router → legacy path: dispatches to this.runnerId unchanged ────
552
+
553
+ test("no router → legacy single-runner path, no setChatRunner called", async () => {
554
+ const SHARED = "shared-legacy"
555
+ natsServer = await NatsServer.start({})
556
+ clientNc = await connect({ servers: natsServer.url })
557
+ runnerNc = await connect({ servers: natsServer.url })
558
+ const sharedMock = createMockRunner(runnerNc, SHARED)
559
+ await runnerNc.flush()
560
+
561
+ const store = createMockStore({ runnerId: undefined })
562
+
563
+ const proxy = new RunnerProxy({
564
+ nc: clientNc,
565
+ store,
566
+ runnerId: SHARED,
567
+ getActiveStatuses: () => new Map<string, SessionStatus>(),
568
+ // No router — legacy behavior
569
+ getRunnerReadiness: (_id) => ({
570
+ incompatible: false,
571
+ protocolVersion: 1,
572
+ capabilities: { providers: ["claude" as const, "codex" as const] },
573
+ }),
574
+ })
575
+
576
+ await proxy.send({ type: "chat.send", chatId: "chat-1", content: "hello", model: "sonnet" })
577
+
578
+ expect(store._calls.find((c) => c.method === "setChatRunner")).toBeUndefined()
579
+ expect(sharedMock.received).toHaveLength(1)
580
+ expect(sharedMock.received[0]!.subject).toBe(`runtime.runner.cmd.${SHARED}.start_turn`)
581
+
582
+ sharedMock.dispose()
583
+ })
584
+ })