kimaki 1.3.0 → 1.5.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 (346) hide show
  1. package/assets/fonts/Geist-Bold.ttf +0 -0
  2. package/assets/fonts/Geist-Regular.ttf +0 -0
  3. package/assets/fonts/GeistMono-Regular.ttf +0 -0
  4. package/assets/fonts/OFL.txt +92 -0
  5. package/dist/agent-ui.e2e.test.js +27 -13
  6. package/dist/agent-ui.e2e.test.js.map +1 -1
  7. package/dist/agent-ui.js +2 -1
  8. package/dist/agent-ui.js.map +1 -1
  9. package/dist/analytics.e2e.test.js +1 -1
  10. package/dist/analytics.e2e.test.js.map +1 -1
  11. package/dist/autostart.d.ts +4 -0
  12. package/dist/autostart.js +55 -10
  13. package/dist/autostart.js.map +1 -1
  14. package/dist/bot.d.ts +13 -6
  15. package/dist/bot.js +17 -15
  16. package/dist/bot.js.map +1 -1
  17. package/dist/btw.e2e.test.js +3 -3
  18. package/dist/builtin-skills.d.ts +3 -0
  19. package/dist/builtin-skills.js +29 -0
  20. package/dist/builtin-skills.js.map +1 -1
  21. package/dist/builtin-skills.test.js +72 -1
  22. package/dist/builtin-skills.test.js.map +1 -1
  23. package/dist/catalog-refresh.e2e.test.js +2 -2
  24. package/dist/cli/bot.d.ts +1 -1
  25. package/dist/cli/bot.js +33 -18
  26. package/dist/cli/bot.js.map +1 -1
  27. package/dist/cli/discord.js +32 -6
  28. package/dist/cli/discord.js.map +1 -1
  29. package/dist/cli/schedule.js +23 -9
  30. package/dist/cli/schedule.js.map +1 -1
  31. package/dist/cli/session.js +37 -12
  32. package/dist/cli/session.js.map +1 -1
  33. package/dist/cli/shared.d.ts +5 -3
  34. package/dist/cli/shared.js +45 -10
  35. package/dist/cli/shared.js.map +1 -1
  36. package/dist/cli-discord.e2e.test.js +13 -8
  37. package/dist/cli-discord.e2e.test.js.map +1 -1
  38. package/dist/cli-lock.e2e.test.js +87 -20
  39. package/dist/cli-lock.e2e.test.js.map +1 -1
  40. package/dist/cli-read.e2e.test.js +18 -9
  41. package/dist/cli-read.e2e.test.js.map +1 -1
  42. package/dist/cli-sessions.e2e.test.js +4 -3
  43. package/dist/cli-sessions.e2e.test.js.map +1 -1
  44. package/dist/cli.js +2 -0
  45. package/dist/cli.js.map +1 -1
  46. package/dist/commands/preference-commands.d.ts +4 -0
  47. package/dist/commands/preference-commands.js +38 -4
  48. package/dist/commands/preference-commands.js.map +1 -1
  49. package/dist/commands/session-commands.js +11 -8
  50. package/dist/commands/session-commands.js.map +1 -1
  51. package/dist/commands/thread-commands.js +94 -24
  52. package/dist/commands/thread-commands.js.map +1 -1
  53. package/dist/commands/worktree-commands.d.ts +5 -6
  54. package/dist/commands/worktree-commands.js +8 -7
  55. package/dist/commands/worktree-commands.js.map +1 -1
  56. package/dist/commands.e2e.test.js +25 -21
  57. package/dist/commands.e2e.test.js.map +1 -1
  58. package/dist/coordinator-agents.e2e.test.d.ts +1 -0
  59. package/dist/coordinator-agents.e2e.test.js +113 -0
  60. package/dist/coordinator-agents.e2e.test.js.map +1 -0
  61. package/dist/coordinator-idle.e2e.test.d.ts +1 -0
  62. package/dist/coordinator-idle.e2e.test.js +100 -0
  63. package/dist/coordinator-idle.e2e.test.js.map +1 -0
  64. package/dist/coordinator-queue.e2e.test.d.ts +1 -0
  65. package/dist/coordinator-queue.e2e.test.js +244 -0
  66. package/dist/coordinator-queue.e2e.test.js.map +1 -0
  67. package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
  68. package/dist/coordinator-warmup.e2e.test.js +131 -0
  69. package/dist/coordinator-warmup.e2e.test.js.map +1 -0
  70. package/dist/coordinator.d.ts +6 -0
  71. package/dist/coordinator.e2e.test.d.ts +1 -0
  72. package/dist/coordinator.e2e.test.js +505 -0
  73. package/dist/coordinator.e2e.test.js.map +1 -0
  74. package/dist/coordinator.js +164 -0
  75. package/dist/coordinator.js.map +1 -0
  76. package/dist/db.d.ts +78 -3
  77. package/dist/db.test.js +1 -0
  78. package/dist/db.test.js.map +1 -1
  79. package/dist/diagram.d.ts +11 -0
  80. package/dist/diagram.js +232 -0
  81. package/dist/diagram.js.map +1 -0
  82. package/dist/diagram.test.d.ts +1 -0
  83. package/dist/diagram.test.js +52 -0
  84. package/dist/diagram.test.js.map +1 -0
  85. package/dist/duration.js +13 -5
  86. package/dist/duration.js.map +1 -1
  87. package/dist/effects.d.ts +6 -1
  88. package/dist/effects.js +97 -26
  89. package/dist/effects.js.map +1 -1
  90. package/dist/event-loop.d.ts +2 -3
  91. package/dist/event-loop.js +74 -44
  92. package/dist/event-loop.js.map +1 -1
  93. package/dist/format-parts.d.ts +21 -4
  94. package/dist/format-parts.js +40 -23
  95. package/dist/format-parts.js.map +1 -1
  96. package/dist/gateway-replay.e2e.test.js +4 -4
  97. package/dist/harness.e2e.test.js +2 -2
  98. package/dist/harness.e2e.test.js.map +1 -1
  99. package/dist/hello-thread.e2e.test.js +56 -5
  100. package/dist/hello-thread.e2e.test.js.map +1 -1
  101. package/dist/ingress.js +47 -12
  102. package/dist/ingress.js.map +1 -1
  103. package/dist/interaction-context.d.ts +4 -4
  104. package/dist/interaction-context.js +9 -7
  105. package/dist/interaction-context.js.map +1 -1
  106. package/dist/interrupt.e2e.test.js +8 -5
  107. package/dist/interrupt.e2e.test.js.map +1 -1
  108. package/dist/legacy.e2e.test.js +1 -1
  109. package/dist/lock-routes.d.ts +83 -31
  110. package/dist/lock-routes.js +68 -8
  111. package/dist/lock-routes.js.map +1 -1
  112. package/dist/lock-server.d.ts +3 -2
  113. package/dist/lock-server.js +23 -7
  114. package/dist/lock-server.js.map +1 -1
  115. package/dist/main.d.ts +6 -3
  116. package/dist/main.js +14 -4
  117. package/dist/main.js.map +1 -1
  118. package/dist/mainmast-idle.e2e.test.d.ts +1 -0
  119. package/dist/mainmast-idle.e2e.test.js +99 -0
  120. package/dist/mainmast-idle.e2e.test.js.map +1 -0
  121. package/dist/mainmast-queue.e2e.test.d.ts +1 -0
  122. package/dist/mainmast-queue.e2e.test.js +244 -0
  123. package/dist/mainmast-queue.e2e.test.js.map +1 -0
  124. package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
  125. package/dist/mainmast-warmup.e2e.test.js +130 -0
  126. package/dist/mainmast-warmup.e2e.test.js.map +1 -0
  127. package/dist/mainmast.d.ts +6 -0
  128. package/dist/mainmast.e2e.test.d.ts +1 -0
  129. package/dist/mainmast.e2e.test.js +428 -0
  130. package/dist/mainmast.e2e.test.js.map +1 -0
  131. package/dist/mainmast.js +156 -0
  132. package/dist/mainmast.js.map +1 -0
  133. package/dist/markdown/markdown.test.js +14 -4
  134. package/dist/markdown/markdown.test.js.map +1 -1
  135. package/dist/markdown/render-markdown.js +9 -6
  136. package/dist/markdown/render-markdown.js.map +1 -1
  137. package/dist/onboarding-cli.e2e.test.js +7 -6
  138. package/dist/onboarding-cli.e2e.test.js.map +1 -1
  139. package/dist/onboarding.d.ts +3 -10
  140. package/dist/onboarding.e2e.test.js +8 -10
  141. package/dist/onboarding.e2e.test.js.map +1 -1
  142. package/dist/onboarding.js +14 -24
  143. package/dist/onboarding.js.map +1 -1
  144. package/dist/permissions.e2e.test.js +3 -3
  145. package/dist/plugin/coordinator.d.ts +4 -0
  146. package/dist/plugin/coordinator.js +389 -0
  147. package/dist/plugin/coordinator.js.map +1 -0
  148. package/dist/plugin/index.js +93 -61
  149. package/dist/plugin/index.js.map +1 -1
  150. package/dist/plugin/mainmast.d.ts +4 -0
  151. package/dist/plugin/mainmast.js +379 -0
  152. package/dist/plugin/mainmast.js.map +1 -0
  153. package/dist/plugin.e2e.test.js +1 -1
  154. package/dist/preference-commands.e2e.test.js +103 -24
  155. package/dist/preference-commands.e2e.test.js.map +1 -1
  156. package/dist/project.d.ts +1 -1
  157. package/dist/prompt.d.ts +22 -9
  158. package/dist/prompt.js +64 -48
  159. package/dist/prompt.js.map +1 -1
  160. package/dist/questions.e2e.test.js +13 -13
  161. package/dist/queue-sources.e2e.test.d.ts +1 -0
  162. package/dist/queue-sources.e2e.test.js +168 -0
  163. package/dist/queue-sources.e2e.test.js.map +1 -0
  164. package/dist/queue.d.ts +9 -1
  165. package/dist/queue.e2e.test.js +48 -30
  166. package/dist/queue.e2e.test.js.map +1 -1
  167. package/dist/queue.js +97 -24
  168. package/dist/queue.js.map +1 -1
  169. package/dist/queue.test.d.ts +1 -0
  170. package/dist/queue.test.js +57 -0
  171. package/dist/queue.test.js.map +1 -0
  172. package/dist/remote-send.e2e.test.js +6 -6
  173. package/dist/remote-send.js +2 -1
  174. package/dist/remote-send.js.map +1 -1
  175. package/dist/renderer.e2e.test.js +7 -7
  176. package/dist/restart.e2e.test.js +2 -2
  177. package/dist/routines.e2e.test.js +2 -2
  178. package/dist/scheduler.d.ts +3 -3
  179. package/dist/scheduler.e2e.test.js +43 -20
  180. package/dist/scheduler.e2e.test.js.map +1 -1
  181. package/dist/scheduler.js +4 -2
  182. package/dist/scheduler.js.map +1 -1
  183. package/dist/schema-sql.d.ts +1 -1
  184. package/dist/schema-sql.js +1 -1
  185. package/dist/schema-sql.js.map +1 -1
  186. package/dist/schema.d.ts +152 -6
  187. package/dist/schema.js +12 -0
  188. package/dist/schema.js.map +1 -1
  189. package/dist/session-commands.e2e.test.js +36 -11
  190. package/dist/session-commands.e2e.test.js.map +1 -1
  191. package/dist/session-events.d.ts +18 -6
  192. package/dist/session-events.js +175 -27
  193. package/dist/session-events.js.map +1 -1
  194. package/dist/session-ops.e2e.test.js +63 -12
  195. package/dist/session-ops.e2e.test.js.map +1 -1
  196. package/dist/sessions.d.ts +44 -21
  197. package/dist/sessions.js +177 -65
  198. package/dist/sessions.js.map +1 -1
  199. package/dist/shell.e2e.test.js +4 -4
  200. package/dist/slash-commands.js +6 -4
  201. package/dist/slash-commands.js.map +1 -1
  202. package/dist/slash-commands.test.js +2 -0
  203. package/dist/slash-commands.test.js.map +1 -1
  204. package/dist/sleep.e2e.test.js +29 -14
  205. package/dist/sleep.e2e.test.js.map +1 -1
  206. package/dist/sleeps.d.ts +2 -1
  207. package/dist/sleeps.js +21 -3
  208. package/dist/sleeps.js.map +1 -1
  209. package/dist/store.d.ts +1 -0
  210. package/dist/store.js +3 -2
  211. package/dist/store.js.map +1 -1
  212. package/dist/system-prompt.d.ts +5 -15
  213. package/dist/system-prompt.js +180 -131
  214. package/dist/system-prompt.js.map +1 -1
  215. package/dist/test/generate-log-plugin/index.d.ts +3 -0
  216. package/dist/test/generate-log-plugin/index.js +15 -0
  217. package/dist/test/generate-log-plugin/index.js.map +1 -0
  218. package/dist/test/harness.d.ts +20 -2
  219. package/dist/test/harness.js +51 -11
  220. package/dist/test/harness.js.map +1 -1
  221. package/dist/test/replay.js +11 -8
  222. package/dist/test/replay.js.map +1 -1
  223. package/dist/thread-reducer.d.ts +29 -6
  224. package/dist/thread-reducer.fixtures.test.js +58 -51
  225. package/dist/thread-reducer.fixtures.test.js.map +1 -1
  226. package/dist/thread-reducer.js +109 -50
  227. package/dist/thread-reducer.js.map +1 -1
  228. package/dist/thread-reducer.test.js +190 -48
  229. package/dist/thread-reducer.test.js.map +1 -1
  230. package/dist/tunnel.d.ts +9 -0
  231. package/dist/tunnel.js +117 -28
  232. package/dist/tunnel.js.map +1 -1
  233. package/dist/tunnel.test.js +81 -5
  234. package/dist/tunnel.test.js.map +1 -1
  235. package/dist/voice-attachments.e2e.test.js +108 -18
  236. package/dist/voice-attachments.e2e.test.js.map +1 -1
  237. package/dist/voice-call.e2e.test.js +9 -9
  238. package/dist/voice-call.js +6 -6
  239. package/dist/voice-call.js.map +1 -1
  240. package/dist/voice-openai.e2e.test.js +59 -23
  241. package/dist/voice-openai.e2e.test.js.map +1 -1
  242. package/dist/voice.d.ts +1 -1
  243. package/dist/voice.js +72 -24
  244. package/dist/voice.js.map +1 -1
  245. package/dist/working-directory.e2e.test.js +11 -11
  246. package/dist/working-directory.e2e.test.js.map +1 -1
  247. package/package.json +11 -5
  248. package/skills/kimaki-tunnel/SKILL.md +3 -2
  249. package/src/agent-ui.e2e.test.ts +27 -13
  250. package/src/agent-ui.ts +2 -1
  251. package/src/analytics.e2e.test.ts +1 -1
  252. package/src/autostart.ts +55 -10
  253. package/src/bot.ts +36 -19
  254. package/src/btw.e2e.test.ts +3 -3
  255. package/src/builtin-skills.test.ts +73 -1
  256. package/src/builtin-skills.ts +33 -0
  257. package/src/catalog-refresh.e2e.test.ts +2 -2
  258. package/src/cli/bot.ts +30 -19
  259. package/src/cli/discord.ts +30 -6
  260. package/src/cli/schedule.ts +23 -9
  261. package/src/cli/session.ts +35 -12
  262. package/src/cli/shared.ts +43 -8
  263. package/src/cli-discord.e2e.test.ts +13 -8
  264. package/src/cli-lock.e2e.test.ts +88 -20
  265. package/src/cli-read.e2e.test.ts +17 -8
  266. package/src/cli-sessions.e2e.test.ts +4 -3
  267. package/src/cli.ts +2 -0
  268. package/src/commands/preference-commands.ts +38 -4
  269. package/src/commands/session-commands.ts +12 -7
  270. package/src/commands/thread-commands.ts +92 -24
  271. package/src/commands/worktree-commands.ts +10 -9
  272. package/src/commands.e2e.test.ts +25 -21
  273. package/src/coordinator-agents.e2e.test.ts +145 -0
  274. package/src/coordinator-idle.e2e.test.ts +122 -0
  275. package/src/coordinator-queue.e2e.test.ts +279 -0
  276. package/src/coordinator-warmup.e2e.test.ts +158 -0
  277. package/src/coordinator.e2e.test.ts +545 -0
  278. package/src/coordinator.ts +168 -0
  279. package/src/db.test.ts +1 -0
  280. package/src/diagram.test.ts +56 -0
  281. package/src/diagram.ts +243 -0
  282. package/src/duration.ts +13 -5
  283. package/src/effects.ts +99 -27
  284. package/src/event-loop.ts +71 -45
  285. package/src/format-parts.ts +53 -22
  286. package/src/gateway-replay.e2e.test.ts +4 -4
  287. package/src/harness.e2e.test.ts +2 -2
  288. package/src/hello-thread.e2e.test.ts +59 -5
  289. package/src/ingress.ts +44 -11
  290. package/src/interaction-context.ts +12 -11
  291. package/src/interrupt.e2e.test.ts +8 -5
  292. package/src/legacy.e2e.test.ts +1 -1
  293. package/src/lock-routes.ts +62 -8
  294. package/src/lock-server.ts +23 -7
  295. package/src/main.ts +26 -10
  296. package/src/markdown/markdown.test.ts +15 -4
  297. package/src/markdown/render-markdown.ts +9 -6
  298. package/src/onboarding-cli.e2e.test.ts +7 -6
  299. package/src/onboarding.e2e.test.ts +8 -10
  300. package/src/onboarding.ts +13 -29
  301. package/src/permissions.e2e.test.ts +3 -3
  302. package/src/plugin/coordinator.ts +401 -0
  303. package/src/plugin/index.ts +86 -59
  304. package/src/plugin.e2e.test.ts +1 -1
  305. package/src/preference-commands.e2e.test.ts +103 -23
  306. package/src/prompt.ts +87 -43
  307. package/src/questions.e2e.test.ts +13 -13
  308. package/src/queue-sources.e2e.test.ts +197 -0
  309. package/src/queue.e2e.test.ts +48 -30
  310. package/src/queue.test.ts +59 -0
  311. package/src/queue.ts +104 -24
  312. package/src/remote-send.e2e.test.ts +6 -6
  313. package/src/remote-send.ts +2 -1
  314. package/src/renderer.e2e.test.ts +7 -7
  315. package/src/restart.e2e.test.ts +2 -2
  316. package/src/routines.e2e.test.ts +2 -2
  317. package/src/scheduler.e2e.test.ts +45 -19
  318. package/src/scheduler.ts +4 -2
  319. package/src/schema-sql.ts +1 -1
  320. package/src/schema.ts +13 -0
  321. package/src/session-commands.e2e.test.ts +38 -11
  322. package/src/session-events.ts +168 -25
  323. package/src/session-ops.e2e.test.ts +64 -11
  324. package/src/sessions.ts +217 -67
  325. package/src/shell.e2e.test.ts +4 -4
  326. package/src/slash-commands.test.ts +2 -0
  327. package/src/slash-commands.ts +6 -4
  328. package/src/sleep.e2e.test.ts +30 -13
  329. package/src/sleeps.ts +20 -3
  330. package/src/store.ts +5 -2
  331. package/src/system-prompt.ts +190 -134
  332. package/src/test/generate-log-plugin/index.ts +16 -0
  333. package/src/test/harness.ts +77 -17
  334. package/src/test/replay.ts +12 -7
  335. package/src/thread-reducer.fixtures.test.ts +58 -51
  336. package/src/thread-reducer.test.ts +199 -49
  337. package/src/thread-reducer.ts +135 -66
  338. package/src/tunnel.test.ts +81 -5
  339. package/src/tunnel.ts +124 -25
  340. package/src/voice-attachments.e2e.test.ts +110 -18
  341. package/src/voice-call.e2e.test.ts +9 -9
  342. package/src/voice-call.ts +6 -6
  343. package/src/voice-openai.e2e.test.ts +64 -24
  344. package/src/voice.ts +93 -26
  345. package/src/working-directory.e2e.test.ts +11 -11
  346. package/src/message-tool.e2e.test.ts +0 -45
