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,902 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { startClaudeSessionPTY, buildPtyEnv, buildPtyCliArgs, OutputRing, PTY_STDERR_RING_BYTES, PTY_DISALLOWED_NATIVE_TOOLS, deriveAccountInfoFromOauth, PLAN_MODE_EXIT_UNSUPPORTED, SHIFT_TAB_KEY, type PtyUsageSample } from "./driver"
6
+ import type { ProcessTreeSample } from "./pty-memory-sampler.adapter"
7
+ import type { TranscriptStream } from "./tui-source.adapter"
8
+ import type { PtyProcess, SpawnPtyProcessArgs } from "./pty-process.adapter"
9
+ import { KANNA_SYSTEM_PROMPT_APPEND } from "../../shared/tinkaria-system-prompt"
10
+ import { OAuthSettingsStore } from "../oauth-pool/oauth-settings-store"
11
+ import type { McpServerConfig } from "../../shared/types"
12
+
13
+
14
+
15
+ describe("startClaudeSessionPTY", () => {
16
+ test("auth precheck fails when credentials missing", async () => {
17
+ if (process.platform === "win32") return
18
+ const homeDir = await mkdtemp(path.join(tmpdir(), "kanna-pty-driver-"))
19
+ try {
20
+ let err: unknown
21
+ try {
22
+ await startClaudeSessionPTY({
23
+ chatId: "c",
24
+ projectId: "p",
25
+ localPath: "/tmp",
26
+ model: "claude-sonnet-4-6",
27
+ planMode: false,
28
+ forkSession: false,
29
+ oauthToken: null,
30
+ sessionToken: null,
31
+ onToolRequest: async () => null,
32
+ homeDir,
33
+ env: {},
34
+ })
35
+ } catch (e) {
36
+ err = e
37
+ }
38
+ expect(err).toBeInstanceOf(Error)
39
+ expect((err as Error).message).toMatch(/OAuth pool token/)
40
+ } finally {
41
+ await rm(homeDir, { recursive: true, force: true })
42
+ }
43
+ })
44
+
45
+ // ANTHROPIC_API_KEY in the parent env no longer fails the auth precheck:
46
+ // PTY mode is OAuth-only and buildPtyEnv unconditionally strips the key
47
+ // from the child env, so the CLI can never bill API. Coverage moved to
48
+ // auth.test.ts ("ANTHROPIC_API_KEY in parent env does not block ...") and
49
+ // the "strips ANTHROPIC_API_KEY defensively" buildPtyEnv test below.
50
+
51
+ // Preflight gate removed: kanna trusts the claude CLI as the source of
52
+ // truth for tool execution. The PreflightGate arg is still accepted on the
53
+ // driver interface (back-compat with callers) but is never invoked.
54
+
55
+ test.skipIf(process.env.TINKARIA_PTY_E2E !== "1")(
56
+ "E2E: spawn claude, send one prompt, observe one transcript event",
57
+ async () => {
58
+ const store = new OAuthSettingsStore()
59
+ await store.load()
60
+ const activeEntry = store.getSnapshot().tokens.find((t) => t.status === "active")
61
+ if (!activeEntry) {
62
+ console.warn("[e2e] no active OAuth token in Tinkaria settings — skipping spawn E2E")
63
+ return
64
+ }
65
+ const dir = await mkdtemp(path.join(tmpdir(), "tinkaria-pty-e2e-"))
66
+ const passingGate: import("./smoke-test").SmokeTestGate = {
67
+ async canSpawn() { return { ok: true } },
68
+ }
69
+ try {
70
+ const handle = await startClaudeSessionPTY({
71
+ chatId: "e2e",
72
+ projectId: "e2e",
73
+ localPath: dir,
74
+ model: "claude-haiku-4-5-20251001",
75
+ planMode: false,
76
+ forkSession: false,
77
+ oauthToken: activeEntry.token,
78
+ sessionToken: null,
79
+ onToolRequest: async () => null,
80
+ smokeTestGate: passingGate,
81
+ })
82
+ await handle.sendPrompt("Reply with exactly the word: ok")
83
+ // Wait up to 30s for claude CLI to flush response into JSONL.
84
+ await new Promise((r) => setTimeout(r, 30_000))
85
+ // Locate JSONL written by claude under ~/.claude/projects/<encoded-cwd>/.
86
+ const { homedir } = await import("node:os")
87
+ const { realpathSync } = await import("node:fs")
88
+ const encoded = realpathSync(dir).normalize("NFC").replace(/[^A-Za-z0-9]/g, "-")
89
+ const projectsDir = path.join(homedir(), ".claude", "projects", encoded)
90
+ const files = await readdir(projectsDir).catch(() => [] as string[])
91
+ const jsonl = files.find((f) => f.endsWith(".jsonl"))
92
+ expect(jsonl).toBeDefined()
93
+ const raw = await readFile(path.join(projectsDir, jsonl!), "utf8")
94
+ const lines = raw.split("\n").filter((l) => l.trim())
95
+ const types = new Set(lines.map((l) => { try { return JSON.parse(l).type as string } catch { return "?" } }))
96
+ // E2E success = claude received the user prompt AND emitted at least one assistant entry.
97
+ expect(types.has("user") || types.has("last-prompt")).toBe(true)
98
+ expect(types.has("assistant")).toBe(true)
99
+ handle.close()
100
+ } finally {
101
+ await rm(dir, { recursive: true, force: true })
102
+ }
103
+ },
104
+ 60_000,
105
+ )
106
+
107
+ test.skipIf(process.env.TINKARIA_PTY_E2E !== "1")(
108
+ "E2E: setPermissionMode(true/false) — plan mode enter via /plan, exit via Shift+Tab",
109
+ async () => {
110
+ if (process.platform === "win32") return
111
+ const store = new OAuthSettingsStore()
112
+ await store.load()
113
+ const settings = store.getSnapshot()
114
+ const activeEntry = settings.tokens.find((t) => t.status === "active")
115
+ if (!activeEntry) {
116
+ console.warn("[e2e] no active OAuth token in Tinkaria settings — skipping plan-mode E2E")
117
+ return
118
+ }
119
+ const dir = await mkdtemp(path.join(tmpdir(), "tinkaria-pty-pm-e2e-"))
120
+ const passingGate: import("./smoke-test").SmokeTestGate = {
121
+ async canSpawn() { return { ok: true } },
122
+ }
123
+ try {
124
+ const handle = await startClaudeSessionPTY({
125
+ chatId: "e2e-pm", projectId: "e2e-pm", localPath: dir,
126
+ model: "claude-haiku-4-5-20251001",
127
+ planMode: false, forkSession: false,
128
+ oauthToken: activeEntry.token,
129
+ sessionToken: null,
130
+ onToolRequest: async () => null,
131
+ smokeTestGate: passingGate,
132
+ })
133
+ try {
134
+ // Resolve JSONL directory once cwd is locked in.
135
+ const { homedir } = await import("node:os")
136
+ const { realpathSync } = await import("node:fs")
137
+ const encoded = realpathSync(dir).normalize("NFC").replace(/[^A-Za-z0-9]/g, "-")
138
+ const projectsDir = path.join(homedir(), ".claude", "projects", encoded)
139
+
140
+ async function awaitAssistantContaining(label: string, snippet: string, timeoutMs = 45_000) {
141
+ const deadline = Date.now() + timeoutMs
142
+ while (Date.now() < deadline) {
143
+ const files = await readdir(projectsDir).catch(() => [] as string[])
144
+ const jsonl = files.find((f) => f.endsWith(".jsonl"))
145
+ if (jsonl) {
146
+ const raw = await readFile(path.join(projectsDir, jsonl), "utf8").catch(() => "")
147
+ if (raw.includes(snippet)) return true
148
+ }
149
+ await new Promise((r) => setTimeout(r, 500))
150
+ }
151
+ throw new Error(`${label}: timed out waiting for assistant text containing "${snippet}"`)
152
+ }
153
+
154
+ await handle.setPermissionMode(true)
155
+ await new Promise((r) => setTimeout(r, 800))
156
+ await handle.sendPrompt("Reply with exactly the word: plantest")
157
+ await awaitAssistantContaining("plan-mode prompt", "plantest")
158
+
159
+ await handle.setPermissionMode(false)
160
+ await new Promise((r) => setTimeout(r, 800))
161
+ await handle.sendPrompt("Reply with exactly the word: normaltest")
162
+ await awaitAssistantContaining("post-shift-tab prompt", "normaltest")
163
+ } finally {
164
+ handle.close()
165
+ }
166
+ } finally {
167
+ await rm(dir, { recursive: true, force: true })
168
+ }
169
+ },
170
+ 90_000,
171
+ )
172
+
173
+ // OS sandbox wrap removed: kanna trusts the claude CLI as the source of
174
+ // truth and runs it directly under the kanna server's own process boundary.
175
+ })
176
+
177
+ describe("startClaudeSessionPTY smoke-test gate", () => {
178
+ test("refuses spawn when gate returns ok:false", async () => {
179
+ const failingGate: import("./smoke-test").SmokeTestGate = {
180
+ async canSpawn() { return { ok: false, reason: "disallowedTools regression" } },
181
+ }
182
+ await expect(startClaudeSessionPTY({
183
+ chatId: "c1", projectId: "p1", localPath: "/tmp",
184
+ model: "claude-opus-4-7", planMode: false, forkSession: false,
185
+ oauthToken: "test-token", sessionToken: null,
186
+ onToolRequest: async () => null,
187
+ smokeTestGate: failingGate,
188
+ env: { HOME: "/tmp", CLAUDE_CODE_OAUTH_TOKEN: "test-token", CLAUDE_EXECUTABLE: "/bin/sh" },
189
+ })).rejects.toThrow(/smoke-test refused/i)
190
+ })
191
+ })
192
+
193
+ describe("buildPtyEnv", () => {
194
+ test("sets CLAUDE_CODE_OAUTH_TOKEN when oauthToken present", () => {
195
+ const env = buildPtyEnv({
196
+ baseEnv: {},
197
+ homeDir: "/tmp/home",
198
+ oauthToken: "sk-ant-oat-test",
199
+ })
200
+ expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBe("sk-ant-oat-test")
201
+ expect(env.HOME).toBe("/tmp/home")
202
+ expect(env.DISABLE_AUTOUPDATER).toBe("1")
203
+ })
204
+
205
+ test("omits CLAUDE_CODE_OAUTH_TOKEN when oauthToken null", () => {
206
+ const env = buildPtyEnv({
207
+ baseEnv: {},
208
+ homeDir: "/tmp/home",
209
+ oauthToken: null,
210
+ })
211
+ expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined()
212
+ })
213
+
214
+ test("omits CLAUDE_CODE_OAUTH_TOKEN when oauthToken empty string", () => {
215
+ const env = buildPtyEnv({
216
+ baseEnv: {},
217
+ homeDir: "/tmp/home",
218
+ oauthToken: "",
219
+ })
220
+ expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined()
221
+ })
222
+
223
+ test("strips ANTHROPIC_API_KEY defensively", () => {
224
+ const env = buildPtyEnv({
225
+ baseEnv: { ANTHROPIC_API_KEY: "should-be-removed" },
226
+ homeDir: "/tmp/home",
227
+ oauthToken: null,
228
+ })
229
+ expect(env.ANTHROPIC_API_KEY).toBeUndefined()
230
+ })
231
+ })
232
+
233
+ describe("buildPtyCliArgs TUI mode", () => {
234
+ test("does NOT include --print", () => {
235
+ const args = buildPtyCliArgs({
236
+ sessionId: "s1", model: "m", planMode: false,
237
+ sessionToken: null, forkSession: false,
238
+ })
239
+ expect(args).not.toContain("--print")
240
+ })
241
+
242
+ test("does NOT include --output-format / --input-format / --verbose", () => {
243
+ const args = buildPtyCliArgs({
244
+ sessionId: "s1", model: "m", planMode: false,
245
+ sessionToken: null, forkSession: false,
246
+ })
247
+ expect(args.find((a) => a.startsWith("--output-format"))).toBeUndefined()
248
+ expect(args.find((a) => a.startsWith("--input-format"))).toBeUndefined()
249
+ expect(args).not.toContain("--verbose")
250
+ })
251
+
252
+ test("includes core TUI args", () => {
253
+ const args = buildPtyCliArgs({
254
+ sessionId: "s1", model: "claude-opus-4-7", planMode: false,
255
+ sessionToken: null, forkSession: false,
256
+ })
257
+ expect(args).toContain("--model")
258
+ expect(args).toContain("claude-opus-4-7")
259
+ expect(args).toContain("--permission-mode")
260
+ expect(args).toContain("acceptEdits")
261
+ expect(args).toContain("--dangerously-skip-permissions")
262
+ })
263
+
264
+ test("new sessions omit --session-id (interactive TUI ignores it; mtime filter handles JSONL discovery)", () => {
265
+ const args = buildPtyCliArgs({
266
+ sessionId: "s1", model: "m", planMode: false,
267
+ sessionToken: null, forkSession: false,
268
+ })
269
+ expect(args).not.toContain("--session-id")
270
+ expect(args).not.toContain("--resume")
271
+ })
272
+
273
+ test("resume passes --resume <token> without --session-id", () => {
274
+ const args = buildPtyCliArgs({
275
+ sessionId: "s1", model: "m", planMode: false,
276
+ sessionToken: "tok-abc", forkSession: false,
277
+ })
278
+ expect(args).toContain("--resume")
279
+ expect(args).toContain("tok-abc")
280
+ expect(args).not.toContain("--session-id")
281
+ expect(args).not.toContain("--fork-session")
282
+ })
283
+
284
+ test("fork passes --session-id + --resume + --fork-session", () => {
285
+ const args = buildPtyCliArgs({
286
+ sessionId: "fork-uuid", model: "m", planMode: false,
287
+ sessionToken: "old-tok", forkSession: true,
288
+ })
289
+ expect(args).toContain("--session-id")
290
+ expect(args).toContain("fork-uuid")
291
+ expect(args).toContain("--resume")
292
+ expect(args).toContain("old-tok")
293
+ expect(args).toContain("--fork-session")
294
+ })
295
+
296
+ test("plan mode uses plan permission mode", () => {
297
+ const args = buildPtyCliArgs({
298
+ sessionId: "s1", model: "m", planMode: true,
299
+ sessionToken: null, forkSession: false,
300
+ })
301
+ expect(args).toContain("plan")
302
+ })
303
+ })
304
+
305
+ describe("buildPtyCliArgs", () => {
306
+ const baseInput = {
307
+ sessionId: "sess-123",
308
+ model: "claude-sonnet-4-6",
309
+ planMode: false,
310
+ sessionToken: null,
311
+ forkSession: false,
312
+ }
313
+
314
+ test("emits required base flags", () => {
315
+ const args = buildPtyCliArgs(baseInput)
316
+ expect(args).toContain("--model")
317
+ expect(args).toContain("claude-sonnet-4-6")
318
+ expect(args).not.toContain("--no-update")
319
+ expect(args).toContain("--permission-mode")
320
+ expect(args).toContain("acceptEdits")
321
+ })
322
+
323
+ test("does NOT restrict tools — model uses claude built-ins", () => {
324
+ const args = buildPtyCliArgs(baseInput)
325
+ expect(args).not.toContain("--tools")
326
+ expect(args).not.toContain("mcp__kanna__*")
327
+ })
328
+
329
+ test("loads user/project/local setting sources (no --settings override)", () => {
330
+ const args = buildPtyCliArgs(baseInput)
331
+ expect(args).not.toContain("--settings")
332
+ const idx = args.indexOf("--setting-sources")
333
+ expect(idx).toBeGreaterThan(-1)
334
+ expect(args[idx + 1]).toBe("user,project,local")
335
+ })
336
+
337
+ test("emits --dangerously-skip-permissions (personal-use bypass)", () => {
338
+ const args = buildPtyCliArgs(baseInput)
339
+ expect(args).toContain("--dangerously-skip-permissions")
340
+ })
341
+
342
+ test("plan mode picks 'plan' permission", () => {
343
+ const args = buildPtyCliArgs({ ...baseInput, planMode: true })
344
+ const idx = args.indexOf("--permission-mode")
345
+ expect(args[idx + 1]).toBe("plan")
346
+ })
347
+
348
+ test("--effort omitted when undefined", () => {
349
+ const args = buildPtyCliArgs(baseInput)
350
+ expect(args).not.toContain("--effort")
351
+ })
352
+
353
+ test("--effort omitted when empty string", () => {
354
+ const args = buildPtyCliArgs({ ...baseInput, effort: "" })
355
+ expect(args).not.toContain("--effort")
356
+ })
357
+
358
+ test("--effort appended when provided", () => {
359
+ const args = buildPtyCliArgs({ ...baseInput, effort: "high" })
360
+ const idx = args.indexOf("--effort")
361
+ expect(idx).toBeGreaterThan(-1)
362
+ expect(args[idx + 1]).toBe("high")
363
+ })
364
+
365
+ test("resume mode: --resume only, no --session-id (claude rejects both together)", () => {
366
+ const args = buildPtyCliArgs({ ...baseInput, sessionToken: "tok-abc" })
367
+ expect(args).not.toContain("--session-id")
368
+ expect(args).not.toContain("--fork-session")
369
+ const idx = args.indexOf("--resume")
370
+ expect(idx).toBeGreaterThan(-1)
371
+ expect(args[idx + 1]).toBe("tok-abc")
372
+ })
373
+
374
+ test("new-session mode (no token, no fork): no --session-id, no --resume, no --fork-session", () => {
375
+ const args = buildPtyCliArgs(baseInput)
376
+ expect(args).not.toContain("--resume")
377
+ expect(args).not.toContain("--fork-session")
378
+ expect(args).not.toContain("--session-id")
379
+ })
380
+
381
+ test("fork mode: --session-id + --resume + --fork-session all three", () => {
382
+ const args = buildPtyCliArgs({ ...baseInput, sessionToken: "tok-abc", forkSession: true })
383
+ expect(args).toContain("--fork-session")
384
+ const sid = args.indexOf("--session-id")
385
+ expect(sid).toBeGreaterThan(-1)
386
+ expect(args[sid + 1]).toBe("sess-123")
387
+ const resume = args.indexOf("--resume")
388
+ expect(resume).toBeGreaterThan(-1)
389
+ expect(args[resume + 1]).toBe("tok-abc")
390
+ })
391
+
392
+ test("--add-dir per additional directory", () => {
393
+ const args = buildPtyCliArgs({ ...baseInput, additionalDirectories: ["/a", "/b"] })
394
+ const addDirs = args.reduce<string[]>((acc, val, i) => {
395
+ if (val === "--add-dir") acc.push(args[i + 1])
396
+ return acc
397
+ }, [])
398
+ expect(addDirs).toEqual(["/a", "/b"])
399
+ })
400
+
401
+ test("default appended kanna system prompt when no override", () => {
402
+ const args = buildPtyCliArgs(baseInput)
403
+ const idx = args.indexOf("--append-system-prompt")
404
+ expect(idx).toBeGreaterThan(-1)
405
+ expect(args[idx + 1]).toContain("Tinkaria coding agent")
406
+ })
407
+
408
+ test("D8: appended prompt is the shared KANNA_SYSTEM_PROMPT_APPEND when no override is supplied", () => {
409
+ const args = buildPtyCliArgs(baseInput)
410
+ const idx = args.indexOf("--append-system-prompt")
411
+ expect(args[idx + 1]).toBe(KANNA_SYSTEM_PROMPT_APPEND)
412
+ // Regression guard: PTY must carry the full trusted-developer /
413
+ // security-research guidance, not the old one-sentence stub.
414
+ expect(args[idx + 1]).toContain("Reverse-engineering, security research")
415
+ })
416
+
417
+ test("D8b: systemPromptAppend overrides the static default (dynamic subagent roster path)", () => {
418
+ const dynamic = `${KANNA_SYSTEM_PROMPT_APPEND}\n\n## Available subagents\n\n- codereview [id=sa-1]: review PR diffs`
419
+ const args = buildPtyCliArgs({ ...baseInput, systemPromptAppend: dynamic })
420
+ const idx = args.indexOf("--append-system-prompt")
421
+ expect(args[idx + 1]).toBe(dynamic)
422
+ expect(args[idx + 1]).toContain("Available subagents")
423
+ expect(args[idx + 1]).toContain("codereview [id=sa-1]")
424
+ })
425
+
426
+ test("--system-prompt override replaces default append", () => {
427
+ const args = buildPtyCliArgs({ ...baseInput, systemPromptOverride: "custom prompt body" })
428
+ expect(args).not.toContain("--append-system-prompt")
429
+ const idx = args.indexOf("--system-prompt")
430
+ expect(args[idx + 1]).toBe("custom prompt body")
431
+ })
432
+
433
+ test("--mcp-config appended WITH --strict-mcp-config (TUI mode: strict so CLI ignores user MCP config)", () => {
434
+ const args = buildPtyCliArgs({ ...baseInput, mcpConfigPath: "/tmp/mcp-config.json" })
435
+ const idx = args.indexOf("--mcp-config")
436
+ expect(idx).toBeGreaterThan(-1)
437
+ expect(args[idx + 1]).toBe("/tmp/mcp-config.json")
438
+ expect(args).toContain("--strict-mcp-config")
439
+ })
440
+
441
+ test("--mcp-config omitted when path absent", () => {
442
+ const args = buildPtyCliArgs(baseInput)
443
+ expect(args).not.toContain("--mcp-config")
444
+ })
445
+
446
+ // ── Issue #215: disallow native AskUserQuestion/ExitPlanMode under PTY ────
447
+
448
+ test("disallows native AskUserQuestion + ExitPlanMode (forces the mcp__kanna__ shims)", () => {
449
+ const args = buildPtyCliArgs(baseInput)
450
+ const idx = args.indexOf("--disallowedTools")
451
+ expect(idx).toBeGreaterThan(-1)
452
+ expect(args.slice(idx + 1)).toEqual(["AskUserQuestion", "ExitPlanMode"])
453
+ expect(PTY_DISALLOWED_NATIVE_TOOLS).toEqual(["AskUserQuestion", "ExitPlanMode"])
454
+ // EnterPlanMode is intentionally NOT disallowed (no user round-trip;
455
+ // SDK canUseTool never intercepts it — keeps SDK↔PTY parity).
456
+ expect(args).not.toContain("EnterPlanMode")
457
+ })
458
+
459
+ test("--disallowedTools is last so its variadic args cannot swallow another flag", () => {
460
+ const args = buildPtyCliArgs({ ...baseInput, mcpConfigPath: "/tmp/mcp-config.json" })
461
+ const idx = args.indexOf("--disallowedTools")
462
+ expect(idx).toBe(args.length - PTY_DISALLOWED_NATIVE_TOOLS.length - 1)
463
+ })
464
+
465
+ test("--disallowedTools coexists with --append-system-prompt (index assertion still holds)", () => {
466
+ const args = buildPtyCliArgs(baseInput)
467
+ const idx = args.indexOf("--append-system-prompt")
468
+ expect(idx).toBeGreaterThan(-1)
469
+ expect(args[idx + 1]).toBe(KANNA_SYSTEM_PROMPT_APPEND)
470
+ expect(args).toContain("--disallowedTools")
471
+ })
472
+ })
473
+
474
+ describe("OutputRing (B4 stderr ring buffer)", () => {
475
+ test("retains short content verbatim", () => {
476
+ const ring = new OutputRing()
477
+ ring.append("hello ")
478
+ ring.append("world")
479
+ expect(ring.tail()).toBe("hello world")
480
+ })
481
+
482
+ test("caps at PTY_STDERR_RING_BYTES, keeping the most recent tail", () => {
483
+ const ring = new OutputRing()
484
+ const big = "A".repeat(PTY_STDERR_RING_BYTES)
485
+ ring.append(big)
486
+ ring.append("TAIL_MARKER")
487
+ const tail = ring.tail()
488
+ expect(tail.length).toBe(PTY_STDERR_RING_BYTES)
489
+ expect(tail.endsWith("TAIL_MARKER")).toBe(true)
490
+ // Oldest bytes evicted.
491
+ expect(tail.startsWith("A")).toBe(true)
492
+ expect(tail).not.toBe(big)
493
+ })
494
+
495
+ test("ring size constant is 256 KB", () => {
496
+ expect(PTY_STDERR_RING_BYTES).toBe(256 * 1024)
497
+ })
498
+
499
+ test("empty ring tail is empty string", () => {
500
+ expect(new OutputRing().tail()).toBe("")
501
+ })
502
+ })
503
+
504
+ describe("deriveAccountInfoFromOauth (C1)", () => {
505
+ test("no label and no masked key → null (UI falls back, no bogus chip)", () => {
506
+ expect(deriveAccountInfoFromOauth({})).toBeNull()
507
+ })
508
+
509
+ test("empty label and empty masked → null", () => {
510
+ expect(deriveAccountInfoFromOauth({ label: "", oauthKeyMasked: "" })).toBeNull()
511
+ })
512
+
513
+ test("label only → AccountInfo with organization + kanna-oauth-pool source", () => {
514
+ expect(deriveAccountInfoFromOauth({ label: "work-account" })).toEqual({
515
+ organization: "work-account",
516
+ tokenSource: "kanna-oauth-pool",
517
+ })
518
+ })
519
+
520
+ test("masked key only → AccountInfo with oauthKeyMasked + kanna-oauth-pool source", () => {
521
+ expect(deriveAccountInfoFromOauth({ oauthKeyMasked: "sk-ant-oat01...1234" })).toEqual({
522
+ oauthKeyMasked: "sk-ant-oat01...1234",
523
+ tokenSource: "kanna-oauth-pool",
524
+ })
525
+ })
526
+
527
+ test("label + masked → AccountInfo with both fields", () => {
528
+ expect(deriveAccountInfoFromOauth({ label: "work-account", oauthKeyMasked: "sk-ant-oat01...1234" })).toEqual({
529
+ organization: "work-account",
530
+ oauthKeyMasked: "sk-ant-oat01...1234",
531
+ tokenSource: "kanna-oauth-pool",
532
+ })
533
+ })
534
+ })
535
+
536
+ describe("PLAN_MODE_EXIT_UNSUPPORTED (state-unknown warning)", () => {
537
+ test("PLAN_MODE_EXIT_UNSUPPORTED references plan mode and acceptEdits", () => {
538
+ expect(PLAN_MODE_EXIT_UNSUPPORTED).toContain("plan mode")
539
+ expect(PLAN_MODE_EXIT_UNSUPPORTED).toContain("acceptEdits")
540
+ })
541
+ })
542
+
543
+ describe("SHIFT_TAB_KEY constant", () => {
544
+ test("is the VT100 Shift+Tab sequence", () => {
545
+ expect(SHIFT_TAB_KEY).toBe("\x1b[Z")
546
+ })
547
+ })
548
+
549
+ // ── F1: setPermissionMode — plan mode exit via Shift+Tab ────────────────────
550
+
551
+ async function makeTestHandle(opts?: { planMode?: boolean }) {
552
+ const homeDir = await mkdtemp(path.join(tmpdir(), "kanna-pm-"))
553
+ const sentInputs: string[] = []
554
+ let exitResolve!: (code: number) => void
555
+ const exited = new Promise<number>((r) => { exitResolve = r })
556
+
557
+ const fakePty: PtyProcess = {
558
+ pid: 99999,
559
+ async sendInput(data) { sentInputs.push(data) },
560
+ resize() {},
561
+ exited,
562
+ close() { exitResolve(0) },
563
+ kill() { exitResolve(137) },
564
+ }
565
+
566
+ const fakeSpawn = async (spawnArgs: SpawnPtyProcessArgs): Promise<PtyProcess> => {
567
+ spawnArgs.onOutput?.("❯ ")
568
+ return fakePty
569
+ }
570
+
571
+ const fakeSmoke: import("./smoke-test").SmokeTestGate = {
572
+ async canSpawn() { return { ok: true } },
573
+ }
574
+
575
+ const neverStream: TranscriptStream = {
576
+ lines: {
577
+ [Symbol.asyncIterator]() {
578
+ return { next(): Promise<IteratorResult<string, undefined>> { return new Promise(() => {}) } }
579
+ },
580
+ },
581
+ filePath: new Promise<string>(() => {}),
582
+ close() {},
583
+ }
584
+
585
+ const handle = await startClaudeSessionPTY({
586
+ chatId: "test", projectId: "test", localPath: homeDir,
587
+ model: "claude-haiku-4-5-20251001",
588
+ planMode: opts?.planMode ?? false,
589
+ forkSession: false,
590
+ oauthToken: "test-token",
591
+ sessionToken: null,
592
+ onToolRequest: async () => null,
593
+ homeDir,
594
+ env: {
595
+ HOME: homeDir,
596
+ CLAUDE_CODE_OAUTH_TOKEN: "test-token",
597
+ KANNA_PTY_TRUST_DISMISS: "disabled",
598
+ CLAUDE_EXECUTABLE: "/bin/sh",
599
+ },
600
+ spawnPtyProcess: fakeSpawn,
601
+ startKannaMcpHttpServer: async () => ({ url: "http://127.0.0.1:0/mcp", bearerToken: "test", close: async () => {} }),
602
+ startTranscriptStreamFn: async () => neverStream,
603
+ smokeTestGate: fakeSmoke,
604
+ })
605
+
606
+ return {
607
+ handle,
608
+ sentInputs,
609
+ async cleanup() {
610
+ exitResolve(0)
611
+ handle.close()
612
+ await rm(homeDir, { recursive: true, force: true })
613
+ },
614
+ }
615
+ }
616
+
617
+ describe("setPermissionMode (F1 — plan mode exit)", () => {
618
+ test("setPermissionMode(true) sends /plan\\r and tracks state", async () => {
619
+ if (process.platform === "win32") return
620
+ const { handle, sentInputs, cleanup } = await makeTestHandle()
621
+ try {
622
+ await handle.setPermissionMode(true)
623
+ expect(sentInputs).toContain("/plan\r")
624
+ } finally {
625
+ await cleanup()
626
+ }
627
+ }, 10_000)
628
+
629
+ test("setPermissionMode(false) after true sends Shift+Tab \\x1b[Z", async () => {
630
+ if (process.platform === "win32") return
631
+ const { handle, sentInputs, cleanup } = await makeTestHandle()
632
+ try {
633
+ await handle.setPermissionMode(true)
634
+ sentInputs.length = 0
635
+ await handle.setPermissionMode(false)
636
+ expect(sentInputs).toContain(SHIFT_TAB_KEY)
637
+ } finally {
638
+ await cleanup()
639
+ }
640
+ }, 10_000)
641
+
642
+ test("setPermissionMode(false) when started with planMode:true sends Shift+Tab", async () => {
643
+ if (process.platform === "win32") return
644
+ const { handle, sentInputs, cleanup } = await makeTestHandle({ planMode: true })
645
+ try {
646
+ await handle.setPermissionMode(false)
647
+ expect(sentInputs).toContain(SHIFT_TAB_KEY)
648
+ } finally {
649
+ await cleanup()
650
+ }
651
+ }, 10_000)
652
+
653
+ test("setPermissionMode(false) without prior entry does NOT send Shift+Tab", async () => {
654
+ if (process.platform === "win32") return
655
+ const { handle, sentInputs, cleanup } = await makeTestHandle()
656
+ try {
657
+ await handle.setPermissionMode(false)
658
+ expect(sentInputs).not.toContain(SHIFT_TAB_KEY)
659
+ } finally {
660
+ await cleanup()
661
+ }
662
+ }, 10_000)
663
+ })
664
+
665
+ // ── Task 6: customMcpServers wired through PTY mcp-config.json ────────────────
666
+
667
+ /**
668
+ * Helper to boot a PTY session and return the written mcp-config.json.
669
+ * Uses the same fake-spawn / fake-smoke / never-stream harness as makeTestHandle.
670
+ * Passes a known sessionToken so we can find the runtimeDir by prefix scan.
671
+ */
672
+ async function spawnAndReadMcpConfig(opts: {
673
+ sessionToken: string
674
+ customMcpServers?: readonly McpServerConfig[]
675
+ }): Promise<{ parsed: { mcpServers: Record<string, unknown> }; cleanup: () => Promise<void> }> {
676
+ const homeDir = await mkdtemp(path.join(tmpdir(), "kanna-t6-mcp-"))
677
+ let exitResolve!: (code: number) => void
678
+ const exited = new Promise<number>((r) => { exitResolve = r })
679
+
680
+ const fakePty: PtyProcess = {
681
+ pid: 88888,
682
+ async sendInput() { /* swallow */ },
683
+ resize() {},
684
+ exited,
685
+ close() { exitResolve(0) },
686
+ kill() { exitResolve(137) },
687
+ }
688
+ const fakeSpawn = async (spawnArgs: SpawnPtyProcessArgs): Promise<PtyProcess> => {
689
+ spawnArgs.onOutput?.("❯ ")
690
+ return fakePty
691
+ }
692
+ const fakeSmoke: import("./smoke-test").SmokeTestGate = {
693
+ async canSpawn() { return { ok: true } },
694
+ }
695
+ const neverStream: TranscriptStream = {
696
+ lines: {
697
+ [Symbol.asyncIterator]() {
698
+ return { next(): Promise<IteratorResult<string, undefined>> { return new Promise(() => {}) } }
699
+ },
700
+ },
701
+ filePath: new Promise<string>(() => {}),
702
+ close() {},
703
+ }
704
+
705
+ const handle = await startClaudeSessionPTY({
706
+ chatId: "t6-test", projectId: "test", localPath: homeDir,
707
+ model: "claude-haiku-4-5-20251001",
708
+ planMode: false, forkSession: false,
709
+ oauthToken: "test-token", sessionToken: opts.sessionToken,
710
+ onToolRequest: async () => null,
711
+ homeDir,
712
+ env: {
713
+ HOME: homeDir,
714
+ CLAUDE_CODE_OAUTH_TOKEN: "test-token",
715
+ KANNA_PTY_TRUST_DISMISS: "disabled",
716
+ CLAUDE_EXECUTABLE: "/bin/sh",
717
+ },
718
+ customMcpServers: opts.customMcpServers,
719
+ spawnPtyProcess: fakeSpawn,
720
+ startKannaMcpHttpServer: async () => ({ url: "http://127.0.0.1:0/mcp", bearerToken: "test", close: async () => {} }),
721
+ startTranscriptStreamFn: async () => neverStream,
722
+ smokeTestGate: fakeSmoke,
723
+ })
724
+
725
+ // Locate the runtimeDir: mkdtemp creates `kanna-pty-<first8ofSessionId>-XXXX`
726
+ const prefix = `kanna-pty-${opts.sessionToken.slice(0, 8)}-`
727
+ const osTmp = tmpdir()
728
+ const entries = await readdir(osTmp)
729
+ const runtimeDirName = entries.find((e) => e.startsWith(prefix))
730
+ if (!runtimeDirName) {
731
+ throw new Error(`Could not find runtimeDir with prefix ${prefix} in ${osTmp}`)
732
+ }
733
+ const mcpConfigPath = path.join(osTmp, runtimeDirName, "mcp-config.json")
734
+ const raw = await readFile(mcpConfigPath, "utf8")
735
+ const parsed = JSON.parse(raw) as { mcpServers: Record<string, unknown> }
736
+
737
+ return {
738
+ parsed,
739
+ async cleanup() {
740
+ exitResolve(0)
741
+ handle.close()
742
+ await rm(homeDir, { recursive: true, force: true })
743
+ await rm(path.join(osTmp, runtimeDirName), { recursive: true, force: true })
744
+ },
745
+ }
746
+ }
747
+
748
+ describe("PTY customMcpServers wiring (Task 6)", () => {
749
+ test("mcp-config.json includes enabled user customMcpServers", async () => {
750
+ if (process.platform === "win32") return
751
+ const userServer: McpServerConfig = {
752
+ id: "u1", name: "fs-tool", enabled: true,
753
+ createdAt: "", updatedAt: "", lastTest: { status: "untested" },
754
+ transport: "stdio", command: "/bin/ls", args: [], env: {},
755
+ }
756
+ const { parsed, cleanup } = await spawnAndReadMcpConfig({
757
+ sessionToken: "t6-inc-001",
758
+ customMcpServers: [userServer],
759
+ })
760
+ try {
761
+ expect(parsed.mcpServers["fs-tool"]).toBeDefined()
762
+ expect((parsed.mcpServers["fs-tool"] as { type: string }).type).toBe("stdio")
763
+ } finally {
764
+ await cleanup()
765
+ }
766
+ }, 10_000)
767
+
768
+ test("mcp-config.json omits disabled customMcpServers", async () => {
769
+ if (process.platform === "win32") return
770
+ const enabled: McpServerConfig = {
771
+ id: "u2", name: "enabled-srv", enabled: true,
772
+ createdAt: "", updatedAt: "", lastTest: { status: "untested" },
773
+ transport: "stdio", command: "/bin/echo", args: [], env: {},
774
+ }
775
+ const disabled: McpServerConfig = {
776
+ id: "u3", name: "disabled-srv", enabled: false,
777
+ createdAt: "", updatedAt: "", lastTest: { status: "untested" },
778
+ transport: "stdio", command: "/bin/false", args: [], env: {},
779
+ }
780
+ const { parsed, cleanup } = await spawnAndReadMcpConfig({
781
+ sessionToken: "t6-omit-001",
782
+ customMcpServers: [enabled, disabled],
783
+ })
784
+ try {
785
+ expect(parsed.mcpServers["enabled-srv"]).toBeDefined()
786
+ expect(parsed.mcpServers["disabled-srv"]).toBeUndefined()
787
+ } finally {
788
+ await cleanup()
789
+ }
790
+ }, 10_000)
791
+ })
792
+
793
+ describe("session close escalation (graceful → SIGTERM → SIGKILL)", () => {
794
+ test("close() escalates to SIGKILL when SIGTERM does not terminate within the grace window", async () => {
795
+ if (process.platform === "win32") return
796
+ // Stand-alone fake PTY: ignore SIGTERM (close()), only exit on SIGKILL (kill()).
797
+ let killSignal: NodeJS.Signals | number | undefined
798
+ let exitResolve!: (code: number) => void
799
+ const exited = new Promise<number>((r) => { exitResolve = r })
800
+ const stubbornPty: PtyProcess = {
801
+ pid: 99998,
802
+ async sendInput() { /* swallow */ },
803
+ resize() {},
804
+ exited,
805
+ close() { /* deliberately ignore SIGTERM to simulate a hung TUI */ },
806
+ kill(signal) { killSignal = signal; exitResolve(137) },
807
+ }
808
+ const tmp = await mkdtemp(path.join(tmpdir(), "kanna-pty-close-"))
809
+ try {
810
+ const handle = await startClaudeSessionPTY({
811
+ chatId: "test-close", projectId: "p", localPath: tmp,
812
+ model: "claude-haiku-4-5-20251001",
813
+ planMode: false, forkSession: false,
814
+ oauthToken: "test-token", sessionToken: null,
815
+ onToolRequest: async () => null,
816
+ homeDir: tmp,
817
+ env: { HOME: tmp, CLAUDE_CODE_OAUTH_TOKEN: "test-token", KANNA_PTY_TRUST_DISMISS: "disabled", CLAUDE_EXECUTABLE: "/bin/sh" },
818
+ spawnPtyProcess: async (s) => { s.onOutput?.("❯ "); return stubbornPty },
819
+ startKannaMcpHttpServer: async () => ({ url: "http://127.0.0.1:0/mcp", bearerToken: "t", close: async () => {} }),
820
+ startTranscriptStreamFn: async () => ({
821
+ lines: { [Symbol.asyncIterator]() { return { next(): Promise<IteratorResult<string, undefined>> { return new Promise(() => {}) } } } },
822
+ filePath: new Promise<string>(() => {}),
823
+ close() {},
824
+ }),
825
+ smokeTestGate: { async canSpawn() { return { ok: true } } },
826
+ })
827
+ handle.close()
828
+ // 2 s SIGTERM grace + 3 s SIGKILL grace + a safety margin.
829
+ const code = await Promise.race([
830
+ exited,
831
+ new Promise<number>((_, reject) => setTimeout(() => reject(new Error("escalation timed out")), 8_000)),
832
+ ])
833
+ expect(code).toBe(137)
834
+ expect(killSignal).toBe("SIGKILL")
835
+ } finally {
836
+ await rm(tmp, { recursive: true, force: true })
837
+ }
838
+ }, 10_000)
839
+ })
840
+
841
+ describe("startClaudeSessionPTY memory sampler → onUsageSample", () => {
842
+ test("emits mapped rss/cpu with monotonic peaks", async () => {
843
+ if (process.platform === "win32") return
844
+ const homeDir = await mkdtemp(path.join(tmpdir(), "kanna-usage-"))
845
+ let exitResolve!: (code: number) => void
846
+ const exited = new Promise<number>((r) => { exitResolve = r })
847
+ const fakePty: PtyProcess = {
848
+ pid: 99997,
849
+ async sendInput() {},
850
+ resize() {},
851
+ exited,
852
+ close() { exitResolve(0) },
853
+ kill() { exitResolve(137) },
854
+ }
855
+ const fakeSpawn = async (s: SpawnPtyProcessArgs): Promise<PtyProcess> => {
856
+ s.onOutput?.("❯ ")
857
+ return fakePty
858
+ }
859
+ const neverStream: TranscriptStream = {
860
+ lines: { [Symbol.asyncIterator]() { return { next() { return new Promise<IteratorResult<string, undefined>>(() => {}) } } } },
861
+ filePath: new Promise<string>(() => {}),
862
+ close() {},
863
+ }
864
+ // rss drops then cpu rises: peak rss must stay at the high-water mark,
865
+ // peak cpu must climb. Later ticks repeat the last sample.
866
+ const samples: ProcessTreeSample[] = [
867
+ { rssBytes: 100, cpuPercent: 10 },
868
+ { rssBytes: 50, cpuPercent: 40 },
869
+ ]
870
+ let i = 0
871
+ const received: PtyUsageSample[] = []
872
+ const handle = await startClaudeSessionPTY({
873
+ chatId: "usage", projectId: "usage", localPath: homeDir,
874
+ model: "claude-haiku-4-5-20251001",
875
+ planMode: false, forkSession: false,
876
+ oauthToken: "test-token", sessionToken: null,
877
+ onToolRequest: async () => null,
878
+ homeDir,
879
+ env: { HOME: homeDir, CLAUDE_CODE_OAUTH_TOKEN: "test-token", KANNA_PTY_TRUST_DISMISS: "disabled", CLAUDE_EXECUTABLE: "/bin/sh" },
880
+ spawnPtyProcess: fakeSpawn,
881
+ startKannaMcpHttpServer: async () => ({ url: "http://127.0.0.1:0/mcp", bearerToken: "test", close: async () => {} }),
882
+ startTranscriptStreamFn: async () => neverStream,
883
+ smokeTestGate: { async canSpawn() { return { ok: true } } },
884
+ sampleProcessTreeUsage: async () => samples[Math.min(i++, samples.length - 1)] ?? null,
885
+ memorySamplerIntervalMs: 10,
886
+ onUsageSample: (u) => { received.push(u) },
887
+ })
888
+ try {
889
+ const deadline = Date.now() + 2000
890
+ while (received.length < 2 && Date.now() < deadline) {
891
+ await new Promise((r) => setTimeout(r, 10))
892
+ }
893
+ expect(received.length).toBeGreaterThanOrEqual(2)
894
+ expect(received[0]).toEqual({ rssBytes: 100, rssPeakBytes: 100, cpuPercent: 10, cpuPeakPercent: 10 })
895
+ expect(received[1]).toEqual({ rssBytes: 50, rssPeakBytes: 100, cpuPercent: 40, cpuPeakPercent: 40 })
896
+ } finally {
897
+ exitResolve(0)
898
+ handle.close()
899
+ await rm(homeDir, { recursive: true, force: true })
900
+ }
901
+ }, 10_000)
902
+ })