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,586 @@
1
+ import { Cron } from 'croner';
2
+ import { CADENCE_TAGS, generateCronId } from '../cron/run-stats.js';
3
+ import { detectCadence } from '../cron/cadence.js';
4
+ import { autoTagCron, classifyCronModel } from '../cron/auto-tag.js';
5
+ import { buildCronThreadName, ensureStatusMessage, resolveForumChannel } from '../cron/discord-sync.js';
6
+ import { reloadCronTagMapInPlace } from '../cron/tag-map.js';
7
+ import { getDefaultTimezone } from '../cron/default-timezone.js';
8
+ const CRON_TYPE_MAP = {
9
+ cronCreate: true,
10
+ cronUpdate: true,
11
+ cronList: true,
12
+ cronShow: true,
13
+ cronPause: true,
14
+ cronResume: true,
15
+ cronDelete: true,
16
+ cronTrigger: true,
17
+ cronSync: true,
18
+ cronTagMapReload: true,
19
+ };
20
+ export const CRON_ACTION_TYPES = new Set(Object.keys(CRON_TYPE_MAP));
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+ function buildStarterContent(schedule, timezone, channel, prompt) {
25
+ return `**Schedule:** \`${schedule}\` (${timezone})\n**Channel:** #${channel}\n\n${prompt}`;
26
+ }
27
+ function validateCronDefinition(def) {
28
+ const timezone = String(def.timezone ?? '').trim();
29
+ if (!timezone) {
30
+ return 'timezone is required';
31
+ }
32
+ try {
33
+ Intl.DateTimeFormat(undefined, { timeZone: timezone });
34
+ }
35
+ catch {
36
+ return `invalid timezone "${def.timezone}"`;
37
+ }
38
+ try {
39
+ new Cron(def.schedule, { timezone }).stop();
40
+ return null;
41
+ }
42
+ catch (err) {
43
+ const msg = err instanceof Error ? err.message : String(err);
44
+ return msg || 'invalid schedule';
45
+ }
46
+ }
47
+ function requestRunningJobCancel(cronCtx, threadId, cronId) {
48
+ const canceled = cronCtx.executorCtx?.runControl?.requestCancel(threadId) ?? false;
49
+ if (canceled) {
50
+ cronCtx.log?.info({ cronId, threadId }, 'cron:action requested cancel for in-flight run');
51
+ }
52
+ return canceled;
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Executor
56
+ // ---------------------------------------------------------------------------
57
+ export async function executeCronAction(action, ctx, cronCtx) {
58
+ switch (action.type) {
59
+ case 'cronCreate': {
60
+ if (!action.name || !action.schedule || !action.channel || !action.prompt) {
61
+ return { ok: false, error: 'cronCreate requires name, schedule, channel, and prompt' };
62
+ }
63
+ const cronId = generateCronId();
64
+ const timezone = action.timezone ?? getDefaultTimezone();
65
+ const def = { triggerType: 'schedule', schedule: action.schedule, timezone, channel: action.channel, prompt: action.prompt };
66
+ const validationError = validateCronDefinition(def);
67
+ if (validationError) {
68
+ return { ok: false, error: `Invalid cron definition: ${validationError}` };
69
+ }
70
+ const cadence = detectCadence(def.schedule);
71
+ // Create forum thread.
72
+ const forum = await resolveForumChannel(cronCtx.client, cronCtx.forumId);
73
+ if (!forum) {
74
+ return { ok: false, error: 'Cron forum channel not found' };
75
+ }
76
+ // Reload shared cache from disk (best-effort; failure keeps cached)
77
+ await reloadCronTagMapInPlace(cronCtx.tagMapPath, cronCtx.tagMap).catch((err) => {
78
+ cronCtx.log?.warn({ err, tagMapPath: cronCtx.tagMapPath }, 'cron:action tag-map reload failed; using cached');
79
+ });
80
+ // Snapshot for deterministic use within this action
81
+ const tagMap = { ...cronCtx.tagMap };
82
+ // Auto-tag if enabled.
83
+ const cadenceSet = new Set(CADENCE_TAGS);
84
+ const purposeTagNames = Object.keys(tagMap).filter((k) => !cadenceSet.has(k));
85
+ let purposeTags = [];
86
+ let model = null;
87
+ if (action.tags) {
88
+ purposeTags = action.tags.split(',').map((t) => t.trim()).filter(Boolean);
89
+ }
90
+ if (cronCtx.autoTag && purposeTagNames.length > 0 && purposeTags.length === 0) {
91
+ try {
92
+ purposeTags = await autoTagCron(cronCtx.runtime, action.name, action.prompt, purposeTagNames, { model: cronCtx.autoTagModel, cwd: cronCtx.cwd });
93
+ }
94
+ catch (err) {
95
+ cronCtx.log?.warn({ err, cronId }, 'cron:action:create auto-tag failed');
96
+ }
97
+ }
98
+ // Classify model.
99
+ if (action.model) {
100
+ model = action.model;
101
+ }
102
+ else {
103
+ try {
104
+ model = await classifyCronModel(cronCtx.runtime, action.name, action.prompt, cadence, { model: cronCtx.autoTagModel, cwd: cronCtx.cwd });
105
+ }
106
+ catch {
107
+ model = 'fast';
108
+ }
109
+ }
110
+ // Resolve tag IDs for forum.
111
+ const allTagNames = [...purposeTags, cadence];
112
+ const appliedTagIds = allTagNames.map((t) => tagMap[t]).filter(Boolean);
113
+ const uniqueTagIds = [...new Set(appliedTagIds)].slice(0, 5);
114
+ const threadName = buildCronThreadName(action.name, cadence);
115
+ const starterContent = buildStarterContent(action.schedule, timezone, action.channel, action.prompt);
116
+ let thread;
117
+ try {
118
+ thread = await forum.threads.create({
119
+ name: threadName,
120
+ message: {
121
+ content: starterContent.slice(0, 2000),
122
+ allowedMentions: { parse: [] },
123
+ },
124
+ appliedTags: uniqueTagIds,
125
+ });
126
+ }
127
+ catch (err) {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ return { ok: false, error: `Failed to create forum thread: ${msg}` };
130
+ }
131
+ // Mark thread as pending so the threadCreate listener skips it.
132
+ cronCtx.pendingThreadIds.add(thread.id);
133
+ // Register with scheduler, then clear the pending marker.
134
+ try {
135
+ cronCtx.scheduler.register(thread.id, thread.id, ctx.guild.id, action.name, def, cronId);
136
+ }
137
+ catch (err) {
138
+ const msg = err instanceof Error ? err.message : String(err);
139
+ return { ok: false, error: `Invalid cron definition: ${msg}` };
140
+ }
141
+ finally {
142
+ cronCtx.pendingThreadIds.delete(thread.id);
143
+ }
144
+ // Save stats. On create, set the classified model but don't set modelOverride —
145
+ // override is only for explicit user changes via cronUpdate.
146
+ const record = await cronCtx.statsStore.upsertRecord(cronId, thread.id, {
147
+ cadence,
148
+ purposeTags,
149
+ model,
150
+ });
151
+ // Create status message.
152
+ try {
153
+ await ensureStatusMessage(cronCtx.client, thread.id, cronId, record, cronCtx.statsStore, { log: cronCtx.log });
154
+ }
155
+ catch { }
156
+ cronCtx.forumCountSync?.requestUpdate();
157
+ return { ok: true, summary: `Cron "${action.name}" created (${cronId}), schedule: ${action.schedule}, model: ${model}` };
158
+ }
159
+ case 'cronUpdate': {
160
+ if (!action.cronId) {
161
+ return { ok: false, error: 'cronUpdate requires cronId' };
162
+ }
163
+ const record = cronCtx.statsStore.getRecord(action.cronId);
164
+ if (!record) {
165
+ return { ok: false, error: `Cron "${action.cronId}" not found` };
166
+ }
167
+ const job = cronCtx.scheduler.getJob(record.threadId);
168
+ if (!job) {
169
+ return { ok: false, error: `Cron "${action.cronId}" not registered in scheduler` };
170
+ }
171
+ const updates = {};
172
+ const changes = [];
173
+ // Model override.
174
+ if (action.model) {
175
+ updates.modelOverride = action.model;
176
+ changes.push(`model → ${action.model}`);
177
+ }
178
+ // Tags override.
179
+ if (action.tags) {
180
+ updates.purposeTags = action.tags.split(',').map((t) => t.trim()).filter(Boolean);
181
+ changes.push(`tags → ${updates.purposeTags.join(', ')}`);
182
+ }
183
+ // Definition changes (schedule, timezone, channel, prompt).
184
+ const newSchedule = action.schedule ?? job.def.schedule ?? '';
185
+ const newTimezone = action.timezone ?? job.def.timezone;
186
+ const newChannel = action.channel ?? job.def.channel;
187
+ const newPrompt = action.prompt ?? job.def.prompt;
188
+ const newDef = { triggerType: job.def.triggerType, schedule: newSchedule, timezone: newTimezone, channel: newChannel, prompt: newPrompt };
189
+ const defChanged = action.schedule !== undefined || action.timezone !== undefined || action.channel !== undefined || action.prompt !== undefined;
190
+ if (defChanged) {
191
+ const validationError = validateCronDefinition(newDef);
192
+ if (validationError) {
193
+ return { ok: false, error: `Invalid cron definition: ${validationError}` };
194
+ }
195
+ // Update cadence if schedule changed.
196
+ if (action.schedule) {
197
+ updates.cadence = detectCadence(action.schedule);
198
+ changes.push(`schedule → ${action.schedule}`);
199
+ }
200
+ if (action.timezone !== undefined)
201
+ changes.push(`timezone → ${action.timezone}`);
202
+ if (action.channel !== undefined)
203
+ changes.push(`channel → ${action.channel}`);
204
+ if (action.prompt !== undefined)
205
+ changes.push(`prompt updated`);
206
+ // Try to edit the thread's starter message (works for bot-created threads).
207
+ const thread = cronCtx.client.channels.cache.get(record.threadId);
208
+ if (thread && thread.isThread()) {
209
+ try {
210
+ const starter = await thread.fetchStarterMessage();
211
+ if (starter && starter.author.id === cronCtx.client.user?.id) {
212
+ const newContent = buildStarterContent(newSchedule, newTimezone, newChannel, newPrompt);
213
+ await starter.edit({ content: newContent.slice(0, 2000), allowedMentions: { parse: [] } });
214
+ }
215
+ else {
216
+ // Can't edit user's message — post update note.
217
+ const note = `**Cron Updated**\n**Schedule:** \`${newSchedule}\` (${newTimezone})\n**Channel:** #${newChannel}\n\nPlease update the starter message to reflect these changes.`;
218
+ await thread.send({ content: note, allowedMentions: { parse: [] } });
219
+ }
220
+ }
221
+ catch (err) {
222
+ cronCtx.log?.warn({ err, cronId: action.cronId }, 'cron:action:update edit failed');
223
+ }
224
+ }
225
+ // Reload scheduler.
226
+ try {
227
+ cronCtx.scheduler.register(record.threadId, record.threadId, job.guildId, job.name, newDef, action.cronId);
228
+ }
229
+ catch (err) {
230
+ const msg = err instanceof Error ? err.message : String(err);
231
+ return { ok: false, error: `Invalid cron definition: ${msg}` };
232
+ }
233
+ }
234
+ await cronCtx.statsStore.upsertRecord(action.cronId, record.threadId, updates);
235
+ // Update status message.
236
+ try {
237
+ const updatedRecord = cronCtx.statsStore.getRecord(action.cronId);
238
+ if (updatedRecord) {
239
+ await ensureStatusMessage(cronCtx.client, record.threadId, action.cronId, updatedRecord, cronCtx.statsStore, { log: cronCtx.log });
240
+ }
241
+ }
242
+ catch { }
243
+ // Update thread tags if needed.
244
+ if (action.tags !== undefined || action.schedule !== undefined) {
245
+ try {
246
+ // Reload shared cache from disk (best-effort; failure keeps cached)
247
+ await reloadCronTagMapInPlace(cronCtx.tagMapPath, cronCtx.tagMap).catch((err) => {
248
+ cronCtx.log?.warn({ err, tagMapPath: cronCtx.tagMapPath }, 'cron:action tag-map reload failed; using cached');
249
+ });
250
+ // Snapshot for deterministic use within this action
251
+ const tagMap = { ...cronCtx.tagMap };
252
+ const updatedRecord = cronCtx.statsStore.getRecord(action.cronId);
253
+ if (updatedRecord) {
254
+ const allTags = [...updatedRecord.purposeTags];
255
+ if (updatedRecord.cadence)
256
+ allTags.push(updatedRecord.cadence);
257
+ const tagIds = allTags.map((t) => tagMap[t]).filter(Boolean);
258
+ const uniqueTagIds = [...new Set(tagIds)].slice(0, 5);
259
+ if (uniqueTagIds.length > 0) {
260
+ const thread = cronCtx.client.channels.cache.get(record.threadId);
261
+ if (thread && thread.isThread()) {
262
+ await thread.edit({ appliedTags: uniqueTagIds });
263
+ }
264
+ }
265
+ }
266
+ }
267
+ catch { }
268
+ }
269
+ return { ok: true, summary: `Cron ${action.cronId} updated: ${changes.join(', ') || 'no changes'}` };
270
+ }
271
+ case 'cronList': {
272
+ const jobs = cronCtx.scheduler.listJobs();
273
+ if (jobs.length === 0) {
274
+ return { ok: true, summary: 'No cron jobs registered.' };
275
+ }
276
+ const lines = jobs.map((j) => {
277
+ const fullJob = cronCtx.scheduler.getJob(j.id);
278
+ const record = fullJob?.cronId ? cronCtx.statsStore.getRecord(fullJob.cronId) : undefined;
279
+ const status = record?.disabled ? 'paused' : (record?.lastRunStatus ?? 'pending');
280
+ const displayStatus = fullJob?.running ? `${status} \uD83D\uDD04` : status;
281
+ const model = record?.modelOverride ?? record?.model ?? '?';
282
+ const runs = record?.runCount ?? 0;
283
+ const tags = record?.purposeTags?.join(', ') || '';
284
+ const nextRun = j.nextRun ? `<t:${Math.floor(j.nextRun.getTime() / 1000)}:R>` : 'N/A';
285
+ const cronId = fullJob?.cronId ?? '?';
286
+ return `\`${cronId}\` **${j.name}** | \`${j.schedule}\` | ${displayStatus} | ${model} | ${runs} runs | next: ${nextRun}${tags ? ` | ${tags}` : ''}`;
287
+ });
288
+ return { ok: true, summary: lines.join('\n') };
289
+ }
290
+ case 'cronShow': {
291
+ if (!action.cronId) {
292
+ return { ok: false, error: 'cronShow requires cronId' };
293
+ }
294
+ const record = cronCtx.statsStore.getRecord(action.cronId);
295
+ if (!record) {
296
+ return { ok: false, error: `Cron "${action.cronId}" not found` };
297
+ }
298
+ const job = cronCtx.scheduler.getJob(record.threadId);
299
+ const lines = [];
300
+ lines.push(`**Cron: ${job?.name ?? 'Unknown'}** (\`${action.cronId}\`)`);
301
+ lines.push(`Thread: ${record.threadId}`);
302
+ if (job) {
303
+ lines.push(`Schedule: \`${job.def.schedule}\` (${job.def.timezone})`);
304
+ const nextRun = job.cron?.nextRun() ?? null;
305
+ lines.push(`Next run: ${nextRun ? `<t:${Math.floor(nextRun.getTime() / 1000)}:F>` : 'N/A'}`);
306
+ }
307
+ lines.push(`Status: ${record.disabled ? 'paused' : 'active'}`);
308
+ if (job?.running) {
309
+ lines.push(`Runtime: \uD83D\uDD04 running`);
310
+ }
311
+ lines.push(`Model: ${record.modelOverride ?? record.model ?? 'N/A'}${record.modelOverride ? ' (override)' : ''}`);
312
+ lines.push(`Cadence: ${record.cadence ?? 'N/A'}`);
313
+ lines.push(`Runs: ${record.runCount} | Last: ${record.lastRunStatus ?? 'never'}`);
314
+ if (record.lastRunAt)
315
+ lines.push(`Last run: <t:${Math.floor(new Date(record.lastRunAt).getTime() / 1000)}:R>`);
316
+ if (record.purposeTags.length > 0)
317
+ lines.push(`Tags: ${record.purposeTags.join(', ')}`);
318
+ if (record.lastErrorMessage)
319
+ lines.push(`Last error: ${record.lastErrorMessage}`);
320
+ return { ok: true, summary: lines.join('\n') };
321
+ }
322
+ case 'cronPause': {
323
+ if (!action.cronId) {
324
+ return { ok: false, error: 'cronPause requires cronId' };
325
+ }
326
+ const record = cronCtx.statsStore.getRecord(action.cronId);
327
+ if (!record) {
328
+ return { ok: false, error: `Cron "${action.cronId}" not found` };
329
+ }
330
+ const disabled = cronCtx.scheduler.disable(record.threadId);
331
+ if (!disabled) {
332
+ return { ok: false, error: `Cron "${action.cronId}" not registered in scheduler` };
333
+ }
334
+ const canceled = requestRunningJobCancel(cronCtx, record.threadId, action.cronId);
335
+ await cronCtx.statsStore.upsertRecord(action.cronId, record.threadId, { disabled: true });
336
+ // Post notification.
337
+ try {
338
+ const thread = cronCtx.client.channels.cache.get(record.threadId);
339
+ if (thread && thread.isThread()) {
340
+ await thread.send({ content: '\u23F8\uFE0F **Cron paused**', allowedMentions: { parse: [] } });
341
+ }
342
+ }
343
+ catch { }
344
+ return { ok: true, summary: canceled ? `Cron ${action.cronId} paused (active run cancel requested)` : `Cron ${action.cronId} paused` };
345
+ }
346
+ case 'cronResume': {
347
+ if (!action.cronId) {
348
+ return { ok: false, error: 'cronResume requires cronId' };
349
+ }
350
+ const record = cronCtx.statsStore.getRecord(action.cronId);
351
+ if (!record) {
352
+ return { ok: false, error: `Cron "${action.cronId}" not found` };
353
+ }
354
+ const enabled = cronCtx.scheduler.enable(record.threadId);
355
+ if (!enabled) {
356
+ return { ok: false, error: `Cron "${action.cronId}" not registered in scheduler` };
357
+ }
358
+ await cronCtx.statsStore.upsertRecord(action.cronId, record.threadId, { disabled: false });
359
+ // Post notification.
360
+ try {
361
+ const thread = cronCtx.client.channels.cache.get(record.threadId);
362
+ if (thread && thread.isThread()) {
363
+ await thread.send({ content: '\u25B6\uFE0F **Cron resumed**', allowedMentions: { parse: [] } });
364
+ }
365
+ }
366
+ catch { }
367
+ return { ok: true, summary: `Cron ${action.cronId} resumed` };
368
+ }
369
+ case 'cronDelete': {
370
+ if (!action.cronId) {
371
+ return { ok: false, error: 'cronDelete requires cronId' };
372
+ }
373
+ const record = cronCtx.statsStore.getRecord(action.cronId);
374
+ if (!record) {
375
+ return { ok: false, error: `Cron "${action.cronId}" not found` };
376
+ }
377
+ const canceled = requestRunningJobCancel(cronCtx, record.threadId, action.cronId);
378
+ cronCtx.scheduler.unregister(record.threadId);
379
+ await cronCtx.statsStore.removeRecord(action.cronId);
380
+ cronCtx.forumCountSync?.requestUpdate();
381
+ // Archive the thread.
382
+ const thread = cronCtx.client.channels.cache.get(record.threadId);
383
+ if (thread && thread.isThread()) {
384
+ try {
385
+ await thread.send({ content: '\uD83D\uDDD1\uFE0F **Cron deleted**', allowedMentions: { parse: [] } });
386
+ }
387
+ catch { }
388
+ try {
389
+ await thread.setArchived(true);
390
+ }
391
+ catch (err) {
392
+ cronCtx.log?.warn({ err, cronId: action.cronId, threadId: record.threadId }, 'cron:action:delete archive failed');
393
+ return {
394
+ ok: true,
395
+ summary: canceled
396
+ ? `Cron ${action.cronId} deleted (active run cancel requested) but thread could not be archived — archive it manually`
397
+ : `Cron ${action.cronId} deleted but thread could not be archived — archive it manually`,
398
+ };
399
+ }
400
+ }
401
+ return {
402
+ ok: true,
403
+ summary: canceled
404
+ ? `Cron ${action.cronId} deleted and thread archived (active run cancel requested)`
405
+ : `Cron ${action.cronId} deleted and thread archived`,
406
+ };
407
+ }
408
+ case 'cronTrigger': {
409
+ if (!action.cronId) {
410
+ return { ok: false, error: 'cronTrigger requires cronId' };
411
+ }
412
+ const record = cronCtx.statsStore.getRecord(action.cronId);
413
+ if (!record) {
414
+ return { ok: false, error: `Cron "${action.cronId}" not found` };
415
+ }
416
+ const job = cronCtx.scheduler.getJob(record.threadId);
417
+ if (!job) {
418
+ return { ok: false, error: `Cron "${action.cronId}" not found in scheduler` };
419
+ }
420
+ if (action.force) {
421
+ return {
422
+ ok: false,
423
+ error: 'cronTrigger force is disabled in Discord actions; use an admin terminal flow for break-glass overrides',
424
+ };
425
+ }
426
+ // Fire the executor (deferred import to avoid circular).
427
+ try {
428
+ const { executeCronJob } = await import('../cron/executor.js');
429
+ // Use the real executor context if available (wired in from index.ts),
430
+ // falling back to a minimal context with reduced capabilities.
431
+ const execCtx = cronCtx.executorCtx ?? {
432
+ client: cronCtx.client,
433
+ runtime: cronCtx.runtime,
434
+ model: record.modelOverride ?? record.model ?? 'fast',
435
+ cwd: cronCtx.cwd,
436
+ tools: [],
437
+ timeoutMs: 600_000,
438
+ status: null,
439
+ log: cronCtx.log,
440
+ discordActionsEnabled: false,
441
+ actionFlags: { channels: false, messaging: false, guild: false, moderation: false, polls: false, tasks: false, crons: false, botProfile: false, forge: false, plan: false, memory: false, config: false, defer: false },
442
+ deferScheduler: cronCtx.deferScheduler,
443
+ statsStore: cronCtx.statsStore,
444
+ };
445
+ void executeCronJob(job, execCtx);
446
+ return { ok: true, summary: `Cron ${action.cronId} triggered (running in background)` };
447
+ }
448
+ catch (err) {
449
+ const msg = err instanceof Error ? err.message : String(err);
450
+ return { ok: false, error: `Trigger failed: ${msg}` };
451
+ }
452
+ }
453
+ case 'cronSync': {
454
+ try {
455
+ if (cronCtx.syncCoordinator) {
456
+ const result = await cronCtx.syncCoordinator.sync();
457
+ if (result === null) {
458
+ return { ok: true, summary: 'Cron sync already running; request coalesced' };
459
+ }
460
+ return {
461
+ ok: true,
462
+ summary: `Cron sync complete: ${result.tagsApplied} tags, ${result.namesUpdated} names, ${result.statusMessagesUpdated} status msgs, ${result.orphansDetected} orphans`,
463
+ };
464
+ }
465
+ else {
466
+ // Fallback (no coordinator): reload + snapshot + runCronSync + forumCountSync
467
+ await reloadCronTagMapInPlace(cronCtx.tagMapPath, cronCtx.tagMap).catch((err) => {
468
+ cronCtx.log?.warn({ err, tagMapPath: cronCtx.tagMapPath }, 'cron:sync tag-map reload failed; using cached');
469
+ });
470
+ const tagMapSnapshot = { ...cronCtx.tagMap };
471
+ const { runCronSync } = await import('../cron/cron-sync.js');
472
+ const result = await runCronSync({
473
+ client: cronCtx.client,
474
+ forumId: cronCtx.forumId,
475
+ scheduler: cronCtx.scheduler,
476
+ statsStore: cronCtx.statsStore,
477
+ runtime: cronCtx.runtime,
478
+ tagMap: tagMapSnapshot,
479
+ autoTag: cronCtx.autoTag,
480
+ autoTagModel: cronCtx.autoTagModel,
481
+ cwd: cronCtx.cwd,
482
+ log: cronCtx.log,
483
+ });
484
+ cronCtx.forumCountSync?.requestUpdate();
485
+ return {
486
+ ok: true,
487
+ summary: `Cron sync complete: ${result.tagsApplied} tags, ${result.namesUpdated} names, ${result.statusMessagesUpdated} status msgs, ${result.orphansDetected} orphans`,
488
+ };
489
+ }
490
+ }
491
+ catch (err) {
492
+ const msg = err instanceof Error ? err.message : String(err);
493
+ return { ok: false, error: `Cron sync failed: ${msg}` };
494
+ }
495
+ }
496
+ case 'cronTagMapReload': {
497
+ const oldCount = Object.keys(cronCtx.tagMap).length;
498
+ try {
499
+ const newCount = await reloadCronTagMapInPlace(cronCtx.tagMapPath, cronCtx.tagMap);
500
+ const tagNames = Object.keys(cronCtx.tagMap).slice(0, 10);
501
+ const tagList = tagNames.join(', ') + (Object.keys(cronCtx.tagMap).length > 10 ? ', ...' : '');
502
+ let summary = `Tag map reloaded: ${oldCount} → ${newCount} tags [${tagList}]`;
503
+ if (cronCtx.syncCoordinator) {
504
+ cronCtx.syncCoordinator.sync().catch((err) => {
505
+ cronCtx.log?.warn({ err }, 'cron:tagMapReload post-reload sync failed');
506
+ });
507
+ summary += '; sync queued';
508
+ }
509
+ else {
510
+ summary += '; no sync coordinator configured';
511
+ }
512
+ return { ok: true, summary };
513
+ }
514
+ catch (err) {
515
+ const msg = err instanceof Error ? err.message : String(err);
516
+ return { ok: false, error: `Tag map reload failed: ${msg}` };
517
+ }
518
+ }
519
+ }
520
+ }
521
+ // ---------------------------------------------------------------------------
522
+ // Prompt section
523
+ // ---------------------------------------------------------------------------
524
+ export function cronActionsPromptSection() {
525
+ return `### Cron Scheduled Tasks
526
+
527
+ **cronCreate** — Create a new scheduled task:
528
+ \`\`\`
529
+ <discord-action>{"type":"cronCreate","name":"Morning Report","schedule":"0 7 * * 1-5","timezone":"America/Los_Angeles","channel":"general","prompt":"Generate a brief morning status update","model":"fast"}</discord-action>
530
+ \`\`\`
531
+ - \`name\` (required): Human-readable name.
532
+ - \`schedule\` (required): 5-field cron expression (e.g., "0 7 * * 1-5").
533
+ - \`channel\` (required): Target channel name or ID.
534
+ - \`prompt\` (required): The instruction text.
535
+ - \`timezone\` (optional, default: system timezone, or DEFAULT_TIMEZONE env if set): IANA timezone.
536
+ - \`tags\` (optional): Comma-separated purpose tags.
537
+ - \`model\` (optional): "fast" or "capable" (auto-classified if omitted).
538
+
539
+ **cronUpdate** — Update a cron's settings:
540
+ \`\`\`
541
+ <discord-action>{"type":"cronUpdate","cronId":"cron-a1b2c3d4","schedule":"0 9 * * *","model":"capable"}</discord-action>
542
+ \`\`\`
543
+ - \`cronId\` (required): The stable cron ID.
544
+ - \`schedule\`, \`timezone\`, \`channel\`, \`prompt\`, \`model\`, \`tags\` (optional).
545
+
546
+ **cronList** — List all cron jobs:
547
+ \`\`\`
548
+ <discord-action>{"type":"cronList"}</discord-action>
549
+ \`\`\`
550
+
551
+ **cronShow** — Show full details for a cron:
552
+ \`\`\`
553
+ <discord-action>{"type":"cronShow","cronId":"cron-a1b2c3d4"}</discord-action>
554
+ \`\`\`
555
+
556
+ **cronPause** / **cronResume** — Pause or resume a cron:
557
+ \`\`\`
558
+ <discord-action>{"type":"cronPause","cronId":"cron-a1b2c3d4"}</discord-action>
559
+ <discord-action>{"type":"cronResume","cronId":"cron-a1b2c3d4"}</discord-action>
560
+ \`\`\`
561
+
562
+ **cronDelete** — Remove a cron job and archive its thread:
563
+ \`\`\`
564
+ <discord-action>{"type":"cronDelete","cronId":"cron-a1b2c3d4"}</discord-action>
565
+ \`\`\`
566
+ Note: cronDelete **archives** the thread (reversible) — it does not permanently
567
+ delete it. The thread history is preserved and the thread can be unarchived later
568
+ via the Discord UI, which will re-register the cron job automatically. Permanent
569
+ thread deletion can only be done manually through Discord.
570
+
571
+ **cronTrigger** — Immediately execute a cron (manual fire):
572
+ \`\`\`
573
+ <discord-action>{"type":"cronTrigger","cronId":"cron-a1b2c3d4"}</discord-action>
574
+ \`\`\`
575
+ Note: \`force\` overrides are disabled in Discord actions.
576
+
577
+ **cronSync** — Run full bidirectional sync:
578
+ \`\`\`
579
+ <discord-action>{"type":"cronSync"}</discord-action>
580
+ \`\`\`
581
+
582
+ **cronTagMapReload** — Reload tag map from disk and optionally trigger sync:
583
+ \`\`\`
584
+ <discord-action>{"type":"cronTagMapReload"}</discord-action>
585
+ \`\`\``;
586
+ }