@@ -34,16 +34,23 @@ test('CLI send starts and continues a session through the authenticated bot API'
34
34
  const first = await cli(['send', '--channel', twin.channelId, '--prompt', 'CLI first'])
35
35
  const ids = JSON.parse(first.stdout) as { threadId: string; sessionId: string }
36
36
  await waitForFooter({ discord: twin.discord, threadId: ids.threadId })
37
+ // Follow-ups into an idle thread have no Discord message: the thread must still show them.
37
38
  await cli(['send', '--thread', ids.threadId, '--prompt', 'CLI second. queue'])
38
39
  await waitForFooter({ discord: twin.discord, threadId: ids.threadId, count: 2 })
40
+ await cli(['send', '--thread', ids.threadId, '--prompt', 'CLI third'])
41
+ await waitForFooter({ discord: twin.discord, threadId: ids.threadId, count: 3 })
39
42
  expect(await twin.discord.thread(ids.threadId).text()).toMatchInlineSnapshot(`
40
43
  "--- from: assistant (TestBot)
41
- » **CLI:** CLI first
44
+ -# » **CLI:** CLI first
42
45
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
43
46
  ok
44
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*
47
+ -# *main ⋅ Ns ⋅ deterministic-v2*
48
+ -# » **CLI:** CLI second
45
49
  ok
46
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*"
50
+ -# *main ⋅ Ns ⋅ deterministic-v2*
51
+ -# » **CLI:** CLI third
52
+ ok
53
+ -# *main ⋅ Ns ⋅ deterministic-v2*"
47
54
  `)
48
55
  expect(ids.sessionId).toMatch(/^ses_/)
49
56
  const tokenFile = path.join(dataDir, 'lock-token')
@@ -57,12 +64,58 @@ test('CLI send starts and continues a session through the authenticated bot API'
57
64
  expect(invalid.status).toBe(400)
58
65
  })
