@ag-eco/agentplate-cli 0.13.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 (455) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +462 -0
  3. package/agents/ap-co-creation.md +90 -0
  4. package/agents/builder.md +144 -0
  5. package/agents/coordinator.md +377 -0
  6. package/agents/lead.md +435 -0
  7. package/agents/merger.md +164 -0
  8. package/agents/monitor.md +214 -0
  9. package/agents/orchestrator.md +239 -0
  10. package/agents/reviewer.md +140 -0
  11. package/agents/scout.md +125 -0
  12. package/agents/supervisor.md +427 -0
  13. package/package.json +66 -0
  14. package/src/agents/capabilities.test.ts +85 -0
  15. package/src/agents/capabilities.ts +125 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +101 -0
  18. package/src/agents/copilot-hooks-deployer.test.ts +162 -0
  19. package/src/agents/copilot-hooks-deployer.ts +93 -0
  20. package/src/agents/guard-rules.test.ts +372 -0
  21. package/src/agents/guard-rules.ts +97 -0
  22. package/src/agents/headless-mail-injector.test.ts +709 -0
  23. package/src/agents/headless-mail-injector.ts +377 -0
  24. package/src/agents/headless-prompt.test.ts +102 -0
  25. package/src/agents/headless-prompt.ts +68 -0
  26. package/src/agents/hooks-deployer.test.ts +3119 -0
  27. package/src/agents/hooks-deployer.ts +804 -0
  28. package/src/agents/identity.test.ts +604 -0
  29. package/src/agents/identity.ts +384 -0
  30. package/src/agents/lifecycle.test.ts +196 -0
  31. package/src/agents/lifecycle.ts +183 -0
  32. package/src/agents/mail-poll-detect.test.ts +153 -0
  33. package/src/agents/mail-poll-detect.ts +73 -0
  34. package/src/agents/manifest.test.ts +1026 -0
  35. package/src/agents/manifest.ts +376 -0
  36. package/src/agents/overlay.test.ts +1058 -0
  37. package/src/agents/overlay.ts +490 -0
  38. package/src/agents/scope-detect.test.ts +190 -0
  39. package/src/agents/scope-detect.ts +146 -0
  40. package/src/agents/turn-lock.test.ts +181 -0
  41. package/src/agents/turn-lock.ts +235 -0
  42. package/src/agents/turn-runner-dispatch.test.ts +182 -0
  43. package/src/agents/turn-runner-dispatch.ts +105 -0
  44. package/src/agents/turn-runner.test.ts +2312 -0
  45. package/src/agents/turn-runner.ts +1383 -0
  46. package/src/beads/client.test.ts +217 -0
  47. package/src/beads/client.ts +230 -0
  48. package/src/beads/molecules.test.ts +338 -0
  49. package/src/beads/molecules.ts +198 -0
  50. package/src/commands/agents.test.ts +328 -0
  51. package/src/commands/agents.ts +299 -0
  52. package/src/commands/clean.test.ts +797 -0
  53. package/src/commands/clean.ts +791 -0
  54. package/src/commands/completions.test.ts +348 -0
  55. package/src/commands/completions.ts +981 -0
  56. package/src/commands/coordinator.test.ts +2975 -0
  57. package/src/commands/coordinator.ts +1841 -0
  58. package/src/commands/costs.test.ts +1183 -0
  59. package/src/commands/costs.ts +599 -0
  60. package/src/commands/dashboard.test.ts +954 -0
  61. package/src/commands/dashboard.ts +1212 -0
  62. package/src/commands/discover.test.ts +288 -0
  63. package/src/commands/discover.ts +202 -0
  64. package/src/commands/doctor.test.ts +303 -0
  65. package/src/commands/doctor.ts +311 -0
  66. package/src/commands/ecosystem.test.ts +226 -0
  67. package/src/commands/ecosystem.ts +248 -0
  68. package/src/commands/errors.test.ts +654 -0
  69. package/src/commands/errors.ts +197 -0
  70. package/src/commands/feed.test.ts +709 -0
  71. package/src/commands/feed.ts +260 -0
  72. package/src/commands/group.test.ts +475 -0
  73. package/src/commands/group.ts +546 -0
  74. package/src/commands/hooks.test.ts +458 -0
  75. package/src/commands/hooks.ts +263 -0
  76. package/src/commands/init.test.ts +1011 -0
  77. package/src/commands/init.ts +967 -0
  78. package/src/commands/inspect.test.ts +1239 -0
  79. package/src/commands/inspect.ts +648 -0
  80. package/src/commands/log.test.ts +1913 -0
  81. package/src/commands/log.ts +958 -0
  82. package/src/commands/logs.test.ts +801 -0
  83. package/src/commands/logs.ts +483 -0
  84. package/src/commands/mail.test.ts +1501 -0
  85. package/src/commands/mail.ts +848 -0
  86. package/src/commands/merge.test.ts +864 -0
  87. package/src/commands/merge.ts +381 -0
  88. package/src/commands/metrics.test.ts +458 -0
  89. package/src/commands/metrics.ts +129 -0
  90. package/src/commands/monitor.test.ts +191 -0
  91. package/src/commands/monitor.ts +409 -0
  92. package/src/commands/nudge.test.ts +579 -0
  93. package/src/commands/nudge.ts +646 -0
  94. package/src/commands/orchestrator.ts +42 -0
  95. package/src/commands/prime.test.ts +612 -0
  96. package/src/commands/prime.ts +359 -0
  97. package/src/commands/replay.test.ts +757 -0
  98. package/src/commands/replay.ts +231 -0
  99. package/src/commands/run.test.ts +469 -0
  100. package/src/commands/run.ts +353 -0
  101. package/src/commands/serve/agent-actions.test.ts +210 -0
  102. package/src/commands/serve/agent-actions.ts +192 -0
  103. package/src/commands/serve/build.test.ts +202 -0
  104. package/src/commands/serve/build.ts +206 -0
  105. package/src/commands/serve/coordinator-actions.test.ts +339 -0
  106. package/src/commands/serve/coordinator-actions.ts +410 -0
  107. package/src/commands/serve/dev.test.ts +168 -0
  108. package/src/commands/serve/dev.ts +117 -0
  109. package/src/commands/serve/mail-actions.test.ts +312 -0
  110. package/src/commands/serve/mail-actions.ts +167 -0
  111. package/src/commands/serve/rest.test.ts +1680 -0
  112. package/src/commands/serve/rest.ts +1130 -0
  113. package/src/commands/serve/static.ts +51 -0
  114. package/src/commands/serve/ws.test.ts +361 -0
  115. package/src/commands/serve/ws.ts +332 -0
  116. package/src/commands/serve.test.ts +459 -0
  117. package/src/commands/serve.ts +654 -0
  118. package/src/commands/sling.test.ts +1583 -0
  119. package/src/commands/sling.ts +1351 -0
  120. package/src/commands/spec.test.ts +179 -0
  121. package/src/commands/spec.ts +105 -0
  122. package/src/commands/status.test.ts +614 -0
  123. package/src/commands/status.ts +403 -0
  124. package/src/commands/stop.test.ts +964 -0
  125. package/src/commands/stop.ts +319 -0
  126. package/src/commands/supervisor.test.ts +185 -0
  127. package/src/commands/supervisor.ts +537 -0
  128. package/src/commands/trace.test.ts +762 -0
  129. package/src/commands/trace.ts +205 -0
  130. package/src/commands/update.test.ts +466 -0
  131. package/src/commands/update.ts +263 -0
  132. package/src/commands/upgrade.test.ts +48 -0
  133. package/src/commands/upgrade.ts +240 -0
  134. package/src/commands/watch.test.ts +257 -0
  135. package/src/commands/watch.ts +308 -0
  136. package/src/commands/worktree.test.ts +1297 -0
  137. package/src/commands/worktree.ts +451 -0
  138. package/src/config.test.ts +1535 -0
  139. package/src/config.ts +1064 -0
  140. package/src/doctor/agents.test.ts +523 -0
  141. package/src/doctor/agents.ts +399 -0
  142. package/src/doctor/config-check.test.ts +191 -0
  143. package/src/doctor/config-check.ts +183 -0
  144. package/src/doctor/consistency.test.ts +807 -0
  145. package/src/doctor/consistency.ts +347 -0
  146. package/src/doctor/databases.test.ts +350 -0
  147. package/src/doctor/databases.ts +243 -0
  148. package/src/doctor/dependencies.test.ts +296 -0
  149. package/src/doctor/dependencies.ts +272 -0
  150. package/src/doctor/ecosystem.test.ts +308 -0
  151. package/src/doctor/ecosystem.ts +156 -0
  152. package/src/doctor/logs.test.ts +253 -0
  153. package/src/doctor/logs.ts +295 -0
  154. package/src/doctor/merge-queue.test.ts +315 -0
  155. package/src/doctor/merge-queue.ts +167 -0
  156. package/src/doctor/providers.test.ts +409 -0
  157. package/src/doctor/providers.ts +250 -0
  158. package/src/doctor/serve.test.ts +95 -0
  159. package/src/doctor/serve.ts +86 -0
  160. package/src/doctor/structure.test.ts +423 -0
  161. package/src/doctor/structure.ts +285 -0
  162. package/src/doctor/types.ts +43 -0
  163. package/src/doctor/version.test.ts +241 -0
  164. package/src/doctor/version.ts +132 -0
  165. package/src/doctor/watchdog.test.ts +167 -0
  166. package/src/doctor/watchdog.ts +214 -0
  167. package/src/e2e/init-sling-lifecycle.test.ts +283 -0
  168. package/src/errors.test.ts +350 -0
  169. package/src/errors.ts +217 -0
  170. package/src/events/store.test.ts +660 -0
  171. package/src/events/store.ts +369 -0
  172. package/src/events/tailer.test.ts +719 -0
  173. package/src/events/tailer.ts +332 -0
  174. package/src/events/tool-filter.test.ts +330 -0
  175. package/src/events/tool-filter.ts +126 -0
  176. package/src/index.ts +533 -0
  177. package/src/insights/analyzer.test.ts +466 -0
  178. package/src/insights/analyzer.ts +203 -0
  179. package/src/insights/quality-gates.test.ts +141 -0
  180. package/src/insights/quality-gates.ts +156 -0
  181. package/src/json.test.ts +72 -0
  182. package/src/json.ts +53 -0
  183. package/src/loam/client.test.ts +752 -0
  184. package/src/loam/client.ts +664 -0
  185. package/src/logging/color.test.ts +252 -0
  186. package/src/logging/color.ts +105 -0
  187. package/src/logging/format.test.ts +110 -0
  188. package/src/logging/format.ts +255 -0
  189. package/src/logging/logger.test.ts +814 -0
  190. package/src/logging/logger.ts +266 -0
  191. package/src/logging/reporter.test.ts +259 -0
  192. package/src/logging/reporter.ts +110 -0
  193. package/src/logging/sanitizer.test.ts +190 -0
  194. package/src/logging/sanitizer.ts +57 -0
  195. package/src/logging/theme.ts +140 -0
  196. package/src/mail/broadcast.test.ts +204 -0
  197. package/src/mail/broadcast.ts +92 -0
  198. package/src/mail/client.test.ts +774 -0
  199. package/src/mail/client.ts +236 -0
  200. package/src/mail/store.test.ts +898 -0
  201. package/src/mail/store.ts +425 -0
  202. package/src/merge/lock.test.ts +149 -0
  203. package/src/merge/lock.ts +140 -0
  204. package/src/merge/predict.test.ts +387 -0
  205. package/src/merge/predict.ts +249 -0
  206. package/src/merge/queue.test.ts +426 -0
  207. package/src/merge/queue.ts +246 -0
  208. package/src/merge/resolver.test.ts +1993 -0
  209. package/src/merge/resolver.ts +926 -0
  210. package/src/metrics/pricing.test.ts +258 -0
  211. package/src/metrics/pricing.ts +135 -0
  212. package/src/metrics/store.test.ts +978 -0
  213. package/src/metrics/store.ts +501 -0
  214. package/src/metrics/summary.test.ts +398 -0
  215. package/src/metrics/summary.ts +178 -0
  216. package/src/metrics/transcript.test.ts +483 -0
  217. package/src/metrics/transcript.ts +114 -0
  218. package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
  219. package/src/runtimes/aider.test.ts +124 -0
  220. package/src/runtimes/aider.ts +147 -0
  221. package/src/runtimes/amp.test.ts +164 -0
  222. package/src/runtimes/amp.ts +154 -0
  223. package/src/runtimes/claude.test.ts +1474 -0
  224. package/src/runtimes/claude.ts +579 -0
  225. package/src/runtimes/codex.test.ts +805 -0
  226. package/src/runtimes/codex.ts +273 -0
  227. package/src/runtimes/connections.test.ts +214 -0
  228. package/src/runtimes/connections.ts +103 -0
  229. package/src/runtimes/copilot.test.ts +707 -0
  230. package/src/runtimes/copilot.ts +316 -0
  231. package/src/runtimes/cursor.test.ts +497 -0
  232. package/src/runtimes/cursor.ts +205 -0
  233. package/src/runtimes/gemini.test.ts +537 -0
  234. package/src/runtimes/gemini.ts +243 -0
  235. package/src/runtimes/goose.test.ts +133 -0
  236. package/src/runtimes/goose.ts +157 -0
  237. package/src/runtimes/headless-connection.test.ts +264 -0
  238. package/src/runtimes/headless-connection.ts +158 -0
  239. package/src/runtimes/opencode.test.ts +325 -0
  240. package/src/runtimes/opencode.ts +188 -0
  241. package/src/runtimes/pi-guards.test.ts +486 -0
  242. package/src/runtimes/pi-guards.ts +367 -0
  243. package/src/runtimes/pi.test.ts +789 -0
  244. package/src/runtimes/pi.ts +305 -0
  245. package/src/runtimes/registry.test.ts +196 -0
  246. package/src/runtimes/registry.ts +99 -0
  247. package/src/runtimes/sapling.test.ts +1267 -0
  248. package/src/runtimes/sapling.ts +710 -0
  249. package/src/runtimes/types.ts +266 -0
  250. package/src/schema-consistency.test.ts +246 -0
  251. package/src/sessions/compat.test.ts +281 -0
  252. package/src/sessions/compat.ts +105 -0
  253. package/src/sessions/store.test.ts +1748 -0
  254. package/src/sessions/store.ts +858 -0
  255. package/src/test-helpers.test.ts +124 -0
  256. package/src/test-helpers.ts +145 -0
  257. package/src/test-setup.test.ts +31 -0
  258. package/src/test-setup.ts +28 -0
  259. package/src/tools/loam/api.ts +368 -0
  260. package/src/tools/loam/cli.ts +278 -0
  261. package/src/tools/loam/commands/add.ts +52 -0
  262. package/src/tools/loam/commands/archive.ts +214 -0
  263. package/src/tools/loam/commands/audit.ts +276 -0
  264. package/src/tools/loam/commands/compact.ts +1062 -0
  265. package/src/tools/loam/commands/completions.ts +79 -0
  266. package/src/tools/loam/commands/config.ts +381 -0
  267. package/src/tools/loam/commands/delete-domain.ts +121 -0
  268. package/src/tools/loam/commands/delete.ts +316 -0
  269. package/src/tools/loam/commands/diff.ts +200 -0
  270. package/src/tools/loam/commands/doctor.ts +1113 -0
  271. package/src/tools/loam/commands/edit.ts +226 -0
  272. package/src/tools/loam/commands/init.ts +31 -0
  273. package/src/tools/loam/commands/learn.ts +179 -0
  274. package/src/tools/loam/commands/move.ts +323 -0
  275. package/src/tools/loam/commands/onboard.ts +374 -0
  276. package/src/tools/loam/commands/outcome.ts +185 -0
  277. package/src/tools/loam/commands/prime.ts +688 -0
  278. package/src/tools/loam/commands/prune.ts +614 -0
  279. package/src/tools/loam/commands/query.ts +218 -0
  280. package/src/tools/loam/commands/rank.ts +180 -0
  281. package/src/tools/loam/commands/ready.ts +189 -0
  282. package/src/tools/loam/commands/record.ts +1210 -0
  283. package/src/tools/loam/commands/restore.ts +166 -0
  284. package/src/tools/loam/commands/search.ts +327 -0
  285. package/src/tools/loam/commands/setup.ts +887 -0
  286. package/src/tools/loam/commands/status.ts +103 -0
  287. package/src/tools/loam/commands/sync.ts +298 -0
  288. package/src/tools/loam/commands/update.ts +19 -0
  289. package/src/tools/loam/commands/upgrade.ts +93 -0
  290. package/src/tools/loam/commands/validate.ts +190 -0
  291. package/src/tools/loam/index.ts +62 -0
  292. package/src/tools/loam/log.ts +127 -0
  293. package/src/tools/loam/registry/builtins.ts +409 -0
  294. package/src/tools/loam/registry/custom.ts +431 -0
  295. package/src/tools/loam/registry/init.ts +55 -0
  296. package/src/tools/loam/registry/template.ts +40 -0
  297. package/src/tools/loam/registry/type-registry.ts +113 -0
  298. package/src/tools/loam/schemas/config-schema.ts +489 -0
  299. package/src/tools/loam/schemas/config.ts +245 -0
  300. package/src/tools/loam/schemas/index.ts +18 -0
  301. package/src/tools/loam/schemas/record-schema.ts +191 -0
  302. package/src/tools/loam/schemas/record.ts +115 -0
  303. package/src/tools/loam/utils/active-work.ts +205 -0
  304. package/src/tools/loam/utils/anchor-validity.ts +80 -0
  305. package/src/tools/loam/utils/archive.ts +146 -0
  306. package/src/tools/loam/utils/audit.ts +667 -0
  307. package/src/tools/loam/utils/bm25.ts +238 -0
  308. package/src/tools/loam/utils/budget.ts +142 -0
  309. package/src/tools/loam/utils/config.ts +344 -0
  310. package/src/tools/loam/utils/dir-anchors.ts +62 -0
  311. package/src/tools/loam/utils/domain-rules.ts +114 -0
  312. package/src/tools/loam/utils/expertise.ts +393 -0
  313. package/src/tools/loam/utils/format-helpers.ts +96 -0
  314. package/src/tools/loam/utils/format.ts +1234 -0
  315. package/src/tools/loam/utils/git-context.ts +50 -0
  316. package/src/tools/loam/utils/git.ts +183 -0
  317. package/src/tools/loam/utils/hooks.ts +299 -0
  318. package/src/tools/loam/utils/index.ts +52 -0
  319. package/src/tools/loam/utils/json-output.ts +13 -0
  320. package/src/tools/loam/utils/lock.ts +76 -0
  321. package/src/tools/loam/utils/markers.ts +48 -0
  322. package/src/tools/loam/utils/numeric-flags.ts +20 -0
  323. package/src/tools/loam/utils/palette.ts +44 -0
  324. package/src/tools/loam/utils/prime-ranking.ts +135 -0
  325. package/src/tools/loam/utils/recipe-discovery.ts +195 -0
  326. package/src/tools/loam/utils/runtime-flags.ts +28 -0
  327. package/src/tools/loam/utils/scoring.ts +94 -0
  328. package/src/tools/loam/utils/version.ts +116 -0
  329. package/src/tools/sprout/commands/block.ts +64 -0
  330. package/src/tools/sprout/commands/blocked.ts +86 -0
  331. package/src/tools/sprout/commands/close.ts +129 -0
  332. package/src/tools/sprout/commands/completions.ts +198 -0
  333. package/src/tools/sprout/commands/config.ts +238 -0
  334. package/src/tools/sprout/commands/create.ts +164 -0
  335. package/src/tools/sprout/commands/dep.ts +148 -0
  336. package/src/tools/sprout/commands/doctor.ts +979 -0
  337. package/src/tools/sprout/commands/init.ts +83 -0
  338. package/src/tools/sprout/commands/label.ts +178 -0
  339. package/src/tools/sprout/commands/list.ts +210 -0
  340. package/src/tools/sprout/commands/migrate.ts +133 -0
  341. package/src/tools/sprout/commands/onboard.ts +207 -0
  342. package/src/tools/sprout/commands/plan-show.ts +278 -0
  343. package/src/tools/sprout/commands/plan.ts +2526 -0
  344. package/src/tools/sprout/commands/prime.ts +399 -0
  345. package/src/tools/sprout/commands/ready.ts +245 -0
  346. package/src/tools/sprout/commands/search.ts +221 -0
  347. package/src/tools/sprout/commands/show.ts +277 -0
  348. package/src/tools/sprout/commands/stats.ts +146 -0
  349. package/src/tools/sprout/commands/sync.ts +134 -0
  350. package/src/tools/sprout/commands/tpl.ts +364 -0
  351. package/src/tools/sprout/commands/unblock.ts +115 -0
  352. package/src/tools/sprout/commands/update.ts +257 -0
  353. package/src/tools/sprout/commands/upgrade.ts +91 -0
  354. package/src/tools/sprout/config-schema.ts +152 -0
  355. package/src/tools/sprout/config.ts +355 -0
  356. package/src/tools/sprout/filter.ts +107 -0
  357. package/src/tools/sprout/format.ts +43 -0
  358. package/src/tools/sprout/id.ts +22 -0
  359. package/src/tools/sprout/index.ts +204 -0
  360. package/src/tools/sprout/log.ts +76 -0
  361. package/src/tools/sprout/markers.ts +22 -0
  362. package/src/tools/sprout/output.ts +121 -0
  363. package/src/tools/sprout/plan-backref.ts +93 -0
  364. package/src/tools/sprout/plan-context.ts +81 -0
  365. package/src/tools/sprout/plan-domain.ts +139 -0
  366. package/src/tools/sprout/plan-lifecycle.ts +65 -0
  367. package/src/tools/sprout/plan-loam.ts +207 -0
  368. package/src/tools/sprout/plan-schema.ts +209 -0
  369. package/src/tools/sprout/sort.ts +31 -0
  370. package/src/tools/sprout/store.ts +172 -0
  371. package/src/tools/sprout/types.ts +118 -0
  372. package/src/tools/sprout/validation.ts +119 -0
  373. package/src/tools/sprout/version.ts +1 -0
  374. package/src/tools/sprout/yaml.ts +387 -0
  375. package/src/tools/trellis/commands/archive.ts +87 -0
  376. package/src/tools/trellis/commands/completions.ts +610 -0
  377. package/src/tools/trellis/commands/config.ts +382 -0
  378. package/src/tools/trellis/commands/create.ts +252 -0
  379. package/src/tools/trellis/commands/diff.ts +150 -0
  380. package/src/tools/trellis/commands/doctor.ts +771 -0
  381. package/src/tools/trellis/commands/emit.ts +365 -0
  382. package/src/tools/trellis/commands/history.ts +83 -0
  383. package/src/tools/trellis/commands/import.ts +198 -0
  384. package/src/tools/trellis/commands/init.ts +81 -0
  385. package/src/tools/trellis/commands/list.ts +103 -0
  386. package/src/tools/trellis/commands/onboard.ts +156 -0
  387. package/src/tools/trellis/commands/pin.ts +172 -0
  388. package/src/tools/trellis/commands/prime.ts +193 -0
  389. package/src/tools/trellis/commands/render.ts +122 -0
  390. package/src/tools/trellis/commands/schema.ts +353 -0
  391. package/src/tools/trellis/commands/show.ts +115 -0
  392. package/src/tools/trellis/commands/stats.ts +65 -0
  393. package/src/tools/trellis/commands/sync.ts +112 -0
  394. package/src/tools/trellis/commands/tree.ts +123 -0
  395. package/src/tools/trellis/commands/update.ts +330 -0
  396. package/src/tools/trellis/commands/upgrade.ts +95 -0
  397. package/src/tools/trellis/commands/validate.ts +166 -0
  398. package/src/tools/trellis/config-schema.ts +81 -0
  399. package/src/tools/trellis/config.ts +108 -0
  400. package/src/tools/trellis/frontmatter.ts +348 -0
  401. package/src/tools/trellis/id.ts +24 -0
  402. package/src/tools/trellis/index.ts +209 -0
  403. package/src/tools/trellis/markers.ts +28 -0
  404. package/src/tools/trellis/output.ts +84 -0
  405. package/src/tools/trellis/render.ts +212 -0
  406. package/src/tools/trellis/store.ts +144 -0
  407. package/src/tools/trellis/types.ts +82 -0
  408. package/src/tools/trellis/validate.ts +199 -0
  409. package/src/tools/trellis/yaml.ts +309 -0
  410. package/src/tracker/beads.test.ts +454 -0
  411. package/src/tracker/beads.ts +56 -0
  412. package/src/tracker/factory.test.ts +90 -0
  413. package/src/tracker/factory.ts +65 -0
  414. package/src/tracker/sprout.test.ts +461 -0
  415. package/src/tracker/sprout.ts +182 -0
  416. package/src/tracker/types.ts +52 -0
  417. package/src/trellis/client.test.ts +107 -0
  418. package/src/trellis/client.ts +179 -0
  419. package/src/types.ts +970 -0
  420. package/src/utils/bin.test.ts +10 -0
  421. package/src/utils/bin.ts +37 -0
  422. package/src/utils/browser.test.ts +49 -0
  423. package/src/utils/browser.ts +48 -0
  424. package/src/utils/fs.test.ts +119 -0
  425. package/src/utils/fs.ts +62 -0
  426. package/src/utils/pid.test.ts +152 -0
  427. package/src/utils/pid.ts +130 -0
  428. package/src/utils/process-scan.test.ts +53 -0
  429. package/src/utils/process-scan.ts +76 -0
  430. package/src/utils/time.test.ts +43 -0
  431. package/src/utils/time.ts +37 -0
  432. package/src/utils/version.test.ts +33 -0
  433. package/src/utils/version.ts +70 -0
  434. package/src/version.ts +5 -0
  435. package/src/watchdog/daemon.test.ts +3721 -0
  436. package/src/watchdog/daemon.ts +1257 -0
  437. package/src/watchdog/health.test.ts +830 -0
  438. package/src/watchdog/health.ts +434 -0
  439. package/src/watchdog/triage.test.ts +205 -0
  440. package/src/watchdog/triage.ts +205 -0
  441. package/src/worktree/manager.test.ts +720 -0
  442. package/src/worktree/manager.ts +405 -0
  443. package/src/worktree/process.test.ts +172 -0
  444. package/src/worktree/process.ts +131 -0
  445. package/src/worktree/tmux.test.ts +1616 -0
  446. package/src/worktree/tmux.ts +721 -0
  447. package/templates/CLAUDE.md.tmpl +100 -0
  448. package/templates/copilot-hooks.json.tmpl +13 -0
  449. package/templates/hooks.json.tmpl +109 -0
  450. package/templates/overlay.md.tmpl +88 -0
  451. package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
  452. package/ui/dist/chunk-8s31f05k.css +1 -0
  453. package/ui/dist/chunk-vm5rz679.js +300 -0
  454. package/ui/dist/favicon-nzb39vza.svg +4 -0
  455. package/ui/dist/index.html +17 -0
