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,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { TaskStore } from '../tasks/store.js';
3
+ import { findBeadByThreadId } from './bead-thread-cache.js';
4
+ function makeStore(externalRefs) {
5
+ const store = new TaskStore({ prefix: 'ws' });
6
+ for (const externalRef of externalRefs) {
7
+ const b = store.create({ title: 'Test' });
8
+ store.update(b.id, { externalRef });
9
+ }
10
+ return store;
11
+ }
12
+ describe('findBeadByThreadId', () => {
13
+ it('returns bead when external_ref matches as discord:<threadId>', () => {
14
+ const store = makeStore([
15
+ 'discord:111222333444555666',
16
+ 'discord:999888777666555444',
17
+ ]);
18
+ const result = findBeadByThreadId('111222333444555666', store);
19
+ expect(result?.id).toBe('ws-001');
20
+ });
21
+ it('returns bead when external_ref is raw numeric ID', () => {
22
+ const store = makeStore(['111222333444555666']);
23
+ const result = findBeadByThreadId('111222333444555666', store);
24
+ expect(result?.id).toBe('ws-001');
25
+ });
26
+ it('returns null when no match', () => {
27
+ const store = makeStore(['discord:999888777666555444']);
28
+ const result = findBeadByThreadId('111222333444555666', store);
29
+ expect(result).toBeNull();
30
+ });
31
+ it('returns null when store is empty', () => {
32
+ const store = new TaskStore();
33
+ const result = findBeadByThreadId('111222333444555666', store);
34
+ expect(result).toBeNull();
35
+ });
36
+ });
@@ -0,0 +1,2 @@
1
+ import { initTasksForumGuard, } from '../tasks/forum-guard.js';
2
+ export const initBeadsForumGuard = initTasksForumGuard;
@@ -0,0 +1,204 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { TaskStore } from '../tasks/store.js';
3
+ import { initBeadsForumGuard } from './forum-guard.js';
4
+ import { initTasksForumGuard } from '../tasks/forum-guard.js';
5
+ vi.mock('../tasks/thread-cache.js', () => ({
6
+ findTaskByThreadId: vi.fn(),
7
+ }));
8
+ vi.mock('../tasks/discord-sync.js', () => ({
9
+ buildAppliedTagsWithStatus: vi.fn(() => ['tag-closed']),
10
+ buildThreadName: vi.fn(() => '✅ [001] My Bead'),
11
+ }));
12
+ import { findTaskByThreadId } from '../tasks/thread-cache.js';
13
+ function makeClient(botUserId = 'bot-user-1') {
14
+ const listeners = {};
15
+ return {
16
+ on: vi.fn((event, cb) => {
17
+ (listeners[event] ??= []).push(cb);
18
+ }),
19
+ user: { id: botUserId },
20
+ _listeners: listeners,
21
+ };
22
+ }
23
+ function makeThread(overrides) {
24
+ return {
25
+ id: 'thread-1',
26
+ name: 'Bead 1',
27
+ parentId: 'beads-forum-1',
28
+ ownerId: 'bot-user-1',
29
+ appliedTags: [],
30
+ send: vi.fn().mockResolvedValue(undefined),
31
+ setArchived: vi.fn().mockResolvedValue(undefined),
32
+ setName: vi.fn().mockResolvedValue(undefined),
33
+ edit: vi.fn().mockResolvedValue(undefined),
34
+ client: { user: { id: 'bot-user-1' } },
35
+ ...overrides,
36
+ };
37
+ }
38
+ const MOCK_BEAD = { id: 'ws-001', title: 'My Bead', status: 'closed' };
39
+ describe('initBeadsForumGuard', () => {
40
+ it('keeps compatibility export aligned to canonical task guard', () => {
41
+ expect(initBeadsForumGuard).toBe(initTasksForumGuard);
42
+ });
43
+ function setup(botUserId = 'bot-user-1') {
44
+ const client = makeClient(botUserId);
45
+ const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
46
+ initBeadsForumGuard({ client: client, forumId: 'beads-forum-1', log });
47
+ const listeners = client._listeners['threadCreate'] ?? [];
48
+ expect(listeners.length).toBeGreaterThan(0);
49
+ return { listener: listeners[0], log };
50
+ }
51
+ it('rejects manually-created threads with guidance and archives', async () => {
52
+ const { listener } = setup();
53
+ const thread = makeThread({ ownerId: 'some-user' });
54
+ await listener(thread);
55
+ expect(thread.send).toHaveBeenCalledWith(expect.stringContaining('bd create'));
56
+ expect(thread.setArchived).toHaveBeenCalledWith(true);
57
+ });
58
+ it('allows bot-created threads through without sending or archiving', async () => {
59
+ const { listener } = setup();
60
+ const thread = makeThread({ ownerId: 'bot-user-1' });
61
+ await listener(thread);
62
+ expect(thread.send).not.toHaveBeenCalled();
63
+ expect(thread.setArchived).not.toHaveBeenCalled();
64
+ });
65
+ it('ignores threads from other forums', async () => {
66
+ const { listener } = setup();
67
+ const thread = makeThread({ parentId: 'other-forum', ownerId: 'some-user' });
68
+ await listener(thread);
69
+ expect(thread.send).not.toHaveBeenCalled();
70
+ expect(thread.setArchived).not.toHaveBeenCalled();
71
+ });
72
+ it('handles send failure without preventing archive attempt', async () => {
73
+ const { listener } = setup();
74
+ const thread = makeThread({ ownerId: 'some-user' });
75
+ thread.send.mockRejectedValue(new Error('Missing Access'));
76
+ await listener(thread);
77
+ expect(thread.setArchived).toHaveBeenCalledWith(true);
78
+ });
79
+ });
80
+ describe('initBeadsForumGuard threadUpdate', () => {
81
+ function setup(botUserId = 'bot-user-1') {
82
+ const client = makeClient(botUserId);
83
+ const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
84
+ initBeadsForumGuard({ client: client, forumId: 'beads-forum-1', log });
85
+ const listeners = client._listeners['threadUpdate'] ?? [];
86
+ expect(listeners.length).toBeGreaterThan(0);
87
+ return { listener: listeners[0], log };
88
+ }
89
+ it('rejects unarchived manual thread', async () => {
90
+ const { listener } = setup();
91
+ const oldThread = makeThread({ ownerId: 'some-user', archived: true });
92
+ const newThread = makeThread({ ownerId: 'some-user', archived: false });
93
+ await listener(oldThread, newThread);
94
+ expect(newThread.send).toHaveBeenCalledWith(expect.stringContaining('bd create'));
95
+ expect(newThread.setArchived).toHaveBeenCalledWith(true);
96
+ });
97
+ it('allows bot-owned unarchived thread through', async () => {
98
+ const { listener } = setup();
99
+ const oldThread = makeThread({ archived: true });
100
+ const newThread = makeThread({ archived: false });
101
+ await listener(oldThread, newThread);
102
+ expect(newThread.send).not.toHaveBeenCalled();
103
+ expect(newThread.setArchived).not.toHaveBeenCalled();
104
+ });
105
+ it('ignores archive transitions (thread being archived)', async () => {
106
+ const { listener } = setup();
107
+ const oldThread = makeThread({ ownerId: 'some-user', archived: false });
108
+ const newThread = makeThread({ ownerId: 'some-user', archived: true });
109
+ await listener(oldThread, newThread);
110
+ expect(newThread.send).not.toHaveBeenCalled();
111
+ });
112
+ });
113
+ describe('initBeadsForumGuard bead-aware re-archive (threadCreate)', () => {
114
+ function setup() {
115
+ const client = makeClient();
116
+ const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
117
+ initBeadsForumGuard({
118
+ client: client,
119
+ forumId: 'beads-forum-1',
120
+ log,
121
+ store: new TaskStore(),
122
+ tagMap: { closed: 'tag-closed' },
123
+ });
124
+ const listener = (client._listeners['threadCreate'] ?? [])[0];
125
+ return { listener, log };
126
+ }
127
+ it('re-archives known bead thread without sending rejection message', async () => {
128
+ vi.mocked(findTaskByThreadId).mockReturnValue(MOCK_BEAD);
129
+ const { listener } = setup();
130
+ const thread = makeThread({ ownerId: 'some-user' });
131
+ await listener(thread);
132
+ expect(thread.send).not.toHaveBeenCalled();
133
+ expect(thread.edit).toHaveBeenCalled();
134
+ expect(thread.setName).toHaveBeenCalledWith('✅ [001] My Bead');
135
+ expect(thread.setArchived).toHaveBeenCalledWith(true);
136
+ });
137
+ it('falls through to rejection when bead lookup returns null', async () => {
138
+ vi.mocked(findTaskByThreadId).mockReturnValue(null);
139
+ const { listener } = setup();
140
+ const thread = makeThread({ ownerId: 'some-user' });
141
+ await listener(thread);
142
+ expect(thread.send).toHaveBeenCalledWith(expect.stringContaining('bd create'));
143
+ expect(thread.setArchived).toHaveBeenCalledWith(true);
144
+ });
145
+ it('falls through to rejection when bead lookup throws', async () => {
146
+ vi.mocked(findTaskByThreadId).mockImplementation(() => { throw new Error('fs error'); });
147
+ const { listener } = setup();
148
+ const thread = makeThread({ ownerId: 'some-user' });
149
+ await listener(thread);
150
+ expect(thread.send).toHaveBeenCalledWith(expect.stringContaining('bd create'));
151
+ expect(thread.setArchived).toHaveBeenCalledWith(true);
152
+ });
153
+ it('still archives even when edit throws', async () => {
154
+ vi.mocked(findTaskByThreadId).mockReturnValue(MOCK_BEAD);
155
+ const { listener } = setup();
156
+ const thread = makeThread({ ownerId: 'some-user' });
157
+ thread.edit.mockRejectedValue(new Error('edit failed'));
158
+ await listener(thread);
159
+ expect(thread.setArchived).toHaveBeenCalledWith(true);
160
+ });
161
+ it('still archives even when setName throws', async () => {
162
+ vi.mocked(findTaskByThreadId).mockReturnValue(MOCK_BEAD);
163
+ const { listener } = setup();
164
+ const thread = makeThread({ ownerId: 'some-user' });
165
+ thread.setName.mockRejectedValue(new Error('setName failed'));
166
+ await listener(thread);
167
+ expect(thread.setArchived).toHaveBeenCalledWith(true);
168
+ });
169
+ });
170
+ describe('initBeadsForumGuard bead-aware re-archive (threadUpdate)', () => {
171
+ function setup() {
172
+ const client = makeClient();
173
+ const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
174
+ initBeadsForumGuard({
175
+ client: client,
176
+ forumId: 'beads-forum-1',
177
+ log,
178
+ store: new TaskStore(),
179
+ tagMap: { closed: 'tag-closed' },
180
+ });
181
+ const listener = (client._listeners['threadUpdate'] ?? [])[0];
182
+ return { listener, log };
183
+ }
184
+ it('re-archives known bead thread on unarchive without rejection message', async () => {
185
+ vi.mocked(findTaskByThreadId).mockReturnValue(MOCK_BEAD);
186
+ const { listener } = setup();
187
+ const oldThread = makeThread({ ownerId: 'some-user', archived: true });
188
+ const newThread = makeThread({ ownerId: 'some-user', archived: false });
189
+ await listener(oldThread, newThread);
190
+ expect(newThread.send).not.toHaveBeenCalled();
191
+ expect(newThread.edit).toHaveBeenCalled();
192
+ expect(newThread.setName).toHaveBeenCalledWith('✅ [001] My Bead');
193
+ expect(newThread.setArchived).toHaveBeenCalledWith(true);
194
+ });
195
+ it('falls through to rejection when bead not found on threadUpdate', async () => {
196
+ vi.mocked(findTaskByThreadId).mockReturnValue(null);
197
+ const { listener } = setup();
198
+ const oldThread = makeThread({ ownerId: 'some-user', archived: true });
199
+ const newThread = makeThread({ ownerId: 'some-user', archived: false });
200
+ await listener(oldThread, newThread);
201
+ expect(newThread.send).toHaveBeenCalledWith(expect.stringContaining('bd create'));
202
+ expect(newThread.setArchived).toHaveBeenCalledWith(true);
203
+ });
204
+ });
@@ -0,0 +1,3 @@
1
+ import { initializeTasksContext, wireTaskSync, } from '../tasks/initialize.js';
2
+ export const initializeBeadsContext = initializeTasksContext;
3
+ export const wireBeadsSync = wireTaskSync;
@@ -0,0 +1,304 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ // ---------------------------------------------------------------------------
3
+ // Module mocks
4
+ // ---------------------------------------------------------------------------
5
+ vi.mock('../tasks/discord-sync.js', () => ({
6
+ loadTagMap: vi.fn().mockResolvedValue({ bug: '111', feature: '222' }),
7
+ }));
8
+ vi.mock('../tasks/forum-guard.js', () => ({
9
+ initTasksForumGuard: vi.fn(),
10
+ }));
11
+ vi.mock('../tasks/sync-coordinator.js', () => ({
12
+ TaskSyncCoordinator: vi.fn().mockImplementation(() => ({
13
+ sync: vi.fn().mockResolvedValue(undefined),
14
+ })),
15
+ }));
16
+ import { initTasksForumGuard } from '../tasks/forum-guard.js';
17
+ import { TaskSyncCoordinator } from '../tasks/sync-coordinator.js';
18
+ import { initializeBeadsContext, wireBeadsSync } from './initialize.js';
19
+ import { initializeTasksContext, wireTaskSync } from '../tasks/initialize.js';
20
+ import { TaskStore } from '../tasks/store.js';
21
+ import { withDirectTaskLifecycle } from '../tasks/task-lifecycle.js';
22
+ function fakeLog() {
23
+ return {
24
+ info: vi.fn(),
25
+ warn: vi.fn(),
26
+ error: vi.fn(),
27
+ };
28
+ }
29
+ function baseOpts(overrides = {}) {
30
+ return {
31
+ enabled: true,
32
+ tasksCwd: '/tmp/beads',
33
+ tasksForum: 'forum-123',
34
+ tasksTagMapPath: '/tmp/tag-map.json',
35
+ tasksSidebar: false,
36
+ tasksAutoTag: true,
37
+ tasksAutoTagModel: 'haiku',
38
+ runtime: {},
39
+ log: fakeLog(),
40
+ ...overrides,
41
+ };
42
+ }
43
+ describe('initializeBeadsContext', () => {
44
+ it('keeps compatibility exports aligned to canonical task initialize helpers', () => {
45
+ expect(initializeBeadsContext).toBe(initializeTasksContext);
46
+ expect(wireBeadsSync).toBe(wireTaskSync);
47
+ });
48
+ it('returns undefined with no warnings when disabled', async () => {
49
+ const log = fakeLog();
50
+ const result = await initializeBeadsContext(baseOpts({ enabled: false, log }));
51
+ expect(result.taskCtx).toBeUndefined();
52
+ expect(log.warn).not.toHaveBeenCalled();
53
+ });
54
+ it('returns undefined and warns when no forum resolved', async () => {
55
+ const log = fakeLog();
56
+ const result = await initializeBeadsContext(baseOpts({
57
+ tasksForum: '',
58
+ systemTasksForumId: undefined,
59
+ log,
60
+ }));
61
+ expect(result.taskCtx).toBeUndefined();
62
+ expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('no forum resolved'));
63
+ });
64
+ it('returns TaskContext when all prerequisites met', async () => {
65
+ const log = fakeLog();
66
+ const result = await initializeBeadsContext(baseOpts({ log }));
67
+ expect(result.taskCtx).toBeDefined();
68
+ expect(result.taskCtx.forumId).toBe('forum-123');
69
+ expect(result.taskCtx.autoTag).toBe(true);
70
+ });
71
+ it('resolves forum from systemTasksForumId when tasksForum is empty', async () => {
72
+ const result = await initializeBeadsContext(baseOpts({
73
+ tasksForum: '',
74
+ systemTasksForumId: 'system-forum-456',
75
+ }));
76
+ expect(result.taskCtx).toBeDefined();
77
+ expect(result.taskCtx.forumId).toBe('system-forum-456');
78
+ });
79
+ it('sets sidebarMentionUserId when sidebar enabled with mention user', async () => {
80
+ const log = fakeLog();
81
+ const result = await initializeBeadsContext(baseOpts({
82
+ tasksSidebar: true,
83
+ tasksMentionUser: 'user-789',
84
+ log,
85
+ }));
86
+ expect(result.taskCtx).toBeDefined();
87
+ expect(result.taskCtx.sidebarMentionUserId).toBe('user-789');
88
+ expect(log.warn).not.toHaveBeenCalled();
89
+ });
90
+ it('warns when sidebar enabled but mention user not set', async () => {
91
+ const log = fakeLog();
92
+ const result = await initializeBeadsContext(baseOpts({
93
+ tasksSidebar: true,
94
+ tasksMentionUser: undefined,
95
+ log,
96
+ }));
97
+ expect(result.taskCtx).toBeDefined();
98
+ expect(result.taskCtx.sidebarMentionUserId).toBeUndefined();
99
+ expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('sidebar mentions will be inactive'));
100
+ });
101
+ it('does not set sidebarMentionUserId when sidebar disabled', async () => {
102
+ const log = fakeLog();
103
+ const result = await initializeBeadsContext(baseOpts({
104
+ tasksSidebar: false,
105
+ tasksMentionUser: 'user-789',
106
+ log,
107
+ }));
108
+ expect(result.taskCtx).toBeDefined();
109
+ expect(result.taskCtx.sidebarMentionUserId).toBeUndefined();
110
+ expect(log.warn).not.toHaveBeenCalled();
111
+ });
112
+ it('propagates tagMapPath to TaskContext', async () => {
113
+ const result = await initializeBeadsContext(baseOpts({
114
+ tasksTagMapPath: '/my/custom/tag-map.json',
115
+ }));
116
+ expect(result.taskCtx).toBeDefined();
117
+ expect(result.taskCtx.tagMapPath).toBe('/my/custom/tag-map.json');
118
+ });
119
+ it('uses provided store instead of creating a new one', async () => {
120
+ const store = new TaskStore();
121
+ const result = await initializeBeadsContext(baseOpts({ store }));
122
+ expect(result.taskCtx).toBeDefined();
123
+ expect(result.taskCtx.store).toBe(store);
124
+ });
125
+ });
126
+ describe('wireBeadsSync', () => {
127
+ it('wires forum guard, coordinator, and store event listeners', async () => {
128
+ const log = fakeLog();
129
+ const store = new TaskStore();
130
+ const taskCtx = {
131
+ tasksCwd: '/tmp/beads',
132
+ forumId: 'forum-123',
133
+ tagMap: { bug: '111' },
134
+ store,
135
+ log,
136
+ };
137
+ const client = {};
138
+ const guild = {};
139
+ const result = await wireBeadsSync({
140
+ taskCtx,
141
+ client,
142
+ guild,
143
+ guildId: 'guild-1',
144
+ tasksCwd: '/tmp/beads',
145
+ sidebarMentionUserId: 'user-1',
146
+ log,
147
+ });
148
+ expect(initTasksForumGuard).toHaveBeenCalledWith({
149
+ client,
150
+ forumId: 'forum-123',
151
+ log,
152
+ store,
153
+ tagMap: { bug: '111' },
154
+ });
155
+ expect(TaskSyncCoordinator).toHaveBeenCalledWith(expect.objectContaining({
156
+ client,
157
+ guild,
158
+ forumId: 'forum-123',
159
+ mentionUserId: 'user-1',
160
+ }));
161
+ // The coordinator's sync() should have been called (fire-and-forget startup sync).
162
+ const coordinatorInstance = vi.mocked(TaskSyncCoordinator).mock.results[0]?.value;
163
+ expect(coordinatorInstance.sync).toHaveBeenCalled();
164
+ expect(taskCtx.syncCoordinator).toBeDefined();
165
+ expect(result).toHaveProperty('stop');
166
+ expect(log.info).toHaveBeenCalledWith(expect.objectContaining({ tasksCwd: '/tmp/beads' }), 'tasks:store-event watcher started');
167
+ });
168
+ it('store events trigger coordinator sync', async () => {
169
+ const log = fakeLog();
170
+ const store = new TaskStore({ prefix: 'test' });
171
+ const taskCtx = {
172
+ tasksCwd: '/tmp/beads',
173
+ forumId: 'forum-123',
174
+ tagMap: { bug: '111' },
175
+ tagMapPath: '/tmp/tag-map.json',
176
+ store,
177
+ log,
178
+ };
179
+ vi.mocked(TaskSyncCoordinator).mockClear();
180
+ await wireBeadsSync({
181
+ taskCtx,
182
+ client: {},
183
+ guild: {},
184
+ guildId: 'guild-1',
185
+ tasksCwd: '/tmp/beads',
186
+ log,
187
+ });
188
+ const coordinatorInstance = vi.mocked(TaskSyncCoordinator).mock.results[0]?.value;
189
+ // 'created' is intentionally NOT wired — beadCreate handles thread creation directly.
190
+ const callsBeforeCreate = coordinatorInstance.sync.mock.calls.length;
191
+ const bead = store.create({ title: 'Test task' });
192
+ expect(coordinatorInstance.sync.mock.calls.length).toBe(callsBeforeCreate);
193
+ // 'updated' IS wired — should trigger sync.
194
+ const callsBeforeUpdate = coordinatorInstance.sync.mock.calls.length;
195
+ store.update(bead.id, { title: 'Updated task' });
196
+ expect(coordinatorInstance.sync.mock.calls.length).toBeGreaterThan(callsBeforeUpdate);
197
+ });
198
+ it('does not trigger coordinator sync while direct task lifecycle ownership is active', async () => {
199
+ const log = fakeLog();
200
+ const store = new TaskStore({ prefix: 'test' });
201
+ const taskCtx = {
202
+ tasksCwd: '/tmp/beads',
203
+ forumId: 'forum-123',
204
+ tagMap: { bug: '111' },
205
+ tagMapPath: '/tmp/tag-map.json',
206
+ store,
207
+ log,
208
+ };
209
+ vi.mocked(TaskSyncCoordinator).mockClear();
210
+ await wireBeadsSync({
211
+ taskCtx,
212
+ client: {},
213
+ guild: {},
214
+ guildId: 'guild-1',
215
+ tasksCwd: '/tmp/beads',
216
+ log,
217
+ });
218
+ const coordinatorInstance = vi.mocked(TaskSyncCoordinator).mock.results[0]?.value;
219
+ const task = store.create({ title: 'Owned lifecycle task' });
220
+ const callsBeforeUpdate = coordinatorInstance.sync.mock.calls.length;
221
+ await withDirectTaskLifecycle(task.id, async () => {
222
+ store.update(task.id, { title: 'Updated while owned' });
223
+ });
224
+ expect(coordinatorInstance.sync.mock.calls.length).toBe(callsBeforeUpdate);
225
+ });
226
+ it('stop() removes store event listeners', async () => {
227
+ const log = fakeLog();
228
+ const store = new TaskStore({ prefix: 'test' });
229
+ const taskCtx = {
230
+ tasksCwd: '/tmp/beads',
231
+ forumId: 'forum-123',
232
+ tagMap: { bug: '111' },
233
+ tagMapPath: '/tmp/tag-map.json',
234
+ store,
235
+ log,
236
+ };
237
+ vi.mocked(TaskSyncCoordinator).mockClear();
238
+ const result = await wireBeadsSync({
239
+ taskCtx,
240
+ client: {},
241
+ guild: {},
242
+ guildId: 'guild-1',
243
+ tasksCwd: '/tmp/beads',
244
+ log,
245
+ });
246
+ result.stop();
247
+ const coordinatorInstance = vi.mocked(TaskSyncCoordinator).mock.results[0]?.value;
248
+ const callsAfterStop = coordinatorInstance.sync.mock.calls.length;
249
+ // After stop(), store mutations should NOT trigger additional syncs
250
+ const bead = store.create({ title: 'Another task' });
251
+ store.update(bead.id, { title: 'Modified' });
252
+ expect(coordinatorInstance.sync.mock.calls.length).toBe(callsAfterStop);
253
+ });
254
+ it('skips forum guard when skipForumGuard is true', async () => {
255
+ const log = fakeLog();
256
+ const store = new TaskStore();
257
+ const taskCtx = {
258
+ tasksCwd: '/tmp/beads',
259
+ forumId: 'forum-123',
260
+ tagMap: { bug: '111' },
261
+ tagMapPath: '/tmp/tag-map.json',
262
+ store,
263
+ log,
264
+ };
265
+ vi.mocked(initTasksForumGuard).mockClear();
266
+ await wireBeadsSync({
267
+ taskCtx,
268
+ client: {},
269
+ guild: {},
270
+ guildId: 'guild-1',
271
+ tasksCwd: '/tmp/beads',
272
+ log,
273
+ skipForumGuard: true,
274
+ });
275
+ expect(initTasksForumGuard).not.toHaveBeenCalled();
276
+ // Coordinator should still be wired
277
+ expect(TaskSyncCoordinator).toHaveBeenCalled();
278
+ });
279
+ it('propagates tagMapPath to CoordinatorOptions', async () => {
280
+ const log = fakeLog();
281
+ const tagMap = { bug: '111' };
282
+ const store = new TaskStore();
283
+ const taskCtx = {
284
+ tasksCwd: '/tmp/beads',
285
+ forumId: 'forum-123',
286
+ tagMap,
287
+ tagMapPath: '/config/tag-map.json',
288
+ store,
289
+ log,
290
+ };
291
+ await wireBeadsSync({
292
+ taskCtx,
293
+ client: {},
294
+ guild: {},
295
+ guildId: 'guild-1',
296
+ tasksCwd: '/tmp/beads',
297
+ log,
298
+ });
299
+ expect(TaskSyncCoordinator).toHaveBeenCalledWith(expect.objectContaining({
300
+ tagMapPath: '/config/tag-map.json',
301
+ tagMap,
302
+ }));
303
+ });
304
+ });
@@ -0,0 +1,10 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Re-export shim — canonical types live in src/tasks/types.ts.
3
+ // All Bead* names are aliased from their Task* counterparts so existing
4
+ // consumers compile unchanged.
5
+ // ---------------------------------------------------------------------------
6
+ export { TASK_STATUSES, isTaskStatus } from '../tasks/types.js';
7
+ export { TASK_STATUSES as BEAD_STATUSES, isTaskStatus as isBeadStatus } from '../tasks/types.js';
8
+ import { STATUS_EMOJI as _STATUS_EMOJI } from '../tasks/types.js';
9
+ /** Status → emoji prefix for thread names. Widened to Record<string, string> for consumers that index with a plain string. */
10
+ export const STATUS_EMOJI = _STATUS_EMOJI;