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,359 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import path from 'node:path';
3
+ import { parseBdJson, normalizeTaskData, bdList, ensureBdDatabaseReady, buildTaskContextSummary } from './bd-cli.js';
4
+ import { TaskStore } from './store.js';
5
+ vi.mock('execa', () => ({
6
+ execa: vi.fn(),
7
+ }));
8
+ vi.mock('node:fs/promises', () => ({
9
+ default: {
10
+ realpath: vi.fn(async (p) => p),
11
+ },
12
+ }));
13
+ // ---------------------------------------------------------------------------
14
+ // parseBdJson
15
+ // ---------------------------------------------------------------------------
16
+ describe('parseBdJson', () => {
17
+ it('parses array output', () => {
18
+ const input = JSON.stringify([
19
+ { id: 'ws-001', title: 'Test', status: 'open' },
20
+ { id: 'ws-002', title: 'Test 2', status: 'closed' },
21
+ ]);
22
+ const result = parseBdJson(input);
23
+ expect(result).toHaveLength(2);
24
+ expect(result[0].id).toBe('ws-001');
25
+ expect(result[1].id).toBe('ws-002');
26
+ });
27
+ it('parses single-object output', () => {
28
+ const input = JSON.stringify({ id: 'ws-001', title: 'Test', status: 'open' });
29
+ const result = parseBdJson(input);
30
+ expect(result).toHaveLength(1);
31
+ expect(result[0].id).toBe('ws-001');
32
+ });
33
+ it('strips markdown fences', () => {
34
+ const input = '```json\n[{"id":"ws-001","title":"Test"}]\n```';
35
+ const result = parseBdJson(input);
36
+ expect(result).toHaveLength(1);
37
+ expect(result[0].id).toBe('ws-001');
38
+ });
39
+ it('strips bare markdown fences (no language tag)', () => {
40
+ const input = '```\n{"id":"ws-001","title":"Test"}\n```';
41
+ const result = parseBdJson(input);
42
+ expect(result).toHaveLength(1);
43
+ });
44
+ it('returns empty array for empty input', () => {
45
+ expect(parseBdJson('')).toEqual([]);
46
+ expect(parseBdJson(' \n ')).toEqual([]);
47
+ });
48
+ it('throws on error-only object', () => {
49
+ const input = JSON.stringify({ error: 'not found' });
50
+ expect(() => parseBdJson(input)).toThrow('not found');
51
+ });
52
+ it('throws on malformed JSON', () => {
53
+ expect(() => parseBdJson('{bad json}')).toThrow();
54
+ });
55
+ it('returns empty array for non-object JSON', () => {
56
+ expect(parseBdJson('"just a string"')).toEqual([]);
57
+ expect(parseBdJson('42')).toEqual([]);
58
+ expect(parseBdJson('null')).toEqual([]);
59
+ });
60
+ });
61
+ // ---------------------------------------------------------------------------
62
+ // normalizeTaskData
63
+ // ---------------------------------------------------------------------------
64
+ describe('normalizeTaskData', () => {
65
+ const baseTask = {
66
+ id: 'ws-001',
67
+ title: 'Test task',
68
+ status: 'open',
69
+ };
70
+ it('maps "done" → "closed"', () => {
71
+ const task = { ...baseTask, status: 'done' };
72
+ expect(normalizeTaskData(task).status).toBe('closed');
73
+ });
74
+ it('maps "tombstone" → "closed"', () => {
75
+ const task = { ...baseTask, status: 'tombstone' };
76
+ expect(normalizeTaskData(task).status).toBe('closed');
77
+ });
78
+ it('does not mutate the original task when mapping', () => {
79
+ const task = { ...baseTask, status: 'done' };
80
+ normalizeTaskData(task);
81
+ expect(task.status).toBe('done');
82
+ });
83
+ it.each(['open', 'in_progress', 'blocked', 'closed'])('passes through valid status "%s" unchanged', (status) => {
84
+ const task = { ...baseTask, status };
85
+ const result = normalizeTaskData(task);
86
+ expect(result.status).toBe(status);
87
+ expect(result).toBe(task); // same reference — no copy
88
+ });
89
+ });
90
+ // ---------------------------------------------------------------------------
91
+ // runBd (via bdList) — argument construction (--db, --no-daemon pinning)
92
+ // ---------------------------------------------------------------------------
93
+ describe('runBd argument construction', () => {
94
+ let mockExeca;
95
+ beforeEach(async () => {
96
+ const mod = await import('execa');
97
+ mockExeca = mod.execa;
98
+ mockExeca.mockReset();
99
+ });
100
+ it('prepends --db and --no-daemon to execa args', async () => {
101
+ mockExeca.mockResolvedValueOnce({
102
+ exitCode: 0,
103
+ stdout: '[]',
104
+ stderr: '',
105
+ });
106
+ await bdList({}, '/home/user/workspace');
107
+ expect(mockExeca).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([
108
+ '--db', '/home/user/workspace/.beads/beads.db',
109
+ '--no-daemon',
110
+ ]), expect.objectContaining({ cwd: '/home/user/workspace' }));
111
+ });
112
+ it('resolves relative cwd to absolute dbPath', async () => {
113
+ mockExeca.mockResolvedValueOnce({
114
+ exitCode: 0,
115
+ stdout: '[]',
116
+ stderr: '',
117
+ });
118
+ await bdList({}, 'workspace');
119
+ const calledArgs = mockExeca.mock.calls[0][1];
120
+ const dbArg = calledArgs[calledArgs.indexOf('--db') + 1];
121
+ // path.resolve('workspace', ...) produces an absolute path
122
+ expect(path.isAbsolute(dbArg)).toBe(true);
123
+ expect(dbArg).toBe(path.resolve('workspace', '.beads', 'beads.db'));
124
+ });
125
+ it('passes --limit 0 when status is all and no explicit limit', async () => {
126
+ mockExeca.mockResolvedValueOnce({
127
+ exitCode: 0,
128
+ stdout: '[]',
129
+ stderr: '',
130
+ });
131
+ await bdList({ status: 'all' }, '/tmp');
132
+ const calledArgs = mockExeca.mock.calls[0][1];
133
+ expect(calledArgs).toContain('--all');
134
+ expect(calledArgs).toContain('--limit');
135
+ const limitIdx = calledArgs.indexOf('--limit');
136
+ expect(calledArgs[limitIdx + 1]).toBe('0');
137
+ });
138
+ it('respects explicit limit when status is all', async () => {
139
+ mockExeca.mockResolvedValueOnce({
140
+ exitCode: 0,
141
+ stdout: '[]',
142
+ stderr: '',
143
+ });
144
+ await bdList({ status: 'all', limit: 10 }, '/tmp');
145
+ const calledArgs = mockExeca.mock.calls[0][1];
146
+ expect(calledArgs).toContain('--all');
147
+ expect(calledArgs).toContain('--limit');
148
+ const limitIdx = calledArgs.indexOf('--limit');
149
+ expect(calledArgs[limitIdx + 1]).toBe('10');
150
+ });
151
+ it('places --db and --no-daemon before subcommand args', async () => {
152
+ mockExeca.mockResolvedValueOnce({
153
+ exitCode: 0,
154
+ stdout: '[]',
155
+ stderr: '',
156
+ });
157
+ await bdList({ status: 'open' }, '/tmp');
158
+ const calledArgs = mockExeca.mock.calls[0][1];
159
+ const dbIdx = calledArgs.indexOf('--db');
160
+ const noDaemonIdx = calledArgs.indexOf('--no-daemon');
161
+ const listIdx = calledArgs.indexOf('list');
162
+ expect(dbIdx).toBeLessThan(listIdx);
163
+ expect(noDaemonIdx).toBeLessThan(listIdx);
164
+ });
165
+ });
166
+ // ---------------------------------------------------------------------------
167
+ // ensureBdDatabaseReady
168
+ // ---------------------------------------------------------------------------
169
+ describe('ensureBdDatabaseReady', () => {
170
+ let mockExeca;
171
+ let mockRealpath;
172
+ beforeEach(async () => {
173
+ const mod = await import('execa');
174
+ mockExeca = mod.execa;
175
+ mockExeca.mockReset();
176
+ const fsMod = await import('node:fs/promises');
177
+ mockRealpath = fsMod.default.realpath;
178
+ // Default: identity (no symlink). Tests that need symlink behavior override this.
179
+ mockRealpath.mockReset();
180
+ mockRealpath.mockImplementation(async (p) => p);
181
+ });
182
+ it('returns ready with prefix when prefix already set', async () => {
183
+ mockExeca.mockResolvedValueOnce({
184
+ exitCode: 0,
185
+ stdout: 'dev',
186
+ stderr: '',
187
+ });
188
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
189
+ expect(result).toEqual({ ready: true, prefix: 'dev' });
190
+ // Should only call config get, never config set
191
+ expect(mockExeca).toHaveBeenCalledTimes(1);
192
+ expect(mockExeca).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['config', 'get', 'issue_prefix']), expect.objectContaining({ cwd: '/home/user/discoclaw-data/workspace', reject: false }));
193
+ });
194
+ it('auto-initializes prefix when "(not set)" and set succeeds', async () => {
195
+ // First call: config get returns "(not set)"
196
+ mockExeca.mockResolvedValueOnce({
197
+ exitCode: 0,
198
+ stdout: 'issue_prefix (not set)',
199
+ stderr: '',
200
+ });
201
+ // Second call: config set succeeds
202
+ mockExeca.mockResolvedValueOnce({
203
+ exitCode: 0,
204
+ stdout: '',
205
+ stderr: '',
206
+ });
207
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
208
+ expect(result).toEqual({ ready: true, prefix: 'data' });
209
+ expect(mockExeca).toHaveBeenCalledTimes(2);
210
+ // Verify the set call used the derived prefix
211
+ const setArgs = mockExeca.mock.calls[1][1];
212
+ expect(setArgs).toEqual(expect.arrayContaining(['config', 'set', 'issue_prefix', 'data']));
213
+ });
214
+ it('auto-initializes prefix when config get returns empty output', async () => {
215
+ // exitCode 0 but empty stdout → falls through to auto-init
216
+ mockExeca.mockResolvedValueOnce({
217
+ exitCode: 0,
218
+ stdout: '',
219
+ stderr: '',
220
+ });
221
+ mockExeca.mockResolvedValueOnce({
222
+ exitCode: 0,
223
+ stdout: '',
224
+ stderr: '',
225
+ });
226
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-personal/workspace');
227
+ expect(result).toEqual({ ready: true, prefix: 'personal' });
228
+ });
229
+ it('auto-initializes prefix when config get returns non-zero exit code', async () => {
230
+ mockExeca.mockResolvedValueOnce({
231
+ exitCode: 1,
232
+ stdout: '',
233
+ stderr: 'config key not found',
234
+ });
235
+ mockExeca.mockResolvedValueOnce({
236
+ exitCode: 0,
237
+ stdout: '',
238
+ stderr: '',
239
+ });
240
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
241
+ expect(result).toEqual({ ready: true, prefix: 'data' });
242
+ });
243
+ it('returns not ready when auto-init set fails', async () => {
244
+ mockExeca.mockResolvedValueOnce({
245
+ exitCode: 0,
246
+ stdout: 'issue_prefix (not set)',
247
+ stderr: '',
248
+ });
249
+ mockExeca.mockResolvedValueOnce({
250
+ exitCode: 1,
251
+ stdout: '',
252
+ stderr: 'permission denied',
253
+ });
254
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
255
+ expect(result).toEqual({ ready: false });
256
+ });
257
+ it('returns not ready when execa throws', async () => {
258
+ mockExeca.mockRejectedValueOnce(new Error('ENOENT: bd not found'));
259
+ const result = await ensureBdDatabaseReady('/tmp/workspace');
260
+ expect(result).toEqual({ ready: false });
261
+ });
262
+ // ---- Prefix derivation logic ----
263
+ it('derives prefix from "discoclaw-personal" → "personal"', async () => {
264
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
265
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
266
+ await ensureBdDatabaseReady('/home/user/discoclaw-personal/workspace');
267
+ const setArgs = mockExeca.mock.calls[1][1];
268
+ expect(setArgs).toContain('personal');
269
+ });
270
+ it('derives prefix from "discoclaw-data" → "data"', async () => {
271
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
272
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
273
+ await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
274
+ const setArgs = mockExeca.mock.calls[1][1];
275
+ expect(setArgs).toContain('data');
276
+ });
277
+ it('derives prefix from bare "discoclaw" → "dc" (fallback)', async () => {
278
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
279
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
280
+ await ensureBdDatabaseReady('/home/user/discoclaw/workspace');
281
+ const setArgs = mockExeca.mock.calls[1][1];
282
+ expect(setArgs).toContain('dc');
283
+ });
284
+ it('strips non-alphanumeric chars from derived prefix', async () => {
285
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
286
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
287
+ // Parent dir has special chars: "my-project_v2" → "myprojectv2"
288
+ await ensureBdDatabaseReady('/home/user/my-project_v2/workspace');
289
+ const setArgs = mockExeca.mock.calls[1][1];
290
+ expect(setArgs).toContain('myprojectv2');
291
+ });
292
+ it('uses correct --db path derived from cwd', async () => {
293
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: 'ws', stderr: '' });
294
+ await ensureBdDatabaseReady('/home/user/discoclaw-personal/workspace');
295
+ const getArgs = mockExeca.mock.calls[0][1];
296
+ const dbIdx = getArgs.indexOf('--db');
297
+ expect(getArgs[dbIdx + 1]).toBe(path.resolve('/home/user/discoclaw-personal/workspace', '.beads', 'beads.db'));
298
+ });
299
+ it('resolves symlinks before deriving prefix', async () => {
300
+ // Symlink: code/discoclaw/workspace → discoclaw-data/workspace
301
+ // Without realpath, parent would be "discoclaw" → "dc" (wrong)
302
+ // With realpath, parent is "discoclaw-data" → "data" (correct)
303
+ mockRealpath.mockResolvedValueOnce('/home/user/discoclaw-data/workspace');
304
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
305
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
306
+ const result = await ensureBdDatabaseReady('/home/user/code/discoclaw/workspace');
307
+ expect(result).toEqual({ ready: true, prefix: 'data' });
308
+ expect(mockRealpath).toHaveBeenCalledWith('/home/user/code/discoclaw/workspace');
309
+ const setArgs = mockExeca.mock.calls[1][1];
310
+ expect(setArgs).toContain('data');
311
+ });
312
+ });
313
+ // ---------------------------------------------------------------------------
314
+ // buildTaskContextSummary — in-process TaskStore path
315
+ // ---------------------------------------------------------------------------
316
+ describe('buildTaskContextSummary', () => {
317
+ it('returns undefined when taskId is undefined', () => {
318
+ const store = new TaskStore();
319
+ expect(buildTaskContextSummary(undefined, store)).toBeUndefined();
320
+ });
321
+ it('returns undefined when store is undefined', () => {
322
+ expect(buildTaskContextSummary('t-001', undefined)).toBeUndefined();
323
+ });
324
+ it('returns undefined when task is not found in store', () => {
325
+ const store = new TaskStore();
326
+ expect(buildTaskContextSummary('t-001', store)).toBeUndefined();
327
+ });
328
+ it('returns summary with title only when no description', () => {
329
+ const store = new TaskStore();
330
+ const task = store.create({ title: 'Fix the bug' });
331
+ const result = buildTaskContextSummary(task.id, store);
332
+ expect(result?.summary).toBe('Task context for this thread:\nTitle: Fix the bug');
333
+ expect(result?.description).toBeUndefined();
334
+ });
335
+ it('returns summary with title and truncated description', () => {
336
+ const store = new TaskStore();
337
+ const task = store.create({ title: 'My task', description: 'a'.repeat(500) });
338
+ const result = buildTaskContextSummary(task.id, store);
339
+ expect(result?.summary).toContain('Title: My task');
340
+ expect(result?.description).toHaveLength(400);
341
+ expect(result?.description?.endsWith('\u2026')).toBe(true);
342
+ });
343
+ it('collapses whitespace in description', () => {
344
+ const store = new TaskStore();
345
+ const task = store.create({ title: 'T', description: 'hello\n world' });
346
+ const result = buildTaskContextSummary(task.id, store);
347
+ expect(result?.description).toBe('hello world');
348
+ });
349
+ it('does not call execa — store.get is synchronous', () => {
350
+ // This test verifies no subprocess is spawned. Since buildTaskContextSummary
351
+ // is now a synchronous function, it cannot be an async subprocess call.
352
+ const store = new TaskStore();
353
+ const task = store.create({ title: 'Sync task' });
354
+ const returnValue = buildTaskContextSummary(task.id, store);
355
+ // Must return a plain object, not a Promise.
356
+ expect(returnValue).not.toBeInstanceOf(Promise);
357
+ expect(returnValue?.summary).toContain('Sync task');
358
+ });
359
+ });
@@ -0,0 +1 @@
1
+ export * from './task-sync-engine.js';
@@ -0,0 +1,27 @@
1
+ function truncateText(value, max) {
2
+ return value.length > max ? `${value.slice(0, max - 1)}…` : value;
3
+ }
4
+ /**
5
+ * Build a thread-scoped task context summary from the in-process TaskStore.
6
+ */
7
+ export function buildTaskContextSummary(taskId, store, _log) {
8
+ if (!taskId || !store)
9
+ return undefined;
10
+ const task = store.get(taskId);
11
+ if (!task)
12
+ return undefined;
13
+ const lines = ['Task context for this thread:'];
14
+ if (task.title)
15
+ lines.push(`Title: ${task.title}`);
16
+ let description;
17
+ if (task.description) {
18
+ const desc = task.description.trim().replace(/\s+/g, ' ');
19
+ const truncated = truncateText(desc, 400);
20
+ lines.push(`Description: ${truncated}`);
21
+ description = truncated;
22
+ }
23
+ return {
24
+ summary: lines.join('\n'),
25
+ description,
26
+ };
27
+ }
@@ -0,0 +1,3 @@
1
+ export * from './tag-map.js';
2
+ export * from './thread-helpers.js';
3
+ export * from './thread-ops.js';