aicodeman 1.44.0 → 1.46.0

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 (351) hide show
  1. package/README.md +32 -8
  2. package/dist/agent-codex-identity.d.ts +26 -0
  3. package/dist/agent-codex-identity.d.ts.map +1 -0
  4. package/dist/agent-codex-identity.js +253 -0
  5. package/dist/agent-codex-identity.js.map +1 -0
  6. package/dist/agent-inspection.d.ts +32 -0
  7. package/dist/agent-inspection.d.ts.map +1 -0
  8. package/dist/agent-inspection.js +196 -0
  9. package/dist/agent-inspection.js.map +1 -0
  10. package/dist/agent-staging.d.ts +130 -0
  11. package/dist/agent-staging.d.ts.map +1 -0
  12. package/dist/agent-staging.js +312 -0
  13. package/dist/agent-staging.js.map +1 -0
  14. package/dist/case-rename.d.ts +65 -0
  15. package/dist/case-rename.d.ts.map +1 -0
  16. package/dist/case-rename.js +198 -0
  17. package/dist/case-rename.js.map +1 -0
  18. package/dist/cli-agent.d.ts +91 -3
  19. package/dist/cli-agent.d.ts.map +1 -1
  20. package/dist/cli-agent.js +501 -44
  21. package/dist/cli-agent.js.map +1 -1
  22. package/dist/cli-history.d.ts +20 -0
  23. package/dist/cli-history.d.ts.map +1 -0
  24. package/dist/cli-history.js +61 -0
  25. package/dist/cli-history.js.map +1 -0
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +6 -1
  28. package/dist/cli.js.map +1 -1
  29. package/dist/codex-rollout-format.d.ts +66 -0
  30. package/dist/codex-rollout-format.d.ts.map +1 -0
  31. package/dist/codex-rollout-format.js +111 -0
  32. package/dist/codex-rollout-format.js.map +1 -0
  33. package/dist/codex-transcript.d.ts +3 -0
  34. package/dist/codex-transcript.d.ts.map +1 -1
  35. package/dist/codex-transcript.js +13 -41
  36. package/dist/codex-transcript.js.map +1 -1
  37. package/dist/config/auth-config.d.ts +16 -1
  38. package/dist/config/auth-config.d.ts.map +1 -1
  39. package/dist/config/auth-config.js +16 -1
  40. package/dist/config/auth-config.js.map +1 -1
  41. package/dist/config/cli-registry/schema.d.ts +12 -7
  42. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  43. package/dist/config/cli-registry/schema.js +26 -0
  44. package/dist/config/cli-registry/schema.js.map +1 -1
  45. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  46. package/dist/config/cli-registry/stock.js +40 -1
  47. package/dist/config/cli-registry/stock.js.map +1 -1
  48. package/dist/config/cli-registry/types.d.ts +57 -0
  49. package/dist/config/cli-registry/types.d.ts.map +1 -1
  50. package/dist/config/server-timing.d.ts +6 -0
  51. package/dist/config/server-timing.d.ts.map +1 -1
  52. package/dist/config/server-timing.js +9 -0
  53. package/dist/config/server-timing.js.map +1 -1
  54. package/dist/daemon-control.d.ts +1 -0
  55. package/dist/daemon-control.d.ts.map +1 -1
  56. package/dist/daemon-control.js +2 -0
  57. package/dist/daemon-control.js.map +1 -1
  58. package/dist/deepseek-transcript.d.ts +9 -7
  59. package/dist/deepseek-transcript.d.ts.map +1 -1
  60. package/dist/deepseek-transcript.js +9 -7
  61. package/dist/deepseek-transcript.js.map +1 -1
  62. package/dist/docker-hosts.d.ts +8 -0
  63. package/dist/docker-hosts.d.ts.map +1 -1
  64. package/dist/docker-hosts.js +29 -0
  65. package/dist/docker-hosts.js.map +1 -1
  66. package/dist/git-fetch-scheduler.d.ts +124 -0
  67. package/dist/git-fetch-scheduler.d.ts.map +1 -0
  68. package/dist/git-fetch-scheduler.js +249 -0
  69. package/dist/git-fetch-scheduler.js.map +1 -0
  70. package/dist/git-fetch.d.ts +241 -0
  71. package/dist/git-fetch.d.ts.map +1 -0
  72. package/dist/git-fetch.js +392 -0
  73. package/dist/git-fetch.js.map +1 -0
  74. package/dist/git-workspace-status.d.ts +36 -1
  75. package/dist/git-workspace-status.d.ts.map +1 -1
  76. package/dist/git-workspace-status.js +42 -5
  77. package/dist/git-workspace-status.js.map +1 -1
  78. package/dist/hooks-config.d.ts +14 -2
  79. package/dist/hooks-config.d.ts.map +1 -1
  80. package/dist/hooks-config.js +39 -3
  81. package/dist/hooks-config.js.map +1 -1
  82. package/dist/host-sensors.d.ts +115 -0
  83. package/dist/host-sensors.d.ts.map +1 -0
  84. package/dist/host-sensors.js +457 -0
  85. package/dist/host-sensors.js.map +1 -0
  86. package/dist/local-history.d.ts +71 -0
  87. package/dist/local-history.d.ts.map +1 -0
  88. package/dist/local-history.js +750 -0
  89. package/dist/local-history.js.map +1 -0
  90. package/dist/mcp-server.d.ts.map +1 -1
  91. package/dist/mcp-server.js +2 -2
  92. package/dist/mcp-server.js.map +1 -1
  93. package/dist/mux-interface.d.ts +5 -2
  94. package/dist/mux-interface.d.ts.map +1 -1
  95. package/dist/prompt-cache-keepwarm.d.ts.map +1 -1
  96. package/dist/prompt-cache-keepwarm.js +5 -3
  97. package/dist/prompt-cache-keepwarm.js.map +1 -1
  98. package/dist/remote-hosts.d.ts +7 -0
  99. package/dist/remote-hosts.d.ts.map +1 -1
  100. package/dist/remote-hosts.js +17 -3
  101. package/dist/remote-hosts.js.map +1 -1
  102. package/dist/remote-pane-history.d.ts.map +1 -1
  103. package/dist/remote-pane-history.js +2 -2
  104. package/dist/remote-pane-history.js.map +1 -1
  105. package/dist/session.d.ts +113 -0
  106. package/dist/session.d.ts.map +1 -1
  107. package/dist/session.js +354 -13
  108. package/dist/session.js.map +1 -1
  109. package/dist/terminal-input-classifier.d.ts +8 -0
  110. package/dist/terminal-input-classifier.d.ts.map +1 -0
  111. package/dist/terminal-input-classifier.js +60 -0
  112. package/dist/terminal-input-classifier.js.map +1 -0
  113. package/dist/tmux-manager.d.ts +30 -4
  114. package/dist/tmux-manager.d.ts.map +1 -1
  115. package/dist/tmux-manager.js +101 -21
  116. package/dist/tmux-manager.js.map +1 -1
  117. package/dist/tmux-prefix-table.d.ts +37 -5
  118. package/dist/tmux-prefix-table.d.ts.map +1 -1
  119. package/dist/tmux-prefix-table.js +40 -5
  120. package/dist/tmux-prefix-table.js.map +1 -1
  121. package/dist/tui/tui-app.d.ts +8 -1
  122. package/dist/tui/tui-app.d.ts.map +1 -1
  123. package/dist/tui/tui-app.js +49 -12
  124. package/dist/tui/tui-app.js.map +1 -1
  125. package/dist/tui/tui-client.d.ts +90 -3
  126. package/dist/tui/tui-client.d.ts.map +1 -1
  127. package/dist/tui/tui-client.js +195 -26
  128. package/dist/tui/tui-client.js.map +1 -1
  129. package/dist/types/session.d.ts +15 -0
  130. package/dist/types/session.d.ts.map +1 -1
  131. package/dist/types/session.js.map +1 -1
  132. package/dist/web/agent-inbox-persistence.d.ts +52 -0
  133. package/dist/web/agent-inbox-persistence.d.ts.map +1 -0
  134. package/dist/web/agent-inbox-persistence.js +139 -0
  135. package/dist/web/agent-inbox-persistence.js.map +1 -0
  136. package/dist/web/agent-inbox.d.ts +140 -0
  137. package/dist/web/agent-inbox.d.ts.map +1 -0
  138. package/dist/web/agent-inbox.js +258 -0
  139. package/dist/web/agent-inbox.js.map +1 -0
  140. package/dist/web/middleware/auth.d.ts +2 -0
  141. package/dist/web/middleware/auth.d.ts.map +1 -1
  142. package/dist/web/middleware/auth.js +145 -52
  143. package/dist/web/middleware/auth.js.map +1 -1
  144. package/dist/web/network-auth-policy.d.ts +13 -3
  145. package/dist/web/network-auth-policy.d.ts.map +1 -1
  146. package/dist/web/network-auth-policy.js +15 -3
  147. package/dist/web/network-auth-policy.js.map +1 -1
  148. package/dist/web/ports/infra-port.d.ts +2 -0
  149. package/dist/web/ports/infra-port.d.ts.map +1 -1
  150. package/dist/web/public/admin-ui.js.gz +0 -0
  151. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  152. package/dist/web/public/app.3093527f.js +43 -0
  153. package/dist/web/public/app.3093527f.js.br +0 -0
  154. package/dist/web/public/app.3093527f.js.gz +0 -0
  155. package/dist/web/public/approvals-ui.js.gz +0 -0
  156. package/dist/web/public/{constants.4866ecc3.js → constants.affcdde6.js} +622 -12
  157. package/dist/web/public/constants.affcdde6.js.br +0 -0
  158. package/dist/web/public/constants.affcdde6.js.gz +0 -0
  159. package/dist/web/public/cron-ui.js.gz +0 -0
  160. package/dist/web/public/entrance-animations.js.gz +0 -0
  161. package/dist/web/public/git-status-ui.js +88 -5
  162. package/dist/web/public/git-status-ui.js.br +0 -0
  163. package/dist/web/public/git-status-ui.js.gz +0 -0
  164. package/dist/web/public/header-divider.eb2d2463.js +1 -0
  165. package/dist/web/public/header-divider.eb2d2463.js.br +0 -0
  166. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  167. package/dist/web/public/home-sessions.js.gz +0 -0
  168. package/dist/web/public/host-wake-ui.js.gz +0 -0
  169. package/dist/web/public/i18n.70265257.js +2 -0
  170. package/dist/web/public/i18n.70265257.js.br +0 -0
  171. package/dist/web/public/i18n.70265257.js.gz +0 -0
  172. package/dist/web/public/image-input.941386e7.js.gz +0 -0
  173. package/dist/web/public/index.html +170 -16
  174. package/dist/web/public/index.html.br +0 -0
  175. package/dist/web/public/index.html.gz +0 -0
  176. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  177. package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
  178. package/dist/web/public/{mobile-handlers.44adaa07.js → mobile-handlers.f7c3f692.js} +26 -2
  179. package/dist/web/public/mobile-handlers.f7c3f692.js.br +0 -0
  180. package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
  181. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  182. package/dist/web/public/mobile-overview.js.gz +0 -0
  183. package/dist/web/public/mobile.1bbd2d67.css +1 -0
  184. package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
  185. package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
  186. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  187. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  188. package/dist/web/public/{panels-ui.cbb24ba1.js → panels-ui.4a56ee63.js} +76 -70
  189. package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
  190. package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
  191. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  192. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  193. package/dist/web/public/readmymind-ui.js.gz +0 -0
  194. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  195. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  196. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  197. package/dist/web/public/session-lineage.js.gz +0 -0
  198. package/dist/web/public/session-ui.186f9419.js +106 -0
  199. package/dist/web/public/session-ui.186f9419.js.br +0 -0
  200. package/dist/web/public/session-ui.186f9419.js.gz +0 -0
  201. package/dist/web/public/settings-ui.a191154e.js +106 -0
  202. package/dist/web/public/settings-ui.a191154e.js.br +0 -0
  203. package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
  204. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  205. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  206. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  207. package/dist/web/public/styles.7238eae8.css +1 -0
  208. package/dist/web/public/styles.7238eae8.css.br +0 -0
  209. package/dist/web/public/styles.7238eae8.css.gz +0 -0
  210. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  211. package/dist/web/public/sw.js +2 -2
  212. package/dist/web/public/sw.js.br +0 -0
  213. package/dist/web/public/sw.js.gz +0 -0
  214. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  215. package/dist/web/public/{tab-rail-resize.7157e0bd.js → tab-rail-resize.b5cdceaf.js} +1 -1
  216. package/dist/web/public/tab-rail-resize.b5cdceaf.js.br +0 -0
  217. package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
  218. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  219. package/dist/web/public/terminal-split.js.gz +0 -0
  220. package/dist/web/public/terminal-tile.js +10 -1
  221. package/dist/web/public/terminal-tile.js.br +0 -0
  222. package/dist/web/public/terminal-tile.js.gz +0 -0
  223. package/dist/web/public/terminal-ui.7e787829.js +4 -0
  224. package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
  225. package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
  226. package/dist/web/public/tile-grid.js.gz +0 -0
  227. package/dist/web/public/ultracode-panel.js.gz +0 -0
  228. package/dist/web/public/ultracode-windows.js.gz +0 -0
  229. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  230. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  231. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  232. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  233. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  234. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  235. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  236. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  237. package/dist/web/public/vendor/{xterm-predictive-echo.ae5e3d5d.js → xterm-predictive-echo.276b11d9.js} +1 -1
  238. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.br +0 -0
  239. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
  240. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js +5 -0
  241. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.br +0 -0
  242. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
  243. package/dist/web/public/vendor/xterm.css.gz +0 -0
  244. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  245. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  246. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  247. package/dist/web/public/webview-tabs.js.gz +0 -0
  248. package/dist/web/public/whats-new.json +9 -9
  249. package/dist/web/route-helpers.d.ts +57 -6
  250. package/dist/web/route-helpers.d.ts.map +1 -1
  251. package/dist/web/route-helpers.js +72 -4
  252. package/dist/web/route-helpers.js.map +1 -1
  253. package/dist/web/routes/agent-session-routes.d.ts +20 -0
  254. package/dist/web/routes/agent-session-routes.d.ts.map +1 -0
  255. package/dist/web/routes/agent-session-routes.js +160 -0
  256. package/dist/web/routes/agent-session-routes.js.map +1 -0
  257. package/dist/web/routes/case-routes.d.ts.map +1 -1
  258. package/dist/web/routes/case-routes.js +203 -16
  259. package/dist/web/routes/case-routes.js.map +1 -1
  260. package/dist/web/routes/config-backup-routes.js +2 -2
  261. package/dist/web/routes/config-backup-routes.js.map +1 -1
  262. package/dist/web/routes/git-status-routes.d.ts +21 -7
  263. package/dist/web/routes/git-status-routes.d.ts.map +1 -1
  264. package/dist/web/routes/git-status-routes.js +78 -12
  265. package/dist/web/routes/git-status-routes.js.map +1 -1
  266. package/dist/web/routes/inbox-routes.d.ts +43 -0
  267. package/dist/web/routes/inbox-routes.d.ts.map +1 -0
  268. package/dist/web/routes/inbox-routes.js +148 -0
  269. package/dist/web/routes/inbox-routes.js.map +1 -0
  270. package/dist/web/routes/index.d.ts +2 -0
  271. package/dist/web/routes/index.d.ts.map +1 -1
  272. package/dist/web/routes/index.js +2 -0
  273. package/dist/web/routes/index.js.map +1 -1
  274. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  275. package/dist/web/routes/ralph-routes.js +7 -16
  276. package/dist/web/routes/ralph-routes.js.map +1 -1
  277. package/dist/web/routes/session-routes.d.ts.map +1 -1
  278. package/dist/web/routes/session-routes.js +120 -74
  279. package/dist/web/routes/session-routes.js.map +1 -1
  280. package/dist/web/routes/system-routes.d.ts.map +1 -1
  281. package/dist/web/routes/system-routes.js +107 -59
  282. package/dist/web/routes/system-routes.js.map +1 -1
  283. package/dist/web/routes/ws-routes.d.ts +5 -0
  284. package/dist/web/routes/ws-routes.d.ts.map +1 -1
  285. package/dist/web/routes/ws-routes.js +84 -47
  286. package/dist/web/routes/ws-routes.js.map +1 -1
  287. package/dist/web/schemas.d.ts +56 -7
  288. package/dist/web/schemas.d.ts.map +1 -1
  289. package/dist/web/schemas.js +88 -1
  290. package/dist/web/schemas.js.map +1 -1
  291. package/dist/web/sensitive-path.d.ts.map +1 -1
  292. package/dist/web/sensitive-path.js +2 -0
  293. package/dist/web/sensitive-path.js.map +1 -1
  294. package/dist/web/server.d.ts +7 -2
  295. package/dist/web/server.d.ts.map +1 -1
  296. package/dist/web/server.js +144 -16
  297. package/dist/web/server.js.map +1 -1
  298. package/dist/web/session-wait-registry.d.ts +48 -4
  299. package/dist/web/session-wait-registry.d.ts.map +1 -1
  300. package/dist/web/session-wait-registry.js +144 -6
  301. package/dist/web/session-wait-registry.js.map +1 -1
  302. package/dist/web/settings-writer.d.ts +47 -0
  303. package/dist/web/settings-writer.d.ts.map +1 -0
  304. package/dist/web/settings-writer.js +110 -0
  305. package/dist/web/settings-writer.js.map +1 -0
  306. package/dist/web/sse-events.d.ts +4 -1
  307. package/dist/web/sse-events.d.ts.map +1 -1
  308. package/dist/web/sse-events.js +4 -1
  309. package/dist/web/sse-events.js.map +1 -1
  310. package/package.json +1 -1
  311. package/skills/codeman/SKILL.md +30 -8
  312. package/skills/codeman/preamble.sh +3 -3
  313. package/skills/codeman/reference/endpoints.md +24 -3
  314. package/skills/codeman/reference/history.md +84 -0
  315. package/skills/codeman/reference/inspection-and-staging.md +152 -0
  316. package/skills/codeman/reference/messaging.md +1 -1
  317. package/skills/codeman/reference/verbs.md +65 -3
  318. package/dist/web/public/app.9314ee60.js +0 -43
  319. package/dist/web/public/app.9314ee60.js.br +0 -0
  320. package/dist/web/public/app.9314ee60.js.gz +0 -0
  321. package/dist/web/public/constants.4866ecc3.js.br +0 -0
  322. package/dist/web/public/constants.4866ecc3.js.gz +0 -0
  323. package/dist/web/public/i18n.22ca902b.js +0 -2
  324. package/dist/web/public/i18n.22ca902b.js.br +0 -0
  325. package/dist/web/public/i18n.22ca902b.js.gz +0 -0
  326. package/dist/web/public/mobile-handlers.44adaa07.js.br +0 -0
  327. package/dist/web/public/mobile-handlers.44adaa07.js.gz +0 -0
  328. package/dist/web/public/mobile.12fb18de.css +0 -1
  329. package/dist/web/public/mobile.12fb18de.css.br +0 -0
  330. package/dist/web/public/mobile.12fb18de.css.gz +0 -0
  331. package/dist/web/public/panels-ui.cbb24ba1.js.br +0 -0
  332. package/dist/web/public/panels-ui.cbb24ba1.js.gz +0 -0
  333. package/dist/web/public/session-ui.9c1f009b.js +0 -104
  334. package/dist/web/public/session-ui.9c1f009b.js.br +0 -0
  335. package/dist/web/public/session-ui.9c1f009b.js.gz +0 -0
  336. package/dist/web/public/settings-ui.fea9ddae.js +0 -106
  337. package/dist/web/public/settings-ui.fea9ddae.js.br +0 -0
  338. package/dist/web/public/settings-ui.fea9ddae.js.gz +0 -0
  339. package/dist/web/public/styles.e096fcd6.css +0 -1
  340. package/dist/web/public/styles.e096fcd6.css.br +0 -0
  341. package/dist/web/public/styles.e096fcd6.css.gz +0 -0
  342. package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
  343. package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
  344. package/dist/web/public/terminal-ui.68dbd352.js +0 -2
  345. package/dist/web/public/terminal-ui.68dbd352.js.br +0 -0
  346. package/dist/web/public/terminal-ui.68dbd352.js.gz +0 -0
  347. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.br +0 -0
  348. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  349. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +0 -4
  350. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
  351. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
