discoclaw 0.1.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 (393) hide show
  1. package/.context/README.md +42 -0
  2. package/.context/architecture.md +58 -0
  3. package/.context/bot-setup.md +24 -0
  4. package/.context/dev.md +230 -0
  5. package/.context/discord.md +144 -0
  6. package/.context/memory.md +257 -0
  7. package/.context/ops.md +59 -0
  8. package/.context/pa-safety.md +47 -0
  9. package/.context/pa.md +118 -0
  10. package/.context/project.md +43 -0
  11. package/.context/runtime.md +253 -0
  12. package/.context/tasks.md +71 -0
  13. package/.context/tools.md +75 -0
  14. package/.env.example +88 -0
  15. package/.env.example.full +378 -0
  16. package/LICENSE +21 -0
  17. package/README.md +220 -0
  18. package/dist/beads/auto-tag.js +2 -0
  19. package/dist/beads/auto-tag.test.js +62 -0
  20. package/dist/beads/bd-cli.js +9 -0
  21. package/dist/beads/bd-cli.test.js +495 -0
  22. package/dist/beads/bead-hooks-cli.js +149 -0
  23. package/dist/beads/bead-sync-cli.js +5 -0
  24. package/dist/beads/bead-sync-cli.test.js +72 -0
  25. package/dist/beads/bead-sync-coordinator.js +4 -0
  26. package/dist/beads/bead-sync-coordinator.test.js +239 -0
  27. package/dist/beads/bead-sync-watcher.js +2 -0
  28. package/dist/beads/bead-sync-watcher.test.js +96 -0
  29. package/dist/beads/bead-sync.js +7 -0
  30. package/dist/beads/bead-sync.test.js +876 -0
  31. package/dist/beads/bead-thread-cache.js +8 -0
  32. package/dist/beads/bead-thread-cache.test.js +91 -0
  33. package/dist/beads/discord-sync.js +18 -0
  34. package/dist/beads/discord-sync.test.js +782 -0
  35. package/dist/beads/find-bead-by-thread.test.js +36 -0
  36. package/dist/beads/forum-guard.js +2 -0
  37. package/dist/beads/forum-guard.test.js +204 -0
  38. package/dist/beads/initialize.js +3 -0
  39. package/dist/beads/initialize.test.js +304 -0
  40. package/dist/beads/types.js +10 -0
  41. package/dist/cli/daemon-installer.js +225 -0
  42. package/dist/cli/daemon-installer.test.js +289 -0
  43. package/dist/cli/index.js +42 -0
  44. package/dist/cli/init-wizard.js +374 -0
  45. package/dist/cli/init-wizard.test.js +191 -0
  46. package/dist/config.js +385 -0
  47. package/dist/config.test.js +589 -0
  48. package/dist/cron/auto-tag.js +100 -0
  49. package/dist/cron/auto-tag.test.js +91 -0
  50. package/dist/cron/cadence.js +74 -0
  51. package/dist/cron/cadence.test.js +53 -0
  52. package/dist/cron/cron-sync-coordinator.js +66 -0
  53. package/dist/cron/cron-sync-coordinator.test.js +118 -0
  54. package/dist/cron/cron-sync.js +165 -0
  55. package/dist/cron/cron-sync.test.js +228 -0
  56. package/dist/cron/cron-tag-map-watcher.js +128 -0
  57. package/dist/cron/cron-tag-map-watcher.test.js +155 -0
  58. package/dist/cron/default-timezone.js +23 -0
  59. package/dist/cron/default-timezone.test.js +30 -0
  60. package/dist/cron/discord-sync.js +205 -0
  61. package/dist/cron/discord-sync.test.js +353 -0
  62. package/dist/cron/executor.js +303 -0
  63. package/dist/cron/executor.test.js +614 -0
  64. package/dist/cron/forum-sync.js +347 -0
  65. package/dist/cron/forum-sync.test.js +539 -0
  66. package/dist/cron/job-lock.js +164 -0
  67. package/dist/cron/job-lock.test.js +178 -0
  68. package/dist/cron/parser.js +68 -0
  69. package/dist/cron/parser.test.js +115 -0
  70. package/dist/cron/run-control.js +24 -0
  71. package/dist/cron/run-control.test.js +27 -0
  72. package/dist/cron/run-stats.js +265 -0
  73. package/dist/cron/run-stats.test.js +160 -0
  74. package/dist/cron/scheduler.js +97 -0
  75. package/dist/cron/scheduler.test.js +112 -0
  76. package/dist/cron/tag-map.js +47 -0
  77. package/dist/cron/tag-map.test.js +64 -0
  78. package/dist/cron/types.js +1 -0
  79. package/dist/discoclaw-plan-format.test.js +137 -0
  80. package/dist/discoclaw-recipe-format.test.js +137 -0
  81. package/dist/discord/abort-registry.js +70 -0
  82. package/dist/discord/action-categories.js +36 -0
  83. package/dist/discord/action-types.js +1 -0
  84. package/dist/discord/action-utils.js +58 -0
  85. package/dist/discord/action-utils.test.js +58 -0
  86. package/dist/discord/actions-beads.js +1 -0
  87. package/dist/discord/actions-beads.test.js +372 -0
  88. package/dist/discord/actions-bot-profile.js +107 -0
  89. package/dist/discord/actions-bot-profile.test.js +138 -0
  90. package/dist/discord/actions-channels.js +427 -0
  91. package/dist/discord/actions-channels.test.js +697 -0
  92. package/dist/discord/actions-config.js +173 -0
  93. package/dist/discord/actions-config.test.js +322 -0
  94. package/dist/discord/actions-crons.js +586 -0
  95. package/dist/discord/actions-crons.test.js +499 -0
  96. package/dist/discord/actions-defer.js +60 -0
  97. package/dist/discord/actions-defer.test.js +134 -0
  98. package/dist/discord/actions-forge.js +134 -0
  99. package/dist/discord/actions-forge.test.js +206 -0
  100. package/dist/discord/actions-guild.js +301 -0
  101. package/dist/discord/actions-guild.test.js +386 -0
  102. package/dist/discord/actions-memory.js +106 -0
  103. package/dist/discord/actions-memory.test.js +248 -0
  104. package/dist/discord/actions-messaging.js +401 -0
  105. package/dist/discord/actions-messaging.test.js +738 -0
  106. package/dist/discord/actions-moderation.js +65 -0
  107. package/dist/discord/actions-moderation.test.js +88 -0
  108. package/dist/discord/actions-plan.js +445 -0
  109. package/dist/discord/actions-plan.test.js +610 -0
  110. package/dist/discord/actions-poll.js +38 -0
  111. package/dist/discord/actions-poll.test.js +93 -0
  112. package/dist/discord/actions-tasks.js +3 -0
  113. package/dist/discord/actions-tasks.test.js +418 -0
  114. package/dist/discord/actions.js +600 -0
  115. package/dist/discord/actions.test.js +522 -0
  116. package/dist/discord/allowed-mentions.js +3 -0
  117. package/dist/discord/allowed-mentions.test.js +17 -0
  118. package/dist/discord/allowlist.js +29 -0
  119. package/dist/discord/allowlist.test.js +24 -0
  120. package/dist/discord/audit-handler.js +191 -0
  121. package/dist/discord/audit-handler.test.js +361 -0
  122. package/dist/discord/bot.js +141 -0
  123. package/dist/discord/channel-context.js +181 -0
  124. package/dist/discord/defer-scheduler.js +45 -0
  125. package/dist/discord/destructive-confirmation.js +128 -0
  126. package/dist/discord/destructive-confirmation.test.js +49 -0
  127. package/dist/discord/discord-plan-auto-implement.test.js +18 -0
  128. package/dist/discord/durable-memory.js +145 -0
  129. package/dist/discord/durable-memory.test.js +281 -0
  130. package/dist/discord/durable-write-queue.js +4 -0
  131. package/dist/discord/file-download.js +308 -0
  132. package/dist/discord/file-download.test.js +303 -0
  133. package/dist/discord/forge-audit-verdict.js +140 -0
  134. package/dist/discord/forge-auto-implement.js +80 -0
  135. package/dist/discord/forge-auto-implement.test.js +110 -0
  136. package/dist/discord/forge-commands.js +698 -0
  137. package/dist/discord/forge-commands.test.js +1606 -0
  138. package/dist/discord/forge-plan-registry.js +68 -0
  139. package/dist/discord/forge-plan-registry.test.js +127 -0
  140. package/dist/discord/forum-count-sync.js +130 -0
  141. package/dist/discord/forum-count-sync.test.js +200 -0
  142. package/dist/discord/health-command.js +98 -0
  143. package/dist/discord/health-command.test.js +195 -0
  144. package/dist/discord/help-command.js +22 -0
  145. package/dist/discord/help-command.test.js +49 -0
  146. package/dist/discord/image-download.js +201 -0
  147. package/dist/discord/image-download.test.js +499 -0
  148. package/dist/discord/inflight-replies.js +228 -0
  149. package/dist/discord/inflight-replies.test.js +295 -0
  150. package/dist/discord/json-extract.js +110 -0
  151. package/dist/discord/keyed-queue.js +22 -0
  152. package/dist/discord/memory-commands.js +85 -0
  153. package/dist/discord/memory-commands.test.js +159 -0
  154. package/dist/discord/memory-timing.integration.test.js +159 -0
  155. package/dist/discord/message-coordinator.js +2347 -0
  156. package/dist/discord/message-coordinator.onboarding.test.js +183 -0
  157. package/dist/discord/message-coordinator.plan-run.test.js +264 -0
  158. package/dist/discord/message-history.js +53 -0
  159. package/dist/discord/message-history.test.js +95 -0
  160. package/dist/discord/models-command.js +59 -0
  161. package/dist/discord/models-command.test.js +150 -0
  162. package/dist/discord/nickname.test.js +76 -0
  163. package/dist/discord/onboarding-completion.js +55 -0
  164. package/dist/discord/onboarding-completion.test.js +176 -0
  165. package/dist/discord/output-common.js +178 -0
  166. package/dist/discord/output-common.test.js +198 -0
  167. package/dist/discord/output-utils.js +156 -0
  168. package/dist/discord/parse-identity-name.test.js +129 -0
  169. package/dist/discord/plan-commands.js +612 -0
  170. package/dist/discord/plan-commands.test.js +1622 -0
  171. package/dist/discord/plan-manager.js +1491 -0
  172. package/dist/discord/plan-manager.test.js +2380 -0
  173. package/dist/discord/plan-parser.js +110 -0
  174. package/dist/discord/plan-parser.test.js +63 -0
  175. package/dist/discord/plan-run-phase-start.js +20 -0
  176. package/dist/discord/plan-run-phase-start.test.js +29 -0
  177. package/dist/discord/platform-message.js +45 -0
  178. package/dist/discord/platform-message.test.js +110 -0
  179. package/dist/discord/prompt-common.js +240 -0
  180. package/dist/discord/prompt-common.test.js +423 -0
  181. package/dist/discord/reaction-handler.js +691 -0
  182. package/dist/discord/reaction-handler.test.js +1574 -0
  183. package/dist/discord/reaction-prompts.js +118 -0
  184. package/dist/discord/reaction-prompts.test.js +253 -0
  185. package/dist/discord/reply-reference.js +66 -0
  186. package/dist/discord/reply-reference.test.js +125 -0
  187. package/dist/discord/restart-command.js +143 -0
  188. package/dist/discord/restart-command.test.js +196 -0
  189. package/dist/discord/runtime-utils.js +43 -0
  190. package/dist/discord/runtime-utils.test.js +112 -0
  191. package/dist/discord/session-key.js +7 -0
  192. package/dist/discord/session-key.test.js +13 -0
  193. package/dist/discord/shortterm-memory.js +166 -0
  194. package/dist/discord/shortterm-memory.test.js +345 -0
  195. package/dist/discord/shutdown-context.js +122 -0
  196. package/dist/discord/shutdown-context.test.js +279 -0
  197. package/dist/discord/startup-profile.test.js +214 -0
  198. package/dist/discord/status-channel.js +190 -0
  199. package/dist/discord/status-channel.test.js +282 -0
  200. package/dist/discord/status-command.js +206 -0
  201. package/dist/discord/status-command.test.js +341 -0
  202. package/dist/discord/streaming-progress.js +107 -0
  203. package/dist/discord/streaming-progress.test.js +93 -0
  204. package/dist/discord/summarizer.js +89 -0
  205. package/dist/discord/summarizer.test.js +245 -0
  206. package/dist/discord/system-bootstrap.js +396 -0
  207. package/dist/discord/system-bootstrap.test.js +724 -0
  208. package/dist/discord/thread-context.js +169 -0
  209. package/dist/discord/thread-context.test.js +386 -0
  210. package/dist/discord/tool-aware-queue.js +116 -0
  211. package/dist/discord/tool-aware-queue.test.js +180 -0
  212. package/dist/discord/update-command.js +127 -0
  213. package/dist/discord/update-command.test.js +275 -0
  214. package/dist/discord/user-errors.js +40 -0
  215. package/dist/discord/user-errors.test.js +31 -0
  216. package/dist/discord/user-turn-to-durable.js +111 -0
  217. package/dist/discord/user-turn-to-durable.test.js +273 -0
  218. package/dist/discord-followup.test.js +677 -0
  219. package/dist/discord.channel-context.test.js +95 -0
  220. package/dist/discord.fail-closed.test.js +199 -0
  221. package/dist/discord.health-command.integration.test.js +140 -0
  222. package/dist/discord.js +190 -0
  223. package/dist/discord.prompt-context.test.js +1431 -0
  224. package/dist/discord.render.test.js +621 -0
  225. package/dist/discord.status-wiring.test.js +187 -0
  226. package/dist/engine/claudeCli.js +137 -0
  227. package/dist/engine/types.js +1 -0
  228. package/dist/group-queue.js +25 -0
  229. package/dist/health/credential-check.js +175 -0
  230. package/dist/health/credential-check.test.js +401 -0
  231. package/dist/health/startup-healing.js +139 -0
  232. package/dist/health/startup-healing.test.js +298 -0
  233. package/dist/identity.js +36 -0
  234. package/dist/index.js +1378 -0
  235. package/dist/logging/logger-like.js +1 -0
  236. package/dist/observability/memory-sampler.js +51 -0
  237. package/dist/observability/memory-sampler.test.js +93 -0
  238. package/dist/observability/metrics.js +88 -0
  239. package/dist/observability/metrics.test.js +42 -0
  240. package/dist/onboarding/onboarding-flow.js +246 -0
  241. package/dist/onboarding/onboarding-flow.test.js +238 -0
  242. package/dist/onboarding/onboarding-writer.js +102 -0
  243. package/dist/onboarding/onboarding-writer.test.js +143 -0
  244. package/dist/pidlock.js +187 -0
  245. package/dist/pidlock.test.js +128 -0
  246. package/dist/pipeline/engine.js +206 -0
  247. package/dist/pipeline/engine.test.js +771 -0
  248. package/dist/root-policy.js +21 -0
  249. package/dist/root-policy.test.js +55 -0
  250. package/dist/runtime/claude-code-cli.js +35 -0
  251. package/dist/runtime/claude-code-cli.test.js +1199 -0
  252. package/dist/runtime/cli-adapter.js +584 -0
  253. package/dist/runtime/cli-output-parsers.js +108 -0
  254. package/dist/runtime/cli-shared.js +96 -0
  255. package/dist/runtime/cli-shared.test.js +104 -0
  256. package/dist/runtime/cli-strategy.js +6 -0
  257. package/dist/runtime/codex-cli.js +16 -0
  258. package/dist/runtime/codex-cli.test.js +862 -0
  259. package/dist/runtime/concurrency-limit.js +80 -0
  260. package/dist/runtime/concurrency-limit.test.js +137 -0
  261. package/dist/runtime/gemini-cli.js +16 -0
  262. package/dist/runtime/gemini-cli.test.js +413 -0
  263. package/dist/runtime/long-running-process.js +415 -0
  264. package/dist/runtime/long-running-process.test.js +318 -0
  265. package/dist/runtime/model-smoke-helpers.js +160 -0
  266. package/dist/runtime/model-smoke.test.js +194 -0
  267. package/dist/runtime/model-tiers.js +33 -0
  268. package/dist/runtime/model-tiers.test.js +65 -0
  269. package/dist/runtime/openai-auth.js +151 -0
  270. package/dist/runtime/openai-auth.test.js +361 -0
  271. package/dist/runtime/openai-compat.js +178 -0
  272. package/dist/runtime/openai-compat.test.js +449 -0
  273. package/dist/runtime/process-pool.js +93 -0
  274. package/dist/runtime/process-pool.test.js +148 -0
  275. package/dist/runtime/registry.js +15 -0
  276. package/dist/runtime/registry.test.js +47 -0
  277. package/dist/runtime/session-scanner.js +186 -0
  278. package/dist/runtime/session-scanner.test.js +257 -0
  279. package/dist/runtime/strategies/claude-strategy.js +193 -0
  280. package/dist/runtime/strategies/codex-strategy.js +161 -0
  281. package/dist/runtime/strategies/gemini-strategy.js +64 -0
  282. package/dist/runtime/strategies/template-strategy.js +85 -0
  283. package/dist/runtime/tool-capabilities.js +27 -0
  284. package/dist/runtime/tool-capabilities.test.js +24 -0
  285. package/dist/runtime/tool-labels.js +48 -0
  286. package/dist/runtime/types.js +2 -0
  287. package/dist/sessionManager.js +47 -0
  288. package/dist/sessions.js +18 -0
  289. package/dist/tasks/architecture-contract.js +33 -0
  290. package/dist/tasks/architecture-contract.test.js +90 -0
  291. package/dist/tasks/auto-tag.js +50 -0
  292. package/dist/tasks/auto-tag.test.js +64 -0
  293. package/dist/tasks/bd-cli.js +164 -0
  294. package/dist/tasks/bd-cli.test.js +359 -0
  295. package/dist/tasks/bead-sync.js +1 -0
  296. package/dist/tasks/context-summary.js +27 -0
  297. package/dist/tasks/discord-sync.js +3 -0
  298. package/dist/tasks/discord-sync.test.js +685 -0
  299. package/dist/tasks/discord-types.js +4 -0
  300. package/dist/tasks/find-task-by-thread.test.js +36 -0
  301. package/dist/tasks/forum-guard.js +81 -0
  302. package/dist/tasks/forum-guard.test.js +192 -0
  303. package/dist/tasks/initialize.js +77 -0
  304. package/dist/tasks/initialize.test.js +263 -0
  305. package/dist/tasks/logger-types.js +1 -0
  306. package/dist/tasks/metrics-types.js +3 -0
  307. package/dist/tasks/migrate.js +33 -0
  308. package/dist/tasks/migrate.test.js +156 -0
  309. package/dist/tasks/path-defaults.js +67 -0
  310. package/dist/tasks/path-defaults.test.js +73 -0
  311. package/dist/tasks/runtime-types.js +1 -0
  312. package/dist/tasks/service.js +33 -0
  313. package/dist/tasks/service.test.js +51 -0
  314. package/dist/tasks/store.js +238 -0
  315. package/dist/tasks/store.test.js +417 -0
  316. package/dist/tasks/sync-context.js +1 -0
  317. package/dist/tasks/sync-contract.js +24 -0
  318. package/dist/tasks/sync-contract.test.js +25 -0
  319. package/dist/tasks/sync-coordinator-metrics.js +41 -0
  320. package/dist/tasks/sync-coordinator-retries.js +71 -0
  321. package/dist/tasks/sync-coordinator.js +96 -0
  322. package/dist/tasks/sync-coordinator.test.js +501 -0
  323. package/dist/tasks/sync-types.js +1 -0
  324. package/dist/tasks/sync-watcher.js +27 -0
  325. package/dist/tasks/sync-watcher.test.js +92 -0
  326. package/dist/tasks/tag-map.js +36 -0
  327. package/dist/tasks/tag-map.test.js +54 -0
  328. package/dist/tasks/task-action-contract.js +16 -0
  329. package/dist/tasks/task-action-contract.test.js +16 -0
  330. package/dist/tasks/task-action-executor.js +18 -0
  331. package/dist/tasks/task-action-executor.test.js +420 -0
  332. package/dist/tasks/task-action-mutation-helpers.js +17 -0
  333. package/dist/tasks/task-action-mutations.js +151 -0
  334. package/dist/tasks/task-action-prompt.js +62 -0
  335. package/dist/tasks/task-action-read-ops.js +73 -0
  336. package/dist/tasks/task-action-runner-types.js +1 -0
  337. package/dist/tasks/task-action-thread-sync.js +82 -0
  338. package/dist/tasks/task-actions.js +3 -0
  339. package/dist/tasks/task-cli.js +227 -0
  340. package/dist/tasks/task-context.js +1 -0
  341. package/dist/tasks/task-lifecycle.js +46 -0
  342. package/dist/tasks/task-lifecycle.test.js +35 -0
  343. package/dist/tasks/task-sync-apply-plan.js +95 -0
  344. package/dist/tasks/task-sync-apply-types.js +12 -0
  345. package/dist/tasks/task-sync-apply.js +319 -0
  346. package/dist/tasks/task-sync-cli.js +89 -0
  347. package/dist/tasks/task-sync-cli.test.js +70 -0
  348. package/dist/tasks/task-sync-engine.js +88 -0
  349. package/dist/tasks/task-sync-engine.test.js +934 -0
  350. package/dist/tasks/task-sync-phase-apply.js +171 -0
  351. package/dist/tasks/task-sync-pipeline.js +2 -0
  352. package/dist/tasks/task-sync-pipeline.test.js +265 -0
  353. package/dist/tasks/task-sync-reconcile-plan.js +182 -0
  354. package/dist/tasks/task-sync-reconcile.js +144 -0
  355. package/dist/tasks/task-sync.js +56 -0
  356. package/dist/tasks/task-sync.test.js +86 -0
  357. package/dist/tasks/thread-cache.js +42 -0
  358. package/dist/tasks/thread-cache.test.js +89 -0
  359. package/dist/tasks/thread-contracts.test.js +711 -0
  360. package/dist/tasks/thread-forum-ops.js +68 -0
  361. package/dist/tasks/thread-helpers.js +86 -0
  362. package/dist/tasks/thread-helpers.test.js +33 -0
  363. package/dist/tasks/thread-lifecycle-ops.js +144 -0
  364. package/dist/tasks/thread-ops-shared.js +21 -0
  365. package/dist/tasks/thread-ops.js +2 -0
  366. package/dist/tasks/types.js +20 -0
  367. package/dist/tasks/types.test.js +60 -0
  368. package/dist/test-setup.js +11 -0
  369. package/dist/test-setup.test.js +42 -0
  370. package/dist/transport/types.js +1 -0
  371. package/dist/validate.js +41 -0
  372. package/dist/validate.test.js +94 -0
  373. package/dist/version.js +15 -0
  374. package/dist/version.test.js +31 -0
  375. package/dist/webhook/server.js +199 -0
  376. package/dist/webhook/server.test.js +460 -0
  377. package/dist/workspace-bootstrap.js +135 -0
  378. package/dist/workspace-bootstrap.test.js +514 -0
  379. package/dist/workspace-permissions.js +134 -0
  380. package/dist/workspace-permissions.test.js +181 -0
  381. package/package.json +74 -0
  382. package/scripts/cron/cron-tag-map.json +9 -0
  383. package/scripts/tasks/tag-map.json +10 -0
  384. package/systemd/discoclaw.service +19 -0
  385. package/templates/recipes/integration.discoclaw-recipe.md +171 -0
  386. package/templates/workspace/AGENTS.md +217 -0
  387. package/templates/workspace/BOOTSTRAP.md +1 -0
  388. package/templates/workspace/HEARTBEAT.md +10 -0
  389. package/templates/workspace/IDENTITY.md +16 -0
  390. package/templates/workspace/MEMORY.md +24 -0
  391. package/templates/workspace/SOUL.md +52 -0
  392. package/templates/workspace/TOOLS.md +304 -0
  393. package/templates/workspace/USER.md +37 -0