@@ -0,0 +1,858 @@
1
+ /**
2
+ * SQLite-backed session store for agent lifecycle tracking.
3
+ *
4
+ * Replaces the flat-file sessions.json with a proper database.
5
+ * Uses bun:sqlite for zero-dependency, synchronous database access.
6
+ * WAL mode enables concurrent reads from multiple agent processes.
7
+ */
8
+
9
+ import { Database } from "bun:sqlite";
10
+ import type {
11
+ AgentSession,
12
+ AgentState,
13
+ InsertRun,
14
+ Run,
15
+ RunStatus,
16
+ RunStore,
17
+ TransitionOutcome,
18
+ } from "../types.ts";
19
+
20
+ /**
21
+ * Allowed predecessor states for each target state, enforced by
22
+ * `tryTransitionState` via an atomic SQL compare-and-swap.
23
+ *
24
+ * Invariants:
25
+ * - `completed` is sticky: nothing transitions out of it. The watchdog cannot
26
+ * reclassify a properly-completed agent as zombie.
27
+ * - `zombie` is durable except `ap stop` may promote it to `completed` for
28
+ * cleanup. A turn-runner that "settles to working" after watchdog already
29
+ * wrote zombie is rejected — last writer no longer wins.
30
+ * - Idempotent self-transitions (e.g. `working → working`) are allowed.
31
+ * - `booting` is set only by the initial `upsert` and never re-entered.
32
+ * - `in_turn` and `between_turns` cycle while a spawn-per-turn worker is
33
+ * alive (agentplate-3087): turn-runner advances `between_turns → in_turn`
34
+ * when the next batch produces its first parser event and settles back
35
+ * `in_turn → between_turns` when the turn ends without a terminal mail.
36
+ * Both can advance forward to `stalled`/`zombie`/`completed`. The two
37
+ * paths are kept separate from the tmux/long-lived `working` rank — a
38
+ * spawn-per-turn worker should not flow through `working` during normal
39
+ * operation — so neither lists `working` as a predecessor.
40
+ *
41
+ * See agentplate-a993 for the race symptoms this guard prevents.
42
+ */
43
+ const TRANSITION_ALLOWED_FROM: Record<AgentState, readonly AgentState[]> = {
44
+ booting: [],
45
+ working: ["booting", "working", "stalled"],
46
+ in_turn: ["booting", "in_turn", "between_turns", "stalled"],
47
+ between_turns: ["in_turn", "between_turns", "stalled"],
48
+ stalled: ["booting", "working", "in_turn", "between_turns", "stalled"],
49
+ completed: ["booting", "working", "in_turn", "between_turns", "stalled", "zombie", "completed"],
50
+ zombie: ["booting", "working", "in_turn", "between_turns", "stalled", "zombie"],
51
+ };
52
+
53
+ /**
54
+ * States in which an agent's tmux session no longer exists. When a session
55
+ * lands in one of these, `tmux_session` is cleared to `''` so the agents-side
56
+ * view stops surfacing tmux session names that have been torn down.
57
+ *
58
+ * The live `tmuxSessions` array on `ap status` reflects what tmux actually
59
+ * reports; the stored `tmux_session` column is what the agents-side view reads.
60
+ * Without this clear, completed/zombie agents carry stale tmux strings forever
61
+ * (agentplate-14c0).
62
+ */
63
+ const TERMINAL_STATES: readonly AgentState[] = ["completed", "zombie"];
64
+
65
+ export interface SessionStore {
66
+ /** Insert or update a session. Uses agent_name as the unique key. */
67
+ upsert(session: AgentSession): void;
68
+ /** Get a session by agent name, or null if not found. */
69
+ getByName(agentName: string): AgentSession | null;
70
+ /**
71
+ * Get all active sessions (state IN ('booting', 'working', 'in_turn',
72
+ * 'between_turns', 'stalled')).
73
+ *
74
+ * `in_turn` and `between_turns` are spawn-per-turn equivalents of `working`
75
+ * and must be returned by `getActive` so the watchdog and dashboards see
76
+ * spawn-per-turn workers as alive (agentplate-3087).
77
+ */
78
+ getActive(): AgentSession[];
79
+ /** Get all sessions regardless of state. */
80
+ getAll(): AgentSession[];
81
+ /** Get the total number of sessions. Lightweight alternative to getAll().length. */
82
+ count(): number;
83
+ /** Get sessions belonging to a specific run. */
84
+ getByRun(runId: string): AgentSession[];
85
+ /**
86
+ * Update only the state of a session.
87
+ *
88
+ * Unconditional override — does not validate the prev → next transition.
89
+ * Reserved for forced cleanup paths (`ap clean`, `ap sling` startup failure,
90
+ * supervisor/coordinator/monitor self-management). For race-prone writers
91
+ * (turn-runner settle, `ap stop`, watchdog), use `tryTransitionState`.
92
+ */
93
+ updateState(agentName: string, state: AgentState): void;
94
+ /**
95
+ * Atomically transition a session's state, validated against the matrix in
96
+ * `TRANSITION_ALLOWED_FROM`. Implemented as a single `UPDATE ... WHERE state
97
+ * IN (...)` so concurrent writers cannot both succeed against the same row.
98
+ *
99
+ * Returns a discriminated outcome describing whether the write landed and,
100
+ * on rejection, whether the row was missing or the transition was illegal.
101
+ */
102
+ tryTransitionState(agentName: string, newState: AgentState): TransitionOutcome;
103
+ /** Update lastActivity to current ISO timestamp. */
104
+ updateLastActivity(agentName: string): void;
105
+ /** Update escalation level and stalled timestamp. */
106
+ updateEscalation(agentName: string, level: number, stalledSince: string | null): void;
107
+ /** Update the transcript path for a session. */
108
+ updateTranscriptPath(agentName: string, path: string): void;
109
+ /** Update the runtime-provided session_id (e.g. Claude stream-json session_id). */
110
+ updateClaudeSessionId(agentName: string, sessionId: string): void;
111
+ /** Remove a session by agent name. */
112
+ remove(agentName: string): void;
113
+ /** Purge sessions matching criteria. Returns count of deleted rows. */
114
+ purge(opts: { all?: boolean; state?: AgentState; agent?: string }): number;
115
+ /** Close the database connection. */
116
+ close(): void;
117
+ }
118
+
119
+ /** Row shape as stored in SQLite (snake_case columns). */
120
+ interface SessionRow {
121
+ id: string;
122
+ agent_name: string;
123
+ capability: string;
124
+ worktree_path: string;
125
+ branch_name: string;
126
+ task_id: string;
127
+ tmux_session: string;
128
+ state: string;
129
+ pid: number | null;
130
+ parent_agent: string | null;
131
+ depth: number;
132
+ run_id: string | null;
133
+ started_at: string;
134
+ last_activity: string;
135
+ escalation_level: number;
136
+ stalled_since: string | null;
137
+ transcript_path: string | null;
138
+ prompt_version: string | null;
139
+ claude_session_id: string | null;
140
+ }
141
+
142
+ /** Row shape for runs table as stored in SQLite (snake_case columns). */
143
+ interface RunRow {
144
+ id: string;
145
+ started_at: string;
146
+ completed_at: string | null;
147
+ agent_count: number;
148
+ coordinator_session_id: string | null;
149
+ coordinator_name: string | null;
150
+ status: string;
151
+ }
152
+
153
+ const CREATE_TABLE = `
154
+ CREATE TABLE IF NOT EXISTS sessions (
155
+ id TEXT PRIMARY KEY,
156
+ agent_name TEXT NOT NULL UNIQUE,
157
+ capability TEXT NOT NULL,
158
+ worktree_path TEXT NOT NULL,
159
+ branch_name TEXT NOT NULL,
160
+ task_id TEXT NOT NULL,
161
+ tmux_session TEXT NOT NULL,
162
+ state TEXT NOT NULL DEFAULT 'booting',
163
+ pid INTEGER,
164
+ parent_agent TEXT,
165
+ depth INTEGER NOT NULL DEFAULT 0,
166
+ run_id TEXT,
167
+ started_at TEXT NOT NULL,
168
+ last_activity TEXT NOT NULL,
169
+ escalation_level INTEGER NOT NULL DEFAULT 0,
170
+ stalled_since TEXT,
171
+ transcript_path TEXT,
172
+ prompt_version TEXT,
173
+ claude_session_id TEXT
174
+ )`;
175
+
176
+ const CREATE_INDEXES = `
177
+ CREATE INDEX IF NOT EXISTS idx_sessions_state ON sessions(state);
178
+ CREATE INDEX IF NOT EXISTS idx_sessions_run ON sessions(run_id)`;
179
+
180
+ const CREATE_RUNS_TABLE = `
181
+ CREATE TABLE IF NOT EXISTS runs (
182
+ id TEXT PRIMARY KEY,
183
+ started_at TEXT NOT NULL,
184
+ completed_at TEXT,
185
+ agent_count INTEGER NOT NULL DEFAULT 0,
186
+ coordinator_session_id TEXT,
187
+ coordinator_name TEXT,
188
+ status TEXT NOT NULL DEFAULT 'active'
189
+ CHECK(status IN ('active','completed','failed'))
190
+ )`;
191
+
192
+ const CREATE_RUNS_INDEXES = `
193
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
194
+ CREATE INDEX IF NOT EXISTS idx_runs_coordinator ON runs(coordinator_name)`;
195
+
196
+ /** Convert a database row (snake_case) to an AgentSession object (camelCase). */
197
+ function rowToSession(row: SessionRow): AgentSession {
198
+ return {
199
+ id: row.id,
200
+ agentName: row.agent_name,
201
+ capability: row.capability,
202
+ worktreePath: row.worktree_path,
203
+ branchName: row.branch_name,
204
+ taskId: row.task_id,
205
+ tmuxSession: row.tmux_session,
206
+ state: row.state as AgentState,
207
+ pid: row.pid,
208
+ parentAgent: row.parent_agent,
209
+ depth: row.depth,
210
+ runId: row.run_id,
211
+ startedAt: row.started_at,
212
+ lastActivity: row.last_activity,
213
+ escalationLevel: row.escalation_level,
214
+ stalledSince: row.stalled_since,
215
+ transcriptPath: row.transcript_path,
216
+ ...(row.prompt_version !== null ? { promptVersion: row.prompt_version } : {}),
217
+ ...(row.claude_session_id !== null ? { claudeSessionId: row.claude_session_id } : {}),
218
+ };
219
+ }
220
+
221
+ /** Convert a database row (snake_case) to a Run object (camelCase). */
222
+ function rowToRun(row: RunRow): Run {
223
+ return {
224
+ id: row.id,
225
+ startedAt: row.started_at,
226
+ completedAt: row.completed_at,
227
+ agentCount: row.agent_count,
228
+ coordinatorSessionId: row.coordinator_session_id,
229
+ coordinatorName: row.coordinator_name,
230
+ status: row.status as RunStatus,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Migrate an existing sessions table to add the transcript_path column.
236
+ * Safe to call multiple times — only adds the column if it does not exist.
237
+ */
238
+ function migrateAddTranscriptPath(db: Database): void {
239
+ const rows = db.prepare("PRAGMA table_info(sessions)").all() as Array<{ name: string }>;
240
+ const existingColumns = new Set(rows.map((r) => r.name));
241
+ if (!existingColumns.has("transcript_path")) {
242
+ db.exec("ALTER TABLE sessions ADD COLUMN transcript_path TEXT");
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Migrate an existing sessions table to add the prompt_version column.
248
+ * Safe to call multiple times — only adds the column if it does not exist.
249
+ */
250
+ function migrateAddPromptVersion(db: Database): void {
251
+ const rows = db.prepare("PRAGMA table_info(sessions)").all() as Array<{ name: string }>;
252
+ const existingColumns = new Set(rows.map((r) => r.name));
253
+ if (!existingColumns.has("prompt_version")) {
254
+ db.exec("ALTER TABLE sessions ADD COLUMN prompt_version TEXT");
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Migrate an existing sessions table to add the claude_session_id column.
260
+ * Safe to call multiple times — only adds the column if it does not exist.
261
+ */
262
+ function migrateAddClaudeSessionId(db: Database): void {
263
+ const rows = db.prepare("PRAGMA table_info(sessions)").all() as Array<{ name: string }>;
264
+ const existingColumns = new Set(rows.map((r) => r.name));
265
+ if (!existingColumns.has("claude_session_id")) {
266
+ db.exec("ALTER TABLE sessions ADD COLUMN claude_session_id TEXT");
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Drop the inline CHECK(state IN (...)) constraint from the sessions table
272
+ * (agentplate-3087).
273
+ *
274
+ * The CHECK was defensive — the TypeScript `AgentState` union enforces values
275
+ * at the writer boundary. With the spawn-per-turn substate split (`in_turn` /
276
+ * `between_turns`) and likely future state extensions, keeping the constraint
277
+ * in sync with the union via inline-CHECK rebuilds becomes a recurring tax.
278
+ * Drop it and rely on the type system.
279
+ *
280
+ * SQLite has no `ALTER TABLE DROP CONSTRAINT`, so we detect the old constraint
281
+ * via `sqlite_master.sql` (the recorded CREATE TABLE DDL), then rebuild the
282
+ * table inside a transaction: copy rows verbatim into a new constraint-free
283
+ * schema, drop the original, and rename. Indexes are dropped by the swap and
284
+ * re-created by the caller via CREATE_INDEXES, which is idempotent.
285
+ *
286
+ * Safe to call multiple times — short-circuits when the recorded DDL no
287
+ * longer contains a CHECK on `state`. Must run BEFORE indexes are created
288
+ * (the swap drops them) and BEFORE the column-add migrations that read
289
+ * `PRAGMA table_info` on the legacy table (the new table inherits any added
290
+ * columns via the rebuild, so the column-add migrations become idempotent).
291
+ */
292
+ function migrateRelaxStateCheck(db: Database): void {
293
+ const row = db
294
+ .prepare<{ sql: string | null }, []>(
295
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'sessions'",
296
+ )
297
+ .get();
298
+ if (!row || row.sql === null) return;
299
+ // Detect the inline CHECK on the `state` column. Match conservatively on
300
+ // the literal "CHECK(state IN" — any whitespace variant SQLite stores
301
+ // will still contain this substring.
302
+ if (!row.sql.includes("CHECK(state IN")) return;
303
+
304
+ // Discover the columns that exist on the LIVE table so the rebuild copies
305
+ // every column the column-add migrations have layered on. Hard-coding the
306
+ // column list would silently drop newer columns when this migration runs
307
+ // against a DB that earlier migrations have already extended.
308
+ const colInfo = db.prepare("PRAGMA table_info(sessions)").all() as Array<{
309
+ name: string;
310
+ type: string;
311
+ notnull: number;
312
+ dflt_value: string | null;
313
+ pk: number;
314
+ }>;
315
+
316
+ // Render each column for the new CREATE TABLE. PRIMARY KEY and UNIQUE are
317
+ // preserved on `id` and `agent_name` respectively to match the original
318
+ // schema; everything else is straight type + nullability + default.
319
+ const colDefs = colInfo
320
+ .map((c) => {
321
+ const parts: string[] = [c.name, c.type || "TEXT"];
322
+ if (c.pk === 1) parts.push("PRIMARY KEY");
323
+ if (c.notnull === 1) parts.push("NOT NULL");
324
+ if (c.dflt_value !== null) parts.push(`DEFAULT ${c.dflt_value}`);
325
+ if (c.name === "agent_name") parts.push("UNIQUE");
326
+ return `\t\t\t\t${parts.join(" ")}`;
327
+ })
328
+ .join(",\n");
329
+ const colNames = colInfo.map((c) => c.name).join(", ");
330
+
331
+ db.exec("BEGIN");
332
+ try {
333
+ db.exec(`CREATE TABLE sessions__new_3087 (\n${colDefs}\n\t\t\t)`);
334
+ db.exec(`
335
+ INSERT INTO sessions__new_3087 (${colNames})
336
+ SELECT ${colNames} FROM sessions
337
+ `);
338
+ db.exec("DROP TABLE sessions");
339
+ db.exec("ALTER TABLE sessions__new_3087 RENAME TO sessions");
340
+ db.exec("COMMIT");
341
+ } catch (err) {
342
+ db.exec("ROLLBACK");
343
+ throw err;
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Migrate an existing sessions table from bead_id to task_id column.
349
+ * Safe to call multiple times — only renames if bead_id exists and task_id does not.
350
+ */
351
+ function migrateBeadIdToTaskId(db: Database): void {
352
+ const rows = db.prepare("PRAGMA table_info(sessions)").all() as Array<{ name: string }>;
353
+ const existingColumns = new Set(rows.map((r) => r.name));
354
+ if (existingColumns.has("bead_id") && !existingColumns.has("task_id")) {
355
+ db.exec("ALTER TABLE sessions RENAME COLUMN bead_id TO task_id");
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Create a new SessionStore backed by a SQLite database at the given path.
361
+ *
362
+ * Initializes the database with WAL mode and a 5-second busy timeout.
363
+ * Creates the sessions table and indexes if they do not already exist.
364
+ */
365
+ export function createSessionStore(dbPath: string): SessionStore {
366
+ const db = new Database(dbPath);
367
+
368
+ // Configure for concurrent access from multiple agent processes.
369
+ db.exec("PRAGMA journal_mode = WAL");
370
+ db.exec("PRAGMA synchronous = NORMAL");
371
+ db.exec("PRAGMA busy_timeout = 5000");
372
+
373
+ // Create schema (tables first, then migrations, then indexes)
374
+ db.exec(CREATE_TABLE);
375
+ db.exec(CREATE_RUNS_TABLE);
376
+
377
+ // Migrate existing tables BEFORE creating indexes that reference new columns.
378
+ // `migrateRelaxStateCheck` runs FIRST so the column-add migrations that
379
+ // follow operate on the rebuilt table — they read PRAGMA table_info and
380
+ // ADD COLUMN, both of which work on the new constraint-free schema.
381
+ migrateRelaxStateCheck(db);
382
+ migrateBeadIdToTaskId(db);
383
+ migrateAddTranscriptPath(db);
384
+ migrateAddPromptVersion(db);
385
+ migrateAddClaudeSessionId(db);
386
+ migrateAddCoordinatorName(db);
387
+
388
+ // Now safe to create indexes (all columns exist).
389
+ db.exec(CREATE_INDEXES);
390
+ db.exec(CREATE_RUNS_INDEXES);
391
+
392
+ // Prepare statements for frequent operations
393
+ const upsertStmt = db.prepare<
394
+ void,
395
+ {
396
+ $id: string;
397
+ $agent_name: string;
398
+ $capability: string;
399
+ $worktree_path: string;
400
+ $branch_name: string;
401
+ $task_id: string;
402
+ $tmux_session: string;
403
+ $state: string;
404
+ $pid: number | null;
405
+ $parent_agent: string | null;
406
+ $depth: number;
407
+ $run_id: string | null;
408
+ $started_at: string;
409
+ $last_activity: string;
410
+ $escalation_level: number;
411
+ $stalled_since: string | null;
412
+ $transcript_path: string | null;
413
+ $prompt_version: string | null;
414
+ $claude_session_id: string | null;
415
+ }
416
+ >(`
417
+ INSERT INTO sessions
418
+ (id, agent_name, capability, worktree_path, branch_name, task_id,
419
+ tmux_session, state, pid, parent_agent, depth, run_id,
420
+ started_at, last_activity, escalation_level, stalled_since, transcript_path,
421
+ prompt_version, claude_session_id)
422
+ VALUES
423
+ ($id, $agent_name, $capability, $worktree_path, $branch_name, $task_id,
424
+ $tmux_session, $state, $pid, $parent_agent, $depth, $run_id,
425
+ $started_at, $last_activity, $escalation_level, $stalled_since, $transcript_path,
426
+ $prompt_version, $claude_session_id)
427
+ ON CONFLICT(agent_name) DO UPDATE SET
428
+ id = excluded.id,
429
+ capability = excluded.capability,
430
+ worktree_path = excluded.worktree_path,
431
+ branch_name = excluded.branch_name,
432
+ task_id = excluded.task_id,
433
+ tmux_session = excluded.tmux_session,
434
+ state = excluded.state,
435
+ pid = excluded.pid,
436
+ parent_agent = excluded.parent_agent,
437
+ depth = excluded.depth,
438
+ run_id = excluded.run_id,
439
+ started_at = excluded.started_at,
440
+ last_activity = excluded.last_activity,
441
+ escalation_level = excluded.escalation_level,
442
+ stalled_since = excluded.stalled_since,
443
+ transcript_path = excluded.transcript_path,
444
+ prompt_version = excluded.prompt_version,
445
+ claude_session_id = excluded.claude_session_id
446
+ `);
447
+
448
+ const getByNameStmt = db.prepare<SessionRow, { $agent_name: string }>(`
449
+ SELECT * FROM sessions WHERE agent_name = $agent_name
450
+ `);
451
+
452
+ const getActiveStmt = db.prepare<SessionRow, Record<string, never>>(`
453
+ SELECT * FROM sessions
454
+ WHERE state IN ('booting', 'working', 'in_turn', 'between_turns', 'stalled')
455
+ ORDER BY started_at ASC
456
+ `);
457
+
458
+ const getAllStmt = db.prepare<SessionRow, Record<string, never>>(`
459
+ SELECT * FROM sessions ORDER BY started_at ASC
460
+ `);
461
+
462
+ const countStmt = db.prepare<{ cnt: number }, Record<string, never>>(
463
+ "SELECT COUNT(*) as cnt FROM sessions",
464
+ );
465
+
466
+ const getByRunStmt = db.prepare<SessionRow, { $run_id: string }>(`
467
+ SELECT * FROM sessions WHERE run_id = $run_id ORDER BY started_at ASC
468
+ `);
469
+
470
+ // Clear tmux_session when landing in a terminal state — the tmux session
471
+ // has already been torn down by ap stop / watchdog / coordinator cleanup,
472
+ // so the stored string is stale (agentplate-14c0).
473
+ const terminalInList = TERMINAL_STATES.map((s) => `'${s}'`).join(",");
474
+ const updateStateStmt = db.prepare<void, { $agent_name: string; $state: string }>(`
475
+ UPDATE sessions
476
+ SET state = $state,
477
+ tmux_session = CASE WHEN $state IN (${terminalInList}) THEN '' ELSE tmux_session END
478
+ WHERE agent_name = $agent_name
479
+ `);
480
+
481
+ // Per-target-state CAS statements. The IN-list values come from a static
482
+ // matrix we control (TRANSITION_ALLOWED_FROM), so inlining as literals is
483
+ // safe and lets bun:sqlite re-use the prepared plan without dynamic params.
484
+ const tryTransitionStmts = (() => {
485
+ const stmts: Partial<
486
+ Record<AgentState, ReturnType<typeof db.prepare<void, { $agent_name: string }>>>
487
+ > = {};
488
+ const terminalSet = new Set<AgentState>(TERMINAL_STATES);
489
+ for (const target of Object.keys(TRANSITION_ALLOWED_FROM) as AgentState[]) {
490
+ const allowed = TRANSITION_ALLOWED_FROM[target];
491
+ if (allowed.length === 0) continue;
492
+ const inList = allowed.map((s) => `'${s}'`).join(",");
493
+ const setClause = terminalSet.has(target)
494
+ ? `state = '${target}', tmux_session = ''`
495
+ : `state = '${target}'`;
496
+ stmts[target] = db.prepare<void, { $agent_name: string }>(
497
+ `UPDATE sessions SET ${setClause} WHERE agent_name = $agent_name AND state IN (${inList})`,
498
+ );
499
+ }
500
+ return stmts;
501
+ })();
502
+
503
+ const updateLastActivityStmt = db.prepare<void, { $agent_name: string; $last_activity: string }>(`
504
+ UPDATE sessions SET last_activity = $last_activity WHERE agent_name = $agent_name
505
+ `);
506
+
507
+ const updateEscalationStmt = db.prepare<
508
+ void,
509
+ {
510
+ $agent_name: string;
511
+ $escalation_level: number;
512
+ $stalled_since: string | null;
513
+ }
514
+ >(`
515
+ UPDATE sessions
516
+ SET escalation_level = $escalation_level, stalled_since = $stalled_since
517
+ WHERE agent_name = $agent_name
518
+ `);
519
+
520
+ const removeStmt = db.prepare<void, { $agent_name: string }>(`
521
+ DELETE FROM sessions WHERE agent_name = $agent_name
522
+ `);
523
+
524
+ const updateTranscriptPathStmt = db.prepare<
525
+ void,
526
+ { $agent_name: string; $transcript_path: string }
527
+ >(`
528
+ UPDATE sessions SET transcript_path = $transcript_path WHERE agent_name = $agent_name
529
+ `);
530
+
531
+ const updateClaudeSessionIdStmt = db.prepare<
532
+ void,
533
+ { $agent_name: string; $claude_session_id: string }
534
+ >(`
535
+ UPDATE sessions SET claude_session_id = $claude_session_id WHERE agent_name = $agent_name
536
+ `);
537
+
538
+ return {
539
+ upsert(session: AgentSession): void {
540
+ upsertStmt.run({
541
+ $id: session.id,
542
+ $agent_name: session.agentName,
543
+ $capability: session.capability,
544
+ $worktree_path: session.worktreePath,
545
+ $branch_name: session.branchName,
546
+ $task_id: session.taskId,
547
+ $tmux_session: session.tmuxSession,
548
+ $state: session.state,
549
+ $pid: session.pid,
550
+ $parent_agent: session.parentAgent,
551
+ $depth: session.depth,
552
+ $run_id: session.runId,
553
+ $started_at: session.startedAt,
554
+ $last_activity: session.lastActivity,
555
+ $escalation_level: session.escalationLevel,
556
+ $stalled_since: session.stalledSince,
557
+ $transcript_path: session.transcriptPath,
558
+ $prompt_version: session.promptVersion ?? null,
559
+ $claude_session_id: session.claudeSessionId ?? null,
560
+ });
561
+ },
562
+
563
+ getByName(agentName: string): AgentSession | null {
564
+ const row = getByNameStmt.get({ $agent_name: agentName });
565
+ return row ? rowToSession(row) : null;
566
+ },
567
+
568
+ getActive(): AgentSession[] {
569
+ const rows = getActiveStmt.all({});
570
+ return rows.map(rowToSession);
571
+ },
572
+
573
+ getAll(): AgentSession[] {
574
+ const rows = getAllStmt.all({});
575
+ return rows.map(rowToSession);
576
+ },
577
+
578
+ count(): number {
579
+ const row = countStmt.get({});
580
+ return row?.cnt ?? 0;
581
+ },
582
+
583
+ getByRun(runId: string): AgentSession[] {
584
+ const rows = getByRunStmt.all({ $run_id: runId });
585
+ return rows.map(rowToSession);
586
+ },
587
+
588
+ updateState(agentName: string, state: AgentState): void {
589
+ updateStateStmt.run({ $agent_name: agentName, $state: state });
590
+ },
591
+
592
+ tryTransitionState(agentName: string, newState: AgentState): TransitionOutcome {
593
+ // Read prev for diagnostic accuracy before the CAS. The read is racy
594
+ // against another writer landing first, but the CAS that follows is
595
+ // authoritative — `changes === 0` means the CAS rejected against
596
+ // whatever the row holds NOW, regardless of what we read here.
597
+ const before = getByNameStmt.get({ $agent_name: agentName });
598
+ if (before === null) {
599
+ return { ok: false, reason: "not_found", attempted: newState };
600
+ }
601
+ const stmt = tryTransitionStmts[newState];
602
+ if (stmt !== undefined) {
603
+ const result = stmt.run({ $agent_name: agentName });
604
+ if (result.changes > 0) {
605
+ return { ok: true, prev: before.state as AgentState, next: newState };
606
+ }
607
+ }
608
+ // CAS rejected (or no stmt for this target, e.g. booting). Re-read to
609
+ // report the state that actually blocked us — another writer may have
610
+ // landed between our `before` read and the CAS.
611
+ const after = getByNameStmt.get({ $agent_name: agentName });
612
+ if (after === null) {
613
+ return { ok: false, reason: "not_found", attempted: newState };
614
+ }
615
+ return {
616
+ ok: false,
617
+ reason: "illegal_transition",
618
+ prev: after.state as AgentState,
619
+ attempted: newState,
620
+ };
621
+ },
622
+
623
+ updateLastActivity(agentName: string): void {
624
+ updateLastActivityStmt.run({
625
+ $agent_name: agentName,
626
+ $last_activity: new Date().toISOString(),
627
+ });
628
+ },
629
+
630
+ updateEscalation(agentName: string, level: number, stalledSince: string | null): void {
631
+ updateEscalationStmt.run({
632
+ $agent_name: agentName,
633
+ $escalation_level: level,
634
+ $stalled_since: stalledSince,
635
+ });
636
+ },
637
+
638
+ updateTranscriptPath(agentName: string, path: string): void {
639
+ updateTranscriptPathStmt.run({ $agent_name: agentName, $transcript_path: path });
640
+ },
641
+
642
+ updateClaudeSessionId(agentName: string, sessionId: string): void {
643
+ updateClaudeSessionIdStmt.run({ $agent_name: agentName, $claude_session_id: sessionId });
644
+ },
645
+
646
+ remove(agentName: string): void {
647
+ removeStmt.run({ $agent_name: agentName });
648
+ },
649
+
650
+ purge(opts: { all?: boolean; state?: AgentState; agent?: string }): number {
651
+ if (opts.all) {
652
+ const countRow = db
653
+ .prepare<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM sessions")
654
+ .get();
655
+ const count = countRow?.cnt ?? 0;
656
+ db.prepare("DELETE FROM sessions").run();
657
+ return count;
658
+ }
659
+
660
+ const conditions: string[] = [];
661
+ const params: Record<string, string> = {};
662
+
663
+ if (opts.state !== undefined) {
664
+ conditions.push("state = $state");
665
+ params.$state = opts.state;
666
+ }
667
+
668
+ if (opts.agent !== undefined) {
669
+ conditions.push("agent_name = $agent");
670
+ params.$agent = opts.agent;
671
+ }
672
+
673
+ if (conditions.length === 0) {
674
+ return 0;
675
+ }
676
+
677
+ const whereClause = conditions.join(" AND ");
678
+ const countQuery = `SELECT COUNT(*) as cnt FROM sessions WHERE ${whereClause}`;
679
+ const countRow = db.prepare<{ cnt: number }, Record<string, string>>(countQuery).get(params);
680
+ const count = countRow?.cnt ?? 0;
681
+
682
+ const deleteQuery = `DELETE FROM sessions WHERE ${whereClause}`;
683
+ db.prepare<void, Record<string, string>>(deleteQuery).run(params);
684
+
685
+ return count;
686
+ },
687
+
688
+ close(): void {
689
+ try {
690
+ db.exec("PRAGMA wal_checkpoint(PASSIVE)");
691
+ } catch {
692
+ // Best effort -- checkpoint failure is non-fatal
693
+ }
694
+ db.close();
695
+ },
696
+ };
697
+ }
698
+
699
+ /**
700
+ * Migrate an existing runs table to add the coordinator_name column.
701
+ * Safe to call multiple times — only adds the column if it does not exist.
702
+ */
703
+ function migrateAddCoordinatorName(db: Database): void {
704
+ const rows = db.prepare("PRAGMA table_info(runs)").all() as Array<{ name: string }>;
705
+ const existingColumns = new Set(rows.map((r) => r.name));
706
+ if (!existingColumns.has("coordinator_name")) {
707
+ db.exec("ALTER TABLE runs ADD COLUMN coordinator_name TEXT");
708
+ }
709
+ }
710
+
711
+ /**
712
+ * Create a new RunStore backed by a SQLite database at the given path.
713
+ *
714
+ * Shares the same sessions.db file as SessionStore. Initializes the runs
715
+ * table alongside sessions. Uses WAL mode for concurrent access.
716
+ */
717
+ export function createRunStore(dbPath: string): RunStore {
718
+ const db = new Database(dbPath);
719
+
720
+ // Configure for concurrent access from multiple agent processes.
721
+ db.exec("PRAGMA journal_mode = WAL");
722
+ db.exec("PRAGMA synchronous = NORMAL");
723
+ db.exec("PRAGMA busy_timeout = 5000");
724
+
725
+ // Create schema (idempotent — safe if SessionStore already created these).
726
+ // `agent_count` is derived from the sessions table at read time, so the
727
+ // sessions table must exist when the run-read statements are prepared
728
+ // — even if the caller only opens a RunStore and never opens a SessionStore.
729
+ db.exec(CREATE_TABLE);
730
+ db.exec(CREATE_INDEXES);
731
+ db.exec(CREATE_RUNS_TABLE);
732
+
733
+ // Migrate: add coordinator_name column BEFORE creating indexes that reference it.
734
+ // The migration is a no-op on new databases (column already in CREATE_RUNS_TABLE).
735
+ migrateAddCoordinatorName(db);
736
+
737
+ db.exec(CREATE_RUNS_INDEXES);
738
+
739
+ // Prepare statements for frequent operations
740
+ const insertRunStmt = db.prepare<
741
+ void,
742
+ {
743
+ $id: string;
744
+ $started_at: string;
745
+ $completed_at: string | null;
746
+ $agent_count: number;
747
+ $coordinator_session_id: string | null;
748
+ $coordinator_name: string | null;
749
+ $status: string;
750
+ }
751
+ >(`
752
+ INSERT INTO runs (id, started_at, completed_at, agent_count, coordinator_session_id, coordinator_name, status)
753
+ VALUES ($id, $started_at, $completed_at, $agent_count, $coordinator_session_id, $coordinator_name, $status)
754
+ `);
755
+
756
+ // `agent_count` is derived from the sessions table at read time rather than
757
+ // read from the column. The cached column value drifted because only sling
758
+ // incremented it — coordinator startup never did, so for every run with a
759
+ // coordinator the count was off by one (agentplate-8e69). Sourcing from
760
+ // sessions makes the count match `SELECT * FROM sessions WHERE run_id = ?`
761
+ // and removes the writer/reader asymmetry. The column is still written so
762
+ // older agentplate binaries pointed at the same db can keep functioning.
763
+ const RUN_COLUMNS = `
764
+ id, started_at, completed_at,
765
+ (SELECT COUNT(*) FROM sessions WHERE sessions.run_id = runs.id) AS agent_count,
766
+ coordinator_session_id, coordinator_name, status
767
+ `;
768
+
769
+ const getRunStmt = db.prepare<RunRow, { $id: string }>(`
770
+ SELECT ${RUN_COLUMNS} FROM runs WHERE id = $id
771
+ `);
772
+
773
+ const getActiveRunStmt = db.prepare<RunRow, Record<string, never>>(`
774
+ SELECT ${RUN_COLUMNS} FROM runs WHERE status = 'active'
775
+ ORDER BY started_at DESC
776
+ LIMIT 1
777
+ `);
778
+
779
+ const getActiveRunForCoordinatorStmt = db.prepare<RunRow, { $coordinator_name: string }>(`
780
+ SELECT ${RUN_COLUMNS} FROM runs WHERE status = 'active' AND coordinator_name = $coordinator_name
781
+ ORDER BY started_at DESC
782
+ LIMIT 1
783
+ `);
784
+
785
+ const completeRunStmt = db.prepare<
786
+ void,
787
+ { $id: string; $status: string; $completed_at: string }
788
+ >(`
789
+ UPDATE runs SET status = $status, completed_at = $completed_at WHERE id = $id
790
+ `);
791
+
792
+ return {
793
+ createRun(run: InsertRun): void {
794
+ insertRunStmt.run({
795
+ $id: run.id,
796
+ $started_at: run.startedAt,
797
+ $completed_at: null,
798
+ $agent_count: run.agentCount ?? 0,
799
+ $coordinator_session_id: run.coordinatorSessionId,
800
+ $coordinator_name: run.coordinatorName ?? null,
801
+ $status: run.status,
802
+ });
803
+ },
804
+
805
+ getRun(id: string): Run | null {
806
+ const row = getRunStmt.get({ $id: id });
807
+ return row ? rowToRun(row) : null;
808
+ },
809
+
810
+ getActiveRun(): Run | null {
811
+ const row = getActiveRunStmt.get({});
812
+ return row ? rowToRun(row) : null;
813
+ },
814
+
815
+ getActiveRunForCoordinator(coordinatorName: string): Run | null {
816
+ const row = getActiveRunForCoordinatorStmt.get({ $coordinator_name: coordinatorName });
817
+ return row ? rowToRun(row) : null;
818
+ },
819
+
820
+ listRuns(opts?: { limit?: number; status?: RunStatus }): Run[] {
821
+ const conditions: string[] = [];
822
+ const params: Record<string, string | number> = {};
823
+
824
+ if (opts?.status !== undefined) {
825
+ conditions.push("status = $status");
826
+ params.$status = opts.status;
827
+ }
828
+
829
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
830
+ const limitClause = opts?.limit !== undefined ? `LIMIT ${opts.limit}` : "";
831
+ const query = `SELECT ${RUN_COLUMNS} FROM runs ${whereClause} ORDER BY started_at DESC ${limitClause}`;
832
+
833
+ const rows = db.prepare<RunRow, Record<string, string | number>>(query).all(params);
834
+ return rows.map(rowToRun);
835
+ },
836
+
837
+ // Kept for API stability but a no-op: `agent_count` is now derived from
838
+ // the sessions table on every read (see RUN_COLUMNS above).
839
+ incrementAgentCount(_runId: string): void {},
840
+
841
+ completeRun(runId: string, status: "completed" | "failed"): void {
842
+ completeRunStmt.run({
843
+ $id: runId,
844
+ $status: status,
845
+ $completed_at: new Date().toISOString(),
846
+ });
847
+ },
848
+
849
+ close(): void {
850
+ try {
851
+ db.exec("PRAGMA wal_checkpoint(PASSIVE)");
852
+ } catch {
853
+ // Best effort -- checkpoint failure is non-fatal
854
+ }
855
+ db.close();
856
+ },
857
+ };
858
+ }