package/dist/cli-agent.js CHANGED
@@ -2,15 +2,21 @@
2
2
  * @fileoverview `codeman agent …` — session-to-session verbs for the agent running
3
3
  * inside a Codeman session, in every CLI mode.
4
4
  *
5
- * A thin HTTP client over endpoints that already exist (`quick-start`, `input`,
6
- * `wait`, `wait-output`, `last-response`, `terminal`, `DELETE sessions/:id`). It
7
- * invents no route and no transport: everything goes through `CODEMAN_API_URL`, so
8
- * auth, ownership and the per-session waiter cap apply unchanged. The behaviour is
5
+ * A thin HTTP client over the server's endpoints (`quick-start` for a named
6
+ * case, `POST sessions` + `sessions/:id/interactive|shell` for a worker in an
7
+ * existing directory, `input`, `wait`, `wait-output`, `last-response`, `terminal`,
8
+ * `DELETE sessions/:id`, the agent inbox: `inbox`, `inbox/ack`,
9
+ * `agent-inbox/summary`, and the experimental, opt-in
10
+ * `inspect`/`stage`/`submit` of `web/routes/agent-session-routes.ts`, which answer 403
11
+ * until `agentCoordinationEnabled` is on; that refusal is printed verbatim). It uses no
12
+ * transport of its own: everything goes through `CODEMAN_API_URL`, so auth, ownership
13
+ * and the per-session waiter cap apply unchanged. The behaviour is
9
14
  * the packaged agent skill's (`skills/codeman`), ported from shell prose into code