@@ -0,0 +1,173 @@
1
+ import { resolveModel } from '../runtime/model-tiers.js';
2
+ const CONFIG_TYPE_MAP = {
3
+ modelSet: true,
4
+ modelShow: true,
5
+ };
6
+ export const CONFIG_ACTION_TYPES = new Set(Object.keys(CONFIG_TYPE_MAP));
7
+ // ---------------------------------------------------------------------------
8
+ // Role → field mapping
9
+ // ---------------------------------------------------------------------------
10
+ const ROLE_DESCRIPTIONS = {
11
+ chat: 'Discord messages, plan runs, deferred runs, forge fallback',
12
+ fast: 'All small/fast tasks (summary, cron, cron auto-tag, tasks auto-tag)',
13
+ 'forge-drafter': 'Forge plan drafting/revision',
14
+ 'forge-auditor': 'Forge plan auditing',
15
+ summary: 'Rolling summaries only',
16
+ cron: 'Cron auto-tagging and model classification',
17
+ 'cron-exec': 'Default model for cron job execution (overridden by per-job settings)',
18
+ };
19
+ // ---------------------------------------------------------------------------
20
+ // Executor
21
+ // ---------------------------------------------------------------------------
22
+ export function executeConfigAction(action, configCtx) {
23
+ switch (action.type) {
24
+ case 'modelSet': {
25
+ if (!action.role || !action.model) {
26
+ return { ok: false, error: 'modelSet requires role and model' };
27
+ }
28
+ const model = action.model.trim();
29
+ if (!model || /\s/.test(model)) {
30
+ return { ok: false, error: `Invalid model string: "${action.model}"` };
31
+ }
32
+ // Validate the model string resolves (non-empty result from resolveModel).
33
+ const resolved = resolveModel(model, configCtx.runtime.id);
34
+ if (resolved === '' && model !== '') {
35
+ // Tier name that doesn't map for this runtime — warn but allow.
36
+ }
37
+ const bp = configCtx.botParams;
38
+ const changes = [];
39
+ switch (action.role) {
40
+ case 'chat':
41
+ bp.runtimeModel = model;
42
+ if (bp.planCtx)
43
+ bp.planCtx.model = model;
44
+ if (bp.cronCtx?.executorCtx)
45
+ bp.cronCtx.executorCtx.model = model;
46
+ changes.push(`chat → ${model}`);
47
+ break;
48
+ case 'fast':
49
+ bp.summaryModel = model;
50
+ changes.push(`summary → ${model}`);
51
+ if (bp.cronCtx) {
52
+ bp.cronCtx.autoTagModel = model;
53
+ bp.cronCtx.syncCoordinator?.setAutoTagModel(model);
54
+ changes.push(`cron-auto-tag → ${model}`);
55
+ }
56
+ if (bp.taskCtx) {
57
+ bp.taskCtx.autoTagModel = model;
58
+ changes.push(`tasks-auto-tag → ${model}`);
59
+ }
60
+ break;
61
+ case 'forge-drafter':
62
+ bp.forgeDrafterModel = model;
63
+ changes.push(`forge-drafter → ${model}`);
64
+ break;
65
+ case 'forge-auditor':
66
+ bp.forgeAuditorModel = model;
67
+ changes.push(`forge-auditor → ${model}`);
68
+ break;
69
+ case 'summary':
70
+ bp.summaryModel = model;
71
+ changes.push(`summary → ${model}`);
72
+ break;
73
+ case 'cron':
74
+ if (bp.cronCtx) {
75
+ bp.cronCtx.autoTagModel = model;
76
+ bp.cronCtx.syncCoordinator?.setAutoTagModel(model);
77
+ changes.push(`cron → ${model}`);
78
+ }
79
+ else {
80
+ return { ok: false, error: 'Cron subsystem not configured' };
81
+ }
82
+ break;
83
+ case 'cron-exec':
84
+ if (bp.cronCtx?.executorCtx) {
85
+ if (model === 'default') {
86
+ bp.cronCtx.executorCtx.cronExecModel = undefined;
87
+ changes.push(`cron-exec → (follows chat)`);
88
+ }
89
+ else {
90
+ bp.cronCtx.executorCtx.cronExecModel = model;
91
+ changes.push(`cron-exec → ${model}`);
92
+ }
93
+ }
94
+ else {
95
+ return { ok: false, error: 'Cron subsystem not configured' };
96
+ }
97
+ break;
98
+ default:
99
+ return { ok: false, error: `Unknown role: ${action.role}` };
100
+ }
101
+ const resolvedDisplay = resolveModel(model, configCtx.runtime.id);
102
+ const resolvedNote = resolvedDisplay && resolvedDisplay !== model ? ` (resolves to ${resolvedDisplay})` : '';
103
+ return { ok: true, summary: `Model updated: ${changes.join(', ')}${resolvedNote}` };
104
+ }
105
+ case 'modelShow': {
106
+ const bp = configCtx.botParams;
107
+ const rid = configCtx.runtime.id;
108
+ const rows = [
109
+ ['chat', bp.runtimeModel, ROLE_DESCRIPTIONS.chat],
110
+ ['summary', bp.summaryModel, ROLE_DESCRIPTIONS.summary],
111
+ ['forge-drafter', bp.forgeDrafterModel ?? bp.runtimeModel, ROLE_DESCRIPTIONS['forge-drafter']],
112
+ ['forge-auditor', bp.forgeAuditorModel ?? bp.runtimeModel, ROLE_DESCRIPTIONS['forge-auditor']],
113
+ ];
114
+ if (bp.cronCtx) {
115
+ const cronExecModel = bp.cronCtx.executorCtx?.cronExecModel;
116
+ rows.push(['cron-exec', cronExecModel || `${bp.runtimeModel} (follows chat)`, ROLE_DESCRIPTIONS['cron-exec']]);
117
+ rows.push(['cron-auto-tag', bp.cronCtx.autoTagModel, ROLE_DESCRIPTIONS.cron]);
118
+ }
119
+ const taskAutoTagModel = bp.taskCtx?.autoTagModel;
120
+ if (taskAutoTagModel) {
121
+ rows.push(['tasks-auto-tag', taskAutoTagModel, 'Tasks auto-tagging']);
122
+ }
123
+ const adapterDefault = configCtx.runtime.defaultModel;
124
+ const lines = rows.map(([role, model, desc]) => {
125
+ const resolved = resolveModel(model, rid);
126
+ let display;
127
+ if (model) {
128
+ display = resolved && resolved !== model ? `${model} → ${resolved}` : model;
129
+ }
130
+ else {
131
+ display = adapterDefault || '(adapter default)';
132
+ }
133
+ return `**${role}**: \`${display}\` — ${desc}`;
134
+ });
135
+ return { ok: true, summary: lines.join('\n') };
136
+ }
137
+ }
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Prompt section
141
+ // ---------------------------------------------------------------------------
142
+ export function configActionsPromptSection() {
143
+ return `### Model Configuration
144
+
145
+ **modelShow** — Show current model assignments for all roles:
146
+ \`\`\`
147
+ <discord-action>{"type":"modelShow"}</discord-action>
148
+ \`\`\`
149
+
150
+ **modelSet** — Change the model for a role at runtime:
151
+ \`\`\`
152
+ <discord-action>{"type":"modelSet","role":"chat","model":"sonnet"}</discord-action>
153
+ <discord-action>{"type":"modelSet","role":"fast","model":"haiku"}</discord-action>
154
+ \`\`\`
155
+ - \`role\` (required): One of \`chat\`, \`fast\`, \`forge-drafter\`, \`forge-auditor\`, \`summary\`, \`cron\`, \`cron-exec\`.
156
+ - \`model\` (required): Model tier (\`fast\`, \`capable\`), concrete model name (\`haiku\`, \`sonnet\`, \`opus\`), or \`default\` (for cron-exec only, to revert to following chat).
157
+
158
+ **Roles:**
159
+ | Role | What it controls |
160
+ |------|-----------------|
161
+ | \`chat\` | Discord messages, plan runs, deferred runs, forge fallback |
162
+ | \`fast\` | All small/fast tasks (summary, cron auto-tag, tasks auto-tag) |
163
+ | \`forge-drafter\` | Forge plan drafting/revision |
164
+ | \`forge-auditor\` | Forge plan auditing |
165
+ | \`summary\` | Rolling summaries only (overrides fast) |
166
+ | \`cron\` | Cron auto-tagging and model classification (overrides fast) |
167
+ | \`cron-exec\` | Default model for cron job execution; per-job overrides (via \`cronUpdate\`) take priority |
168
+
169
+ Changes are **ephemeral** — they take effect immediately but revert on restart. Use env vars for persistent configuration.
170
+
171
+ **Cron model priority:** per-job override (cronUpdate) > AI-classified model > cron-exec default > chat fallback.
172
+ Set \`cron-exec\` to \`default\` to clear the override and fall back to the chat model.`;
173
+ }
@@ -0,0 +1,322 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { CONFIG_ACTION_TYPES, executeConfigAction, configActionsPromptSection } from './actions-config.js';
3
+ // ---------------------------------------------------------------------------
4
+ // Helpers
5
+ // ---------------------------------------------------------------------------
6
+ function makeBotParams(overrides) {
7
+ return {
8
+ runtimeModel: 'capable',
9
+ summaryModel: 'fast',
10
+ forgeDrafterModel: undefined,
11
+ forgeAuditorModel: undefined,
12
+ cronCtx: { autoTagModel: 'fast', executorCtx: { model: 'capable' } },
13
+ taskCtx: { autoTagModel: 'fast' },
14
+ ...overrides,
15
+ };
16
+ }
17
+ const stubRuntime = {
18
+ id: 'claude_code',
19
+ capabilities: new Set(),
20
+ async *invoke() { },
21
+ };
22
+ function makeCtx(overrides) {
23
+ return {
24
+ botParams: makeBotParams(overrides),
25
+ runtime: stubRuntime,
26
+ };
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // CONFIG_ACTION_TYPES
30
+ // ---------------------------------------------------------------------------
31
+ describe('CONFIG_ACTION_TYPES', () => {
32
+ it('includes modelSet and modelShow', () => {
33
+ expect(CONFIG_ACTION_TYPES.has('modelSet')).toBe(true);
34
+ expect(CONFIG_ACTION_TYPES.has('modelShow')).toBe(true);
35
+ expect(CONFIG_ACTION_TYPES.size).toBe(2);
36
+ });
37
+ });
38
+ // ---------------------------------------------------------------------------
39
+ // modelShow
40
+ // ---------------------------------------------------------------------------
41
+ describe('modelShow', () => {
42
+ it('returns current model assignments', () => {
43
+ const ctx = makeCtx();
44
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
45
+ expect(result.ok).toBe(true);
46
+ if (!result.ok)
47
+ return;
48
+ expect(result.summary).toContain('chat');
49
+ expect(result.summary).toContain('capable');
50
+ expect(result.summary).toContain('summary');
51
+ expect(result.summary).toContain('forge-drafter');
52
+ expect(result.summary).toContain('forge-auditor');
53
+ expect(result.summary).toContain('cron');
54
+ });
55
+ it('shows forge models falling back to runtimeModel', () => {
56
+ const ctx = makeCtx();
57
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
58
+ expect(result.ok).toBe(true);
59
+ if (!result.ok)
60
+ return;
61
+ // forge-drafter should show runtimeModel since forgeDrafterModel is undefined
62
+ expect(result.summary).toContain('forge-drafter');
63
+ expect(result.summary).toContain('capable');
64
+ });
65
+ it('shows explicit forge model override', () => {
66
+ const ctx = makeCtx({ forgeDrafterModel: 'sonnet' });
67
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
68
+ expect(result.ok).toBe(true);
69
+ if (!result.ok)
70
+ return;
71
+ expect(result.summary).toContain('sonnet');
72
+ });
73
+ it('resolves tier names to concrete models for claude_code runtime', () => {
74
+ const ctx = makeCtx();
75
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
76
+ expect(result.ok).toBe(true);
77
+ if (!result.ok)
78
+ return;
79
+ // 'capable' resolves to 'opus' for claude_code
80
+ expect(result.summary).toContain('opus');
81
+ // 'fast' resolves to 'haiku' for claude_code
82
+ expect(result.summary).toContain('haiku');
83
+ });
84
+ it('omits cron row when cronCtx is not set', () => {
85
+ const ctx = makeCtx({ cronCtx: undefined });
86
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
87
+ expect(result.ok).toBe(true);
88
+ if (!result.ok)
89
+ return;
90
+ expect(result.summary).not.toContain('cron-auto-tag');
91
+ });
92
+ it('labels cron row as cron-auto-tag', () => {
93
+ const ctx = makeCtx();
94
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
95
+ expect(result.ok).toBe(true);
96
+ if (!result.ok)
97
+ return;
98
+ expect(result.summary).toContain('cron-auto-tag');
99
+ });
100
+ it('shows cron-exec following chat when no override set', () => {
101
+ const ctx = makeCtx();
102
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
103
+ expect(result.ok).toBe(true);
104
+ if (!result.ok)
105
+ return;
106
+ expect(result.summary).toContain('cron-exec');
107
+ expect(result.summary).toContain('follows chat');
108
+ });
109
+ it('shows explicit cron-exec model when set', () => {
110
+ const ctx = makeCtx();
111
+ ctx.botParams.cronCtx.executorCtx.cronExecModel = 'haiku';
112
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
113
+ expect(result.ok).toBe(true);
114
+ if (!result.ok)
115
+ return;
116
+ expect(result.summary).toContain('cron-exec');
117
+ expect(result.summary).toContain('haiku');
118
+ expect(result.summary).not.toContain('follows chat');
119
+ });
120
+ it('shows runtime defaultModel when model value is empty', () => {
121
+ const codexRuntime = {
122
+ id: 'codex',
123
+ capabilities: new Set(),
124
+ defaultModel: 'gpt-5-codex-mini',
125
+ async *invoke() { },
126
+ };
127
+ const ctx = {
128
+ botParams: makeBotParams({ runtimeModel: '', summaryModel: '' }),
129
+ runtime: codexRuntime,
130
+ };
131
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
132
+ expect(result.ok).toBe(true);
133
+ if (!result.ok)
134
+ return;
135
+ expect(result.summary).toContain('gpt-5-codex-mini');
136
+ expect(result.summary).not.toContain('(adapter default)');
137
+ });
138
+ it('falls back to (adapter default) when both model and defaultModel are empty', () => {
139
+ const codexRuntime = {
140
+ id: 'codex',
141
+ capabilities: new Set(),
142
+ async *invoke() { },
143
+ };
144
+ const ctx = {
145
+ botParams: makeBotParams({ runtimeModel: '', summaryModel: '' }),
146
+ runtime: codexRuntime,
147
+ };
148
+ const result = executeConfigAction({ type: 'modelShow' }, ctx);
149
+ expect(result.ok).toBe(true);
150
+ if (!result.ok)
151
+ return;
152
+ expect(result.summary).toContain('(adapter default)');
153
+ });
154
+ });
155
+ // ---------------------------------------------------------------------------
156
+ // modelSet
157
+ // ---------------------------------------------------------------------------
158
+ describe('modelSet', () => {
159
+ it('sets chat model (runtimeModel)', () => {
160
+ const ctx = makeCtx();
161
+ const result = executeConfigAction({ type: 'modelSet', role: 'chat', model: 'sonnet' }, ctx);
162
+ expect(result.ok).toBe(true);
163
+ expect(ctx.botParams.runtimeModel).toBe('sonnet');
164
+ });
165
+ it('sets all fast-tier models at once', () => {
166
+ const ctx = makeCtx();
167
+ const result = executeConfigAction({ type: 'modelSet', role: 'fast', model: 'haiku' }, ctx);
168
+ expect(result.ok).toBe(true);
169
+ expect(ctx.botParams.summaryModel).toBe('haiku');
170
+ expect(ctx.botParams.cronCtx.autoTagModel).toBe('haiku');
171
+ expect(ctx.botParams.taskCtx.autoTagModel).toBe('haiku');
172
+ });
173
+ it('sets forge-drafter model', () => {
174
+ const ctx = makeCtx();
175
+ const result = executeConfigAction({ type: 'modelSet', role: 'forge-drafter', model: 'opus' }, ctx);
176
+ expect(result.ok).toBe(true);
177
+ expect(ctx.botParams.forgeDrafterModel).toBe('opus');
178
+ });
179
+ it('sets forge-auditor model', () => {
180
+ const ctx = makeCtx();
181
+ const result = executeConfigAction({ type: 'modelSet', role: 'forge-auditor', model: 'sonnet' }, ctx);
182
+ expect(result.ok).toBe(true);
183
+ expect(ctx.botParams.forgeAuditorModel).toBe('sonnet');
184
+ });
185
+ it('sets summary model independently', () => {
186
+ const ctx = makeCtx();
187
+ const result = executeConfigAction({ type: 'modelSet', role: 'summary', model: 'capable' }, ctx);
188
+ expect(result.ok).toBe(true);
189
+ expect(ctx.botParams.summaryModel).toBe('capable');
190
+ });
191
+ it('sets cron model', () => {
192
+ const ctx = makeCtx();
193
+ const result = executeConfigAction({ type: 'modelSet', role: 'cron', model: 'capable' }, ctx);
194
+ expect(result.ok).toBe(true);
195
+ expect(ctx.botParams.cronCtx.autoTagModel).toBe('capable');
196
+ });
197
+ it('rejects empty model string', () => {
198
+ const ctx = makeCtx();
199
+ const result = executeConfigAction({ type: 'modelSet', role: 'chat', model: '' }, ctx);
200
+ expect(result.ok).toBe(false);
201
+ });
202
+ it('rejects model with whitespace', () => {
203
+ const ctx = makeCtx();
204
+ const result = executeConfigAction({ type: 'modelSet', role: 'chat', model: 'some model' }, ctx);
205
+ expect(result.ok).toBe(false);
206
+ });
207
+ it('rejects missing role', () => {
208
+ const ctx = makeCtx();
209
+ const result = executeConfigAction({ type: 'modelSet', role: '', model: 'sonnet' }, ctx);
210
+ expect(result.ok).toBe(false);
211
+ });
212
+ it('rejects missing model', () => {
213
+ const ctx = makeCtx();
214
+ const result = executeConfigAction({ type: 'modelSet', role: 'chat', model: '' }, ctx);
215
+ expect(result.ok).toBe(false);
216
+ });
217
+ it('fails for cron role when cronCtx is not configured', () => {
218
+ const ctx = makeCtx({ cronCtx: undefined });
219
+ const result = executeConfigAction({ type: 'modelSet', role: 'cron', model: 'fast' }, ctx);
220
+ expect(result.ok).toBe(false);
221
+ if (result.ok)
222
+ return;
223
+ expect(result.error).toContain('Cron subsystem not configured');
224
+ });
225
+ it('includes resolved model note for tier names', () => {
226
+ const ctx = makeCtx();
227
+ const result = executeConfigAction({ type: 'modelSet', role: 'chat', model: 'capable' }, ctx);
228
+ expect(result.ok).toBe(true);
229
+ if (!result.ok)
230
+ return;
231
+ expect(result.summary).toContain('resolves to opus');
232
+ });
233
+ it('fast role succeeds when cronCtx and taskCtx are missing', () => {
234
+ const ctx = makeCtx({ cronCtx: undefined, taskCtx: undefined });
235
+ const result = executeConfigAction({ type: 'modelSet', role: 'fast', model: 'haiku' }, ctx);
236
+ expect(result.ok).toBe(true);
237
+ expect(ctx.botParams.summaryModel).toBe('haiku');
238
+ if (!result.ok)
239
+ return;
240
+ // Only summary changed, cron/beads skipped silently
241
+ expect(result.summary).toContain('summary');
242
+ expect(result.summary).not.toContain('cron');
243
+ expect(result.summary).not.toContain('beads');
244
+ });
245
+ it('accepts concrete model names as passthrough', () => {
246
+ const ctx = makeCtx();
247
+ const result = executeConfigAction({ type: 'modelSet', role: 'chat', model: 'claude-sonnet-4-5-20250929' }, ctx);
248
+ expect(result.ok).toBe(true);
249
+ expect(ctx.botParams.runtimeModel).toBe('claude-sonnet-4-5-20250929');
250
+ });
251
+ it('chat propagates to planCtx.model', () => {
252
+ const ctx = makeCtx();
253
+ ctx.botParams.planCtx = { model: 'old' };
254
+ executeConfigAction({ type: 'modelSet', role: 'chat', model: 'sonnet' }, ctx);
255
+ expect(ctx.botParams.planCtx.model).toBe('sonnet');
256
+ });
257
+ it('chat propagates to cronCtx.executorCtx.model', () => {
258
+ const ctx = makeCtx();
259
+ ctx.botParams.cronCtx.executorCtx = { model: 'old' };
260
+ executeConfigAction({ type: 'modelSet', role: 'chat', model: 'sonnet' }, ctx);
261
+ expect(ctx.botParams.cronCtx.executorCtx.model).toBe('sonnet');
262
+ });
263
+ it('fast propagates to cronCtx.syncCoordinator', () => {
264
+ let updated = '';
265
+ const ctx = makeCtx();
266
+ ctx.botParams.cronCtx.syncCoordinator = { setAutoTagModel: (m) => { updated = m; } };
267
+ executeConfigAction({ type: 'modelSet', role: 'fast', model: 'haiku' }, ctx);
268
+ expect(updated).toBe('haiku');
269
+ });
270
+ it('cron propagates to cronCtx.syncCoordinator', () => {
271
+ let updated = '';
272
+ const ctx = makeCtx();
273
+ ctx.botParams.cronCtx.syncCoordinator = { setAutoTagModel: (m) => { updated = m; } };
274
+ executeConfigAction({ type: 'modelSet', role: 'cron', model: 'capable' }, ctx);
275
+ expect(updated).toBe('capable');
276
+ });
277
+ it('chat skips planCtx/cronExecCtx propagation when not configured', () => {
278
+ const ctx = makeCtx();
279
+ // No planCtx or cronCtx.executorCtx — should not throw
280
+ ctx.botParams.planCtx = undefined;
281
+ const result = executeConfigAction({ type: 'modelSet', role: 'chat', model: 'sonnet' }, ctx);
282
+ expect(result.ok).toBe(true);
283
+ expect(ctx.botParams.runtimeModel).toBe('sonnet');
284
+ });
285
+ it('sets cron-exec model', () => {
286
+ const ctx = makeCtx();
287
+ const result = executeConfigAction({ type: 'modelSet', role: 'cron-exec', model: 'haiku' }, ctx);
288
+ expect(result.ok).toBe(true);
289
+ expect(ctx.botParams.cronCtx.executorCtx.cronExecModel).toBe('haiku');
290
+ });
291
+ it('cron-exec "default" clears the override', () => {
292
+ const ctx = makeCtx();
293
+ ctx.botParams.cronCtx.executorCtx.cronExecModel = 'haiku';
294
+ const result = executeConfigAction({ type: 'modelSet', role: 'cron-exec', model: 'default' }, ctx);
295
+ expect(result.ok).toBe(true);
296
+ expect(ctx.botParams.cronCtx.executorCtx.cronExecModel).toBeUndefined();
297
+ if (!result.ok)
298
+ return;
299
+ expect(result.summary).toContain('follows chat');
300
+ });
301
+ it('cron-exec fails when cron subsystem not configured', () => {
302
+ const ctx = makeCtx({ cronCtx: undefined });
303
+ const result = executeConfigAction({ type: 'modelSet', role: 'cron-exec', model: 'haiku' }, ctx);
304
+ expect(result.ok).toBe(false);
305
+ });
306
+ });
307
+ // ---------------------------------------------------------------------------
308
+ // configActionsPromptSection
309
+ // ---------------------------------------------------------------------------
310
+ describe('configActionsPromptSection', () => {
311
+ it('documents modelSet and modelShow', () => {
312
+ const section = configActionsPromptSection();
313
+ expect(section).toContain('modelShow');
314
+ expect(section).toContain('modelSet');
315
+ expect(section).toContain('role');
316
+ expect(section).toContain('chat');
317
+ expect(section).toContain('fast');
318
+ expect(section).toContain('forge-drafter');
319
+ expect(section).toContain('forge-auditor');
320
+ expect(section).toContain('ephemeral');
321
+ });
322
+ });