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
@@ -1,10 +1,14 @@
1
1
  import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { execFile } from 'node:child_process'
4
+ import { promisify } from 'node:util'
2
5
  import { ComponentType } from 'discord.js'
3
6
  import { afterAll, beforeAll, expect, test } from 'vitest'
4
7
  import type { BotHandle } from './main.ts'
5
8
  import { TEST_USER_ID, seedProjectChannel, startOpencodeTestServer, startTestBot, startTwin, tempDataDir, waitFor, waitForFooter, warmUp, toolParts, textParts, type OpencodeTestServer, type TestTwin } from './test/harness.ts'
6
9
  import { send, shell } from './prompt.ts'
7
10
 
11
+ const exec = promisify(execFile)
8
12
  const dataDir = tempDataDir()
9
13
  let server: OpencodeTestServer
10
14
  let twin: TestTwin
@@ -19,7 +23,6 @@ beforeAll(async () => {
19
23
  { id: 'click', priority: 200, when: { latestUserTextIncludes: 'User clicked: Proceed' }, then: { parts: textParts('Proceed accepted.') } },
20
24
  { id: 'upload', priority: 100, when: { latestUserTextIncludes: 'upload-marker' }, then: { parts: toolParts({ toolCallId: 'upload-call', toolName: 'shell', input: { command: "kimaki upload-request --prompt 'Send the logo' --max-files 2", timeout: 600000, description: 'Request logo', hasSideEffect: true } }) } },
21
25
  { id: 'upload-done', priority: 110, when: { latestUserTextIncludes: 'upload-marker', rawPromptIncludes: 'upload-call' }, then: { parts: textParts('Upload received.') } },
22
- { id: 'branch', priority: 200, when: { latestUserTextIncludes: 'git-marker', rawPromptIncludes: '[current git branch is main]' }, then: { parts: textParts('Git context received.') } },
23
26
  ] }), startTwin()])
24
27
  await seedProjectChannel({ dataDir, channelId: twin.channelId, guildId: twin.discord.guildId, directory: server.projectDirectory })
25
28
  bot = await startTestBot({ dataDir, twin, server })