10
15
  * with tests, so a `codex`/`opencode`/`pi` agent — which never gets the claude-only
11
16
  * preamble — has the same verbs from one line in its AGENTS.md.
12
17
  *
13
- * Invariants (each asserted in `test/cli-agent.test.ts`):
18
+ * Invariants (each asserted in `test/cli-agent.test.ts` or
19
+ * `test/cli-agent-project-spawn.test.ts`):
14
20
  * 1. Refuses outside a Codeman session (`CODEMAN_MUX=1` + `CODEMAN_API_URL`); it
15
21
  * never guesses a URL — a server you are not part of is not yours to drive.
16
22
  * 2. `send` transmits printable text plus `\r` only. ESC exists solely as
@@ -22,14 +28,26 @@
22
28
  * 4. An id shorter than 8 characters refuses (exit 4) before any request, on every
23
29
  * verb. `9` would resolve to whichever session is alone with that first
24
30
  * character, the user's own interactive tab included.
31
+ * 5. `spawn` without a case starts the worker in an existing directory (cwd, or
32
+ * `--working-dir`) via `POST sessions` + `interactive|shell`, without the
33
+ * `X-Codeman-Agent-Origin` header, so no agent-case marker is written. It refuses
34
+ * (exit 4, before any request) an empty `--working-dir` and a bare spawn from a
35
+ * HOME cwd: it always sends an explicit `workingDir`, so the server's skip of
36
+ * workspace hooks for a request that names none cannot protect `~/.claude`.
25
37
  *