59
66
 
67
+ test('CLI send shows a prompt over 2000 chars in full, split into messages', async () => {
68
+ const prompt = Array.from({ length: 30 }, (_, index) => `line ${String(index).padStart(2, '0')}: ${'x'.repeat(70)}`).join('\n')
69
+ const sent = await cli(['send', '--channel', twin.channelId, '--prompt', prompt])
70
+ const ids = JSON.parse(sent.stdout) as { threadId: string; sessionId: string }
71
+ await waitForFooter({ discord: twin.discord, threadId: ids.threadId })
72
+ expect(await twin.discord.thread(ids.threadId).text()).toMatchInlineSnapshot(`
73
+ "--- from: assistant (TestBot)
74
+ -# » **CLI:** line 00: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
75
+ -# line 01: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
76
+ -# line 02: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
77
+ -# line 03: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
78
+ -# line 04: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
79
+ -# line 05: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
80
+ -# line 06: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
81
+ -# line 07: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
82
+ -# line 08: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
83
+ -# line 09: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
84
+ -# line 10: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
85
+ -# line 11: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
86
+ -# line 12: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
87
+ -# line 13: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
88
+ -# line 14: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
89
+ -# line 15: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
90
+ -# line 16: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
91
+ -# line 17: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
92
+ -# line 18: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
93
+ -# line 19: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
94
+ -# line 20: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
95
+ -# line 21: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
96
+ -# line 22: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
97
+ -# line 23: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
98
+ -# line 24: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
99
+ -# line 25: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
100
+ -# line 26: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
101
+ -# line 27: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
102
+ -# line 28: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
103
+ -# line 29: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
104
+ -# *using deterministic-provider/deterministic-v2 ⋅ build*
105
+ ok
106
+ -# *main ⋅ Ns ⋅ deterministic-v2*"
107
+ `)
108
+ const messages = await twin.discord.thread(ids.threadId).getMessages()
109
+ expect(messages.every((message) => message.content.length <= 2000)).toBe(true)
110
+ })
111
+
60
112
  test('CLI help documents the supported P7 commands', async () => {
61
113
  const output = await cli(['--help'])
62
114
  expect(output.stdout).toContain('send')
63
115
  expect(output.stdout).toContain('upload-request')
64
- expect(output.stdout).toMatchInlineSnapshot(`
65
- "kimaki
116
+ // The header is `kimaki/<version>`; keep the snapshot stable across releases.
117
+ expect(output.stdout.replace(/^kimaki\/[^\n]+/, 'kimaki/<version>')).toMatchInlineSnapshot(`
118
+ "kimaki/<version>
66
119
 
67
120
  Usage:
68
121
  $ kimaki [options]
@@ -96,9 +149,9 @@ test('CLI help documents the supported P7 commands', async () => {
96
149
  --rebuild Write transcripts again, also current ones
97
150
  --json Output as JSON
98
151
 
99
- session wait <id> Wait until idle or waiting for input, then print the session
152
+ session wait [...ids] Wait until every session is idle or waits for the user, then print each one and why it returned
100
153
  --data-dir <path> Data directory (default: ~/.kimaki)
101
- --timeout <duration> Timeout, for example 30m or 2h
154
+ --timeout <duration> Timeout, for example 30m or 2h; exit 1 when a session still runs
102
155
 
103
156
  session editors <file> List sessions that last edited a file, newest first
104
157
  --data-dir <path> Data directory (default: ~/.kimaki)
@@ -134,6 +187,9 @@ test('CLI help documents the supported P7 commands', async () => {
134
187
  session archive [id] Archive a session thread
135
188
  --data-dir <path> Data directory (default: ~/.kimaki)
136
189
 
190
+ session compact [id] Summarize the session history after its current turn (default: this session)
191
+ --data-dir <path> Data directory (default: ~/.kimaki)
192
+
137
193
  session title <title> Rename the session and its Discord thread
138
194
  --data-dir <path> Data directory (default: ~/.kimaki)
139
195
  -s, --session <id> Session ID, Discord thread ID or thread URL (default: OPENCODE_SESSION_ID)
@@ -182,7 +238,7 @@ test('CLI help documents the supported P7 commands', async () => {
182
238
  --data-dir <path> Data directory (default: ~/.kimaki)
183
239
  -c, --channel <id> Destination channel (default: channel of the session folder)
184
240
 
185
- buttons Show 1-3 action buttons. Call last, after visible text
241
+ buttons Show 1-3 action buttons. Explain them in your text
186
242
  --data-dir <path> Data directory (default: ~/.kimaki)
187
243
  -s, --session <id> Session ID, Discord thread ID or thread URL (default: OPENCODE_SESSION_ID)
188
244
  -b, --button <spec> Repeatable: Label[:white|blue|green|red]
@@ -212,7 +268,7 @@ test('CLI help documents the supported P7 commands', async () => {
212
268
  -d, --project <path> Project directory of the new thread
213
269
  --thread <id> Continue this thread (ID or URL)
214
270
  -s, --session <id> Continue this session: session ID, thread ID or thread URL
215
- -p, --prompt <text> Prompt; thread suffixes . queue and . btw are supported
271
+ -p, --prompt <text> Prompt; thread suffixes . queue and . btw are supported (required)
216
272
  -f, --file <path> Attach a local file (repeatable)
217
273
  -n, --name <text> Thread name
218
274
  --agent <name> Agent ID
@@ -224,7 +280,7 @@ test('CLI help documents the supported P7 commands', async () => {
224
280
  --parent-session <id> Record the parent session in session metadata
225
281
  --permission <rule> Repeatable: tool[:pattern]:allow|deny|ask
226
282
  --notify-only Post a notification thread without a model turn
227
- --wait Wait until idle or input is needed, then print the session
283
+ --wait [seconds] Wait until idle or input is needed (at most this many seconds, default 1 day), then print the session
228
284
  --send-at <when> Schedule: UTC ISO date ending in Z, or cron expression (UTC)
229
285
  --pre-run <command> Scheduled only: run first in the project. Exit 0 starts, stdout is appended
230
286
  --allow-concurrency Scheduled only: allow overlapping runs of this routine
@@ -232,10 +288,11 @@ test('CLI help documents the supported P7 commands', async () => {
232
288
  session events <id> Print the recorded OpenCode events of a thread as JSONL (root + subagents)
233
289
  --data-dir <path> Data directory (default: ~/.kimaki)
234
290
 
235
- session read <id> Print the messages of a session from OpenCode as markdown
291
+ session read <id> Print the prompts and final answers of a session as markdown
236
292
  --data-dir <path> Data directory (default: ~/.kimaki)
237
- --thinking Include reasoning
238
- --verbose Include full tool inputs and outputs
293
+ --full Every message with tool calls
294
+ --thinking Full output with reasoning
295
+ --verbose Full output with tool inputs and outputs
239
296
  --json Print raw OpenCode messages
240
297
 
241
298
  session cwd [directory] Show or change the working directory at a native safe boundary
@@ -295,7 +352,7 @@ test('CLI help documents the supported P7 commands', async () => {
295
352
  routine run <routineId> Run a routine now
296
353
  --data-dir <path> Data directory (default: ~/.kimaki)
297
354
 
298
- sleep Wake this session later with a new message in the same thread. Run it last, after your text. To monitor slow events (email replies, PR reviews), wait 2h or more; never poll every few minutes
355
+ sleep Wake this session later with a new message in the same thread. Run it last, after your text. To monitor slow events (email replies, PR reviews), wait 2h or more; never poll every few minutes. When blocked on a user action outside the chat (a sudo or permission dialog, a login), ask for it, assume they will do it, and sleep 5m, then check and continue on your own
299
356
  --data-dir <path> Data directory (default: ~/.kimaki)
300
357
  --duration <duration> Relative wait, e.g. 30m, 2h, 1d
301
358
  --until <date> UTC ISO date ending in Z
@@ -320,6 +377,10 @@ test('CLI help documents the supported P7 commands', async () => {
320
377
  -s, --session <id> Session ID, Discord thread ID or thread URL (default: OPENCODE_SESSION_ID)
321
378
  -c, --channel <id> Kimaki voice channel to post in (default in voice calls: KIMAKI_CHANNEL_ID)
322
379
 
380
+ diagram <input> Render a Mermaid diagram (file path, or - for stdin) to a light themed PNG. Upload it with upload-to-discord
381
+ -o, --output <path> PNG file to write (default: input path with .png, or a temp file for stdin)
382
+ --dark Dark theme instead of light
383
+
323
384
  Tools:
324
385
  tunnel Run a command and expose its local port with a public opentunnel.xyz URL. The child gets KIMAKI_TUNNEL_URL
325
386
  -p, --port <port> Local port (default: read from the command output)
@@ -376,12 +437,15 @@ test('CLI help documents the supported P7 commands', async () => {
376
437
  --restart-onboarding Choose credentials again
377
438
  --worktrees Use a fresh Git worktree for new sessions unless the channel overrides it
378
439
  --allow-all-users Every server member can use the bot (role "no-kimaki" still denies; provider login stays admin-only)
379
- --message-tool Discord shows only a concise reply the agent sends with a send_message tool; plain text stays visible in the OpenCode TUI
380
440
  --voice <name> Voice of voice calls, saved for later starts (e.g. cove, juniper, sol). "default" resets it
381
441
  --disable-skills <list> Built-in Kimaki skills to turn off: "all" or a comma separated list (kimaki-debug,kimaki-tunnel)
442
+ --disable-prompt <section> System prompt section to remove from new sessions (repeatable): discord-permissions, kimaki-debugging, file-upload-requests, action-buttons, session-sleep, session-abort, cross-project-commands, waiting-sessions, critique-diffs, markdown-formatting, discord-callouts, search-result-urls, diagrams
382
443
  --no-analytics Disable anonymous usage analytics (same as KIMAKI_STRADA_ENABLED=0)
444
+ --coordinator [enabled] Channel messages go to one long coordinator session that starts threads for the work. "false" turns it off (/toggle-coordinator sets one channel) (default: true)
445
+ --no-autostart Do not open a terminal with the bot when OpenCode starts (for bots run by launchd, systemd or pm2)
383
446
  --if-not-running Start only if no bot runs; never stop the running one (used by autostart)
384
447
  -h, --help Display this message
448
+ -V, --version Display version number
385
449
  "
386
450
  `)
387
451
  })
@@ -408,16 +472,20 @@ test('CLI registry shares thread actions, preferences, title and archive', async
408
472
  await cli(['session', 'queue', 'add', 'Registry queued', '--session', started.threadId])
409
473
  await expect(cli(['session', 'queue', 'list', '--session', 'not-an-id'])).rejects.toThrow('Not a session ID, Discord thread ID or thread URL: not-an-id')
410
474
  await waitForFooter({ discord: twin.discord, threadId: started.threadId, count: 2 })
475
+ // The deterministic model writes no summary template, so the compaction runs and fails.
476
+ expect(JSON.parse((await cli(['session', 'compact', started.threadId])).stdout)).toEqual({ sessionId: started.sessionId, queued: true })
411
477
  await cli(['session', 'abort', started.sessionId])
412
478
  await cli(['session', 'archive', started.threadId])
413
479
  expect(await twin.discord.thread(started.threadId).text()).toMatchInlineSnapshot(`
414
480
  "--- from: assistant (TestBot)
415
- » **CLI:** Registry first
481
+ -# » **CLI:** Registry first
416
482
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
417
483
  ok
418
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*
484
+ -# *main ⋅ Ns ⋅ deterministic-v2*
485
+ -# » **CLI:** Registry queued
419
486
  ok
420
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*"
487
+ -# *main ⋅ Ns ⋅ deterministic-v2 ⋅ build*
488
+ ✗ compaction failed: Compaction summary did not match the required template"
421
489
  `)
422
490
  expect((await twin.discord.channel(twin.channelId).getThreads()).find((thread) => thread.id === started.threadId)?.name).toBe('CLI renamed')
423
491
  })
@@ -430,10 +498,10 @@ test('send settings reach native session creation; notifications create no sessi
430
498
  await waitForFooter({ discord: twin.discord, threadId: ids.threadId })
431
499
  expect(await twin.discord.thread(ids.threadId).text()).toMatchInlineSnapshot(`
432
500
  "--- from: assistant (TestBot)
433
- » **CLI:** Send settings
501
+ -# » **CLI:** Send settings
434
502
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
435
503
  ok
436
- -# *subfolder ⋅ main ⋅ Ns ⋅ deterministic-v2*"
504
+ -# *subfolder ⋅ main ⋅ Ns ⋅ deterministic-v2 ⋅ build*"
437
505
  `)
438
506
  const info = await (await server.client()).session.get({ sessionID: ids.sessionId })
439
507
  expect(info.location.directory).toBe(subdir)
@@ -43,10 +43,10 @@ test('session read, list, sync, wait, url and events read real session history',
43
43
  const events = await cli(['session', 'events', first.sessionId])
44
44
  expect(await twin.discord.thread(first.threadId).text()).toMatchInlineSnapshot(`
45
45
  "--- from: assistant (TestBot)
46
- » **CLI:** Reader content marker
46
+ -# » **CLI:** Reader content marker
47
47
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
48
48
  ok
49
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*"
49
+ -# *main ⋅ Ns ⋅ deterministic-v2*"
50
50
  `)
51
51
  expect(list.stdout).toContain(first.sessionId)
52
52
  expect(sync.written + sync.skipped).toBeGreaterThan(0)
@@ -54,6 +54,7 @@ test('session read, list, sync, wait, url and events read real session history',
54
54
  expect(fs.readFileSync(path.join(dataDir, 'transcripts', `${first.sessionId}.md`), 'utf8')).toContain('## user')
55
55
  expect(fs.readFileSync(path.join(dataDir, 'transcripts', `${first.sessionId}.md`), 'utf8')).toContain('Reader content marker')
56
56
  expect(read.stdout).toContain('Reader content marker')
57
+ expect(read.stdout).toMatch(/\ncontext: \d+k? tokens, last run took \S+( \S+)?, last active \S+( \S+)? ago \(\d{4}-/)
57
58
  expect(wait.stdout).toContain('Reader content marker')
58
59
  expect(url.stdout).toContain(first.threadId)
59
60
  expect(events.stdout).toContain('session.execution.succeeded')
@@ -61,19 +62,27 @@ test('session read, list, sync, wait, url and events read real session history',
61
62
  expect(none instanceof Error && none.code).toBe(1)
62
63
  })
63
64
 
64
- test('session wait returns for a real pending question; active discovery errors use exit 64', async () => {
65
- const first = await send(bot, { channelId: twin.channelId, prompt: 'wait-form-marker', permissions: ['question:allow'] })
66
- if (first instanceof Error || !first.sessionId) throw new Error('Expected a session')
67
- await waitForSelectMenu({ discord: twin.discord, channelId: first.threadId, prefix: 'form:' })
68
- const result = await cli(['session', 'wait', first.sessionId, '--timeout', '3s'])
65
+ test('session wait waits for many sessions and says a question paused one; active discovery errors use exit 64', async () => {
66
+ const [first, done] = await Promise.all([
67
+ send(bot, { channelId: twin.channelId, prompt: 'wait-form-marker', permissions: ['question:allow'] }),
68
+ send(bot, { channelId: twin.channelId, prompt: 'Second waited session' }),
69
+ ])
70
+ if (first instanceof Error || !first.sessionId || done instanceof Error || !done.sessionId) throw new Error('Expected sessions')
71
+ await Promise.all([waitForSelectMenu({ discord: twin.discord, channelId: first.threadId, prefix: 'form:' }), waitForFooter({ discord: twin.discord, threadId: done.threadId })])
72
+ const result = await cli(['session', 'wait', first.sessionId, done.threadId, '--timeout', '8s'])
69
73
  expect(await twin.discord.thread(first.threadId).text()).toMatchInlineSnapshot(`
70
74
  "--- from: assistant (TestBot)
71
- » **CLI:** wait-form-marker
75
+ -# » **CLI:** wait-form-marker
72
76
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
73
77
  **Next**
74
78
  Continue?"
75
79
  `)
76
80
  expect(result.stdout).toContain('Continue?')
81
+ expect(result.stdout.split('\n').filter((line) => line.startsWith('==='))).toEqual([
82
+ `=== ${first.sessionId} is paused: it waits for the user to answer a question, so its work is not done ===`,
83
+ `=== ${done.sessionId} was already idle: its last run ended before the wait started ===`,
84
+ ])
85
+ expect(result.stdout.indexOf('Second waited session')).toBeGreaterThan(result.stdout.indexOf('Continue?'))
77
86
  const failure = await exec(process.execPath, ['--import', 'tsx', path.resolve('src/cli.ts'), 'session', 'list', '--active'], {
78
87
  env: { ...process.env, KIMAKI_OPENCODE_SERVICE_FILE: path.join(server.root, 'missing-service.json') },
79
88
  }).catch((error: Error & { code: number }) => error)
@@ -87,12 +87,13 @@ test('session command, named fork, and resume without a channel use the session
87
87
  const resumed = JSON.parse((await cli(['session', 'resume', source.sessionId])).stdout)
88
88
  expect(await twin.discord.thread(source.threadId).text()).toMatchInlineSnapshot(`
89
89
  "--- from: assistant (TestBot)
90
- » **CLI:** command-source
90
+ -# » **CLI:** command-source
91
91
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
92
92
  ok
93
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*
93
+ -# *main ⋅ Ns ⋅ deterministic-v2*
94
+ -# » **CLI:** /review focus --focus security
94
95
  reviewed with passthrough
95
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
96
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
96
97
  `)
97
98
  expect((await twin.discord.thread(forked.threadId).getChannel())?.name).toBe('named fork')
98
99
  expect(resumed.sessionId).toBe(source.sessionId)
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  // Startup must stay light: bot modules (main.ts, onboarding.ts, scheduler.ts)
5
5
  // are imported lazily inside the commands that need them.
6
6
 
7
+ import { createRequire } from 'node:module'
7
8
  import { goke } from 'goke'
8
9
 
9
10
  import { registerBotCommands, registerLoginCommands, registerStartCommand, registerToolCommands } from './cli/bot.ts'
@@ -48,4 +49,5 @@ cli.section('Bot')
48
49
  registerBotCommands(cli)
49
50
 
50
51
  cli.help()
52
+ cli.version(createRequire(import.meta.url)('../package.json').version, '-V, --version')
51
53
  void cli.parse()
@@ -2,7 +2,8 @@
2
2
  // session in OpenCode (switchAgent / switchModel, applied from the next
3
3
  // step); in a project channel they set the default for new sessions
4
4
  // (channel_agents / channel_models). /model can also set the default of
5
- // channels without their own model (global_models). Verbosity is always per channel.
5
+ // channels without their own model (global_models). Verbosity and coordinator
6
+ // mode are always per channel.
6
7
  //
7
8
  // /agent ─▶ agent select ─▶ session or channel
8
9
  // /<agent>-agent ─▶ same, without the select
@@ -10,6 +11,7 @@
10
11
  // └▶ reset buttons: session model ─▶ channel default, channel model ─▶ removed
11
12
  // /model-variant ─▶ variant of the current model ─▶ scope
12
13
  // /verbosity ─▶ text | tools for the channel
14
+ // /toggle-coordinator ─▶ coordinator mode on | off for the channel
13
15
  //
14
16
  // The /model wizard keeps its picks in the createPreferenceRoutes closure
15
17
  // keyed by a short hash (custom IDs max 100 chars), dropped after 10 minutes.
@@ -25,7 +27,7 @@ import {
25
27
  type StringSelectMenuInteraction,
26
28
  } from 'discord.js'
27
29
 
28
- import { globalModel, oc, parseModel, type Bot, type ModelChoice } from '../bot.ts'
30
+ import { globalModel, isCoordinatorChannel, oc, parseModel, type Bot, type ModelChoice } from '../bot.ts'
29
31
  import { verbosityFromV1, verbosityToV1, type Verbosity } from '../db.ts'
30
32
  import { ConfigError, DbError } from '../errors.ts'
31
33
  import { button, buttonRow, paginate, selectedPage, selectRow, type UiMessage } from '../format-parts.ts'
@@ -111,6 +113,19 @@ export async function setVerbosity(bot: Bot, { channelId, verbosity }: { channel
111
113
  bot.store.setState((current) => ({ verbosity: { ...current.verbosity, [channelId]: verbosity } }))
112
114
  }
113
115
 
116
+ // Coordinator mode of a project channel. Turning it off keeps the coordinator
117
+ // binding: turning it on again resumes that session (/reset-coordinator drops it).
118
+ export async function setCoordinatorMode(bot: Bot, { channelId, enabled }: { channelId: string; enabled: boolean }): Promise<DbError | void> {
119
+ const value = enabled ? 1 : 0
120
+ const result = await bot.db
121
+ .insert(schema.channel_coordinator)
122
+ .values({ channel_id: channelId, enabled: value })
123
+ .onConflictDoUpdate({ target: schema.channel_coordinator.channel_id, set: { enabled: value } })
124
+ .catch((cause) => new DbError({ operation: 'write channel_coordinator', cause }))
125
+ if (result instanceof Error) return result
126
+ bot.store.setState((current) => ({ coordinator: { ...current.coordinator, [channelId]: enabled } }))
127
+ }
128
+
114
129
  export async function setChannelAgent(bot: Bot, { channelId, agent }: { channelId: string; agent: string }): Promise<DbError | void> {
115
130
  const result = await bot.db
116
131
  .insert(schema.channel_agents)
@@ -264,7 +279,7 @@ async function setAgent(bot: Bot, { target, agent, model }: { target: Interactio
264
279
  if (before instanceof Error) return before
265
280
  const sessionId = target.sessionId
266
281
  const selected = sessionId
267
- ? await switchAgent(bot, { sessionId, directory: target.directory, agent, model })
282
+ ? await switchAgent(bot, { sessionId, directory: target.directory, agent, model, reselect: true })
268
283
  : await agentSelection(bot, { directory: target.directory, agent })
269
284
  if (selected instanceof Error) return selected
270
285
  if (!sessionId) {
@@ -479,9 +494,24 @@ async function handleVerbositySelect(bot: Bot, interaction: StringSelectMenuInte
479
494
  })
480
495
  }
481
496
 
497
+ // --- /toggle-coordinator
498
+
499
+ async function toggleCoordinatorCommand(bot: Bot, interaction: ChatInputCommandInteraction) {
500
+ const target = await resolveTarget(bot, interaction.channelId)
501
+ if (target instanceof Error) return replyError(interaction, target)
502
+ const enabled = !isCoordinatorChannel(bot, target.channelId)
503
+ const result = await setCoordinatorMode(bot, { channelId: target.channelId, enabled })
504
+ if (result instanceof Error) return replyError(interaction, result)
505
+ const resumes = enabled && bot.store.getState().roots[target.channelId] ? ' It continues the coordinator session this channel had before; /reset-coordinator starts a fresh one.' : ''
506
+ const content = enabled
507
+ ? `Coordinator mode **on** for this channel. Channel messages go to one long coordinator session that starts threads for the work.${resumes}`
508
+ : 'Coordinator mode **off** for this channel. Each channel message starts a new thread.'
509
+ await interaction.reply({ content })
510
+ }
511
+
482
512
  // --- Routes
483
513
 
484
- // /agent, /model, /model-variant, /verbosity and their selects. One per bot:
514
+ // /agent, /model, /model-variant, /verbosity, /toggle-coordinator and their selects. One per bot:
485
515
  // the /model wizard picks stay in this closure.
486
516
  export function createPreferenceRoutes(): InteractionRoutes {
487
517
  const wizards = new Map<string, ModelWizard>()
@@ -592,6 +622,10 @@ export function createPreferenceRoutes(): InteractionRoutes {
592
622
  run: variantMenu,
593
623
  },
594
624
  verbosity: { definition: new SlashCommandBuilder().setName('verbosity').setDescription('Set what the bot shows in this channel'), run: verbosityMenu },
625
+ 'toggle-coordinator': {
626
+ definition: new SlashCommandBuilder().setName('toggle-coordinator').setDescription('Turn coordinator mode on or off: one long session in this channel that starts threads'),
627
+ run: toggleCoordinatorCommand,
628
+ },
595
629
  },
596
630
  buttons: { [MODEL_RESET_PREFIX]: modelReset },
597
631
  selects: { [AGENT_PREFIX]: handleAgentSelect, [MODEL_PREFIX]: modelSelect, [VERBOSITY_PREFIX]: handleVerbositySelect },
@@ -2,7 +2,8 @@
2
2
  // /fork, /fork-subagent. Selects carry only the source session ID; the
3
3
  // source thread comes from the interaction channel.
4
4
  //
5
- // /new-session prompt files? agent? ─▶ startSession (new thread, no start message)
5
+ // /new-session prompt? files? agent? ─▶ startSession (new thread, no start message;
6
+ // no prompt: run: false, the first thread message runs it)
6
7
  // /resume session (autocomplete) ─▶ resume (binding moves to the new thread)
7
8
  // /fork ─▶ select a user message ─▶ fork({ before })
8
9
  // /fork-subagent ─▶ select a child ─▶ fork(child)
@@ -60,12 +61,13 @@ export function parseFileList({ value, directory }: { value: string; directory:
60
61
  async function newSession(bot: Bot, interaction: ChatInputCommandInteraction) {
61
62
  const target = await resolveTarget(bot, interaction.channelId)
62
63
  if (target instanceof Error) return replyError(interaction, target)
63
- const text = interaction.options.getString('prompt', true).trim()
64
- if (!text) return replyError(interaction, new ConfigError({ reason: 'Write a prompt for the new session' }))
64
+ const text = interaction.options.getString('prompt')?.trim() ?? ''
65
65
  const files = parseFileList({ value: interaction.options.getString('files') ?? '', directory: target.directory })
66
66
  if (files instanceof Error) return replyError(interaction, files)
67
+ if (!text && files.length > 0) return replyError(interaction, new ConfigError({ reason: 'Write a prompt to attach files' }))
67
68
  const agent = interaction.options.getString('agent')?.trim()
68
69
  await interaction.deferReply()
70
+ // No prompt: the session waits in the thread for its first message.
69
71
  const started = await startSession(bot, {
70
72
  channelId: target.channelId,
71
73
  directory: target.directory,
@@ -74,6 +76,7 @@ async function newSession(bot: Bot, interaction: ChatInputCommandInteraction) {
74
76
  messageId: interaction.id,
75
77
  startMessageId: null,
76
78
  files,
79
+ run: Boolean(text),
77
80
  })
78
81
  if (started instanceof Error) return replyError(interaction, started)
79
82
  await interaction.editReply({ content: `Created new session in <#${started.threadId}>` })
@@ -82,7 +85,7 @@ async function newSession(bot: Bot, interaction: ChatInputCommandInteraction) {
82
85
  async function resumeCommand(bot: Bot, interaction: ChatInputCommandInteraction) {
83
86
  const target = await resolveTarget(bot, interaction.channelId)
84
87
  if (target instanceof Error) return replyError(interaction, target)
85
- if (target.thread) {
88
+ if (target.conversation?.isThread()) {
86
89
  return replyError(interaction, new ConfigError({ reason: 'This command can only be used in project channels, not threads' }))
87
90
  }
88
91
  await interaction.deferReply()
@@ -173,7 +176,7 @@ async function forkSelected(bot: Bot, { interaction, subagents }: { interaction:
173
176
  const selected = interaction.values[0]
174
177
  const target = await resolveTarget(bot, interaction.channelId)
175
178
  if (target instanceof Error) return replyError(interaction, target)
176
- if (!selected || !target.thread || target.sessionId !== sourceSessionId) {
179
+ if (!selected || !target.conversation || target.sessionId !== sourceSessionId) {
177
180
  await interaction.update({ content: 'This selection expired. Run the command again.', components: [] })
178
181
  return
179
182
  }
@@ -186,7 +189,7 @@ async function forkSelected(bot: Bot, { interaction, subagents }: { interaction:
186
189
  })()
187
190
  if (subagent instanceof Error) return replyError(interaction, subagent)
188
191
  const result = await fork(bot, {
189
- sourceThread: target.thread,
192
+ source: target.conversation,
190
193
  sessionId: subagents ? selected : sourceSessionId,
191
194
  ...(!subagents && { before: selected }),
192
195
  ...(subagent && { subagent }),
@@ -258,7 +261,9 @@ export const sessionRoutes: InteractionRoutes = {
258
261
  definition: new SlashCommandBuilder()
259
262
  .setName('new-session')
260
263
  .setDescription('Start a new OpenCode session')
261
- .addStringOption((option) => option.setName('prompt').setDescription('Prompt content for the session').setRequired(true))
264
+ .addStringOption((option) =>
265
+ option.setName('prompt').setDescription('Prompt content for the session; empty waits for the first message in the thread'),
266
+ )
262
267
  .addStringOption((option) =>
263
268
  option.setName('files').setDescription('Files to attach (comma separated; autocomplete)').setAutocomplete(true).setMaxLength(6000),
264
269
  )