@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,347 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { openSessionStore } from "../sessions/compat.ts";
4
+ import type { AgentplateConfig, AgentSession } from "../types.ts";
5
+ import { listWorktrees } from "../worktree/manager.ts";
6
+ import { isProcessAlive, listSessions, sanitizeTmuxName } from "../worktree/tmux.ts";
7
+ import type { DoctorCheck } from "./types.ts";
8
+
9
+ /**
10
+ * Dependencies for consistency checks.
11
+ * Allows injection for testing without module-level mocks.
12
+ */
13
+ export interface ConsistencyCheckDeps {
14
+ listSessions: () => Promise<Array<{ name: string; pid: number }>>;
15
+ isProcessAlive: (pid: number) => boolean;
16
+ }
17
+
18
+ /**
19
+ * Cross-subsystem consistency checks.
20
+ * Validates SessionStore vs worktrees, tmux sessions vs sessions, etc.
21
+ *
22
+ * @param config - Agentplate configuration
23
+ * @param agentplateDir - Absolute path to .agentplate/
24
+ * @param deps - Optional dependencies for testing (defaults to real implementations)
25
+ */
26
+ export async function checkConsistency(
27
+ config: AgentplateConfig,
28
+ agentplateDir: string,
29
+ deps?: ConsistencyCheckDeps,
30
+ ): Promise<DoctorCheck[]> {
31
+ // Use injected dependencies or defaults
32
+ const { listSessions: listSessionsFn, isProcessAlive: isProcessAliveFn } = deps || {
33
+ listSessions,
34
+ isProcessAlive,
35
+ };
36
+ const checks: DoctorCheck[] = [];
37
+
38
+ // Gather data from all three sources
39
+ let worktrees: Array<{ path: string; branch: string; head: string }> = [];
40
+ let tmuxSessions: Array<{ name: string; pid: number }> = [];
41
+ let storeSessions: AgentSession[] = [];
42
+
43
+ // 1. List git worktrees
44
+ try {
45
+ worktrees = await listWorktrees(config.project.root);
46
+ } catch (error) {
47
+ checks.push({
48
+ name: "worktree-listing",
49
+ category: "consistency",
50
+ status: "fail",
51
+ message: "Failed to list git worktrees",
52
+ details: [error instanceof Error ? error.message : String(error)],
53
+ });
54
+ // Can't continue consistency checks without worktree data
55
+ return checks;
56
+ }
57
+
58
+ // 2. List tmux sessions
59
+ try {
60
+ tmuxSessions = await listSessionsFn();
61
+ } catch (error) {
62
+ // Tmux not installed or not running is not necessarily a fatal error
63
+ checks.push({
64
+ name: "tmux-listing",
65
+ category: "consistency",
66
+ status: "warn",
67
+ message: "Failed to list tmux sessions (tmux may not be installed)",
68
+ details: [error instanceof Error ? error.message : String(error)],
69
+ });
70
+ // Continue with empty tmux session list
71
+ tmuxSessions = [];
72
+ }
73
+
74
+ // 3. Open SessionStore and get all sessions
75
+ let storeHandle: ReturnType<typeof openSessionStore>["store"] | null = null;
76
+ try {
77
+ const { store } = openSessionStore(agentplateDir);
78
+ storeHandle = store;
79
+ storeSessions = store.getAll();
80
+ } catch (error) {
81
+ checks.push({
82
+ name: "sessionstore-open",
83
+ category: "consistency",
84
+ status: "fail",
85
+ message: "Failed to open SessionStore",
86
+ details: [error instanceof Error ? error.message : String(error)],
87
+ });
88
+ // Can't do consistency checks without SessionStore
89
+ return checks;
90
+ }
91
+
92
+ // Completed/zombie sessions are retained for history and metrics. Their tmux
93
+ // sessions, PIDs, and worktrees may legitimately be gone.
94
+ const liveSessions = storeSessions.filter((s) => s.state !== "completed" && s.state !== "zombie");
95
+
96
+ // Now perform cross-validation checks
97
+
98
+ // 4. Check for orphaned worktrees (worktree exists but no SessionStore entry)
99
+ // Normalize all paths to handle symlinks like /tmp -> /private/tmp on macOS
100
+ const worktreeBasePath = realpathSync(join(agentplateDir, "worktrees"));
101
+ const agentplateWorktrees = worktrees.filter((wt) => wt.path.startsWith(worktreeBasePath));
102
+
103
+ // Normalize SessionStore paths for comparison
104
+ const storeWorktreePaths = new Set(
105
+ storeSessions.map((s) => {
106
+ try {
107
+ return realpathSync(s.worktreePath);
108
+ } catch {
109
+ // Path doesn't exist, use as-is
110
+ return s.worktreePath;
111
+ }
112
+ }),
113
+ );
114
+
115
+ const orphanedWorktrees = agentplateWorktrees.filter((wt) => !storeWorktreePaths.has(wt.path));
116
+
117
+ if (orphanedWorktrees.length > 0) {
118
+ checks.push({
119
+ name: "orphaned-worktrees",
120
+ category: "consistency",
121
+ status: "warn",
122
+ message: `Found ${orphanedWorktrees.length} orphaned worktree(s) with no SessionStore entry`,
123
+ details: orphanedWorktrees.map((wt) => `${wt.path} (branch: ${wt.branch})`),
124
+ fixable: true,
125
+ });
126
+ } else {
127
+ checks.push({
128
+ name: "orphaned-worktrees",
129
+ category: "consistency",
130
+ status: "pass",
131
+ message: "No orphaned worktrees found",
132
+ });
133
+ }
134
+
135
+ // 5. Check for orphaned tmux sessions (tmux session exists but no SessionStore entry)
136
+ const projectName = config.project.name;
137
+ const agentplateTmuxPrefix = `agentplate-${sanitizeTmuxName(projectName)}-`;
138
+ const agentplateTmuxSessions = tmuxSessions.filter((s) =>
139
+ s.name.startsWith(agentplateTmuxPrefix),
140
+ );
141
+ const storeTmuxNames = new Set(storeSessions.map((s) => s.tmuxSession));
142
+
143
+ const orphanedTmux = agentplateTmuxSessions.filter((s) => !storeTmuxNames.has(s.name));
144
+
145
+ if (orphanedTmux.length > 0) {
146
+ checks.push({
147
+ name: "orphaned-tmux",
148
+ category: "consistency",
149
+ status: "warn",
150
+ message: `Found ${orphanedTmux.length} orphaned tmux session(s) with no SessionStore entry`,
151
+ details: orphanedTmux.map((s) => `${s.name} (pid: ${s.pid})`),
152
+ fixable: true,
153
+ });
154
+ } else {
155
+ checks.push({
156
+ name: "orphaned-tmux",
157
+ category: "consistency",
158
+ status: "pass",
159
+ message: "No orphaned tmux sessions found",
160
+ });
161
+ }
162
+
163
+ // 6. Check for dead processes in SessionStore
164
+ const deadSessions = liveSessions.filter((s) => s.pid !== null && !isProcessAliveFn(s.pid));
165
+
166
+ if (deadSessions.length > 0) {
167
+ checks.push({
168
+ name: "dead-pids",
169
+ category: "consistency",
170
+ status: "warn",
171
+ message: `Found ${deadSessions.length} session(s) with dead PIDs`,
172
+ details: deadSessions.map((s) => `${s.agentName} (pid: ${s.pid}, state: ${s.state})`),
173
+ fixable: true,
174
+ });
175
+ } else {
176
+ checks.push({
177
+ name: "dead-pids",
178
+ category: "consistency",
179
+ status: "pass",
180
+ message: "All SessionStore PIDs are alive or null",
181
+ });
182
+ }
183
+
184
+ // 7. Check for SessionStore entries with missing worktrees
185
+ const existingWorktreePaths = new Set(worktrees.map((wt) => wt.path));
186
+ const missingWorktrees = liveSessions.filter((s) => {
187
+ // Try to normalize the SessionStore path for comparison
188
+ try {
189
+ const normalizedPath = realpathSync(s.worktreePath);
190
+ return !existingWorktreePaths.has(normalizedPath);
191
+ } catch {
192
+ // Path doesn't exist or can't be resolved, check as-is
193
+ return !existingWorktreePaths.has(s.worktreePath);
194
+ }
195
+ });
196
+
197
+ if (missingWorktrees.length > 0) {
198
+ checks.push({
199
+ name: "missing-worktrees",
200
+ category: "consistency",
201
+ status: "warn",
202
+ message: `Found ${missingWorktrees.length} session(s) with missing worktrees`,
203
+ details: missingWorktrees.map((s) => `${s.agentName}: ${s.worktreePath}`),
204
+ fixable: true,
205
+ });
206
+ } else {
207
+ checks.push({
208
+ name: "missing-worktrees",
209
+ category: "consistency",
210
+ status: "pass",
211
+ message: "All SessionStore worktrees exist",
212
+ });
213
+ }
214
+
215
+ // 8. Check for SessionStore entries with missing tmux sessions
216
+ const existingTmuxNames = new Set(tmuxSessions.map((s) => s.name));
217
+ const missingTmux = liveSessions.filter(
218
+ (s) => s.tmuxSession.length > 0 && !existingTmuxNames.has(s.tmuxSession),
219
+ );
220
+
221
+ if (missingTmux.length > 0) {
222
+ checks.push({
223
+ name: "missing-tmux",
224
+ category: "consistency",
225
+ status: "warn",
226
+ message: `Found ${missingTmux.length} session(s) with missing tmux sessions`,
227
+ details: missingTmux.map((s) => `${s.agentName}: ${s.tmuxSession}`),
228
+ fixable: true,
229
+ });
230
+ } else {
231
+ checks.push({
232
+ name: "missing-tmux",
233
+ category: "consistency",
234
+ status: "pass",
235
+ message: "All SessionStore tmux sessions exist",
236
+ });
237
+ }
238
+
239
+ // 8b. Check for orphaned claude spawn PIDs (agentplate-505d).
240
+ //
241
+ // An orphan is a session whose pid is still alive but should not be:
242
+ // - the session reached a terminal state (completed/zombie) yet the
243
+ // spawn didn't exit, or
244
+ // - the tmux container is gone but the claude child survived (was
245
+ // reparented to init when its bash wrapper got SIGHUP).
246
+ // Run `ap clean --all` to reap. Distinct from `dead-pids` (the inverse:
247
+ // session is live but its pid already died).
248
+ const orphanedSpawns: Array<{ session: AgentSession; reason: string }> = [];
249
+ for (const s of storeSessions) {
250
+ if (s.pid === null || !isProcessAliveFn(s.pid)) continue;
251
+ if (s.state === "completed" || s.state === "zombie") {
252
+ orphanedSpawns.push({
253
+ session: s,
254
+ reason: `state=${s.state} but pid ${s.pid} still alive`,
255
+ });
256
+ continue;
257
+ }
258
+ if (s.tmuxSession.length > 0 && !existingTmuxNames.has(s.tmuxSession)) {
259
+ orphanedSpawns.push({
260
+ session: s,
261
+ reason: `tmux session "${s.tmuxSession}" missing but pid ${s.pid} alive`,
262
+ });
263
+ }
264
+ }
265
+
266
+ if (orphanedSpawns.length > 0) {
267
+ checks.push({
268
+ name: "orphan-spawns",
269
+ category: "consistency",
270
+ status: "warn",
271
+ message: `Found ${orphanedSpawns.length} orphaned spawn process(es) — run "ap clean --all" to reap`,
272
+ details: orphanedSpawns.map(({ session, reason }) => `${session.agentName}: ${reason}`),
273
+ fixable: true,
274
+ });
275
+ } else {
276
+ checks.push({
277
+ name: "orphan-spawns",
278
+ category: "consistency",
279
+ status: "pass",
280
+ message: "No orphaned spawn processes detected",
281
+ });
282
+ }
283
+
284
+ // 9. Check reviewer-to-builder ratio per lead
285
+ const parentGroups = new Map<string, { builders: number; reviewers: number }>();
286
+ for (const session of storeSessions) {
287
+ if (
288
+ session.parentAgent &&
289
+ (session.capability === "builder" || session.capability === "reviewer")
290
+ ) {
291
+ const group = parentGroups.get(session.parentAgent) ?? { builders: 0, reviewers: 0 };
292
+ if (session.capability === "builder") {
293
+ group.builders++;
294
+ } else {
295
+ group.reviewers++;
296
+ }
297
+ parentGroups.set(session.parentAgent, group);
298
+ }
299
+ }
300
+
301
+ const leadsWithoutReview: string[] = [];
302
+ const leadsWithPartialReview: string[] = [];
303
+ for (const [parent, counts] of parentGroups) {
304
+ if (counts.builders > 0 && counts.reviewers === 0) {
305
+ leadsWithoutReview.push(`${parent}: ${counts.builders} builder(s), 0 reviewers`);
306
+ } else if (counts.builders > 0 && counts.reviewers < counts.builders) {
307
+ leadsWithPartialReview.push(
308
+ `${parent}: ${counts.builders} builder(s), ${counts.reviewers} reviewer(s)`,
309
+ );
310
+ }
311
+ }
312
+
313
+ if (leadsWithoutReview.length > 0) {
314
+ checks.push({
315
+ name: "reviewer-coverage",
316
+ category: "consistency",
317
+ status: "warn",
318
+ message: `${leadsWithoutReview.length} lead(s) spawned builders without any reviewers`,
319
+ details: [...leadsWithoutReview, ...leadsWithPartialReview],
320
+ });
321
+ } else if (leadsWithPartialReview.length > 0) {
322
+ checks.push({
323
+ name: "reviewer-coverage",
324
+ category: "consistency",
325
+ status: "warn",
326
+ message: `${leadsWithPartialReview.length} lead(s) have partial reviewer coverage`,
327
+ details: leadsWithPartialReview,
328
+ });
329
+ } else {
330
+ checks.push({
331
+ name: "reviewer-coverage",
332
+ category: "consistency",
333
+ status: "pass",
334
+ message:
335
+ parentGroups.size > 0
336
+ ? "All leads have reviewer coverage for builders"
337
+ : "No builder sessions found (nothing to check)",
338
+ });
339
+ }
340
+
341
+ // Close the SessionStore
342
+ if (storeHandle) {
343
+ storeHandle.close();
344
+ }
345
+
346
+ return checks;
347
+ }
@@ -0,0 +1,350 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import type { AgentplateConfig } from "../types.ts";
7
+ import { checkDatabases } from "./databases.ts";
8
+ import type { DoctorCheck } from "./types.ts";
9
+
10
+ describe("checkDatabases", () => {
11
+ let tempDir: string;
12
+ let mockConfig: AgentplateConfig;
13
+
14
+ beforeEach(() => {
15
+ tempDir = mkdtempSync(join(tmpdir(), "agentplate-test-"));
16
+ mockConfig = {
17
+ project: { name: "test", root: tempDir, canonicalBranch: "main" },
18
+ agents: {
19
+ manifestPath: "",
20
+ baseDir: "",
21
+ maxConcurrent: 5,
22
+ staggerDelayMs: 100,
23
+ maxDepth: 2,
24
+ maxSessionsPerRun: 0,
25
+ maxAgentsPerLead: 5,
26
+ },
27
+ worktrees: { baseDir: "" },
28
+ taskTracker: { backend: "auto", enabled: true },
29
+ loam: { enabled: true, domains: [], primeFormat: "markdown" },
30
+ merge: { aiResolveEnabled: false, reimagineEnabled: false },
31
+ providers: {
32
+ anthropic: { type: "native" },
33
+ },
34
+ watchdog: {
35
+ tier0Enabled: true,
36
+ tier0IntervalMs: 30000,
37
+ tier1Enabled: false,
38
+ tier2Enabled: false,
39
+ staleThresholdMs: 300000,
40
+ zombieThresholdMs: 600000,
41
+ nudgeIntervalMs: 60000,
42
+ },
43
+ models: {},
44
+ logging: { verbose: false, redactSecrets: true },
45
+ };
46
+ });
47
+
48
+ afterEach(() => {
49
+ rmSync(tempDir, { recursive: true, force: true });
50
+ });
51
+
52
+ test("fails when required database files do not exist (merge-queue.db is optional)", () => {
53
+ const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
54
+
55
+ expect(checks).toHaveLength(4);
56
+ expect(checks[0]?.status).toBe("fail");
57
+ expect(checks[0]?.name).toBe("mail.db exists");
58
+ expect(checks[1]?.status).toBe("fail");
59
+ expect(checks[1]?.name).toBe("metrics.db exists");
60
+ expect(checks[2]?.status).toBe("fail");
61
+ expect(checks[2]?.name).toBe("sessions.db exists");
62
+ // merge-queue.db is created lazily on first merge — its absence is normal.
63
+ expect(checks[3]?.status).toBe("pass");
64
+ expect(checks[3]?.name).toBe("merge-queue.db exists");
65
+ });
66
+
67
+ test("passes when databases exist with correct schema", () => {
68
+ // Create mail.db
69
+ const mailDb = new Database(join(tempDir, "mail.db"));
70
+ mailDb.exec("PRAGMA journal_mode=WAL");
71
+ mailDb.exec(`
72
+ CREATE TABLE messages (
73
+ id TEXT PRIMARY KEY,
74
+ from_agent TEXT NOT NULL,
75
+ to_agent TEXT NOT NULL,
76
+ subject TEXT NOT NULL,
77
+ body TEXT NOT NULL,
78
+ type TEXT NOT NULL DEFAULT 'status',
79
+ priority TEXT NOT NULL DEFAULT 'normal',
80
+ thread_id TEXT,
81
+ payload TEXT,
82
+ read INTEGER NOT NULL DEFAULT 0,
83
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
84
+ )
85
+ `);
86
+ mailDb.close();
87
+
88
+ // Create metrics.db
89
+ const metricsDb = new Database(join(tempDir, "metrics.db"));
90
+ metricsDb.exec("PRAGMA journal_mode=WAL");
91
+ metricsDb.exec(`
92
+ CREATE TABLE sessions (
93
+ agent_name TEXT NOT NULL,
94
+ task_id TEXT NOT NULL,
95
+ capability TEXT NOT NULL,
96
+ started_at TEXT NOT NULL,
97
+ completed_at TEXT,
98
+ duration_ms INTEGER NOT NULL DEFAULT 0,
99
+ exit_code INTEGER,
100
+ merge_result TEXT,
101
+ parent_agent TEXT,
102
+ input_tokens INTEGER NOT NULL DEFAULT 0,
103
+ output_tokens INTEGER NOT NULL DEFAULT 0,
104
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
105
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
106
+ estimated_cost_usd REAL,
107
+ model_used TEXT,
108
+ PRIMARY KEY (agent_name, task_id)
109
+ )
110
+ `);
111
+ metricsDb.close();
112
+
113
+ // Create sessions.db
114
+ const sessionsDb = new Database(join(tempDir, "sessions.db"));
115
+ sessionsDb.exec("PRAGMA journal_mode=WAL");
116
+ sessionsDb.exec(`
117
+ CREATE TABLE sessions (
118
+ id TEXT PRIMARY KEY,
119
+ agent_name TEXT NOT NULL UNIQUE,
120
+ capability TEXT NOT NULL,
121
+ worktree_path TEXT NOT NULL,
122
+ branch_name TEXT NOT NULL,
123
+ task_id TEXT NOT NULL,
124
+ tmux_session TEXT NOT NULL,
125
+ state TEXT NOT NULL DEFAULT 'booting',
126
+ pid INTEGER,
127
+ parent_agent TEXT,
128
+ depth INTEGER NOT NULL DEFAULT 0,
129
+ run_id TEXT,
130
+ started_at TEXT NOT NULL,
131
+ last_activity TEXT NOT NULL,
132
+ escalation_level INTEGER NOT NULL DEFAULT 0,
133
+ stalled_since TEXT
134
+ )
135
+ `);
136
+ sessionsDb.exec(`
137
+ CREATE TABLE runs (
138
+ id TEXT PRIMARY KEY,
139
+ started_at TEXT NOT NULL,
140
+ completed_at TEXT,
141
+ agent_count INTEGER NOT NULL DEFAULT 0,
142
+ coordinator_session_id TEXT,
143
+ status TEXT NOT NULL DEFAULT 'active'
144
+ )
145
+ `);
146
+ sessionsDb.close();
147
+
148
+ // Create merge-queue.db
149
+ const mergeDb = new Database(join(tempDir, "merge-queue.db"));
150
+ mergeDb.exec("PRAGMA journal_mode=WAL");
151
+ mergeDb.exec(`
152
+ CREATE TABLE merge_queue (
153
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
154
+ branch_name TEXT NOT NULL,
155
+ task_id TEXT NOT NULL,
156
+ agent_name TEXT NOT NULL,
157
+ files_modified TEXT NOT NULL DEFAULT '[]',
158
+ enqueued_at TEXT NOT NULL,
159
+ status TEXT NOT NULL DEFAULT 'pending',
160
+ resolved_tier TEXT
161
+ )
162
+ `);
163
+ mergeDb.close();
164
+
165
+ const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
166
+
167
+ expect(checks).toHaveLength(4);
168
+ expect(checks.every((c) => c?.status === "pass")).toBe(true);
169
+ expect(checks[0]?.name).toBe("mail.db health");
170
+ expect(checks[1]?.name).toBe("metrics.db health");
171
+ expect(checks[2]?.name).toBe("sessions.db health");
172
+ expect(checks[3]?.name).toBe("merge-queue.db health");
173
+ });
174
+
175
+ test("fails when table is missing", () => {
176
+ // Create mail.db without messages table
177
+ const mailDb = new Database(join(tempDir, "mail.db"));
178
+ mailDb.exec("PRAGMA journal_mode=WAL");
179
+ mailDb.close();
180
+
181
+ // Create other databases properly to isolate the test
182
+ const metricsDb = new Database(join(tempDir, "metrics.db"));
183
+ metricsDb.exec("PRAGMA journal_mode=WAL");
184
+ metricsDb.exec(`
185
+ CREATE TABLE sessions (
186
+ agent_name TEXT NOT NULL,
187
+ task_id TEXT NOT NULL,
188
+ capability TEXT NOT NULL,
189
+ started_at TEXT NOT NULL,
190
+ completed_at TEXT,
191
+ duration_ms INTEGER NOT NULL DEFAULT 0,
192
+ exit_code INTEGER,
193
+ merge_result TEXT,
194
+ parent_agent TEXT,
195
+ input_tokens INTEGER NOT NULL DEFAULT 0,
196
+ output_tokens INTEGER NOT NULL DEFAULT 0,
197
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
198
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
199
+ estimated_cost_usd REAL,
200
+ model_used TEXT,
201
+ PRIMARY KEY (agent_name, task_id)
202
+ )
203
+ `);
204
+ metricsDb.close();
205
+
206
+ const sessionsDb = new Database(join(tempDir, "sessions.db"));
207
+ sessionsDb.exec("PRAGMA journal_mode=WAL");
208
+ sessionsDb.exec(`
209
+ CREATE TABLE sessions (
210
+ id TEXT PRIMARY KEY,
211
+ agent_name TEXT NOT NULL UNIQUE,
212
+ capability TEXT NOT NULL,
213
+ worktree_path TEXT NOT NULL,
214
+ branch_name TEXT NOT NULL,
215
+ task_id TEXT NOT NULL,
216
+ tmux_session TEXT NOT NULL,
217
+ state TEXT NOT NULL DEFAULT 'booting',
218
+ pid INTEGER,
219
+ parent_agent TEXT,
220
+ depth INTEGER NOT NULL DEFAULT 0,
221
+ run_id TEXT,
222
+ started_at TEXT NOT NULL,
223
+ last_activity TEXT NOT NULL,
224
+ escalation_level INTEGER NOT NULL DEFAULT 0,
225
+ stalled_since TEXT
226
+ )
227
+ `);
228
+ sessionsDb.exec(`
229
+ CREATE TABLE runs (
230
+ id TEXT PRIMARY KEY,
231
+ started_at TEXT NOT NULL,
232
+ completed_at TEXT,
233
+ agent_count INTEGER NOT NULL DEFAULT 0,
234
+ coordinator_session_id TEXT,
235
+ status TEXT NOT NULL DEFAULT 'active'
236
+ )
237
+ `);
238
+ sessionsDb.close();
239
+
240
+ const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
241
+
242
+ const mailCheck = checks.find((c) => c?.name === "mail.db schema");
243
+ expect(mailCheck?.status).toBe("fail");
244
+ expect(mailCheck?.details).toContain("Missing tables: messages");
245
+ });
246
+
247
+ test("fails when column is missing", () => {
248
+ // Create messages table without payload column
249
+ const mailDb = new Database(join(tempDir, "mail.db"));
250
+ mailDb.exec("PRAGMA journal_mode=WAL");
251
+ mailDb.exec(`
252
+ CREATE TABLE messages (
253
+ id TEXT PRIMARY KEY,
254
+ from_agent TEXT NOT NULL,
255
+ to_agent TEXT NOT NULL,
256
+ subject TEXT NOT NULL,
257
+ body TEXT NOT NULL,
258
+ type TEXT NOT NULL DEFAULT 'status',
259
+ priority TEXT NOT NULL DEFAULT 'normal',
260
+ thread_id TEXT,
261
+ read INTEGER NOT NULL DEFAULT 0,
262
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
263
+ )
264
+ `);
265
+ mailDb.close();
266
+
267
+ const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
268
+
269
+ const mailCheck = checks.find((c) => c?.name === "mail.db schema");
270
+ expect(mailCheck?.status).toBe("fail");
271
+ expect(mailCheck?.details?.some((d) => d.includes("missing column: payload"))).toBe(true);
272
+ });
273
+
274
+ test("warns when WAL mode is not enabled", () => {
275
+ // Create database without WAL mode
276
+ const mailDb = new Database(join(tempDir, "mail.db"));
277
+ mailDb.exec(`
278
+ CREATE TABLE messages (
279
+ id TEXT PRIMARY KEY,
280
+ from_agent TEXT NOT NULL,
281
+ to_agent TEXT NOT NULL,
282
+ subject TEXT NOT NULL,
283
+ body TEXT NOT NULL,
284
+ type TEXT NOT NULL DEFAULT 'status',
285
+ priority TEXT NOT NULL DEFAULT 'normal',
286
+ thread_id TEXT,
287
+ payload TEXT,
288
+ read INTEGER NOT NULL DEFAULT 0,
289
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
290
+ )
291
+ `);
292
+ mailDb.close();
293
+
294
+ const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
295
+
296
+ const walCheck = checks.find((c) => c?.name === "mail.db WAL mode");
297
+ expect(walCheck?.status).toBe("warn");
298
+ expect(walCheck?.message).toContain("not using WAL mode");
299
+ });
300
+
301
+ test("fails when database is corrupted", () => {
302
+ // Create a corrupt database file (just write garbage)
303
+ const { writeFileSync } = require("node:fs");
304
+ writeFileSync(join(tempDir, "mail.db"), "not a valid sqlite database");
305
+
306
+ const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
307
+
308
+ const integrityCheck = checks.find((c) => c?.name === "mail.db integrity");
309
+ expect(integrityCheck?.status).toBe("fail");
310
+ expect(integrityCheck?.message).toContain("Failed to open or validate");
311
+ });
312
+
313
+ test("fix() enables WAL mode on database", () => {
314
+ // Create mail.db without WAL mode
315
+ const mailDb = new Database(join(tempDir, "mail.db"));
316
+ mailDb.exec(`
317
+ CREATE TABLE messages (
318
+ id TEXT PRIMARY KEY,
319
+ from_agent TEXT NOT NULL,
320
+ to_agent TEXT NOT NULL,
321
+ subject TEXT NOT NULL,
322
+ body TEXT NOT NULL,
323
+ type TEXT NOT NULL DEFAULT 'status',
324
+ priority TEXT NOT NULL DEFAULT 'normal',
325
+ thread_id TEXT,
326
+ payload TEXT,
327
+ read INTEGER NOT NULL DEFAULT 0,
328
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
329
+ )
330
+ `);
331
+ mailDb.close();
332
+
333
+ const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
334
+
335
+ const walCheck = checks.find((c) => c?.name === "mail.db WAL mode");
336
+ expect(walCheck?.status).toBe("warn");
337
+ expect(walCheck?.fix).toBeDefined();
338
+
339
+ const actions = walCheck?.fix?.();
340
+ expect(Array.isArray(actions)).toBe(true);
341
+ expect((actions as string[]).some((a) => a.includes("WAL mode"))).toBe(true);
342
+ expect((actions as string[]).some((a) => a.includes("mail.db"))).toBe(true);
343
+
344
+ // Verify WAL mode is now enabled
345
+ const verifyDb = new Database(join(tempDir, "mail.db"));
346
+ const journalMode = verifyDb.prepare<{ journal_mode: string }, []>("PRAGMA journal_mode").get();
347
+ verifyDb.close();
348
+ expect(journalMode?.journal_mode?.toLowerCase()).toBe("wal");
349
+ });
350
+ });