@@ -49,6 +52,11 @@ test('agent shim shows buttons after text and a click resumes the real session',
49
52
  const shown = await ui(threadId, 'action_button:')
50
53
  await waitForFooter({ discord: twin.discord, threadId })
51
54
  const sessionId = bot.store.getState().roots[threadId]!
55
+ // The turn ended with buttons: `session wait` must say the session waits for a click.
56
+ const waited = await exec(process.execPath, ['--import', 'tsx', path.resolve('src/cli.ts'), 'session', 'wait', threadId, '--timeout', '8s', '--data-dir', dataDir], {
57
+ env: { ...process.env, KIMAKI_LOCK_PORT: String(bot.lock.port), KIMAKI_OPENCODE_SERVICE_FILE: server.serviceFile },
58
+ })
59
+ expect(waited.stdout.split('\n')[0]).toBe(`=== ${sessionId} is paused: it showed buttons (Proceed, Build) and waits for the user to click one, so its work may not be done ===`)
52
60
  const started = shell(bot, { threadId, sessionId, command: 'printf synthetic' })
53
61
  if (started instanceof Error) throw started
54
62
  await waitFor({ label: 'synthetic shell output', check: async () => (await twin.discord.thread(threadId).text()).includes('```\nsynthetic\n```') })
@@ -56,22 +64,22 @@ test('agent shim shows buttons after text and a click resumes the real session',
56
64
  await waitForFooter({ discord: twin.discord, threadId, count: 2 })
57
65
  expect(await twin.discord.thread(threadId).text()).toMatchInlineSnapshot(`
58
66
  "--- from: assistant (TestBot)
59
- » **CLI:** button-marker
67
+ -# » **CLI:** button-marker
60
68
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
61
69
  Choose the next action.
62
70
 
63
71
  -# ┣ shell _Show choices_
64
- -# ⬦ clicked "Proceed"
72
+ -# clicked "Proceed"
65
73
 
66
74
  Buttons shown.
67
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
75
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
68
76
  -# $ printf synthetic
69
77
  \`\`\`
70
78
  synthetic
71
79
  \`\`\`
72
80
 
73
81
  Proceed accepted.
74
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
82
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
75
83
  `)
76
84
  const messages = await twin.discord.thread(threadId).getMessages()
77
85
  expect(messages.findIndex((message) => message.content.includes('Choose the next action.'))).toBeLessThan(messages.findIndex((message) => message.id === shown.message.id))
@@ -89,14 +97,14 @@ test('native file upload modal returns local paths to the agent shell', async ()
89
97
  await waitForFooter({ discord: twin.discord, threadId })
90
98
  expect(await twin.discord.thread(threadId).text()).toMatchInlineSnapshot(`
91
99
  "--- from: assistant (TestBot)
92
- » **CLI:** upload-marker
100
+ -# » **CLI:** upload-marker
93
101
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
94
102
  -# ┣ shell _Request logo_
95
- -# ⬦ dismissed
103
+ -# dismissed
96
104
  Uploaded 1 file(s)
97
105
 
98
106
  Upload received.
99
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
107
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
100
108
  `)
101
109
  const dirs = fs.readdirSync(`${server.projectDirectory}/uploads`)
102
110
  expect(fs.readFileSync(`${server.projectDirectory}/uploads/${dirs[0]}/0-logo.txt`, 'utf8')).toBe('logo')
@@ -106,17 +114,21 @@ test('git context is added only to marked Discord sessions', async () => {
106
114
  await waitForFooter({ discord: twin.discord, threadId })
107
115
  expect(await twin.discord.thread(threadId).text()).toMatchInlineSnapshot(`
108
116
  "--- from: assistant (TestBot)
109
- » **CLI:** git-marker
117
+ -# » **CLI:** git-marker
110
118
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
111
- Git context received.
112
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
119
+ ok
120
+ -# *main ⋅ Ns ⋅ deterministic-v2*"
113
121
  `)
114
- expect((await twin.discord.thread(threadId).text()).includes('Git context received.')).toBe(true)
122
+ const branchSeen = (requests: typeof server.model.requests) => requests.some((request) => request.latestUserText.includes('git-marker') && request.raw.includes('[current git branch is main]'))
123
+ expect(branchSeen(server.model.requests)).toBe(true)
115
124
  const client = await server.client()
116
125
  // The branch is a synthetic message before the prompt, added again only when it changes.
117
126
  const sessionId = bot.store.getState().roots[threadId]!
118
127
  await client.session.prompt({ sessionID: sessionId, text: 'git-marker again' })
119
128
  await client.session.wait({ sessionID: sessionId })
129
+ // Only a prompt after a reply carries the time since that reply.
130
+ const gaps = server.model.requests.filter((request) => request.latestUserText.includes('git-marker')).map((request) => /\[\d+s since your last message\]/.test(request.latestUserText))
131
+ expect([gaps[0], gaps.at(-1)]).toEqual([false, true])
120
132
  const history = await client.session.context({ sessionID: sessionId })
121
133
  expect(history.flatMap((message) => message.type === 'synthetic' || message.type === 'user' ? [`${message.type}: ${message.text.split('\n')[0]}`] : [])).toMatchInlineSnapshot(`
122
134
  [
@@ -128,7 +140,9 @@ test('git context is added only to marked Discord sessions', async () => {
128
140
  const plugins = await client.plugin.list({ location: { directory: server.projectDirectory } })
129
141
  expect(plugins.data.find((plugin) => plugin.id === 'kimaki')?.state.status).toBe('active')
130
142
  const session = await client.session.create({ location: { directory: server.projectDirectory } })
143
+ const before = server.model.requests.length
131
144
  await client.session.prompt({ sessionID: session.id, text: 'git-marker' })
132
145
  await client.session.wait({ sessionID: session.id })
133
- expect(JSON.stringify(await client.message.list({ sessionID: session.id }))).not.toContain('Git context received.')
146
+ expect(server.model.requests.length).toBeGreaterThan(before)
147
+ expect(branchSeen(server.model.requests.slice(before))).toBe(false)
134
148
  })
package/src/agent-ui.ts CHANGED
@@ -206,7 +206,8 @@ async function clickAgentButton(bot: Bot, interaction: ButtonInteraction) {
206
206
  const item = prompt.buttons[Number(index)]
207
207
  if (!item) return
208
208
  bot.eventLoop.dispatch(interaction.channelId, { type: 'kimaki.agent-ui-dismiss', id, result: `clicked "${item.label}"` })
209
- const result = await send(bot, { threadId: interaction.channelId, prompt: `User clicked: ${item.label}`, user: [interaction.user.id] })
209
+ // The dismissed buttons show the click.
210
+ const result = await send(bot, { threadId: interaction.channelId, prompt: `User clicked: ${item.label}`, user: [interaction.user.id] }, { shownIn: interaction.message.id })
210
211
  if (result instanceof Error) await interaction.followUp({ content: result.message, flags: MessageFlags.Ephemeral })
211
212
  }
212
213
 
@@ -42,7 +42,7 @@ beforeAll(async () => {
42
42
  const chunks: Buffer[] = []
43
43
  request.on('data', (chunk: Buffer) => chunks.push(chunk))
44
44
  request.on('end', () => {
45
- if (request.url === '/v1/logs') {
45
+ if (request.url === '/v1/batch') {
46
46
  const body = JSON.parse(Buffer.concat(chunks).toString('utf8')) as OtlpLogs
47
47
  for (const record of body.resourceLogs.flatMap((logs) => logs.scopeLogs.flatMap((scope) => scope.logRecords))) {
48
48
  received.push(Object.fromEntries(record.attributes.map(({ key, value }) => [key, value.stringValue ?? value.boolValue ?? value.doubleValue ?? Number(value.intValue)])))
package/src/autostart.ts CHANGED
@@ -1,20 +1,22 @@
1
1
  // Start the bot when OpenCode starts. The bot writes <dataDir>/autostart.command
2
- // on every start; the Kimaki OpenCode plugin opens it when no bot answers on
2
+ // on every start; the Kimaki OpenCode plugin opens it when nothing listens on
3
3
  // the lock port:
4
4
  //
5
5
  // kimaki start ──writes──▶ ~/.kimaki/autostart.command (exec <bot node> <pkg>/dist/cli.js ...)
6
6
  // ▲
7
- // OpenCode loads plugin ──no /health on 29988──▶ open -g autostart.command
8
- // (default terminal app, in background)
7
+ // OpenCode loads plugin ──29988 refuses TCP──▶ open -g autostart.command
8
+ // (default terminal app, in background)
9
9
  //
10
10
  // The script exists only after onboarding succeeded, so a fresh install never
11
- // opens a terminal with the onboarding prompts. It always runs dist/cli.js,
11
+ // opens a terminal with the onboarding prompts. `kimaki --no-autostart` deletes it
12
+ // on every start instead (bots under launchd, systemd or pm2). It always runs dist/cli.js,
12
13
  // also when the bot ran from src: keep dist built. --if-not-running makes a bot that
13
14
  // lost the race exit instead of stopping the running one.
14
15
  // Imported by the plugin (OpenCode process): no logger, no bot modules.
15
16
 
16
17
  import { spawn } from 'node:child_process'
17
18
  import fs from 'node:fs'
19
+ import net from 'node:net'
18
20
  import os from 'node:os'
19
21
  import path from 'node:path'
20
22
  import { fileURLToPath } from 'node:url'
@@ -31,6 +33,30 @@ export function distCliEntry(): string {
31
33
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'cli.js')
32
34
  }
33
35
 
36
+ // process.execPath is the resolved binary, for example Homebrew's
37
+ // Cellar/node/26.12.0/bin/node, which `brew upgrade node` deletes. Prefer a
38
+ // PATH entry that links to the same binary (/opt/homebrew/bin/node): it
39
+ // survives upgrades. Scripts and respawns that outlive this process use it.
40
+ export function stableNodePath(): string {
41
+ const real = realpath(process.execPath)
42
+ if (!real) return process.execPath
43
+ const name = path.basename(process.execPath)
44
+ for (const directory of (process.env['PATH'] ?? '').split(path.delimiter)) {
45
+ if (!path.isAbsolute(directory)) continue
46
+ const candidate = path.join(directory, name)
47
+ if (realpath(candidate) === real) return candidate
48
+ }
49
+ return process.execPath
50
+ }
51
+
52
+ function realpath(file: string): string | null {
53
+ try {
54
+ return fs.realpathSync(file)
55
+ } catch {
56
+ return null
57
+ }
58
+ }
59
+
34
60
  // Windows has no autostart: it would need a .cmd script and `start`.
35
61
  export async function writeAutostartScript({ dataDir, args }: { dataDir: string; args: readonly string[] }): Promise<Error | void> {
36
62
  if (process.platform === 'win32') return
@@ -45,13 +71,14 @@ export async function writeAutostartScript({ dataDir, args }: { dataDir: string;
45
71
  #!/bin/sh
46
72
  # Generated by Kimaki on bot start. The Kimaki OpenCode plugin opens it when no bot runs.
47
73
  unset KIMAKI_LOCK_PORT KIMAKI_BOT_TOKEN KIMAKI_OPENCODE_SERVICE_FILE KIMAKI_DATA_DIR KIMAKI_SUPERVISED
48
- node=${quote(process.execPath)}
74
+ node=${quote(stableNodePath())}
49
75
  [ -x "$node" ] || node=node
50
76
  echo 'Kimaki autostart'
51
- echo 'OpenCode started and no Kimaki bot answered on port ${DEFAULT_LOCK_PORT}, so the Kimaki'
77
+ echo 'OpenCode started and no Kimaki bot listened on port ${DEFAULT_LOCK_PORT}, so the Kimaki'
52
78
  echo 'OpenCode plugin opened this window to start the Discord bot.'
53
79
  echo 'The bot lets you talk to your OpenCode sessions from Discord.'
54
80
  echo 'Close this window or press Ctrl+C to stop the bot.'
81
+ echo 'To turn autostart off, start kimaki once with --no-autostart.'
55
82
  echo "Node: $node"
56
83
  echo "Script: $0"
57
84
  echo
@@ -61,16 +88,34 @@ export async function writeAutostartScript({ dataDir, args }: { dataDir: string;
61
88
  .catch((cause: Error) => cause)
62
89
  }
63
90
 
91
+ export function removeAutostartScript({ dataDir }: { dataDir: string }): Promise<Error | void> {
92
+ return fs.promises.rm(path.join(dataDir, SCRIPT_NAME), { force: true }).catch((cause: Error) => cause)
93
+ }
94
+
95
+ // True only when the connect is refused. The kernel accepts into the listen
96
+ // backlog even while the bot's event loop is busy, and a timeout (busy OpenCode
97
+ // start) is not proof of a free port: opening a window then is the bug of #243.
98
+ function lockPortIsFree(port: number): Promise<boolean> {
99
+ return new Promise((resolve) => {
100
+ const socket = net.connect({ host: '127.0.0.1', port })
101
+ const done = (free: boolean) => {
102
+ socket.destroy()
103
+ resolve(free)
104
+ }
105
+ socket.setTimeout(5_000, () => done(false))
106
+ socket.once('connect', () => done(false))
107
+ socket.once('error', (error) => done(Reflect.get(error, 'code') === 'ECONNREFUSED'))
108
+ })
109
+ }
110
+
64
111
  // Only the default install: HOME and the default port, never KIMAKI_* env.
65
112
  // Agent shells export KIMAKI_DATA_DIR, so test OpenCode servers started from
66
113
  // them would otherwise start the real bot. Test servers set HOME to a temp dir.
67
114
  async function startIfStopped(): Promise<void> {
68
115
  const file = path.join(os.homedir(), '.kimaki', SCRIPT_NAME)
69
116
  if (!fs.existsSync(file)) return
70
- // Any answer means the port is taken (V2 or V1 bot).
71
- const running = await fetch(`http://127.0.0.1:${DEFAULT_LOCK_PORT}/health`, { signal: AbortSignal.timeout(2_000) })
72
- .then(() => true, () => false)
73
- if (running) return
117
+ // Any listener means the port is taken (V2 or V1 bot); it could not bind anyway.
118
+ if (!(await lockPortIsFree(DEFAULT_LOCK_PORT))) return
74
119
  // macOS opens .command files in the default terminal app; -g keeps it in the background.
75
120
  const [command, args] = process.platform === 'darwin' ? ['open', ['-g', file]] as const : ['sh', [file]] as const
76
121
  const child = spawn(command, args, { detached: true, stdio: 'ignore' })
package/src/bot.ts CHANGED
@@ -11,6 +11,7 @@ import { ChannelType, type Client, type TextChannel, type ThreadChannel } from '
11
11
 
12
12
  import type { AgentUi } from './agent-ui.ts'
13
13
  import type { Analytics } from './analytics.ts'
14
+ import type { PromptSection } from './builtin-skills.ts'
14
15
  import type { KimakiDb } from './db.ts'
15
16
  import type { EffectsRunner } from './effects.ts'
16
17
  import { ConfigError, DbError, DiscordError, OpenCodeError, OpenCodeUnavailableError } from './errors.ts'
@@ -29,6 +30,11 @@ export type Author = { id: string; username: string }
29
30
 
30
31
  export type PromptFile = { uri: string; name: string }
31
32
 
33
+ // Prompt cache warmup windows (plugin/coordinator.ts): a keep-alive every
34
+ // intervalMs while a coordinator's channel is busy (at most maxMs after its
35
+ // last turn) or a session sleeps.
36
+ export type WarmupTimers = { intervalMs: number; maxMs: number }
37
+
32
38
  export type ModelChoice = { providerID: string; id: string; variant: string | null }
33
39
 
34
40
  // Operations of feature-owned state that non-interaction code needs. Wizard
@@ -69,8 +75,13 @@ export type Bot = {
69
75
  allowAllUsers: boolean
70
76
  // Built-in skill ids (builtin-skills.ts) denied through the permissions of new sessions.
71
77
  disabledSkills: readonly string[]
72
- // `kimaki --message-tool`: root sessions also reply through send_message; Discord hides their plain text.
73
- messageTool: boolean
78
+ // `kimaki --disable-prompt`: static instruction parts left out of new sessions.
79
+ disabledPrompt: readonly PromptSection[]
80
+ // Default for channels without a channel_coordinator row (`kimaki --coordinator`, on unless false):
81
+ // project channel messages go to one long coordinator session in the channel.
82
+ coordinatorDefault: boolean
83
+ // Short warmup windows (tests), written into session markers; null uses the plugin defaults.
84
+ warmupTimers: WarmupTimers | null
74
85
  features: BotFeatures
75
86
  }
76
87
 
@@ -101,6 +112,11 @@ export async function sessionDirectory(bot: Pick<Bot, 'opencode'>, sessionId: st
101
112
  return info.location.directory
102
113
  }
103
114
 
115
+ // A project channel in coordinator mode: its /toggle-coordinator choice, else the default.
116
+ export function isCoordinatorChannel(bot: Bot, channelId: string): boolean {
117
+ return bot.store.getState().coordinator[channelId] ?? bot.coordinatorDefault
118
+ }
119
+
104
120
  export function rootSession(bot: Bot, threadId: string): OpenCodeError | string {
105
121
  return bot.store.getState().roots[threadId] ?? new OpenCodeError({ operation: `find the session of thread ${threadId}` })
106
122
  }
@@ -112,13 +128,22 @@ export function threadOfSession(bot: Bot, sessionId: string): string | null {
112
128
  return threadId && roots[threadId] === sessionId ? threadId : null
113
129
  }
114
130
 
115
- export async function fetchThread(bot: Bot, threadId: string): Promise<DiscordError | ConfigError | ThreadChannel> {
131
+ // Where a root session talks to the user: its thread, or a coordinator project
132
+ // channel (the channel itself is the conversation, roots[channelId]).
133
+ export type Conversation = ThreadChannel | TextChannel
134
+
135
+ // The project channel of a conversation.
136
+ export function projectChannelOf(conversation: Conversation): string | null {
137
+ return conversation.isThread() ? conversation.parentId : conversation.id
138
+ }
139
+
140
+ export async function fetchConversation(bot: Bot, id: string): Promise<DiscordError | ConfigError | Conversation> {
116
141
  const channel = await bot.discord.channels
117
- .fetch(threadId)
118
- .catch((cause) => new DiscordError({ operation: `fetch thread ${threadId}`, cause }))
142
+ .fetch(id)
143
+ .catch((cause) => new DiscordError({ operation: `fetch conversation ${id}`, cause }))
119
144
  if (channel instanceof Error) return channel
120
- if (!channel?.isThread()) return new ConfigError({ reason: 'Target is not a thread' })
121
- return channel
145
+ if (channel?.isThread() || channel?.type === ChannelType.GuildText) return channel
146
+ return new ConfigError({ reason: 'Target is not a thread or a text channel' })
122
147
  }
123
148
 
124
149
  export async function textChannel(bot: Bot, channelId: string): Promise<DiscordError | TextChannel> {
@@ -136,7 +161,6 @@ export type SessionMarker = {
136
161
  fields: { readonly [key: string]: JsonValue }
137
162
  dataDir: string | null
138
163
  lockPort: number | null
139
- messageTool: boolean
140
164
  // Explicit `kimaki send --parent-session`, so /resume keeps the instruction line.
141
165
  parentSessionId: string | null
142
166
  // The scheduled run stored at session.create, so /resume keeps the same instruction section.
@@ -150,7 +174,7 @@ function jsonObject(value: JsonValue | undefined): { readonly [key: string]: Jso
150
174
  export function readMarker(metadata: SessionMetadata | undefined): SessionMarker | null {
151
175
  const fields = jsonObject(metadata?.['kimaki'])
152
176
  if (!fields) return null
153
- const { dataDir, lockPort, messageTool, parentSessionId } = fields
177
+ const { dataDir, lockPort, parentSessionId } = fields
154
178
  const task = jsonObject(fields['task'])
155
179
  const taskId = task?.['id']
156
180
  const cronExpr = task?.['cronExpr']
@@ -159,7 +183,6 @@ export function readMarker(metadata: SessionMetadata | undefined): SessionMarker
159
183
  fields,
160
184
  dataDir: typeof dataDir === 'string' ? dataDir : null,
161
185
  lockPort: typeof lockPort === 'number' ? lockPort : null,
162
- messageTool: messageTool === true,
163
186
  parentSessionId: typeof parentSessionId === 'string' ? parentSessionId : null,
164
187
  task: typeof taskId === 'number'
165
188
  ? { id: taskId, cronExpr: typeof cronExpr === 'string' ? cronExpr : null, timezone: typeof timezone === 'string' ? timezone : null }
@@ -167,15 +190,9 @@ export function readMarker(metadata: SessionMetadata | undefined): SessionMarker
167
190
  }
168
191
  }
169
192
 
170
- // The marker fields that follow this bot run: where the agent's `kimaki` calls
171
- // go, and whether the plugin gives the session send_message.
172
- export function cliContext(bot: Pick<Bot, 'dataDir' | 'lockPort' | 'messageTool'>) {
173
- return { dataDir: bot.dataDir, lockPort: bot.lockPort, messageTool: bot.messageTool }
174
- }
175
-
176
- // False when the session marker was written by another bot run or with another --message-tool.
177
- export function markerIsCurrent(bot: Pick<Bot, 'dataDir' | 'lockPort' | 'messageTool'>, marker: SessionMarker): boolean {
178
- return marker.dataDir === bot.dataDir && marker.lockPort === bot.lockPort && marker.messageTool === bot.messageTool
193
+ // The marker fields that point the agent's `kimaki` calls at this bot.
194
+ export function cliContext(bot: Bot) {
195
+ return { dataDir: bot.dataDir, lockPort: bot.lockPort }
179
196
  }
180
197
 
181
198
  // Default model of new sessions in channels without their own (/model "All channels").
@@ -87,14 +87,14 @@ test('. btw forks into a side thread while the source run continues', async () =
87
87
  --- from: assistant (TestBot)
88
88
  Session forked! Continue in <#THREAD>
89
89
  slow-done
90
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
90
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
91
91
  `)
92
92
  expect(hideIds(await discord.thread(fork.id).text())).toMatchInlineSnapshot(`
93
93
  "--- from: assistant (TestBot)
94
94
  Reusing context from <#THREAD> to answer prompt...
95
95
  What is 2+2 side-marker
96
96
  side answer
97
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
97
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
98
98
  `)
99
99
  const rows = await bot.db.query.thread_sessions.findMany({ where: { thread_id: fork.id } })
100
100
  expect(rows.length).toBe(1)
@@ -125,6 +125,6 @@ test('/btw forks from a thread with an idle session', async () => {
125
125
  Reusing context from <#THREAD> to answer prompt...
126
126
  Side slash-marker
127
127
  slash answer
128
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
128
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
129
129
  `)
130
130
  })
@@ -1,6 +1,7 @@
1
1
  import { expect, test } from 'vitest'
2
2
 
3
- import { BUILTIN_SKILL_IDS, disabledSkillRules, parseDisabledSkills, readBuiltinSkills } from './builtin-skills.ts'
3
+ import { BUILTIN_SKILL_IDS, disabledSkillRules, parseDisabledPromptSections, parseDisabledSkills, PROMPT_SECTIONS, readBuiltinSkills } from './builtin-skills.ts'
4
+ import { baseInstructions } from './system-prompt.ts'
4
5
 
5
6
  test('every built-in skill file parses', async () => {
6
7
  const skills = await readBuiltinSkills()
@@ -27,3 +28,74 @@ test('--disable-skills accepts all, a list, and rejects unknown ids', () => {
27
28
  ]
28
29
  `)
29
30
  })
31
+
32
+ test('--disable-prompt removes exactly the headings of each section', () => {
33
+ expect(parseDisabledPromptSections(['critique-diffs,discord-callouts', 'search-result-urls'])).toEqual(['critique-diffs', 'discord-callouts', 'search-result-urls'])
34
+ expect(parseDisabledPromptSections(['critique'])).toBeInstanceOf(Error)
35
+ const input = { sessionId: 'ses_1', threadId: '2', channelId: '3', guildId: '4', userId: '5', dataDir: '/data', channelTopic: null, agents: [], parentSessionId: null }
36
+ const headings = (text: string) => text.split('\n').filter((line) => line.startsWith('## '))
37
+ const full = headings(baseInstructions(input))
38
+ const removed = Object.fromEntries(PROMPT_SECTIONS.map((section) => {
39
+ const kept = new Set(headings(baseInstructions({ ...input, disabled: [section] })))
40
+ return [section, full.filter((heading) => !kept.has(heading))]
41
+ }))
42
+ expect(removed).toMatchInlineSnapshot(`
43
+ {
44
+ "action-buttons": [
45
+ "## action buttons",
46
+ "## ending conversations with options",
47
+ ],
48
+ "critique-diffs": [
49
+ "## showing diffs",
50
+ ],
51
+ "cross-project-commands": [
52
+ "## cross-project commands",
53
+ ],
54
+ "diagrams": [
55
+ "## diagrams",
56
+ ],
57
+ "discord-callouts": [
58
+ "## Callouts in Kimaki Discord",
59
+ ],
60
+ "discord-permissions": [
61
+ "## permissions",
62
+ ],
63
+ "file-upload-requests": [
64
+ "## requesting files from the user",
65
+ ],
66
+ "kimaki-debugging": [
67
+ "## debugging kimaki issues",
68
+ ],
69
+ "markdown-formatting": [
70
+ "## markdown formatting",
71
+ ],
72
+ "search-result-urls": [
73
+ "## URLs in search results",
74
+ ],
75
+ "session-abort": [
76
+ "## aborting a session",
77
+ ],
78
+ "session-sleep": [
79
+ "## sleeping the session",
80
+ ],
81
+ "waiting-sessions": [
82
+ "## waiting for a session to finish",
83
+ ],
84
+ }
85
+ `)
86
+ expect(headings(baseInstructions({ ...input, disabled: [...PROMPT_SECTIONS] }))).toMatchInlineSnapshot(`
87
+ [
88
+ "## Discord output",
89
+ "## shell tool",
90
+ "## commit messages",
91
+ "## uploading files to discord",
92
+ "## generating audio from text",
93
+ "## archiving the current thread",
94
+ "## updating the session title",
95
+ "## discord user mentions",
96
+ "## starting new sessions from CLI",
97
+ "## scheduled sends and routines",
98
+ "## reading other sessions",
99
+ ]
100
+ `)
101
+ })
@@ -53,6 +53,39 @@ export function parseDisabledSkills(value: string | undefined): ConfigError | Bu
53
53
  return items as BuiltinSkillId[]
54
54
  }
55
55
 
56
+ // Static parts of the session instructions (system-prompt.ts) that
57
+ // `kimaki --disable-prompt <section>` removes. Here, not in system-prompt.ts,
58
+ // so the CLI help can list them without loading zod.
59
+ export const PROMPT_SECTIONS = [
60
+ 'discord-permissions',
61
+ 'kimaki-debugging',
62
+ 'file-upload-requests',
63
+ 'action-buttons',
64
+ 'session-sleep',
65
+ 'session-abort',
66
+ 'cross-project-commands',
67
+ 'waiting-sessions',
68
+ 'critique-diffs',
69
+ 'markdown-formatting',
70
+ 'discord-callouts',
71
+ 'search-result-urls',
72
+ 'diagrams',
73
+ ] as const
74
+
75
+ export type PromptSection = (typeof PROMPT_SECTIONS)[number]
76
+
77
+ function isPromptSection(item: string): item is PromptSection {
78
+ return PROMPT_SECTIONS.some((section) => section === item)
79
+ }
80
+
81
+ // Repeatable flag; each value can also be a comma separated list.
82
+ export function parseDisabledPromptSections(values: readonly string[] | undefined): ConfigError | PromptSection[] {
83
+ const items = (values ?? []).flatMap((value) => value.split(',')).map((item) => item.trim()).filter(Boolean)
84
+ const unknown = items.filter((item) => !isPromptSection(item))
85
+ if (unknown.length > 0) return new ConfigError({ reason: `Unknown prompt section ${unknown.join(', ')}. Choose from: ${PROMPT_SECTIONS.join(', ')}` })
86
+ return items.filter(isPromptSection)
87
+ }
88
+
56
89
  // OpenCode hides a skill from the model and the skill tool when its permission is denied.
57
90
  export function disabledSkillRules(disabled: readonly string[]) {
58
91
  return disabled.map((id) => ({ action: 'skill', resource: id, effect: 'deny' }) as const)
@@ -68,10 +68,10 @@ test('agents loaded after startup get their slash commands', async () => {
68
68
  await waitForFooter({ discord, threadId: thread.id })
69
69
  expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
70
70
  "--- from: assistant (TestBot)
71
- » **tommy:** Go late-marker
71
+ -# » **tommy:** Go late-marker
72
72
  -# *using deterministic-provider/deterministic-v2 ⋅ late*
73
73
  late reply
74
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2 ⋅ late*"
74
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2 ⋅ late*"
75
75
  `)
76
76
  })
77
77