26
38
  * Commands live here as functions returning an exit code, not calling
27
39
  * `process.exit`, so the whole surface is unit-testable against a fake server.
28
40
  *
29
41
  * @module cli-agent
30
42
  */
43
+ import { open, realpath, stat } from 'node:fs/promises';
44
+ import { homedir } from 'node:os';
45
+ import { resolve } from 'node:path';
31
46
  import http from 'node:http';
32
47
  import https from 'node:https';
48
+ import { isUtf8 } from 'node:buffer';
49
+ import { STAGE_MAX_BYTES } from './agent-staging.js';
50
+ import { INSPECT_MAX_BYTES } from './agent-inspection.js';
33
51
  import { basicAuthHeader, credentialsFrom, readCodemanEnvFile, } from './codeman-credentials.js';
34
52
  import { getCli } from './config/cli-registry/registry.js';
35
53
  import { GLYPH, palette, table } from './cli-style.js';
@@ -161,6 +179,19 @@ export function stripAnsi(text) {
161
179
  // eslint-disable-next-line no-control-regex
162
180
  return stripAnsiSequences(text).replace(/\x1b[()][AB0]/g, '');
163
181
  }
182
+ /**
183
+ * Inbox message text for a terminal. Another session wrote it, so an escape sequence
184
+ * in it would otherwise reach the reader's terminal (retitle it, or move the cursor
185
+ * over the `from` line above it). ANSI sequences go as in `read --tail`, and every
186
+ * control character left (C0 other than newline and tab, DEL, C1) is shown as `\xHH`,
187
+ * so a bare `\r` or backspace cannot overwrite the header either. A CRLF line end
188
+ * stays a line end. `--json` prints the text as stored.
189
+ */
190
+ export function terminalSafeText(text) {
191
+ return (stripAnsi(text.replace(/\r\n/g, '\n'))
192
+ // eslint-disable-next-line no-control-regex
193
+ .replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`));
194
+ }
164
195
  /** Parse a positive-integer option (`--timeout` ms, `--tail` bytes); the server rejects anything else. */
165
196
  export function parsePositiveInt(raw, fallback, flag = '--timeout') {
166
197
  if (raw === undefined)
@@ -254,6 +285,18 @@ export const httpRequest = (ctx, options) => {
254
285
  req.end();
255
286
  });
256
287
  };
288
+ /**
289
+ * One line describing a thrown request error. Node 22 rejects a refused connection to a host
290
+ * that resolves to both ::1 and 127.0.0.1 with an AggregateError whose `message` is empty, so
291
+ * fall back to its `code` (ECONNREFUSED) before a generic line rather than printing nothing.
292
+ */
293
+ export function describeRequestError(error) {
294
+ const message = getErrorMessage(error);
295
+ if (message)
296
+ return message;
297
+ const code = error?.code;
298
+ return code ? String(code) : 'request failed without a message';
299
+ }
257
300
  /** One line describing a failed response, for humans. Plain-text guards (401/403/429) have no envelope. */
258
301
  export function describeFailure(res) {
259
302
  if (res.json && !res.json.success) {
@@ -264,7 +307,13 @@ export function describeFailure(res) {
264
307
  return `HTTP 401 ${text}: the server wants a password (CODEMAN_PASSWORD, or the data dir's .env)`;
265
308
  return `HTTP ${res.status}${text ? ` ${text}` : ''}`;
266
309
  }
267
- /** Print `data` as JSON (the `--json` path) — always the envelope's `data`, never a reshaped copy. */
310
+ /**
311
+ * Print `data` as JSON (the `--json` path): the envelope's `data`, never a reshaped copy,
312
+ * except a directory spawn, which prints `{ sessionId, workingDir, created: true }` plus
313
+ * `ready` when a readiness wait ran, `readinessError` when that call failed, and
314
+ * `startupError` when its start fails, because `POST /api/sessions` answers with the
315
+ * whole session row.
316
+ */
268
317
  function emitJson(deps, data) {
269
318
  deps.io.out(JSON.stringify(data, null, 2));
270
319
  }
@@ -313,29 +362,68 @@ export async function resolveSessionId(deps, id) {
313
362
  return { error: `no session starts with "${id}" (see \`agent ls\`)`, code: EXIT.error };
314
363
  return { error: `"${id}" is ambiguous: ${matches.map((s) => s.id.slice(0, 13)).join(', ')}`, code: EXIT.error };
315
364
  }
316
- /** `agent ls` — every session the caller can see, self marked. */
365
+ /**
366
+ * The inbox summary, or undefined when it cannot be had (a server without the route,
367
+ * a transient error). `ls` must still list sessions then, so this never fails the verb.
368
+ */
369
+ async function fetchInboxSummary(deps) {
370
+ try {
371
+ const res = await deps.request(deps.ctx, { method: 'GET', path: '/api/v1/agent-inbox/summary' });
372
+ if (!res.json?.success)
373
+ return undefined;
374
+ return res.json.data?.sessions ?? {};
375
+ }
376
+ catch {
377
+ return undefined;
378
+ }
379
+ }
380
+ /** The INBOX cell of `ls`: `3 (1 new)`, `parked`, both, or empty. */
381
+ export function inboxCell(row) {
382
+ if (!row)
383
+ return '';
384
+ const parts = [];
385
+ if (row.pending > 0)
386
+ parts.push(row.unseen > 0 ? `${row.pending} (${row.unseen} new)` : String(row.pending));
387
+ if (row.parked)
388
+ parts.push('parked');
389
+ return parts.join(', ');
390
+ }
391
+ /** `agent ls` — every session the caller can see, self marked, with its inbox counts. */
317
392
  export async function agentLs(deps) {
318
- const res = await deps.request(deps.ctx, { method: 'GET', path: '/api/v1/sessions' });
393
+ const [res, inbox] = await Promise.all([
394
+ deps.request(deps.ctx, { method: 'GET', path: '/api/v1/sessions' }),
395
+ fetchInboxSummary(deps),
396
+ ]);
319
397
  if (!res.json?.success)
320
398
  return fail(deps, describeFailure(res));
321
399
  const sessions = res.json.data ?? [];
322
400
  if (deps.json) {
323
- emitJson(deps, sessions.map((s) => ({ ...s, self: isSelfSession(deps.ctx.selfId, s.id) })));
401
+ emitJson(deps, sessions.map((s) => ({
402
+ ...s,
403
+ self: isSelfSession(deps.ctx.selfId, s.id),
404
+ // null = empty inbox and not parked. The key is absent when the server did
405
+ // not answer the summary, so "no data" never reads as "no mail".
406
+ ...(inbox ? { inbox: inbox[s.id] ?? null } : {}),
407
+ })));
324
408
  return EXIT.ok;
325
409
  }
326
410
  if (sessions.length === 0) {
327
411
  deps.io.out(palette.muted('(no sessions)'));
328
412
  return EXIT.ok;
329
413
  }
330
- const rows = sessions.map((s) => [
331
- isSelfSession(deps.ctx.selfId, s.id) ? '*' : ' ',
332
- s.id.slice(0, 8),
333
- s.mode ?? '?',
334
- s.status ?? '?',
335
- s.name || s.workingDir || '',
336
- ]);
337
- deps.io.out(table([[' ', 'ID', 'MODE', 'STATUS', 'NAME'], ...rows], { gap: 2 }));
414
+ const rows = sessions.map((s) => {
415
+ const row = [isSelfSession(deps.ctx.selfId, s.id) ? '*' : ' ', s.id.slice(0, 8), s.mode ?? '?', s.status ?? '?'];
416
+ if (inbox)
417
+ row.push(inboxCell(inbox[s.id]));
418
+ row.push(s.name || s.workingDir || '');
419
+ return row;
420
+ });
421
+ const header = inbox ? [' ', 'ID', 'MODE', 'STATUS', 'INBOX', 'NAME'] : [' ', 'ID', 'MODE', 'STATUS', 'NAME'];
422
+ deps.io.out(table([header, ...rows], { gap: 2 }));
338
423
  deps.io.out(palette.muted(`* = this session (${deps.ctx.selfId.slice(0, 8)}). status is a UI hint, never a sync signal.`));
424
+ if (inbox) {
425
+ deps.io.out(palette.muted('INBOX: pending (unseen), parked = the session is waiting on `agent inbox --wait` right now.'));
426
+ }
339
427
  return EXIT.ok;
340
428
  }
341
429
  /**
@@ -352,46 +440,137 @@ export const AGENT_ORIGIN_HEADER = { 'X-Codeman-Agent-Origin': 'codeman-agent-cl
352
440
  export function composerReadyMark(mode) {
353
441
  return getCli(mode)?.capabilities.composerReadyMark;
354
442
  }
355
- /** `agent spawn` — quick-start with lineage, then the readiness ladder where the mode has one. */
443
+ /** realpath, or the plain resolved path when it cannot be resolved; never throws. */
444
+ async function canonicalPath(path) {
445
+ try {
446
+ return await realpath(path);
447
+ }
448
+ catch {
449
+ return resolve(path);
450
+ }
451
+ }
452
+ /**
453
+ * Whether `directory` is the user's home directory, comparing realpaths so a symlinked
454
+ * HOME (or a cwd reached through a symlink) still matches. False when HOME is unknown.
455
+ */
456
+ export async function isHomeDirectory(directory) {
457
+ let home;
458
+ try {
459
+ home = homedir();
460
+ }
461
+ catch {
462
+ return false;
463
+ }
464
+ if (!home)
465
+ return false;
466
+ const [a, b] = await Promise.all([canonicalPath(directory), canonicalPath(home)]);
467
+ return a === b;
468
+ }
469
+ /** `agent spawn`: start in a project directory or named case, preserving lineage and mode readiness. */
356
470
  export async function agentSpawn(deps, options) {
471
+ const cli = getCli(options.mode);
472
+ if (!cli)
473
+ return fail(deps, `Unknown mode: ${options.mode}`, EXIT.refused);
474
+ if (options.caseName !== undefined && options.workingDir !== undefined)
475
+ return fail(deps, 'Use a case OR --working-dir, not both', EXIT.refused);
476
+ // Same rule for the flag: `--working-dir "$WT"` with WT empty (a failed
477
+ // `git worktree add`) must not fall through to the caller's own checkout, which
478
+ // `resolve('')` would make it.
479
+ if (options.workingDir !== undefined && options.workingDir.trim() === '')
480
+ return fail(deps, 'refusing: --working-dir is empty; pass an existing directory or omit the flag', EXIT.refused);
481
+ // Only an OMITTED case means "use a directory": an empty one (`spawn "$CASE"` with
482
+ // CASE unset) still goes to quick-start, whose schema refuses it.
483
+ const workingDir = options.caseName !== undefined ? undefined : resolve(options.workingDir ?? process.cwd());
484
+ if (workingDir) {
485
+ // eslint-disable-next-line no-control-regex
486
+ if (/[\x00-\x1f\x7f]/.test(workingDir))
487
+ return fail(deps, 'Working directory contains control characters', EXIT.refused);
488
+ try {
489
+ if (!(await stat(workingDir)).isDirectory())
490
+ throw new Error('not a directory');
491
+ }
492
+ catch {
493
+ return fail(deps, 'Working directory must be an existing local directory', EXIT.refused);
494
+ }
495
+ // The server installs workspace hooks (and, with agent skill injection on, the
496
+ // skill) into the directory it is given, and it skips that only when the caller
497
+ // names no workingDir, because the cwd fallback is $HOME under installer-created
498
+ // services. This CLI always sends its directory explicitly, so an implicit cwd of
499
+ // HOME must be refused here or it would write ~/.claude/settings.local.json and
500
+ // ~/.claude/skills. An explicit `--working-dir ~` is the caller's choice and stays.
501
+ if (options.workingDir === undefined && (await isHomeDirectory(workingDir)))
502
+ return fail(deps, 'refusing: the current directory is your home directory; pass --working-dir ~ to start a worker there on purpose', EXIT.refused);
503
+ }
357
504
  const body = {
358
- caseName: options.caseName,
505
+ ...(workingDir ? { workingDir } : { caseName: options.caseName }),
359
506
  mode: options.mode,
360
507
  parentSessionId: deps.ctx.selfId,
361
508
  };
362
509
  if (options.name)
363
- body.sessionName = options.name;
510
+ body[workingDir ? 'name' : 'sessionName'] = options.name;
364
511
  const res = await deps.request(deps.ctx, {
365
512
  method: 'POST',
366
- path: '/api/v1/quick-start',
513
+ path: workingDir ? '/api/v1/sessions' : '/api/v1/quick-start',
367
514
  body,
368
- headers: { ...AGENT_ORIGIN_HEADER },
515
+ headers: workingDir ? undefined : { ...AGENT_ORIGIN_HEADER },
369
516
  });
370
- const data = res.json?.data;
517
+ const created = res.json?.data;
518
+ // `POST /api/sessions` answers `{ session: { id, ... } }`; quick-start answers `{ sessionId, ... }`.
519
+ const createdId = created?.session?.id;
520
+ const data = workingDir && createdId ? { sessionId: createdId, workingDir, created: true } : created;
371
521
  if (!res.json?.success || !data?.sessionId)
372
522
  return fail(deps, describeFailure(res));
373
523
  const sid = data.sessionId;
524
+ if (workingDir) {
525
+ // Creation alone leaves a session without a pane. Report the exact created id
526
+ // on an ambiguous or rejected start; never retry a start or hide a leaked tab.
527
+ let startupError;
528
+ try {
529
+ const started = await deps.request(deps.ctx, {
530
+ method: 'POST',
531
+ path: `/api/v1/sessions/${encodeURIComponent(sid)}/${cli.capabilities.startMode}`,
532
+ });
533
+ if (!started.json?.success)
534
+ startupError = describeFailure(started);
535
+ }
536
+ catch (error) {
537
+ startupError = getErrorMessage(error) || 'Startup request failed';
538
+ }
539
+ if (startupError) {
540
+ if (deps.json)
541
+ emitJson(deps, { ...data, ready: false, startupError });
542
+ else
543
+ deps.io.out(sid);
544
+ deps.io.err(`Session ${sid} was created but startup failed or is ambiguous: ${startupError}`);
545
+ return EXIT.error;
546
+ }
547
+ }
374
548
  let ready;
375
549
  let readinessError;
376
550
  let dead = false;
377
551
  const mark = composerReadyMark(options.mode);
378
552
  if (options.ready && mark) {
379
- const wait = await deps.request(deps.ctx, {
380
- method: 'GET',
381
- path: `/api/v1/sessions/${encodeURIComponent(sid)}/wait-output`,
382
- query: { match: mark, from: 'buffer', timeout: options.timeoutMs },
383
- timeoutMs: options.timeoutMs + 10_000,
384
- });
385
- // A failed readiness call (waiter cap, 400, network) is its own error, not "the
386
- // composer never showed up": report the real reason instead of the trust-dialog hint.
387
- if (!wait.json?.success)
388
- readinessError = describeFailure(wait);
389
- else {
390
- const result = wait.json.data?.wait;
391
- // A worker that died while we waited is exit 3 like every other wait, not a
392
- // "composer not seen" timeout that sends the caller looking for a dialog.
393
- dead = waitExitCode(result) === EXIT.dead;
394
- ready = !dead && Boolean(result?.matched);
553
+ try {
554
+ const wait = await deps.request(deps.ctx, {
555
+ method: 'GET',
556
+ path: `/api/v1/sessions/${encodeURIComponent(sid)}/wait-output`,
557
+ query: { match: mark, from: 'buffer', timeout: options.timeoutMs },
558
+ timeoutMs: options.timeoutMs + 10_000,
559
+ });
560
+ // A failed readiness call (waiter cap, 400, network) is its own error, not "the
561
+ // composer never showed up": report the real reason instead of the trust-dialog hint.
562
+ if (!wait.json?.success)
563
+ readinessError = describeFailure(wait);
564
+ else {
565
+ const result = wait.json.data?.wait;
566
+ // A worker that died while we waited is exit 3 like every other wait, not a
567
+ // "composer not seen" timeout that sends the caller looking for a dialog.
568
+ dead = waitExitCode(result) === EXIT.dead;
569
+ ready = !dead && Boolean(result?.matched);
570
+ }
571
+ }
572
+ catch (error) {
573
+ readinessError = describeRequestError(error);
395
574
  }
396
575
  }
397
576
  if (deps.json) {
@@ -400,7 +579,7 @@ export async function agentSpawn(deps, options) {
400
579
  else {
401
580
  // Human lines go to stderr so `SID=$(codeman agent spawn …)` captures the id alone.
402
581
  const say = (line) => deps.io.err(line);
403
- say(palette.ok(`${GLYPH.ok} spawned ${sid} (${options.mode}, case ${data.caseName ?? options.caseName})`));
582
+ say(palette.ok(`${GLYPH.ok} spawned ${sid} (${options.mode}, ${workingDir ?? data.caseName ?? options.caseName})`));
404
583
  if (ready === true)
405
584
  say(palette.muted(' composer up: the worker can take a prompt'));
406
585
  if (dead)
@@ -549,6 +728,99 @@ export async function agentWait(deps, options) {
549
728
  }
550
729
  return waitExitCode(data?.wait);
551
730
  }
731
+ /** Bounded incremental evidence, with an opaque server-owned cursor. */
732
+ export async function agentInspect(deps, options) {
733
+ if (!Number.isInteger(options.maxBytes) || options.maxBytes < 1 || options.maxBytes > INSPECT_MAX_BYTES)
734
+ return fail(deps, '--max-bytes must be between 1 and 65536', EXIT.refused);
735
+ const target = await resolveSessionId(deps, options.id);
736
+ if ('error' in target)
737
+ return fail(deps, target.error, target.code);
738
+ const response = await deps.request(deps.ctx, {
739
+ method: 'GET',
740
+ path: `/api/v1/sessions/${encodeURIComponent(target.id)}/inspect`,
741
+ query: { maxBytes: options.maxBytes, since: options.since },
742
+ });
743
+ if (!response.json?.success)
744
+ return fail(deps, describeFailure(response));
745
+ if (deps.json)
746
+ emitJson(deps, response.json.data);
747
+ else {
748
+ const data = response.json.data;
749
+ deps.io.out(`[${data.source ?? 'unknown'}]${data.truncated ? ' truncated' : ''}${data.blockedReason ? ` (${data.blockedReason})` : ''}`);
750
+ deps.io.out(data.evidence ?? '');
751
+ if (data.cursor)
752
+ deps.io.out(`cursor: ${data.cursor}`);
753
+ }
754
+ return EXIT.ok;
755
+ }
756
+ /** Stage exactly the printable UTF8 file contents; never sends Enter. */
757
+ export async function agentStage(deps, options) {
758
+ if (isSelfSession(deps.ctx.selfId, options.id))
759
+ return fail(deps, `refusing: ${options.id} is me`, EXIT.refused);
760
+ let message;
761
+ try {
762
+ const info = await stat(options.messageFile);
763
+ if (!info.isFile() || info.size > STAGE_MAX_BYTES)
764
+ return fail(deps, 'message file must be a regular file within 8192 UTF8 bytes', EXIT.refused);
765
+ const handle = await open(options.messageFile, 'r');
766
+ try {
767
+ const buffer = Buffer.alloc(STAGE_MAX_BYTES + 1);
768
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
769
+ const bytes = buffer.subarray(0, bytesRead);
770
+ if (bytesRead > STAGE_MAX_BYTES || !isUtf8(bytes))
771
+ return fail(deps, 'message file must contain at most 8192 bytes of UTF8', EXIT.refused);
772
+ message = bytes.toString('utf8');
773
+ }
774
+ finally {
775
+ await handle.close();
776
+ }
777
+ }
778
+ catch (error) {
779
+ return fail(deps, `cannot read message file: ${getErrorMessage(error)}`, EXIT.refused);
780
+ }
781
+ const refusal = inputRefusal(message);
782
+ if (refusal || message !== message.trimEnd())
783
+ return fail(deps, refusal ?? 'stage refuses trailing whitespace', EXIT.refused);
784
+ const target = await resolveSessionId(deps, options.id);
785
+ if ('error' in target)
786
+ return fail(deps, target.error, target.code);
787
+ const response = await deps.request(deps.ctx, {
788
+ method: 'POST',
789
+ path: `/api/v1/sessions/${encodeURIComponent(target.id)}/stage`,
790
+ body: { message },
791
+ });
792
+ if (!response.json?.success)
793
+ return fail(deps, describeFailure(response));
794
+ if (deps.json)
795
+ emitJson(deps, response.json.data);
796
+ else {
797
+ const data = response.json.data;
798
+ deps.io.out(`staged (no Enter): ${data.stageId}\nsha256: ${data.sha256}\nexpires: ${new Date(data.expiresAt).toISOString()}`);
799
+ }
800
+ return EXIT.ok;
801
+ }
802
+ /** Submit one verified staged composer; failure never automatically replays Enter. */
803
+ export async function agentSubmit(deps, options) {
804
+ if (isSelfSession(deps.ctx.selfId, options.id))
805
+ return fail(deps, `refusing: ${options.id} is me`, EXIT.refused);
806
+ if (!/^[a-f0-9]{64}$/.test(options.confirmSha) || !/^[a-f0-9-]{36}$/.test(options.stageId))
807
+ return fail(deps, 'submit requires a stage UUID and its exact SHA-256', EXIT.refused);
808
+ const target = await resolveSessionId(deps, options.id);
809
+ if ('error' in target)
810
+ return fail(deps, target.error, target.code);
811
+ const response = await deps.request(deps.ctx, {
812
+ method: 'POST',
813
+ path: `/api/v1/sessions/${encodeURIComponent(target.id)}/submit`,
814
+ body: { stageId: options.stageId, confirmSha: options.confirmSha },
815
+ });
816
+ if (!response.json?.success)
817
+ return fail(deps, describeFailure(response));
818
+ if (deps.json)
819
+ emitJson(deps, response.json.data);
820
+ else
821
+ deps.io.out('Enter delivered once; inspect or wait for acceptance (no automatic retry).');
822
+ return EXIT.ok;
823
+ }
552
824
  /** `agent read` — the last answer (the route picks the transcript reader or the pane segmenter) or a terminal tail. */
553
825
  export async function agentRead(deps, options) {
554
826
  const target = await resolveSessionId(deps, options.id);
@@ -641,6 +913,136 @@ export async function agentRm(deps, options) {
641
913
  deps.io.out(palette.ok(`${GLYPH.ok} deleted ${target.id} (its case directory stays on disk)`));
642
914
  return EXIT.ok;
643
915
  }
916
+ /**
917
+ * The message `post` was given, which must be ONE argument, as with `send`: several
918
+ * would be an unquoted `$(cat notes.md)` split by the shell, and joining the pieces
919
+ * with spaces loses its newlines. Unlike `send`, newlines inside the one argument are
920
+ * fine: the inbox stores text as data and never types it.
921
+ */
922
+ export function postTextFromArgs(words) {
923
+ if (words.length === 1)
924
+ return { text: words[0] };
925
+ return { error: `refusing: the message must be ONE argument, got ${words.length} — quote it` };
926
+ }
927
+ /** `agent post` — store a message in another session's inbox. Never types into its pane. */
928
+ export async function agentPost(deps, options) {
929
+ if (options.text.length === 0)
930
+ return fail(deps, 'refusing: empty message', EXIT.refused);
931
+ const target = await resolveSessionId(deps, options.id);
932
+ if ('error' in target)
933
+ return fail(deps, target.error, target.code);
934
+ const res = await deps.request(deps.ctx, {
935
+ method: 'POST',
936
+ path: `/api/v1/sessions/${encodeURIComponent(target.id)}/inbox`,
937
+ body: { text: options.text },
938
+ });
939
+ // 409 = the receiver's inbox is full; nothing was stored. Passed through as an error.
940
+ if (!res.json?.success)
941
+ return fail(deps, describeFailure(res));
942
+ const data = res.json.data;
943
+ if (deps.json)
944
+ emitJson(deps, data ?? {});
945
+ else
946
+ deps.io.out(palette.ok(`${GLYPH.ok} posted to ${target.id.slice(0, 8)} (${data?.pending ?? '?'} pending, ${data?.unseen ?? '?'} unseen)`));
947
+ return EXIT.ok;
948
+ }
949
+ /** True only when the session list was read and does not contain `id`. */
950
+ async function sessionIsGone(deps, id) {
951
+ try {
952
+ const res = await deps.request(deps.ctx, { method: 'GET', path: '/api/v1/sessions' });
953
+ if (!res.json?.success)
954
+ return false;
955
+ return !(res.json.data ?? []).some((s) => s.id === id);
956
+ }
957
+ catch {
958
+ return false;
959
+ }
960
+ }
961
+ /** Relative age for humans: `12s`, `4m`, `3h`. */
962
+ function age(createdAt, now) {
963
+ const s = Math.max(0, Math.round((now - createdAt) / 1000));
964
+ if (s < 60)
965
+ return `${s}s`;
966
+ if (s < 3600)
967
+ return `${Math.round(s / 60)}m`;
968
+ return `${Math.round(s / 3600)}h`;
969
+ }
970
+ /**
971
+ * `agent inbox` — read this session's inbox (or peek at another's). Removes nothing:
972
+ * a message stays until `agent ack <id>`. Only reading your OWN inbox marks messages
973
+ * seen. Exit 0 with mail, 2 when a `--wait` ran out with nothing unseen or the server
974
+ * cut it short (restart, detach), 3 when the session is gone from the list.
975
+ */
976
+ export async function agentInboxRead(deps, options) {
977
+ const target = await resolveSessionId(deps, options.id ?? deps.ctx.selfId);
978
+ if ('error' in target)
979
+ return fail(deps, target.error, target.code);
980
+ const res = await deps.request(deps.ctx, {
981
+ method: 'GET',
982
+ path: `/api/v1/sessions/${encodeURIComponent(target.id)}/inbox`,
983
+ query: { wait: options.waitMs },
984
+ timeoutMs: options.waitMs !== undefined ? options.waitMs + 10_000 : undefined,
985
+ });
986
+ if (!res.json?.success)
987
+ return fail(deps, describeFailure(res));
988
+ const data = res.json.data;
989
+ const messages = data?.messages ?? [];
990
+ // `ended` is also what a server restart or a detach answers. Exit 3 tells a receiver
991
+ // loop to stop, so it needs the session list to confirm the session is gone; while
992
+ // it is still listed (or the list cannot be had, as during a restart) the wait was
993
+ // only cut short, which is exit 2: loop again.
994
+ const gone = data?.wait?.ended ? await sessionIsGone(deps, target.id) : false;
995
+ const code = gone ? EXIT.dead : data?.wait?.timedOut || data?.wait?.ended ? EXIT.timeout : EXIT.ok;
996
+ if (deps.json) {
997
+ emitJson(deps, data ?? {});
998
+ return code;
999
+ }
1000
+ const now = (deps.now ?? Date.now)();
1001
+ for (const m of messages) {
1002
+ const sender = m.from ? m.from.slice(0, 8) : 'user';
1003
+ const tag = m.seen ? palette.muted('seen') : palette.emph('new');
1004
+ deps.io.out(palette.muted(`── ${m.id} from ${sender}, ${age(m.createdAt, now)} ago, `) + tag);
1005
+ deps.io.out(terminalSafeText(m.text));
1006
+ }
1007
+ if (gone) {
1008
+ deps.io.err(palette.err(`${GLYPH.fail} the session went away during the wait`));
1009
+ }
1010
+ else if (data?.wait?.ended) {
1011
+ deps.io.err(palette.muted('(the server ended the wait early, e.g. a restart; exit 2, wait again)'));
1012
+ }
1013
+ else if (messages.length === 0) {
1014
+ deps.io.err(palette.muted(data?.wait?.timedOut ? '(no mail; wait timed out, exit 2)' : '(no mail)'));
1015
+ }
1016
+ else if (data?.receiver) {
1017
+ deps.io.err(palette.muted(`${messages.length} pending. Nothing is removed by reading: when done, \`codeman agent ack <id>…\` (full ids above).`));
1018
+ }
1019
+ else {
1020
+ deps.io.err(palette.muted(`${messages.length} pending (peek: nothing was marked seen).`));
1021
+ }
1022
+ return code;
1023
+ }
1024
+ /** `agent ack` — remove messages from THIS session's inbox by id. Idempotent. */
1025
+ export async function agentAck(deps, options) {
1026
+ const ids = options.ids.map((id) => id.trim()).filter(Boolean);
1027
+ if (ids.length === 0)
1028
+ return fail(deps, 'refusing: no message ids', EXIT.refused);
1029
+ const self = await resolveSessionId(deps, deps.ctx.selfId);
1030
+ if ('error' in self)
1031
+ return fail(deps, self.error, self.code);
1032
+ const res = await deps.request(deps.ctx, {
1033
+ method: 'POST',
1034
+ path: `/api/v1/sessions/${encodeURIComponent(self.id)}/inbox/ack`,
1035
+ body: { ids },
1036
+ });
1037
+ if (!res.json?.success)
1038
+ return fail(deps, describeFailure(res));
1039
+ const data = res.json.data;
1040
+ if (deps.json)
1041
+ emitJson(deps, data ?? {});
1042
+ else
1043
+ deps.io.out(palette.ok(`${GLYPH.ok} acked ${data?.removed ?? 0} (${data?.pending ?? '?'} still pending)`));
1044
+ return EXIT.ok;
1045
+ }
644
1046
  // ─────────────────────────────────────────────────────────────────────────────
645
1047
  // Commander wiring
646
1048
  // ─────────────────────────────────────────────────────────────────────────────
@@ -674,7 +1076,7 @@ async function run(json, verb) {
674
1076
  process.exitCode = await verb(deps);
675
1077
  }
676
1078
  catch (err) {
677
- console.error(palette.err(`${GLYPH.fail} ${getErrorMessage(err)}`));
1079
+ console.error(palette.err(`${GLYPH.fail} ${describeRequestError(err)}`));
678
1080
  process.exitCode = EXIT.error;
679
1081
  }
680
1082
  }
@@ -682,7 +1084,7 @@ async function run(json, verb) {
682
1084
  export function registerAgentCommands(program) {
683
1085
  const agent = program
684
1086
  .command('agent')
685
- .description('Talk to other sessions from inside one (any CLI mode): list, spawn, send, wait, read, interrupt, rm');
1087
+ .description('Talk to other sessions from inside one (any CLI mode): list, spawn, send, wait, read, interrupt, rm, the inbox: post, inbox, ack, and the experimental, opt-in inspect, stage, submit');
686
1088
  agent
687
1089
  .command('ls')
688
1090
  .alias('list')
@@ -690,15 +1092,17 @@ export function registerAgentCommands(program) {
690
1092
  .option('--json', 'Machine-readable output')
691
1093
  .action((options) => run(Boolean(options.json), agentLs));
692
1094
  agent
693
- .command('spawn <case>')
694
- .description('Start a worker session in a case (created if missing) and wait for its composer where the mode draws one')
1095
+ .command('spawn [case]')
1096
+ .description('Start a worker in cwd, an existing directory, or a named case; wait for its composer where supported')
695
1097
  .option('-m, --mode <mode>', 'Run mode id, as the Run menu names it', 'claude')
696
1098
  .option('-n, --name <name>', 'Session name shown in the UI')
1099
+ .option('--working-dir <directory>', 'Existing project directory (defaults to cwd when no case is given; a cwd of HOME is refused, pass it explicitly)')
697
1100
  .option('--no-ready', 'Return as soon as the session exists, without the readiness wait')
698
1101
  .option('-t, --timeout <ms>', 'Readiness budget in ms', String(DEFAULT_WAIT_MS))
699
1102
  .option('--json', 'Machine-readable output')
700
1103
  .action((caseName, options) => run(Boolean(options.json), (deps) => agentSpawn(deps, {
701
1104
  caseName,
1105
+ workingDir: options.workingDir,
702
1106
  mode: options.mode,
703
1107
  name: options.name,
704
1108
  ready: options.ready,
@@ -747,6 +1151,30 @@ export function registerAgentCommands(program) {
747
1151
  fresh: options.fresh,
748
1152
  timeoutMs: parsePositiveInt(options.timeout, DEFAULT_WAIT_MS),
749
1153
  })));
1154
+ agent
1155
+ .command('inspect <id>')
1156
+ .description('Experimental, opt-in (agentCoordinationEnabled): bounded transcript evidence or a labeled pane snapshot; opaque incremental cursors')
1157
+ .option('--max-bytes <n>', 'UTF8 evidence budget (1–65536)', '8192')
1158
+ .option('--since <cursor>', 'Resume a cursor from the same owner, pane and transcript')
1159
+ .option('--json', 'Machine-readable output')
1160
+ .action((id, options) => run(Boolean(options.json), (deps) => agentInspect(deps, {
1161
+ id,
1162
+ maxBytes: parsePositiveInt(options.maxBytes, 8192, '--max-bytes'),
1163
+ since: options.since,
1164
+ })));
1165
+ agent
1166
+ .command('stage <id>')
1167
+ .description('Experimental, opt-in (agentCoordinationEnabled): type a printable single-line UTF8 file without Enter; requires a verifiable local Codex composer, and refuses slash or shell commands and a last word starting with @ or $ (use send)')
1168
+ .requiredOption('--message-file <path>', 'Regular UTF8 file, at most 8192 bytes, no trailing newline')
1169
+ .option('--json', 'Machine-readable output')
1170
+ .action((id, options) => run(Boolean(options.json), (deps) => agentStage(deps, { id, messageFile: options.messageFile })));
1171
+ agent
1172
+ .command('submit <id>')
1173
+ .description('Experimental, opt-in (agentCoordinationEnabled): send Enter once only when the staged composer, pane and hash still match')
1174
+ .requiredOption('--stage-id <receipt>', 'One-shot stage receipt')
1175
+ .requiredOption('--confirm-sha <hash>', 'Exact SHA-256 returned by stage')
1176
+ .option('--json', 'Machine-readable output')
1177
+ .action((id, options) => run(Boolean(options.json), (deps) => agentSubmit(deps, { id, stageId: options.stageId, confirmSha: options.confirmSha })));
750
1178
  agent
751
1179
  .command('read <id>')
752
1180
  .description("Print a session's last answer (as the server reads it for that mode) or, with --tail, its terminal")
@@ -763,6 +1191,35 @@ export function registerAgentCommands(program) {
763
1191
  .description('Send a bare ESC to stop the current turn (the conversation survives; deleting would not)')
764
1192
  .option('--json', 'Machine-readable output')
765
1193
  .action((id, options) => run(Boolean(options.json), (deps) => agentInterrupt(deps, { id })));
1194
+ agent
1195
+ .command('post <id> <text...>')
1196
+ .description("Leave a message in another session's inbox (ONE quoted argument; multi-line is fine). Nothing is typed into its pane: it reads with `agent inbox`")
1197
+ .option('--json', 'Machine-readable output')
1198
+ // A message is often a file (`"$(cat task.md)"`) and markdown starts with `- ` or
1199
+ // `---` often enough; without this commander takes it for an unknown option.
1200
+ // `--json` and `-h` are still options: text that is exactly one goes after `--`.
1201
+ .allowUnknownOption()
1202
+ .action((id, words, options) => run(Boolean(options.json), (deps) => {
1203
+ const message = postTextFromArgs(words);
1204
+ if ('error' in message)
1205
+ return Promise.resolve(fail(deps, message.error, EXIT.refused));
1206
+ return agentPost(deps, { id, text: message.text });
1207
+ }));
1208
+ agent
1209
+ .command('inbox [id]')
1210
+ .description("Read this session's inbox (or peek at another's with [id]). Reading removes nothing; `agent ack` does. --wait blocks while nothing is new: exit 2 on timeout")
1211
+ .option('-w, --wait', 'Block until a new (unseen) message arrives')
1212
+ .option('-t, --timeout <ms>', 'Wait budget in ms (with --wait)', String(DEFAULT_WAIT_MS))
1213
+ .option('--json', 'Machine-readable output')
1214
+ .action((id, options) => run(Boolean(options.json), (deps) => agentInboxRead(deps, {
1215
+ id,
1216
+ waitMs: options.wait ? parsePositiveInt(options.timeout, DEFAULT_WAIT_MS) : undefined,
1217
+ })));
1218
+ agent
1219
+ .command('ack <ids...>')
1220
+ .description("Remove messages from this session's inbox by id, once handled (unknown ids are not an error)")
1221
+ .option('--json', 'Machine-readable output')
1222
+ .action((ids, options) => run(Boolean(options.json), (deps) => agentAck(deps, { ids })));
766
1223
  agent
767
1224
  .command('rm <id>')
768
1225
  .description('Delete any session except this one (refuses your own id)')