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,303 @@
1
+ import { acquireCronLock, releaseCronLock } from './job-lock.js';
2
+ import { resolveChannel } from '../discord/action-utils.js';
3
+ import * as discordActions from '../discord/actions.js';
4
+ import { sendChunks, appendUnavailableActionTypesNotice } from '../discord/output-common.js';
5
+ import { buildPromptPreamble, loadWorkspacePaFiles, inlineContextFiles, resolveEffectiveTools } from '../discord/prompt-common.js';
6
+ import { ensureStatusMessage } from './discord-sync.js';
7
+ import { globalMetrics } from '../observability/metrics.js';
8
+ import { mapRuntimeErrorToUserMessage } from '../discord/user-errors.js';
9
+ import { resolveModel } from '../runtime/model-tiers.js';
10
+ async function recordError(ctx, job, msg) {
11
+ if (ctx.statsStore && job.cronId) {
12
+ try {
13
+ await ctx.statsStore.recordRun(job.cronId, 'error', msg.slice(0, 200));
14
+ }
15
+ catch {
16
+ // Best-effort.
17
+ }
18
+ }
19
+ }
20
+ export async function executeCronJob(job, ctx) {
21
+ const metrics = globalMetrics;
22
+ let cancelRequested = false;
23
+ const cancelReason = 'Run canceled by cron control action';
24
+ let runtimeIterator;
25
+ const requestCancel = () => {
26
+ cancelRequested = true;
27
+ if (runtimeIterator?.return) {
28
+ void runtimeIterator.return();
29
+ }
30
+ };
31
+ // Overlap guard: skip if previous run is still going (in-memory, no lock touched).
32
+ if (job.running) {
33
+ metrics.increment('cron.run.skipped');
34
+ ctx.log?.warn({ jobId: job.id, name: job.name }, 'cron:skip (previous run still active)');
35
+ return;
36
+ }
37
+ // File-based lock: prevents duplicate execution across processes.
38
+ let lockToken;
39
+ if (ctx.lockDir && job.cronId) {
40
+ try {
41
+ lockToken = await acquireCronLock(ctx.lockDir, job.cronId);
42
+ }
43
+ catch (err) {
44
+ metrics.increment('cron.run.skipped');
45
+ ctx.log?.warn({ jobId: job.id, cronId: job.cronId, err }, 'cron:skip (lock acquire failed)');
46
+ return;
47
+ }
48
+ }
49
+ job.running = true;
50
+ ctx.runControl?.register(job.id, requestCancel);
51
+ try {
52
+ // Resolve the target channel from the job's owning guild.
53
+ const guild = ctx.client.guilds.cache.get(job.guildId);
54
+ if (!guild) {
55
+ ctx.log?.error({ jobId: job.id, guildId: job.guildId }, 'cron:exec guild not found');
56
+ await ctx.status?.runtimeError({ sessionKey: `cron:${job.id}` }, `Cron "${job.name}": guild ${job.guildId} not found`);
57
+ await recordError(ctx, job, `guild ${job.guildId} not found`);
58
+ return;
59
+ }
60
+ const targetChannel = resolveChannel(guild, job.def.channel);
61
+ if (!targetChannel) {
62
+ ctx.log?.error({ jobId: job.id, channel: job.def.channel }, 'cron:exec target channel not found');
63
+ await ctx.status?.runtimeError({ sessionKey: `cron:${job.id}`, channelName: job.def.channel }, `Cron "${job.name}": target channel "${job.def.channel}" not found`);
64
+ await recordError(ctx, job, `target channel "${job.def.channel}" not found`);
65
+ return;
66
+ }
67
+ if (ctx.allowChannelIds) {
68
+ const ch = targetChannel;
69
+ const isThread = typeof ch?.isThread === 'function' ? ch.isThread() : false;
70
+ const parentId = isThread ? String(ch.parentId ?? '') : '';
71
+ const allowed = ctx.allowChannelIds.has(String(ch.id ?? '')) ||
72
+ (parentId && ctx.allowChannelIds.has(parentId));
73
+ if (!allowed) {
74
+ ctx.log?.error({ jobId: job.id, channel: job.def.channel }, 'cron:exec target channel not allowlisted');
75
+ await ctx.status?.runtimeError({ sessionKey: `cron:${job.id}`, channelName: job.def.channel }, `Cron "${job.name}": target channel "${job.def.channel}" is not allowlisted`);
76
+ await recordError(ctx, job, `target channel "${job.def.channel}" not allowlisted`);
77
+ return;
78
+ }
79
+ }
80
+ let inlinedContext = '';
81
+ try {
82
+ const paFiles = await loadWorkspacePaFiles(ctx.cwd);
83
+ inlinedContext = await inlineContextFiles(paFiles);
84
+ ctx.log?.debug?.({ jobId: job.id, paFileCount: paFiles.length }, 'cron:exec loaded workspace PA files');
85
+ }
86
+ catch (paErr) {
87
+ ctx.log?.warn?.({ jobId: job.id, err: paErr }, 'cron:exec PA file loading failed, continuing without context');
88
+ }
89
+ let prompt = buildPromptPreamble(inlinedContext) + '\n\n' +
90
+ `You are executing a scheduled cron job named "${job.name}".\n\n` +
91
+ `Instruction: ${job.def.prompt}\n\n` +
92
+ `Your output will be posted automatically to the Discord channel #${job.def.channel}. ` +
93
+ `Do NOT explain how to post or suggest using bots/webhooks — just write the message content directly. ` +
94
+ `Keep your response concise and focused on the instruction above.`;
95
+ const tools = await resolveEffectiveTools({
96
+ workspaceCwd: ctx.cwd,
97
+ runtimeTools: ctx.tools,
98
+ runtimeCapabilities: ctx.runtime.capabilities,
99
+ runtimeId: ctx.runtime.id,
100
+ log: ctx.log,
101
+ });
102
+ const effectiveTools = tools.effectiveTools;
103
+ if (tools.permissionNote || tools.runtimeCapabilityNote) {
104
+ const noteLines = [
105
+ tools.permissionNote ? `Permission note: ${tools.permissionNote}` : null,
106
+ tools.runtimeCapabilityNote ? `Runtime capability note: ${tools.runtimeCapabilityNote}` : null,
107
+ ].filter((line) => Boolean(line));
108
+ prompt += `\n\n---\n${noteLines.join('\n')}\n`;
109
+ }
110
+ // Per-cron model selection: per-job override > AI-classified > cron-exec default > chat fallback.
111
+ const cronDefault = ctx.cronExecModel || ctx.model;
112
+ let effectiveModel = cronDefault;
113
+ const preRunRecord = ctx.statsStore && job.cronId ? ctx.statsStore.getRecord(job.cronId) : undefined;
114
+ if (preRunRecord) {
115
+ effectiveModel = preRunRecord.modelOverride ?? preRunRecord.model ?? cronDefault;
116
+ }
117
+ ctx.log?.info({ jobId: job.id, name: job.name, channel: job.def.channel, model: effectiveModel, permissionTier: tools.permissionTier }, 'cron:exec start');
118
+ // Best-effort: update pinned status message to show running indicator.
119
+ if (preRunRecord && job.cronId) {
120
+ try {
121
+ await ensureStatusMessage(ctx.client, job.threadId, job.cronId, preRunRecord, ctx.statsStore, { log: ctx.log, running: true });
122
+ }
123
+ catch {
124
+ // Non-fatal — don't block execution.
125
+ }
126
+ }
127
+ metrics.recordInvokeStart('cron');
128
+ ctx.log?.info({ flow: 'cron', jobId: job.id, cronId: job.cronId }, 'obs.invoke.start');
129
+ let finalText = '';
130
+ let deltaText = '';
131
+ const collectedImages = [];
132
+ const t0 = Date.now();
133
+ try {
134
+ runtimeIterator = ctx.runtime.invoke({
135
+ prompt,
136
+ model: resolveModel(effectiveModel, ctx.runtime.id),
137
+ cwd: ctx.cwd,
138
+ addDirs: [ctx.cwd],
139
+ timeoutMs: ctx.timeoutMs,
140
+ tools: effectiveTools,
141
+ })[Symbol.asyncIterator]();
142
+ if (cancelRequested && runtimeIterator.return) {
143
+ await runtimeIterator.return();
144
+ }
145
+ while (true) {
146
+ const next = await runtimeIterator.next();
147
+ if (next.done)
148
+ break;
149
+ const evt = next.value;
150
+ if (cancelRequested)
151
+ break;
152
+ if (evt.type === 'text_final') {
153
+ finalText = evt.text;
154
+ }
155
+ else if (evt.type === 'text_delta') {
156
+ deltaText += evt.text;
157
+ }
158
+ else if (evt.type === 'image_data') {
159
+ collectedImages.push(evt.image);
160
+ }
161
+ else if (evt.type === 'error') {
162
+ metrics.recordInvokeResult('cron', Date.now() - t0, false, evt.message);
163
+ metrics.increment('cron.run.error');
164
+ ctx.log?.error({ jobId: job.id, error: evt.message }, 'cron:exec runtime error');
165
+ ctx.log?.warn({ flow: 'cron', jobId: job.id, error: evt.message }, 'obs.invoke.error');
166
+ await ctx.status?.runtimeError({ sessionKey: `cron:${job.id}`, channelName: job.def.channel }, `Cron "${job.name}": ${evt.message}`);
167
+ try {
168
+ await sendChunks(targetChannel, mapRuntimeErrorToUserMessage(evt.message));
169
+ }
170
+ catch {
171
+ // Best-effort user-facing signal; status channel/log already carry details.
172
+ }
173
+ await recordError(ctx, job, evt.message);
174
+ return;
175
+ }
176
+ }
177
+ }
178
+ catch (err) {
179
+ if (!cancelRequested)
180
+ throw err;
181
+ }
182
+ if (cancelRequested) {
183
+ if (runtimeIterator?.return) {
184
+ await runtimeIterator.return();
185
+ }
186
+ metrics.increment('cron.run.canceled');
187
+ ctx.log?.warn({ jobId: job.id, cronId: job.cronId }, 'cron:exec canceled');
188
+ await recordError(ctx, job, cancelReason);
189
+ return;
190
+ }
191
+ metrics.recordInvokeResult('cron', Date.now() - t0, true);
192
+ ctx.log?.info({ flow: 'cron', jobId: job.id, ms: Date.now() - t0, ok: true }, 'obs.invoke.end');
193
+ const output = finalText || deltaText;
194
+ if (!output.trim() && collectedImages.length === 0) {
195
+ metrics.increment('cron.run.skipped');
196
+ ctx.log?.warn({ jobId: job.id }, 'cron:exec empty output');
197
+ return;
198
+ }
199
+ let processedText = output;
200
+ let strippedUnrecognizedTypes = [];
201
+ // Handle Discord actions if enabled.
202
+ if (ctx.discordActionsEnabled) {
203
+ const parsed = discordActions.parseDiscordActions(processedText, ctx.actionFlags);
204
+ const { cleanText, actions } = parsed;
205
+ strippedUnrecognizedTypes = parsed.strippedUnrecognizedTypes;
206
+ if (actions.length > 0) {
207
+ const actCtx = {
208
+ guild,
209
+ client: ctx.client,
210
+ channelId: targetChannel.id,
211
+ messageId: '',
212
+ deferScheduler: ctx.deferScheduler,
213
+ confirmation: {
214
+ mode: 'automated',
215
+ },
216
+ };
217
+ const results = await discordActions.executeDiscordActions(actions, actCtx, ctx.log, {
218
+ taskCtx: ctx.taskCtx,
219
+ cronCtx: ctx.cronCtx,
220
+ forgeCtx: ctx.forgeCtx,
221
+ planCtx: ctx.planCtx,
222
+ memoryCtx: ctx.memoryCtx,
223
+ });
224
+ for (const result of results) {
225
+ metrics.recordActionResult(result.ok);
226
+ ctx.log?.info({ flow: 'cron', jobId: job.id, ok: result.ok }, 'obs.action.result');
227
+ }
228
+ const displayLines = discordActions.buildDisplayResultLines(actions, results);
229
+ const anyActionSucceeded = results.some((r) => r.ok);
230
+ processedText = displayLines.length > 0
231
+ ? cleanText.trimEnd() + '\n\n' + displayLines.join('\n')
232
+ : cleanText.trimEnd();
233
+ // When all display lines were suppressed and there's no prose, skip posting.
234
+ if (!processedText.trim() && anyActionSucceeded && strippedUnrecognizedTypes.length === 0) {
235
+ ctx.log?.info({ jobId: job.id }, 'cron:reply suppressed (actions-only, no display text)');
236
+ }
237
+ if (ctx.status) {
238
+ for (let i = 0; i < results.length; i++) {
239
+ if (!results[i].ok) {
240
+ await ctx.status.actionFailed(actions[i].type, results[i].error);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ else {
246
+ processedText = cleanText;
247
+ }
248
+ }
249
+ processedText = appendUnavailableActionTypesNotice(processedText, strippedUnrecognizedTypes);
250
+ await sendChunks(targetChannel, processedText, collectedImages);
251
+ ctx.log?.info({ jobId: job.id, name: job.name, channel: job.def.channel }, 'cron:exec done');
252
+ // Record successful run.
253
+ if (ctx.statsStore && job.cronId) {
254
+ try {
255
+ await ctx.statsStore.recordRun(job.cronId, 'success');
256
+ metrics.increment('cron.run.success');
257
+ }
258
+ catch (statsErr) {
259
+ ctx.log?.warn({ err: statsErr, jobId: job.id }, 'cron:exec stats record failed');
260
+ }
261
+ }
262
+ }
263
+ catch (err) {
264
+ const msg = err instanceof Error ? err.message : String(err);
265
+ metrics.increment('cron.run.error');
266
+ ctx.log?.error({ err, jobId: job.id }, 'cron:exec failed');
267
+ await ctx.status?.runtimeError({ sessionKey: `cron:${job.id}`, channelName: job.def.channel }, `Cron "${job.name}": ${msg}`);
268
+ if (ctx.client) {
269
+ const guild = ctx.client.guilds.cache.get(job.guildId);
270
+ const targetChannel = guild ? resolveChannel(guild, job.def.channel) : null;
271
+ if (targetChannel) {
272
+ try {
273
+ await sendChunks(targetChannel, mapRuntimeErrorToUserMessage(msg));
274
+ }
275
+ catch {
276
+ // Best-effort.
277
+ }
278
+ }
279
+ }
280
+ await recordError(ctx, job, msg);
281
+ }
282
+ finally {
283
+ if (lockToken && ctx.lockDir && job.cronId) {
284
+ await releaseCronLock(ctx.lockDir, job.cronId, lockToken).catch((err) => {
285
+ ctx.log?.warn({ err, jobId: job.id, cronId: job.cronId }, 'cron:exec lock release failed');
286
+ });
287
+ }
288
+ ctx.runControl?.clear(job.id, requestCancel);
289
+ job.running = false;
290
+ // Update bot-owned status message.
291
+ if (ctx.statsStore && job.cronId) {
292
+ try {
293
+ const record = ctx.statsStore.getRecord(job.cronId);
294
+ if (record) {
295
+ await ensureStatusMessage(ctx.client, job.threadId, job.cronId, record, ctx.statsStore, { log: ctx.log });
296
+ }
297
+ }
298
+ catch (statusErr) {
299
+ ctx.log?.warn({ err: statusErr, jobId: job.id }, 'cron:exec status message update failed');
300
+ }
301
+ }
302
+ }
303
+ }