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,128 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { acquirePidLock, releasePidLock } from './pidlock.js';
6
+ async function tmpLockPath() {
7
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pidlock-'));
8
+ return path.join(dir, 'discoclaw.pid');
9
+ }
10
+ function lockDirPath(lockPath) {
11
+ return `${lockPath}.lock`;
12
+ }
13
+ describe('acquirePidLock', () => {
14
+ const paths = [];
15
+ afterEach(async () => {
16
+ for (const p of paths) {
17
+ await fs.rm(path.dirname(p), { recursive: true, force: true });
18
+ }
19
+ paths.length = 0;
20
+ });
21
+ it('creates a lock directory with meta for current PID', async () => {
22
+ const lockPath = await tmpLockPath();
23
+ paths.push(lockPath);
24
+ await acquirePidLock(lockPath);
25
+ const raw = await fs.readFile(path.join(lockDirPath(lockPath), 'meta.json'), 'utf-8');
26
+ const meta = JSON.parse(raw);
27
+ expect(meta.pid).toBe(process.pid);
28
+ expect(typeof meta.token).toBe('string');
29
+ expect(meta.token.length).toBe(32);
30
+ });
31
+ it('takes over stale lock with dead PID', async () => {
32
+ const lockPath = await tmpLockPath();
33
+ paths.push(lockPath);
34
+ const lockDir = lockDirPath(lockPath);
35
+ await fs.mkdir(lockDir);
36
+ await fs.writeFile(path.join(lockDir, 'meta.json'), JSON.stringify({ pid: 999999999, token: 'old-token', acquiredAt: new Date().toISOString() }), 'utf-8');
37
+ await acquirePidLock(lockPath);
38
+ const raw = await fs.readFile(path.join(lockDir, 'meta.json'), 'utf-8');
39
+ const meta = JSON.parse(raw);
40
+ expect(meta.pid).toBe(process.pid);
41
+ });
42
+ it('throws when a live process holds the lock', async () => {
43
+ const lockPath = await tmpLockPath();
44
+ paths.push(lockPath);
45
+ const lockDir = lockDirPath(lockPath);
46
+ await fs.mkdir(lockDir);
47
+ await fs.writeFile(path.join(lockDir, 'meta.json'), JSON.stringify({ pid: 1, token: 'held-token', acquiredAt: new Date().toISOString() }), 'utf-8');
48
+ await expect(acquirePidLock(lockPath)).rejects.toThrow(/already running.*PID 1/);
49
+ });
50
+ it('takes over old lock directory with corrupt meta', async () => {
51
+ const lockPath = await tmpLockPath();
52
+ paths.push(lockPath);
53
+ const lockDir = lockDirPath(lockPath);
54
+ await fs.mkdir(lockDir);
55
+ await fs.writeFile(path.join(lockDir, 'meta.json'), 'not-json', 'utf-8');
56
+ const past = new Date(Date.now() - 3000);
57
+ await fs.utimes(lockDir, past, past);
58
+ await acquirePidLock(lockPath);
59
+ const raw = await fs.readFile(path.join(lockDir, 'meta.json'), 'utf-8');
60
+ const meta = JSON.parse(raw);
61
+ expect(meta.pid).toBe(process.pid);
62
+ });
63
+ it('blocks while lock directory is initializing (meta missing, <2s old)', async () => {
64
+ const lockPath = await tmpLockPath();
65
+ paths.push(lockPath);
66
+ const lockDir = lockDirPath(lockPath);
67
+ await fs.mkdir(lockDir);
68
+ await expect(acquirePidLock(lockPath)).rejects.toThrow(/initializing/);
69
+ });
70
+ it('allows only one winner under concurrent acquisition attempts', async () => {
71
+ const lockPath = await tmpLockPath();
72
+ paths.push(lockPath);
73
+ const [a, b] = await Promise.allSettled([acquirePidLock(lockPath), acquirePidLock(lockPath)]);
74
+ const successes = [a, b].filter((r) => r.status === 'fulfilled');
75
+ const failures = [a, b].filter((r) => r.status === 'rejected');
76
+ expect(successes).toHaveLength(1);
77
+ expect(failures).toHaveLength(1);
78
+ });
79
+ it('removes stale legacy PID lock file before acquiring lock directory', async () => {
80
+ const lockPath = await tmpLockPath();
81
+ paths.push(lockPath);
82
+ await fs.writeFile(lockPath, '999999999', 'utf-8');
83
+ await acquirePidLock(lockPath);
84
+ await expect(fs.access(lockPath)).rejects.toThrow();
85
+ const raw = await fs.readFile(path.join(lockDirPath(lockPath), 'meta.json'), 'utf-8');
86
+ const meta = JSON.parse(raw);
87
+ expect(meta.pid).toBe(process.pid);
88
+ });
89
+ it('rejects when a live legacy PID lock file exists', async () => {
90
+ const lockPath = await tmpLockPath();
91
+ paths.push(lockPath);
92
+ await fs.writeFile(lockPath, '1', 'utf-8');
93
+ await expect(acquirePidLock(lockPath)).rejects.toThrow(/already running.*PID 1/);
94
+ });
95
+ });
96
+ describe('releasePidLock', () => {
97
+ const paths = [];
98
+ afterEach(async () => {
99
+ for (const p of paths) {
100
+ await fs.rm(path.dirname(p), { recursive: true, force: true });
101
+ }
102
+ paths.length = 0;
103
+ });
104
+ it('removes the lock directory for current holder', async () => {
105
+ const lockPath = await tmpLockPath();
106
+ paths.push(lockPath);
107
+ const lockDir = lockDirPath(lockPath);
108
+ await acquirePidLock(lockPath);
109
+ await releasePidLock(lockPath);
110
+ await expect(fs.access(lockDir)).rejects.toThrow();
111
+ });
112
+ it('does not remove lock directory when meta belongs to another PID', async () => {
113
+ const lockPath = await tmpLockPath();
114
+ paths.push(lockPath);
115
+ const lockDir = lockDirPath(lockPath);
116
+ await fs.mkdir(lockDir);
117
+ await fs.writeFile(path.join(lockDir, 'meta.json'), JSON.stringify({ pid: 1, token: 'other-token', acquiredAt: new Date().toISOString() }), 'utf-8');
118
+ await releasePidLock(lockPath);
119
+ const stat = await fs.stat(lockDir);
120
+ expect(stat.isDirectory()).toBe(true);
121
+ });
122
+ it('does nothing when the lock directory does not exist', async () => {
123
+ const lockPath = await tmpLockPath();
124
+ paths.push(lockPath);
125
+ // Should not throw.
126
+ await releasePidLock(lockPath);
127
+ });
128
+ });
@@ -0,0 +1,206 @@
1
+ import { execa } from 'execa';
2
+ /**
3
+ * Replace `{{prev.output}}` and `{{steps.<id>.output}}` variables in a static
4
+ * string. Unresolvable references are returned verbatim.
5
+ */
6
+ function interpolateTemplate(template, stepIndex, steps, outputs) {
7
+ return template.replace(/\{\{([^{}]+)\}\}/g, (match, keyRaw) => {
8
+ const key = keyRaw.trim();
9
+ if (key === 'prev.output') {
10
+ return stepIndex > 0 ? (outputs[stepIndex - 1] ?? '') : '';
11
+ }
12
+ const stepRef = /^steps\.(.+)\.output$/.exec(key);
13
+ if (stepRef) {
14
+ const refId = stepRef[1];
15
+ const refIdx = steps.findIndex((s, idx) => s.id === refId && idx < stepIndex);
16
+ if (refIdx === -1)
17
+ return match; // unresolvable — leave as literal
18
+ return outputs[refIdx] ?? match;
19
+ }
20
+ return match; // unrecognized pattern — leave as literal
21
+ });
22
+ }
23
+ /**
24
+ * Recursively walk a JSON-like value and apply `interpolateTemplate` to every
25
+ * string leaf. Non-string primitives and `null` are returned as-is.
26
+ */
27
+ function interpolateDeep(value, stepIndex, steps, outputs) {
28
+ if (typeof value === 'string') {
29
+ return interpolateTemplate(value, stepIndex, steps, outputs);
30
+ }
31
+ if (Array.isArray(value)) {
32
+ return value.map((item) => interpolateDeep(item, stepIndex, steps, outputs));
33
+ }
34
+ if (value !== null && typeof value === 'object') {
35
+ const result = {};
36
+ for (const [k, v] of Object.entries(value)) {
37
+ result[k] = interpolateDeep(v, stepIndex, steps, outputs);
38
+ }
39
+ return result;
40
+ }
41
+ return value;
42
+ }
43
+ /** Drain a runtime event stream, collecting final text. Throws on error events. */
44
+ async function collectText(events, signal) {
45
+ let finalText = '';
46
+ let deltaText = '';
47
+ for await (const evt of events) {
48
+ if (evt.type === 'text_final') {
49
+ finalText = evt.text;
50
+ }
51
+ else if (evt.type === 'text_delta') {
52
+ deltaText += evt.text;
53
+ }
54
+ else if (evt.type === 'error') {
55
+ throw new Error(evt.message);
56
+ }
57
+ // Check abort after processing the current event so we don't discard it.
58
+ if (signal?.aborted)
59
+ break;
60
+ }
61
+ return finalText || deltaText;
62
+ }
63
+ /**
64
+ * Execute a sequence of steps where each step's text output is made
65
+ * available as context for subsequent steps.
66
+ */
67
+ export async function runPipeline(def) {
68
+ const { steps, runtime, cwd, model, signal, onProgress, confirmAllowed } = def;
69
+ const outputs = [];
70
+ // Validate step IDs — duplicates are rejected up-front.
71
+ const seenIds = new Set();
72
+ for (const s of steps) {
73
+ if (s.id !== undefined) {
74
+ if (seenIds.has(s.id))
75
+ throw new Error(`Duplicate step ID: "${s.id}"`);
76
+ seenIds.add(s.id);
77
+ }
78
+ }
79
+ for (let i = 0; i < steps.length; i++) {
80
+ if (signal?.aborted)
81
+ break;
82
+ const step = steps[i];
83
+ const stepId = step.id ?? String(i);
84
+ const ctx = {
85
+ stepIndex: i,
86
+ previousOutput: outputs.length > 0 ? outputs[outputs.length - 1] : '',
87
+ allOutputs: outputs,
88
+ };
89
+ // --- Shell step ---
90
+ if (step.kind === 'shell') {
91
+ const shellBinary = step.command[0];
92
+ if (!shellBinary || shellBinary.trim() === '') {
93
+ throw new Error(`Pipeline step ${i} failed: shell: command must include a non-empty executable`);
94
+ }
95
+ if (step.confirm && !confirmAllowed) {
96
+ throw new Error(`Pipeline step ${i}: confirm=true requires confirmAllowed on the pipeline`);
97
+ }
98
+ if (step.dryRun) {
99
+ outputs.push('');
100
+ const argCount = step.command.length - 1;
101
+ onProgress?.(`step ${stepId}: dry-run (${shellBinary}, ${argCount} arg${argCount !== 1 ? 's' : ''})`);
102
+ continue;
103
+ }
104
+ // Interpolate template variables in command args before execution.
105
+ const interpolatedCommand = step.command.map((arg) => interpolateTemplate(arg, i, steps, outputs));
106
+ if (!interpolatedCommand[0] || interpolatedCommand[0].trim() === '') {
107
+ throw new Error(`Pipeline step ${i} failed: shell: command resolved to an empty executable`);
108
+ }
109
+ let text;
110
+ try {
111
+ const result = await execa(interpolatedCommand[0], interpolatedCommand.slice(1), {
112
+ reject: false,
113
+ timeout: step.timeoutMs,
114
+ cancelSignal: signal,
115
+ cwd: step.cwd ?? cwd,
116
+ });
117
+ if (result.isCanceled) {
118
+ throw new Error('shell: command canceled');
119
+ }
120
+ if (result.timedOut) {
121
+ throw new Error(`shell: command timed out after ${step.timeoutMs ?? 0}ms`);
122
+ }
123
+ if (result.failed && result.exitCode == null) {
124
+ throw new Error('shell: command failed to spawn');
125
+ }
126
+ if (result.exitCode !== 0) {
127
+ const parts = [`shell: command exited with code ${result.exitCode}`];
128
+ if (result.signal)
129
+ parts.push(`(signal: ${result.signal})`);
130
+ throw new Error(parts.join(' '));
131
+ }
132
+ text = result.stdout.trimEnd();
133
+ }
134
+ catch (err) {
135
+ if (step.onError === 'skip') {
136
+ outputs.push('');
137
+ onProgress?.(`step ${stepId}: skipped`);
138
+ continue;
139
+ }
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ throw new Error(`Pipeline step ${i} failed: ${message}`);
142
+ }
143
+ outputs.push(text);
144
+ onProgress?.(`step ${stepId}: done`);
145
+ continue;
146
+ }
147
+ // --- Discord action step ---
148
+ if (step.kind === 'discord-action') {
149
+ const resolvedActions = typeof step.actions === 'function' ? step.actions(ctx) : step.actions;
150
+ const interpolatedActions = interpolateDeep(resolvedActions, i, steps, outputs);
151
+ let results;
152
+ try {
153
+ results = await step.execute(interpolatedActions);
154
+ const failed = results.filter((r) => !r.ok);
155
+ if (failed.length > 0) {
156
+ const errorMsg = failed.map((r) => r.error ?? 'action failed').join('; ');
157
+ throw new Error(errorMsg);
158
+ }
159
+ }
160
+ catch (err) {
161
+ if (step.onError === 'skip') {
162
+ outputs.push('');
163
+ onProgress?.(`step ${stepId}: skipped`);
164
+ continue;
165
+ }
166
+ const message = err instanceof Error ? err.message : String(err);
167
+ throw new Error(`Pipeline step ${i} failed: ${message}`);
168
+ }
169
+ outputs.push(JSON.stringify(results));
170
+ onProgress?.(`step ${stepId}: done`);
171
+ continue;
172
+ }
173
+ // --- Prompt step ---
174
+ const resolvedPrompt = typeof step.prompt === 'function'
175
+ ? step.prompt(ctx)
176
+ : interpolateTemplate(step.prompt, i, steps, outputs);
177
+ const stepRuntime = step.runtime ?? runtime;
178
+ const invokeParams = {
179
+ prompt: resolvedPrompt,
180
+ model: step.model ?? model,
181
+ cwd,
182
+ signal,
183
+ ...(step.tools !== undefined && { tools: step.tools }),
184
+ ...(step.addDirs !== undefined && { addDirs: step.addDirs }),
185
+ ...(step.timeoutMs !== undefined && { timeoutMs: step.timeoutMs }),
186
+ ...(step.sessionId !== undefined && { sessionId: step.sessionId }),
187
+ ...(step.sessionKey !== undefined && { sessionKey: step.sessionKey }),
188
+ };
189
+ let text;
190
+ try {
191
+ text = await collectText(stepRuntime.invoke(invokeParams), signal);
192
+ }
193
+ catch (err) {
194
+ if (step.onError === 'skip') {
195
+ outputs.push('');
196
+ onProgress?.(`step ${stepId}: skipped`);
197
+ continue;
198
+ }
199
+ const message = err instanceof Error ? err.message : String(err);
200
+ throw new Error(`Pipeline step ${i} failed: ${message}`);
201
+ }
202
+ outputs.push(text);
203
+ onProgress?.(`step ${stepId}: done`);
204
+ }
205
+ return { outputs };
206
+ }