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,85 @@
1
+ // Template strategy for adding a new CLI-based runtime adapter.
2
+ //
3
+ // To add a new model:
4
+ // 1. Copy this file and rename (e.g. gemini-strategy.ts).
5
+ // 2. Implement the required hooks below.
6
+ // 3. Create a thin wrapper in src/runtime/<model>-cli.ts:
7
+ //
8
+ // import { createCliRuntime } from './cli-adapter.js';
9
+ // import { myStrategy } from './strategies/my-strategy.js';
10
+ //
11
+ // export function createMyCliRuntime(opts: MyOpts): RuntimeAdapter {
12
+ // return createCliRuntime(myStrategy, { binary: opts.bin, ... });
13
+ // }
14
+ //
15
+ // 4. Register in src/runtime/registry.ts and src/index.ts.
16
+ //
17
+ // The universal adapter (cli-adapter.ts) provides:
18
+ // - Subprocess tracking + kill-all shutdown
19
+ // - Process pool (multi-turn, process-pool mode)
20
+ // - Session resume map (session-resume mode)
21
+ // - Stream stall detection
22
+ // - Session file scanning (Claude-specific, opt-in via opts)
23
+ // - JSONL line parsing + image dedup
24
+ // - Error handling with strategy delegation
25
+ // - Event queue (push/wait/wake async generator pattern)
26
+ export const templateStrategy = {
27
+ // --- Required: identity ---
28
+ id: 'template', // Replace with your RuntimeId (add to types.ts first).
29
+ binaryDefault: 'my-cli', // Default binary name (overridden by opts.binary).
30
+ defaultModel: 'my-model', // Used when params.model is empty.
31
+ capabilities: [
32
+ 'streaming_text',
33
+ // Add capabilities your model supports:
34
+ // 'sessions', 'workspace_instructions', 'tools_exec', 'tools_fs', 'tools_web', 'mcp'
35
+ ],
36
+ // --- Required: output mode ---
37
+ getOutputMode(_ctx, _opts) {
38
+ // Return 'text' for plain stdout, 'jsonl' for line-delimited JSON.
39
+ // Can vary per invocation (e.g. Codex switches based on session state).
40
+ return 'text';
41
+ },
42
+ // --- Required: arg building ---
43
+ buildArgs(ctx, _opts) {
44
+ // Build the CLI argument array. The universal adapter calls:
45
+ // execa(binary, args, { cwd, timeout, ... })
46
+ //
47
+ // ctx.params contains: model, prompt, cwd, sessionKey, addDirs, tools, images, etc.
48
+ // ctx.useStdin is true when the prompt is too large for a positional arg.
49
+ // ctx.hasImages is true when image content blocks are present.
50
+ // ctx.sessionMap is available for session-resume mode strategies.
51
+ const args = ['--model', ctx.params.model];
52
+ if (!ctx.useStdin) {
53
+ args.push('--', ctx.params.prompt);
54
+ }
55
+ return args;
56
+ },
57
+ // --- Optional: stdin payload ---
58
+ buildStdinPayload(ctx) {
59
+ if (!ctx.useStdin)
60
+ return null;
61
+ // Return raw text or JSON-formatted stdin payload.
62
+ // The universal adapter writes this to subprocess.stdin and closes it.
63
+ return ctx.params.prompt;
64
+ },
65
+ // --- Optional: JSONL line parsing ---
66
+ // Only needed if getOutputMode returns 'jsonl'.
67
+ // Return null to fall through to the default Claude-compatible parser.
68
+ // parseLine(evt: unknown, ctx: CliInvokeContext): ParsedLineResult | null {
69
+ // const anyEvt = evt as Record<string, unknown>;
70
+ // if (anyEvt.type === 'message' && typeof anyEvt.text === 'string') {
71
+ // return { text: anyEvt.text };
72
+ // }
73
+ // return null;
74
+ // },
75
+ // --- Optional: multi-turn mode ---
76
+ // 'process-pool': keeps long-running subprocesses alive (like Claude).
77
+ // 'session-resume': resumes sessions by ID (like Codex).
78
+ // 'none' or omit: one-shot only.
79
+ // multiTurnMode: 'none',
80
+ // --- Optional: error handling ---
81
+ // Return a user-facing message, or null to use default handling.
82
+ // sanitizeError(raw: string, binary: string): string { return raw; },
83
+ // handleSpawnError(err: any, binary: string): string | null { return null; },
84
+ // handleExitError(exitCode: number, stderr: string, stdout: string): string | null { return null; },
85
+ };
@@ -0,0 +1,27 @@
1
+ const TOOL_CAPABILITIES = {
2
+ Bash: 'tools_exec',
3
+ Read: 'tools_fs',
4
+ Write: 'tools_fs',
5
+ Edit: 'tools_fs',
6
+ Glob: 'tools_fs',
7
+ Grep: 'tools_fs',
8
+ WebSearch: 'tools_web',
9
+ WebFetch: 'tools_web',
10
+ };
11
+ export function requiredCapabilityForTool(tool) {
12
+ return TOOL_CAPABILITIES[tool];
13
+ }
14
+ export function filterToolsByCapabilities(tools, capabilities) {
15
+ const kept = [];
16
+ const dropped = [];
17
+ for (const tool of tools) {
18
+ const required = requiredCapabilityForTool(tool);
19
+ if (!required || capabilities.has(required)) {
20
+ kept.push(tool);
21
+ }
22
+ else {
23
+ dropped.push(tool);
24
+ }
25
+ }
26
+ return { tools: kept, dropped };
27
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { filterToolsByCapabilities, requiredCapabilityForTool } from './tool-capabilities.js';
3
+ describe('requiredCapabilityForTool', () => {
4
+ it('returns expected capability for known tools', () => {
5
+ expect(requiredCapabilityForTool('Read')).toBe('tools_fs');
6
+ expect(requiredCapabilityForTool('Bash')).toBe('tools_exec');
7
+ expect(requiredCapabilityForTool('WebSearch')).toBe('tools_web');
8
+ });
9
+ it('returns undefined for unknown tools', () => {
10
+ expect(requiredCapabilityForTool('CustomTool')).toBeUndefined();
11
+ });
12
+ });
13
+ describe('filterToolsByCapabilities', () => {
14
+ it('keeps only tool-compatible entries and reports dropped tools', () => {
15
+ const result = filterToolsByCapabilities(['Read', 'Bash', 'WebSearch', 'CustomTool'], new Set(['tools_fs']));
16
+ expect(result.tools).toEqual(['Read', 'CustomTool']);
17
+ expect(result.dropped).toEqual(['Bash', 'WebSearch']);
18
+ });
19
+ it('keeps all tools when all required capabilities are present', () => {
20
+ const result = filterToolsByCapabilities(['Read', 'Bash', 'WebSearch'], new Set(['tools_fs', 'tools_exec', 'tools_web']));
21
+ expect(result.tools).toEqual(['Read', 'Bash', 'WebSearch']);
22
+ expect(result.dropped).toEqual([]);
23
+ });
24
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Human-readable labels for tool activity display in Discord.
3
+ */
4
+ function shortPath(p) {
5
+ const segments = p.replace(/\\/g, '/').split('/').filter(Boolean);
6
+ if (segments.length <= 2)
7
+ return segments.join('/');
8
+ return '.../' + segments.slice(-2).join('/');
9
+ }
10
+ function extractPath(input) {
11
+ if (!input || typeof input !== 'object')
12
+ return null;
13
+ const obj = input;
14
+ for (const key of ['file_path', 'path', 'pattern']) {
15
+ if (typeof obj[key] === 'string' && obj[key])
16
+ return obj[key];
17
+ }
18
+ return null;
19
+ }
20
+ export function toolActivityLabel(name, input) {
21
+ const p = extractPath(input);
22
+ const short = p ? shortPath(p) : null;
23
+ switch (name) {
24
+ case 'Read':
25
+ return short ? `Reading ${short}` : 'Reading file...';
26
+ case 'Write':
27
+ return short ? `Writing ${short}` : 'Writing file...';
28
+ case 'Edit':
29
+ return short ? `Editing ${short}` : 'Editing file...';
30
+ case 'Bash':
31
+ return 'Running command...';
32
+ case 'Grep':
33
+ return 'Searching content...';
34
+ case 'Glob':
35
+ return 'Finding files...';
36
+ case 'WebSearch':
37
+ return 'Searching web...';
38
+ case 'WebFetch':
39
+ return 'Fetching URL...';
40
+ case 'Task':
41
+ return 'Running subtask...';
42
+ case 'TodoRead':
43
+ case 'TodoWrite':
44
+ return 'Managing tasks...';
45
+ default:
46
+ return `Running ${name}...`;
47
+ }
48
+ }
@@ -0,0 +1,2 @@
1
+ /** Max images per invocation to prevent runaway accumulation. */
2
+ export const MAX_IMAGES_PER_INVOCATION = 10;
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ async function readJsonIfExists(filePath) {
5
+ try {
6
+ const raw = await fs.readFile(filePath, 'utf8');
7
+ const parsed = JSON.parse(raw);
8
+ if (parsed && typeof parsed === 'object')
9
+ return parsed;
10
+ return {};
11
+ }
12
+ catch (err) {
13
+ const code = err.code;
14
+ if (code === 'ENOENT')
15
+ return {};
16
+ throw err;
17
+ }
18
+ }
19
+ async function atomicWriteJson(filePath, value) {
20
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
21
+ const tmp = `${filePath}.tmp.${process.pid}`;
22
+ await fs.writeFile(tmp, JSON.stringify(value, null, 2) + '\n', 'utf8');
23
+ await fs.rename(tmp, filePath);
24
+ }
25
+ export class SessionManager {
26
+ storePath;
27
+ store = null;
28
+ constructor(storePath) {
29
+ this.storePath = storePath;
30
+ }
31
+ async load() {
32
+ if (this.store)
33
+ return this.store;
34
+ this.store = await readJsonIfExists(this.storePath);
35
+ return this.store;
36
+ }
37
+ async getOrCreate(sessionKey) {
38
+ const store = await this.load();
39
+ const existing = store[sessionKey];
40
+ if (existing)
41
+ return existing;
42
+ const id = crypto.randomUUID();
43
+ store[sessionKey] = id;
44
+ await atomicWriteJson(this.storePath, store);
45
+ return id;
46
+ }
47
+ }
@@ -0,0 +1,18 @@
1
+ import crypto from 'node:crypto';
2
+ /**
3
+ * Generates a fresh session UUID for each invocation.
4
+ *
5
+ * Claude CLI >= 2.1.38 rejects `--session-id` if a JSONL transcript file
6
+ * already exists for that UUID ("Session ID already in use"). Since `-p`
7
+ * mode does not load previous conversation history from the JSONL anyway,
8
+ * reusing UUIDs provides no benefit. Discoclaw maintains conversation
9
+ * continuity through its own rolling-summary and durable-memory systems.
10
+ */
11
+ export class SessionManager {
12
+ constructor(_storePath) {
13
+ // storePath kept for backwards-compatible constructor signature.
14
+ }
15
+ async getOrCreate(_sessionKey) {
16
+ return crypto.randomUUID();
17
+ }
18
+ }
@@ -0,0 +1,33 @@
1
+ import { TASK_STATUSES } from './types.js';
2
+ import { TASK_DIRECT_THREAD_ACTIONS, TASK_STORE_MUTATION_EVENTS, TASK_SYNC_TRIGGER_EVENTS, } from './sync-contract.js';
3
+ const ALL_TASK_STATUSES = [...TASK_STATUSES];
4
+ /**
5
+ * Track 1 freezes current runtime behavior:
6
+ * TaskStore status updates are currently permissive across all statuses.
7
+ * Future domain-level restrictions should be introduced via TaskService.
8
+ */
9
+ export const TASK_STORE_ALLOWED_STATUS_TRANSITIONS = {
10
+ open: ALL_TASK_STATUSES,
11
+ in_progress: ALL_TASK_STATUSES,
12
+ blocked: ALL_TASK_STATUSES,
13
+ closed: ALL_TASK_STATUSES,
14
+ };
15
+ export function isTaskStoreStatusTransitionAllowed(from, to) {
16
+ return TASK_STORE_ALLOWED_STATUS_TRANSITIONS[from].includes(to);
17
+ }
18
+ export const TASK_STORE_MUTATION_EVENT_RULES = {
19
+ create: ['created'],
20
+ update: ['updated'],
21
+ close: ['closed'],
22
+ addLabel: ['labeled'],
23
+ removeLabel: ['updated'],
24
+ addLabelNoop: [],
25
+ removeLabelNoop: [],
26
+ };
27
+ export const TASK_ARCHITECTURE_CONTRACT = {
28
+ storeMutationEvents: TASK_STORE_MUTATION_EVENTS,
29
+ syncTriggerEvents: TASK_SYNC_TRIGGER_EVENTS,
30
+ directThreadLifecycleActions: TASK_DIRECT_THREAD_ACTIONS,
31
+ statusTransitions: TASK_STORE_ALLOWED_STATUS_TRANSITIONS,
32
+ mutationEventRules: TASK_STORE_MUTATION_EVENT_RULES,
33
+ };
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { TaskStore } from './store.js';
3
+ import { TASK_STATUSES } from './types.js';
4
+ import { TASK_ARCHITECTURE_CONTRACT, TASK_STORE_MUTATION_EVENT_RULES, isTaskStoreStatusTransitionAllowed, } from './architecture-contract.js';
5
+ import { wireTaskStoreSyncTriggers } from './task-sync.js';
6
+ import { withDirectTaskLifecycle } from './task-lifecycle.js';
7
+ function makeStore() {
8
+ return new TaskStore({ prefix: 'ws' });
9
+ }
10
+ describe('task architecture contract', () => {
11
+ it('freezes permissive TaskStore status transitions across all statuses', () => {
12
+ for (const from of TASK_STATUSES) {
13
+ for (const to of TASK_STATUSES) {
14
+ expect(isTaskStoreStatusTransitionAllowed(from, to)).toBe(true);
15
+ }
16
+ }
17
+ });
18
+ it('keeps stable mutation/sync ownership contract sets', () => {
19
+ expect(TASK_ARCHITECTURE_CONTRACT.storeMutationEvents).toEqual([
20
+ 'created',
21
+ 'updated',
22
+ 'closed',
23
+ 'labeled',
24
+ ]);
25
+ expect(TASK_ARCHITECTURE_CONTRACT.syncTriggerEvents).toEqual([
26
+ 'updated',
27
+ 'closed',
28
+ 'labeled',
29
+ ]);
30
+ expect(TASK_ARCHITECTURE_CONTRACT.directThreadLifecycleActions).toEqual([
31
+ 'taskCreate',
32
+ 'taskUpdate',
33
+ 'taskClose',
34
+ ]);
35
+ });
36
+ });
37
+ describe('TaskStore characterization', () => {
38
+ it('allows status transitions in both directions via update', () => {
39
+ const store = makeStore();
40
+ const task = store.create({ title: 'Refactor tasks subsystem' });
41
+ const transitions = [];
42
+ store.on('updated', (next, prev) => {
43
+ transitions.push([prev.status, next.status]);
44
+ });
45
+ store.update(task.id, { status: 'closed' });
46
+ store.update(task.id, { status: 'blocked' });
47
+ store.update(task.id, { status: 'open' });
48
+ expect(transitions).toEqual([
49
+ ['open', 'closed'],
50
+ ['closed', 'blocked'],
51
+ ['blocked', 'open'],
52
+ ]);
53
+ });
54
+ it('emits expected events for label mutations and no-op label operations', () => {
55
+ const store = makeStore();
56
+ const task = store.create({ title: 'Tag handling' });
57
+ const events = [];
58
+ store.on('labeled', () => events.push('labeled'));
59
+ store.on('updated', () => events.push('updated'));
60
+ store.addLabel(task.id, 'plan');
61
+ store.addLabel(task.id, 'plan');
62
+ store.removeLabel(task.id, 'missing');
63
+ store.removeLabel(task.id, 'plan');
64
+ expect(events).toEqual([
65
+ ...TASK_STORE_MUTATION_EVENT_RULES.addLabel,
66
+ ...TASK_STORE_MUTATION_EVENT_RULES.removeLabel,
67
+ ]);
68
+ });
69
+ });
70
+ describe('store-event sync trigger characterization', () => {
71
+ it('triggers sync for updated/closed/labeled only and skips direct-owned lifecycle updates', async () => {
72
+ const store = makeStore();
73
+ const syncCoordinator = { sync: vi.fn(async () => null) };
74
+ const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
75
+ const wired = wireTaskStoreSyncTriggers({ forumId: 'forum-1', tagMap: {}, store }, syncCoordinator, log);
76
+ const task = store.create({ title: 'Lifecycle owner test' });
77
+ expect(syncCoordinator.sync).toHaveBeenCalledTimes(0);
78
+ store.update(task.id, { title: 'Updated' });
79
+ store.addLabel(task.id, 'autotag');
80
+ store.close(task.id);
81
+ expect(syncCoordinator.sync).toHaveBeenCalledTimes(3);
82
+ await withDirectTaskLifecycle(task.id, async () => {
83
+ store.update(task.id, { title: 'Owned update' });
84
+ });
85
+ expect(syncCoordinator.sync).toHaveBeenCalledTimes(3);
86
+ wired.stop();
87
+ store.update(task.id, { title: 'After stop' });
88
+ expect(syncCoordinator.sync).toHaveBeenCalledTimes(3);
89
+ });
90
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Use an AI model to classify a task into 1-3 forum tags from the available
3
+ * set. Returns an array of valid tag names (silently drops unknown ones).
4
+ */
5
+ export async function autoTagTask(runtime, title, description, availableTags, opts) {
6
+ if (availableTags.length === 0)
7
+ return [];
8
+ const tagList = availableTags.join(', ');
9
+ const prompt = `Classify this task into 1-3 tags from the following list. ` +
10
+ `Reply with ONLY comma-separated tag names, nothing else.\n\n` +
11
+ `Available tags: ${tagList}\n\n` +
12
+ `Title: ${title}\n` +
13
+ (description ? `Description: ${description.slice(0, 500)}\n` : '');
14
+ let finalText = '';
15
+ let deltaText = '';
16
+ const modelResolver = opts?.modelResolver ?? ((model) => model);
17
+ for await (const evt of runtime.invoke({
18
+ prompt,
19
+ model: modelResolver(opts?.model ?? 'fast', runtime.id),
20
+ cwd: opts?.cwd ?? '.',
21
+ timeoutMs: opts?.timeoutMs ?? 15_000,
22
+ tools: [],
23
+ })) {
24
+ if (evt.type === 'text_final' && typeof evt.text === 'string') {
25
+ finalText = evt.text;
26
+ }
27
+ else if (evt.type === 'text_delta' && typeof evt.text === 'string') {
28
+ deltaText += evt.text;
29
+ }
30
+ else if (evt.type === 'error') {
31
+ return [];
32
+ }
33
+ }
34
+ const output = (finalText || deltaText).trim();
35
+ if (!output)
36
+ return [];
37
+ const tagSet = new Set(availableTags.map((tag) => tag.toLowerCase()));
38
+ const candidates = output.split(/[,\n]+/).map((tag) => tag.trim()).filter(Boolean);
39
+ const result = [];
40
+ for (const candidate of candidates) {
41
+ // Find the original-cased tag name.
42
+ const match = availableTags.find((tag) => tag.toLowerCase() === candidate.toLowerCase());
43
+ if (match && tagSet.has(candidate.toLowerCase())) {
44
+ result.push(match);
45
+ }
46
+ if (result.length >= 3)
47
+ break;
48
+ }
49
+ return result;
50
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { autoTagTask } from './auto-tag.js';
3
+ function makeMockRuntime(output) {
4
+ return {
5
+ id: 'other',
6
+ capabilities: new Set(),
7
+ async *invoke() {
8
+ yield { type: 'text_final', text: output };
9
+ },
10
+ };
11
+ }
12
+ function makeMockErrorRuntime() {
13
+ return {
14
+ id: 'other',
15
+ capabilities: new Set(),
16
+ async *invoke() {
17
+ yield { type: 'error', message: 'fail' };
18
+ },
19
+ };
20
+ }
21
+ const TAGS = ['feature', 'bug', 'personal', 'work', 'urgent'];
22
+ describe('autoTagTask', () => {
23
+ it('returns valid tags from AI output', async () => {
24
+ const runtime = makeMockRuntime('feature, personal');
25
+ const result = await autoTagTask(runtime, 'Add login', 'New feature', TAGS);
26
+ expect(result).toEqual(['feature', 'personal']);
27
+ });
28
+ it('drops unknown tags silently', async () => {
29
+ const runtime = makeMockRuntime('feature, nonexistent, bug');
30
+ const result = await autoTagTask(runtime, 'Fix crash', '', TAGS);
31
+ expect(result).toEqual(['feature', 'bug']);
32
+ });
33
+ it('limits to 3 tags', async () => {
34
+ const runtime = makeMockRuntime('feature, bug, personal, work, urgent');
35
+ const result = await autoTagTask(runtime, 'Big task', '', TAGS);
36
+ expect(result).toHaveLength(3);
37
+ });
38
+ it('handles case-insensitive matching', async () => {
39
+ const runtime = makeMockRuntime('Feature, BUG');
40
+ const result = await autoTagTask(runtime, 'Test', '', TAGS);
41
+ expect(result).toEqual(['feature', 'bug']);
42
+ });
43
+ it('returns empty on runtime error', async () => {
44
+ const runtime = makeMockErrorRuntime();
45
+ const result = await autoTagTask(runtime, 'Test', '', TAGS);
46
+ expect(result).toEqual([]);
47
+ });
48
+ it('returns empty when no available tags', async () => {
49
+ const runtime = makeMockRuntime('feature');
50
+ const result = await autoTagTask(runtime, 'Test', '', []);
51
+ expect(result).toEqual([]);
52
+ });
53
+ it('handles empty AI output', async () => {
54
+ const runtime = makeMockRuntime('');
55
+ const result = await autoTagTask(runtime, 'Test', '', TAGS);
56
+ expect(result).toEqual([]);
57
+ });
58
+ it('uses injected model resolver when provided', async () => {
59
+ const runtime = makeMockRuntime('feature');
60
+ const resolve = vi.fn((model) => `resolved-${model}`);
61
+ await autoTagTask(runtime, 'Test', '', TAGS, { model: 'fast', modelResolver: resolve });
62
+ expect(resolve).toHaveBeenCalledWith('fast', runtime.id);
63
+ });
64
+ });
@@ -0,0 +1,164 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { execa } from 'execa';
4
+ export { buildTaskContextSummary } from './context-summary.js';
5
+ // ---------------------------------------------------------------------------
6
+ // Config
7
+ // ---------------------------------------------------------------------------
8
+ const BD_BIN = process.env.BD_BIN || 'bd';
9
+ // ---------------------------------------------------------------------------
10
+ // Legacy status normalization
11
+ // ---------------------------------------------------------------------------
12
+ /** Map removed statuses to their replacement. */
13
+ const LEGACY_STATUS_MAP = {
14
+ done: 'closed',
15
+ tombstone: 'closed',
16
+ };
17
+ /** Normalize legacy task statuses (`done`, `tombstone`) → `closed`. */
18
+ export function normalizeTaskData(task) {
19
+ const mapped = LEGACY_STATUS_MAP[task.status];
20
+ if (mapped)
21
+ return { ...task, status: mapped };
22
+ return task;
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // JSON parsing helper
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Parse bd CLI JSON output. Handles:
29
+ * - Array output (list, show)
30
+ * - Single-object output (create)
31
+ * - Markdown-fenced JSON (```json ... ```)
32
+ * - Empty / error output
33
+ */
34
+ export function parseBdJson(stdout) {
35
+ let text = stdout.trim();
36
+ if (!text)
37
+ return [];
38
+ // Strip markdown fences if present.
39
+ text = text.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '');
40
+ text = text.trim();
41
+ if (!text)
42
+ return [];
43
+ const parsed = JSON.parse(text);
44
+ if (Array.isArray(parsed))
45
+ return parsed;
46
+ if (parsed && typeof parsed === 'object') {
47
+ // bd returns { error: "..." } on failures.
48
+ if ('error' in parsed && Object.keys(parsed).length === 1) {
49
+ throw new Error(String(parsed.error));
50
+ }
51
+ return [parsed];
52
+ }
53
+ return [];
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // Pre-flight check
57
+ // ---------------------------------------------------------------------------
58
+ /** Check whether the bd CLI binary is available. */
59
+ export async function checkBdAvailable() {
60
+ try {
61
+ const result = await execa(BD_BIN, ['--version'], { reject: false });
62
+ if (result.exitCode === 0) {
63
+ return { available: true, version: result.stdout.trim() || undefined };
64
+ }
65
+ return { available: false };
66
+ }
67
+ catch {
68
+ return { available: false };
69
+ }
70
+ }
71
+ /**
72
+ * Verify the legacy bd database at `cwd` is initialized with an issue_prefix.
73
+ * Without a prefix, bd silently falls through to the global daemon registry
74
+ * and may write to a completely different instance's database.
75
+ *
76
+ * If the prefix is missing, attempt to auto-set it from the data directory name.
77
+ * Returns the detected prefix or null if the database cannot be reached.
78
+ */
79
+ export async function ensureBdDatabaseReady(cwd) {
80
+ const dbPath = path.resolve(cwd, '.beads', 'beads.db');
81
+ try {
82
+ const result = await execa(BD_BIN, ['--db', dbPath, '--no-daemon', 'config', 'get', 'issue_prefix'], {
83
+ cwd,
84
+ reject: false,
85
+ });
86
+ const output = result.stdout.trim();
87
+ // bd config get returns "key (not set)" when unset, or just the value when set.
88
+ if (result.exitCode === 0 && output && !output.includes('(not set)')) {
89
+ return { ready: true, prefix: output };
90
+ }
91
+ // Prefix not set — auto-initialize from directory name.
92
+ // Resolve symlinks so that e.g. code/discoclaw/workspace → discoclaw-data/workspace
93
+ // derives "data" from the real target, not "dc" from the symlink parent.
94
+ const realCwd = await fs.realpath(cwd);
95
+ const dirName = path.basename(path.resolve(realCwd, '..'));
96
+ // Derive a short prefix: "discoclaw-personal" → "personal", "discoclaw-data" → "data", fallback to "dc"
97
+ const prefix = dirName.replace(/^discoclaw-?/, '').replace(/[^a-z0-9]/gi, '') || 'dc';
98
+ const setResult = await execa(BD_BIN, ['--db', dbPath, '--no-daemon', 'config', 'set', 'issue_prefix', prefix], {
99
+ cwd,
100
+ reject: false,
101
+ });
102
+ if (setResult.exitCode === 0) {
103
+ return { ready: true, prefix };
104
+ }
105
+ return { ready: false };
106
+ }
107
+ catch {
108
+ return { ready: false };
109
+ }
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // bd CLI wrappers
113
+ // ---------------------------------------------------------------------------
114
+ async function runBd(args, cwd) {
115
+ // Pin bd to the exact database for this workspace. Without this, bd's
116
+ // auto-discovery walks up parent directories and may connect to a daemon
117
+ // belonging to a different discoclaw instance (e.g. dev vs personal).
118
+ const dbPath = path.resolve(cwd, '.beads', 'beads.db');
119
+ const pinnedArgs = ['--db', dbPath, '--no-daemon', ...args];
120
+ const result = await execa(BD_BIN, pinnedArgs, { cwd, reject: false });
121
+ if (result.exitCode !== 0) {
122
+ // Try to extract a structured error from JSON output.
123
+ const out = (result.stdout ?? '').trim();
124
+ if (out) {
125
+ try {
126
+ const parsed = JSON.parse(out);
127
+ if (parsed?.error)
128
+ throw new Error(String(parsed.error));
129
+ }
130
+ catch (e) {
131
+ if (e instanceof SyntaxError) {
132
+ // Not JSON — fall through.
133
+ }
134
+ else {
135
+ throw e;
136
+ }
137
+ }
138
+ }
139
+ const errText = (result.stderr ?? '').trim() || out || `bd exited with code ${result.exitCode}`;
140
+ throw new Error(errText);
141
+ }
142
+ return result.stdout;
143
+ }
144
+ /** List task items matching the given filters. */
145
+ export async function bdList(params, cwd) {
146
+ const args = ['list', '--json'];
147
+ if (params.status === 'all') {
148
+ args.push('--all');
149
+ // The bd CLI applies a default limit of 50. When fetching *all* tasks
150
+ // and no explicit limit was provided, pass --limit 0 to disable the cap.
151
+ if (params.limit == null)
152
+ args.push('--limit', '0');
153
+ }
154
+ else if (params.status) {
155
+ args.push('--status', params.status);
156
+ }
157
+ if (params.label)
158
+ args.push('--label', params.label);
159
+ if (params.limit != null)
160
+ args.push('--limit', String(params.limit));
161
+ const stdout = await runBd(args, cwd);
162
+ const items = parseBdJson(stdout);
163
+ return items.map(normalizeTaskData);
164
+ }