@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,797 @@
1
+ /**
2
+ * Tests for the `agentplate clean` command.
3
+ *
4
+ * Uses real filesystem (temp dirs), real git repos, real SQLite.
5
+ * No mocks. tmux operations are tested indirectly — when no tmux
6
+ * server is running, the command handles it gracefully.
7
+ *
8
+ * Philosophy: "never mock what you can use for real" (mx-252b16).
9
+ */
10
+
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+ import { existsSync } from "node:fs";
13
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
14
+ import { join } from "node:path";
15
+ import { createEventStore } from "../events/store.ts";
16
+ import { createMailStore } from "../mail/store.ts";
17
+ import { createMergeQueue } from "../merge/queue.ts";
18
+ import { createMetricsStore } from "../metrics/store.ts";
19
+ import { openSessionStore } from "../sessions/compat.ts";
20
+ import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
21
+ import type { AgentSession } from "../types.ts";
22
+ import { cleanCommand } from "./clean.ts";
23
+
24
+ let tempDir: string;
25
+ let agentplateDir: string;
26
+ let originalCwd: string;
27
+ let stdoutOutput: string;
28
+ let _stderrOutput: string;
29
+ let originalStdoutWrite: typeof process.stdout.write;
30
+ let originalStderrWrite: typeof process.stderr.write;
31
+
32
+ beforeEach(async () => {
33
+ tempDir = await createTempGitRepo();
34
+ agentplateDir = join(tempDir, ".agentplate");
35
+ await mkdir(agentplateDir, { recursive: true });
36
+
37
+ // Write minimal config.yaml so loadConfig succeeds
38
+ await Bun.write(
39
+ join(agentplateDir, "config.yaml"),
40
+ `project:\n name: test-project\n root: ${tempDir}\n canonicalBranch: main\n`,
41
+ );
42
+
43
+ // Create the standard directories
44
+ await mkdir(join(agentplateDir, "logs"), { recursive: true });
45
+ await mkdir(join(agentplateDir, "agents"), { recursive: true });
46
+ await mkdir(join(agentplateDir, "specs"), { recursive: true });
47
+ await mkdir(join(agentplateDir, "worktrees"), { recursive: true });
48
+
49
+ originalCwd = process.cwd();
50
+ process.chdir(tempDir);
51
+
52
+ // Capture stdout/stderr
53
+ stdoutOutput = "";
54
+ _stderrOutput = "";
55
+ originalStdoutWrite = process.stdout.write;
56
+ originalStderrWrite = process.stderr.write;
57
+ process.stdout.write = ((chunk: string) => {
58
+ stdoutOutput += chunk;
59
+ return true;
60
+ }) as typeof process.stdout.write;
61
+ process.stderr.write = ((chunk: string) => {
62
+ _stderrOutput += chunk;
63
+ return true;
64
+ }) as typeof process.stderr.write;
65
+ });
66
+
67
+ afterEach(async () => {
68
+ process.chdir(originalCwd);
69
+ process.stdout.write = originalStdoutWrite;
70
+ process.stderr.write = originalStderrWrite;
71
+ await cleanupTempDir(tempDir);
72
+ });
73
+
74
+ // === validation ===
75
+
76
+ describe("validation", () => {
77
+ test("no flags throws ValidationError", async () => {
78
+ await expect(cleanCommand({})).rejects.toThrow("No cleanup targets specified");
79
+ });
80
+
81
+ test("--agent and --all throws ValidationError", async () => {
82
+ await expect(cleanCommand({ agent: "my-builder", all: true })).rejects.toThrow(
83
+ "--agent and --all are mutually exclusive",
84
+ );
85
+ });
86
+ });
87
+
88
+ // === --all ===
89
+
90
+ describe("--all", () => {
91
+ test("wipes mail.db and WAL files", async () => {
92
+ // Create a mail DB with messages
93
+ const mailDbPath = join(agentplateDir, "mail.db");
94
+ const store = createMailStore(mailDbPath);
95
+ store.insert({
96
+ id: "msg-1",
97
+ from: "agent-a",
98
+ to: "agent-b",
99
+ subject: "test",
100
+ body: "hello",
101
+ type: "status",
102
+ priority: "normal",
103
+ threadId: null,
104
+ });
105
+ store.close();
106
+
107
+ // Verify DB exists
108
+ expect(await Bun.file(mailDbPath).exists()).toBe(true);
109
+
110
+ await cleanCommand({ all: true });
111
+
112
+ // DB should be gone
113
+ expect(await Bun.file(mailDbPath).exists()).toBe(false);
114
+ expect(stdoutOutput).toContain("Wiped mail.db");
115
+ });
116
+
117
+ test("wipes metrics.db", async () => {
118
+ const metricsDbPath = join(agentplateDir, "metrics.db");
119
+ const store = createMetricsStore(metricsDbPath);
120
+ store.recordSession({
121
+ agentName: "test-agent",
122
+ taskId: "task-1",
123
+ capability: "builder",
124
+ startedAt: new Date().toISOString(),
125
+ completedAt: null,
126
+ durationMs: 0,
127
+ exitCode: null,
128
+ mergeResult: null,
129
+ parentAgent: null,
130
+ inputTokens: 0,
131
+ outputTokens: 0,
132
+ cacheReadTokens: 0,
133
+ cacheCreationTokens: 0,
134
+ estimatedCostUsd: null,
135
+ modelUsed: null,
136
+ runId: null,
137
+ });
138
+ store.close();
139
+
140
+ expect(await Bun.file(metricsDbPath).exists()).toBe(true);
141
+
142
+ await cleanCommand({ all: true });
143
+
144
+ expect(await Bun.file(metricsDbPath).exists()).toBe(false);
145
+ expect(stdoutOutput).toContain("Wiped metrics.db");
146
+ });
147
+
148
+ test("wipes sessions.db", async () => {
149
+ // Use the SessionStore to create sessions.db with data
150
+ const { store } = openSessionStore(agentplateDir);
151
+ store.upsert({
152
+ id: "s1",
153
+ agentName: "test-agent",
154
+ capability: "builder",
155
+ worktreePath: "/tmp/wt",
156
+ branchName: "agentplate/test/task",
157
+ taskId: "task-1",
158
+ tmuxSession: "agentplate-test-agent",
159
+ state: "completed",
160
+ pid: 12345,
161
+ parentAgent: null,
162
+ depth: 1,
163
+ runId: null,
164
+ startedAt: new Date().toISOString(),
165
+ lastActivity: new Date().toISOString(),
166
+ escalationLevel: 0,
167
+ stalledSince: null,
168
+ transcriptPath: null,
169
+ });
170
+ store.close();
171
+
172
+ const sessionsDbPath = join(agentplateDir, "sessions.db");
173
+ expect(await Bun.file(sessionsDbPath).exists()).toBe(true);
174
+
175
+ await cleanCommand({ all: true });
176
+
177
+ expect(await Bun.file(sessionsDbPath).exists()).toBe(false);
178
+ expect(stdoutOutput).toContain("Wiped sessions.db");
179
+ });
180
+
181
+ test("wipes merge-queue.db", async () => {
182
+ const queuePath = join(agentplateDir, "merge-queue.db");
183
+ // Create a queue with an entry so we can verify it gets wiped
184
+ const queue = createMergeQueue(queuePath);
185
+ queue.enqueue({
186
+ branchName: "test-branch",
187
+ taskId: "beads-test",
188
+ agentName: "test",
189
+ filesModified: ["src/test.ts"],
190
+ });
191
+ queue.close();
192
+
193
+ await cleanCommand({ all: true });
194
+
195
+ expect(await Bun.file(queuePath).exists()).toBe(false);
196
+ expect(stdoutOutput).toContain("Wiped merge-queue.db");
197
+ });
198
+
199
+ test("clears logs directory contents", async () => {
200
+ const logsDir = join(agentplateDir, "logs");
201
+ await mkdir(join(logsDir, "agent-a", "2026-01-01"), { recursive: true });
202
+ await writeFile(join(logsDir, "agent-a", "2026-01-01", "session.log"), "log data");
203
+
204
+ await cleanCommand({ all: true });
205
+
206
+ const entries = await readdir(logsDir);
207
+ expect(entries).toHaveLength(0);
208
+ expect(stdoutOutput).toContain("Cleared logs/");
209
+ });
210
+
211
+ test("clears agents directory contents", async () => {
212
+ const agentsDir = join(agentplateDir, "agents");
213
+ await mkdir(join(agentsDir, "test-agent"), { recursive: true });
214
+ await writeFile(join(agentsDir, "test-agent", "identity.yaml"), "name: test-agent");
215
+
216
+ await cleanCommand({ all: true });
217
+
218
+ const entries = await readdir(agentsDir);
219
+ expect(entries).toHaveLength(0);
220
+ expect(stdoutOutput).toContain("Cleared agents/");
221
+ });
222
+
223
+ test("clears specs directory contents", async () => {
224
+ const specsDir = join(agentplateDir, "specs");
225
+ await writeFile(join(specsDir, "task-123.md"), "# Spec");
226
+
227
+ await cleanCommand({ all: true });
228
+
229
+ const entries = await readdir(specsDir);
230
+ expect(entries).toHaveLength(0);
231
+ expect(stdoutOutput).toContain("Cleared specs/");
232
+ });
233
+
234
+ test("deletes nudge-state.json", async () => {
235
+ const nudgePath = join(agentplateDir, "nudge-state.json");
236
+ await Bun.write(nudgePath, "{}");
237
+
238
+ await cleanCommand({ all: true });
239
+
240
+ expect(await Bun.file(nudgePath).exists()).toBe(false);
241
+ expect(stdoutOutput).toContain("Cleared nudge-state.json");
242
+ });
243
+
244
+ test("deletes current-run.txt", async () => {
245
+ const currentRunPath = join(agentplateDir, "current-run.txt");
246
+ await Bun.write(currentRunPath, "run-2026-02-13T10-00-00-000Z");
247
+
248
+ await cleanCommand({ all: true });
249
+
250
+ expect(await Bun.file(currentRunPath).exists()).toBe(false);
251
+ expect(stdoutOutput).toContain("Cleared current-run.txt");
252
+ });
253
+
254
+ test("handles missing current-run.txt gracefully", async () => {
255
+ // current-run.txt does not exist — should not error
256
+ await cleanCommand({ all: true });
257
+ expect(stdoutOutput).not.toContain("Cleared current-run.txt");
258
+ });
259
+ });
260
+
261
+ // === individual flags ===
262
+
263
+ describe("individual flags", () => {
264
+ test("--mail only wipes mail.db, leaves other state intact", async () => {
265
+ // Create mail and sessions
266
+ const mailDbPath = join(agentplateDir, "mail.db");
267
+ const store = createMailStore(mailDbPath);
268
+ store.insert({
269
+ id: "msg-1",
270
+ from: "a",
271
+ to: "b",
272
+ subject: "test",
273
+ body: "hi",
274
+ type: "status",
275
+ priority: "normal",
276
+ threadId: null,
277
+ });
278
+ store.close();
279
+
280
+ const sessionsPath = join(agentplateDir, "sessions.json");
281
+ await Bun.write(sessionsPath, '[{"id":"s1"}]\n');
282
+
283
+ await cleanCommand({ mail: true });
284
+
285
+ // Mail gone
286
+ expect(await Bun.file(mailDbPath).exists()).toBe(false);
287
+ // Sessions untouched
288
+ const sessionsContent = await Bun.file(sessionsPath).text();
289
+ expect(JSON.parse(sessionsContent)).toEqual([{ id: "s1" }]);
290
+ });
291
+
292
+ test("--sessions only wipes sessions.db", async () => {
293
+ // Create sessions.db with data
294
+ const sessionsDbPath = join(agentplateDir, "sessions.db");
295
+ const { store } = openSessionStore(agentplateDir);
296
+ store.upsert({
297
+ id: "s1",
298
+ agentName: "test-agent",
299
+ capability: "builder",
300
+ worktreePath: "/tmp/wt",
301
+ branchName: "agentplate/test/task",
302
+ taskId: "task-1",
303
+ tmuxSession: "agentplate-test-agent",
304
+ state: "completed",
305
+ pid: 12345,
306
+ parentAgent: null,
307
+ depth: 1,
308
+ runId: null,
309
+ startedAt: new Date().toISOString(),
310
+ lastActivity: new Date().toISOString(),
311
+ escalationLevel: 0,
312
+ stalledSince: null,
313
+ transcriptPath: null,
314
+ });
315
+ store.close();
316
+
317
+ // Create a spec file that should survive
318
+ await writeFile(join(agentplateDir, "specs", "task.md"), "spec");
319
+
320
+ await cleanCommand({ sessions: true });
321
+
322
+ // sessions.db should be gone
323
+ expect(await Bun.file(sessionsDbPath).exists()).toBe(false);
324
+
325
+ // Specs untouched
326
+ const specEntries = await readdir(join(agentplateDir, "specs"));
327
+ expect(specEntries).toHaveLength(1);
328
+ });
329
+
330
+ test("--logs clears logs but nothing else", async () => {
331
+ const logsDir = join(agentplateDir, "logs");
332
+ await mkdir(join(logsDir, "agent-x"), { recursive: true });
333
+ await writeFile(join(logsDir, "agent-x", "session.log"), "data");
334
+
335
+ await writeFile(join(agentplateDir, "specs", "task.md"), "spec");
336
+
337
+ await cleanCommand({ logs: true });
338
+
339
+ const logEntries = await readdir(logsDir);
340
+ expect(logEntries).toHaveLength(0);
341
+
342
+ // Specs untouched
343
+ const specEntries = await readdir(join(agentplateDir, "specs"));
344
+ expect(specEntries).toHaveLength(1);
345
+ });
346
+ });
347
+
348
+ // === idempotent ===
349
+
350
+ describe("idempotent", () => {
351
+ test("running --all when nothing exists does not error", async () => {
352
+ await cleanCommand({ all: true });
353
+ expect(stdoutOutput).toContain("Nothing to clean");
354
+ });
355
+
356
+ test("running --all twice does not error", async () => {
357
+ // Create some state
358
+ const mailDbPath = join(agentplateDir, "mail.db");
359
+ const store = createMailStore(mailDbPath);
360
+ store.close();
361
+
362
+ await cleanCommand({ all: true });
363
+ stdoutOutput = "";
364
+ await cleanCommand({ all: true });
365
+ expect(stdoutOutput).toContain("Nothing to clean");
366
+ });
367
+ });
368
+
369
+ // === JSON output ===
370
+
371
+ describe("JSON output", () => {
372
+ test("--json flag produces valid JSON", async () => {
373
+ const mailDbPath = join(agentplateDir, "mail.db");
374
+ const store = createMailStore(mailDbPath);
375
+ store.insert({
376
+ id: "msg-1",
377
+ from: "a",
378
+ to: "b",
379
+ subject: "test",
380
+ body: "hi",
381
+ type: "status",
382
+ priority: "normal",
383
+ threadId: null,
384
+ });
385
+ store.close();
386
+
387
+ await cleanCommand({ all: true, json: true });
388
+
389
+ const result = JSON.parse(stdoutOutput);
390
+ expect(result).toHaveProperty("tmuxKilled");
391
+ expect(result).toHaveProperty("mailWiped");
392
+ expect(result).toHaveProperty("sessionsCleared");
393
+ expect(result).toHaveProperty("metricsWiped");
394
+ expect(result.mailWiped).toBe(true);
395
+ });
396
+
397
+ test("--json includes sessionEndEventsLogged field", async () => {
398
+ await cleanCommand({ all: true, json: true });
399
+ const result = JSON.parse(stdoutOutput);
400
+ expect(result).toHaveProperty("sessionEndEventsLogged");
401
+ });
402
+
403
+ test("--json includes currentRunCleared field", async () => {
404
+ const currentRunPath = join(agentplateDir, "current-run.txt");
405
+ await Bun.write(currentRunPath, "run-2026-02-13T10-00-00-000Z");
406
+
407
+ await cleanCommand({ all: true, json: true });
408
+ const result = JSON.parse(stdoutOutput);
409
+ expect(result).toHaveProperty("currentRunCleared");
410
+ expect(result.currentRunCleared).toBe(true);
411
+ });
412
+ });
413
+
414
+ // === synthetic session-end events ===
415
+
416
+ describe("synthetic session-end events", () => {
417
+ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
418
+ return {
419
+ id: "s1",
420
+ agentName: "test-builder",
421
+ capability: "builder",
422
+ worktreePath: "/tmp/wt",
423
+ branchName: "agentplate/test-builder/task-1",
424
+ taskId: "task-1",
425
+ tmuxSession: "agentplate-test-builder",
426
+ state: "working",
427
+ pid: 12345,
428
+ parentAgent: null,
429
+ depth: 1,
430
+ runId: null,
431
+ startedAt: new Date().toISOString(),
432
+ lastActivity: new Date().toISOString(),
433
+ escalationLevel: 0,
434
+ stalledSince: null,
435
+ transcriptPath: null,
436
+ ...overrides,
437
+ };
438
+ }
439
+
440
+ test("logs session-end events for active agents before killing tmux", async () => {
441
+ // Write sessions.json with an active agent
442
+ const sessionsPath = join(agentplateDir, "sessions.json");
443
+ const sessions = [makeSession({ agentName: "builder-a", state: "working" })];
444
+ await Bun.write(sessionsPath, JSON.stringify(sessions));
445
+
446
+ await cleanCommand({ all: true });
447
+
448
+ // Verify event was written to events.db
449
+ const eventsDbPath = join(agentplateDir, "events.db");
450
+ const eventStore = createEventStore(eventsDbPath);
451
+ const events = eventStore.getByAgent("builder-a");
452
+ eventStore.close();
453
+
454
+ const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
455
+ expect(sessionEndEvents).toHaveLength(1);
456
+ expect(sessionEndEvents[0]?.agentName).toBe("builder-a");
457
+ expect(sessionEndEvents[0]?.level).toBe("info");
458
+
459
+ const data = JSON.parse(sessionEndEvents[0]?.data ?? "{}");
460
+ expect(data.reason).toBe("clean");
461
+ expect(data.capability).toBe("builder");
462
+
463
+ expect(stdoutOutput).toContain("Logged 1 synthetic session-end event");
464
+ });
465
+
466
+ test("logs events for multiple active agents", async () => {
467
+ const sessionsPath = join(agentplateDir, "sessions.json");
468
+ const sessions = [
469
+ makeSession({ id: "s1", agentName: "builder-a", state: "working" }),
470
+ makeSession({ id: "s2", agentName: "scout-b", capability: "scout", state: "booting" }),
471
+ makeSession({ id: "s3", agentName: "builder-c", state: "stalled" }),
472
+ ];
473
+ await Bun.write(sessionsPath, JSON.stringify(sessions));
474
+
475
+ await cleanCommand({ all: true });
476
+
477
+ const eventsDbPath = join(agentplateDir, "events.db");
478
+ const eventStore = createEventStore(eventsDbPath);
479
+
480
+ for (const name of ["builder-a", "scout-b", "builder-c"]) {
481
+ const events = eventStore.getByAgent(name);
482
+ const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
483
+ expect(sessionEndEvents).toHaveLength(1);
484
+ }
485
+ eventStore.close();
486
+
487
+ expect(stdoutOutput).toContain("Logged 3 synthetic session-end events");
488
+ });
489
+
490
+ test("skips completed and zombie sessions", async () => {
491
+ const sessionsPath = join(agentplateDir, "sessions.json");
492
+ const sessions = [
493
+ makeSession({ id: "s1", agentName: "completed-agent", state: "completed" }),
494
+ makeSession({ id: "s2", agentName: "zombie-agent", state: "zombie" }),
495
+ ];
496
+ await Bun.write(sessionsPath, JSON.stringify(sessions));
497
+
498
+ await cleanCommand({ all: true });
499
+
500
+ // events.db may not even be created if there are no events to log
501
+ const eventsDbPath = join(agentplateDir, "events.db");
502
+ if (await Bun.file(eventsDbPath).exists()) {
503
+ const eventStore = createEventStore(eventsDbPath);
504
+ const events1 = eventStore.getByAgent("completed-agent");
505
+ const events2 = eventStore.getByAgent("zombie-agent");
506
+ eventStore.close();
507
+ expect(events1).toHaveLength(0);
508
+ expect(events2).toHaveLength(0);
509
+ }
510
+ });
511
+
512
+ test("--worktrees also logs session-end events (not just --all)", async () => {
513
+ const sessionsPath = join(agentplateDir, "sessions.json");
514
+ const sessions = [makeSession({ agentName: "wt-agent", state: "working" })];
515
+ await Bun.write(sessionsPath, JSON.stringify(sessions));
516
+
517
+ await cleanCommand({ worktrees: true });
518
+
519
+ const eventsDbPath = join(agentplateDir, "events.db");
520
+ const eventStore = createEventStore(eventsDbPath);
521
+ const events = eventStore.getByAgent("wt-agent");
522
+ eventStore.close();
523
+
524
+ const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
525
+ expect(sessionEndEvents).toHaveLength(1);
526
+ });
527
+
528
+ test("includes runId and sessionId from agent session", async () => {
529
+ const sessionsPath = join(agentplateDir, "sessions.json");
530
+ const sessions = [
531
+ makeSession({
532
+ agentName: "tracked-agent",
533
+ id: "session-123",
534
+ runId: "run-456",
535
+ state: "working",
536
+ }),
537
+ ];
538
+ await Bun.write(sessionsPath, JSON.stringify(sessions));
539
+
540
+ await cleanCommand({ all: true });
541
+
542
+ const eventsDbPath = join(agentplateDir, "events.db");
543
+ const eventStore = createEventStore(eventsDbPath);
544
+ const events = eventStore.getByAgent("tracked-agent");
545
+ eventStore.close();
546
+
547
+ const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
548
+ expect(sessionEndEvents).toHaveLength(1);
549
+ expect(sessionEndEvents[0]?.sessionId).toBe("session-123");
550
+ expect(sessionEndEvents[0]?.runId).toBe("run-456");
551
+ });
552
+
553
+ test("handles missing sessions.json gracefully", async () => {
554
+ // No sessions.json file — should not error
555
+ await cleanCommand({ all: true });
556
+ // Just verify it didn't crash
557
+ expect(stdoutOutput).toBeDefined();
558
+ });
559
+ });
560
+
561
+ // === loam health checks ===
562
+
563
+ describe("loam health checks", () => {
564
+ test("runs loam health checks when --all is passed", async () => {
565
+ // Create a real .loam directory with some data
566
+ const loamDir = join(tempDir, ".loam");
567
+ await mkdir(loamDir, { recursive: true });
568
+ await mkdir(join(loamDir, "domains"), { recursive: true });
569
+
570
+ // Create a domain file with some records
571
+ const domainPath = join(loamDir, "domains", "test-domain.jsonl");
572
+ await writeFile(
573
+ domainPath,
574
+ `{"id":"mx-1","type":"convention","description":"Test record 1","recorded_at":"2026-01-01T00:00:00Z"}\n`,
575
+ );
576
+
577
+ await cleanCommand({ all: true });
578
+
579
+ // Loam health checks should have run (might show warnings or might be clean)
580
+ // The output should not error, and if there are no issues, it's fine
581
+ expect(stdoutOutput).toBeDefined();
582
+ });
583
+
584
+ test("handles missing .loam directory gracefully", async () => {
585
+ // No .loam directory — should not error
586
+ await cleanCommand({ all: true });
587
+ expect(stdoutOutput).toBeDefined();
588
+ });
589
+
590
+ test("JSON output includes loamHealth field when loam checks run", async () => {
591
+ // Create a .loam directory
592
+ const loamDir = join(tempDir, ".loam");
593
+ await mkdir(loamDir, { recursive: true });
594
+ await mkdir(join(loamDir, "domains"), { recursive: true });
595
+
596
+ // Create a domain file
597
+ const domainPath = join(loamDir, "domains", "test-domain.jsonl");
598
+ await writeFile(
599
+ domainPath,
600
+ `{"id":"mx-1","type":"convention","description":"Test","recorded_at":"2026-01-01T00:00:00Z"}\n`,
601
+ );
602
+
603
+ await cleanCommand({ all: true, json: true });
604
+
605
+ const result = JSON.parse(stdoutOutput);
606
+ expect(result).toHaveProperty("loamHealth");
607
+
608
+ // If loam checks ran, loamHealth should be an object (not null)
609
+ // If loam was unavailable, it will be null
610
+ if (result.loamHealth !== null) {
611
+ expect(result.loamHealth).toHaveProperty("checked");
612
+ expect(result.loamHealth).toHaveProperty("domainsNearLimit");
613
+ expect(result.loamHealth).toHaveProperty("stalePruneCandidates");
614
+ expect(result.loamHealth).toHaveProperty("doctorIssues");
615
+ expect(result.loamHealth).toHaveProperty("doctorWarnings");
616
+ }
617
+ });
618
+
619
+ test("does not run loam checks when only individual flags are used", async () => {
620
+ // Create a .loam directory
621
+ const loamDir = join(tempDir, ".loam");
622
+ await mkdir(loamDir, { recursive: true });
623
+
624
+ // Run clean with only --mail (not --all)
625
+ const mailDbPath = join(agentplateDir, "mail.db");
626
+ const store = createMailStore(mailDbPath);
627
+ store.close();
628
+
629
+ await cleanCommand({ mail: true, json: true });
630
+
631
+ const result = JSON.parse(stdoutOutput);
632
+ // loamHealth should be null because we didn't use --all
633
+ expect(result.loamHealth).toBeNull();
634
+ });
635
+
636
+ test("warns about domains approaching governance limits", async () => {
637
+ // Create a .loam directory with a domain that has many records
638
+ const loamDir = join(tempDir, ".loam");
639
+ await mkdir(loamDir, { recursive: true });
640
+ await mkdir(join(loamDir, "domains"), { recursive: true });
641
+
642
+ // Create a domain with 410 records (above the 400 warn threshold)
643
+ const domainPath = join(loamDir, "domains", "large-domain.jsonl");
644
+ const records = [];
645
+ for (let i = 1; i <= 410; i++) {
646
+ records.push(
647
+ `{"id":"mx-${i}","type":"convention","description":"Record ${i}","recorded_at":"2026-01-01T00:00:00Z"}`,
648
+ );
649
+ }
650
+ await writeFile(domainPath, `${records.join("\n")}\n`);
651
+
652
+ // Only run if loam CLI is actually available
653
+ const loamAvailable = existsSync(join(loamDir, "domains", "large-domain.jsonl"));
654
+ if (!loamAvailable) {
655
+ return; // Skip this test if loam setup failed
656
+ }
657
+
658
+ await cleanCommand({ all: true });
659
+
660
+ // Should show warning about domain near limit (if loam status worked)
661
+ // The exact output depends on whether loam CLI is available in the test environment
662
+ expect(stdoutOutput).toBeDefined();
663
+ });
664
+ });
665
+
666
+ // === --agent ===
667
+
668
+ describe("--agent", () => {
669
+ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
670
+ return {
671
+ id: "s1",
672
+ agentName: "test-builder",
673
+ capability: "builder",
674
+ worktreePath: join(tempDir, ".agentplate", "worktrees", "test-builder"),
675
+ branchName: "agentplate/test-builder/task-1",
676
+ taskId: "task-1",
677
+ tmuxSession: "agentplate-test-project-test-builder",
678
+ state: "working",
679
+ pid: 99999,
680
+ parentAgent: null,
681
+ depth: 1,
682
+ runId: "run-123",
683
+ startedAt: new Date().toISOString(),
684
+ lastActivity: new Date().toISOString(),
685
+ escalationLevel: 0,
686
+ stalledSince: null,
687
+ transcriptPath: null,
688
+ ...overrides,
689
+ };
690
+ }
691
+
692
+ function saveSession(session: AgentSession): void {
693
+ const { store } = openSessionStore(agentplateDir);
694
+ try {
695
+ store.upsert(session);
696
+ } finally {
697
+ store.close();
698
+ }
699
+ }
700
+
701
+ test("throws AgentError when agent not found", async () => {
702
+ await expect(cleanCommand({ agent: "nonexistent" })).rejects.toThrow("not found");
703
+ });
704
+
705
+ test("clears agent and logs directories", async () => {
706
+ const session = makeSession();
707
+ saveSession(session);
708
+
709
+ // Create agent and logs dirs with content
710
+ const agentDir = join(agentplateDir, "agents", "test-builder");
711
+ const logsDir = join(agentplateDir, "logs", "test-builder");
712
+ await mkdir(agentDir, { recursive: true });
713
+ await mkdir(logsDir, { recursive: true });
714
+ await writeFile(join(agentDir, "identity.yaml"), "name: test-builder");
715
+ await writeFile(join(logsDir, "session.log"), "log data");
716
+
717
+ await cleanCommand({ agent: "test-builder" });
718
+
719
+ // Dirs should be cleared (but still exist)
720
+ const agentEntries = await readdir(agentDir);
721
+ const logEntries = await readdir(logsDir);
722
+ expect(agentEntries).toHaveLength(0);
723
+ expect(logEntries).toHaveLength(0);
724
+
725
+ expect(stdoutOutput).toContain("Agent cleaned");
726
+ expect(stdoutOutput).toContain("test-builder");
727
+ });
728
+
729
+ test("marks agent session as completed", async () => {
730
+ const session = makeSession({ state: "working" });
731
+ saveSession(session);
732
+
733
+ await cleanCommand({ agent: "test-builder" });
734
+
735
+ const { store } = openSessionStore(agentplateDir);
736
+ const updated = store.getByName("test-builder");
737
+ store.close();
738
+ expect(updated?.state).toBe("completed");
739
+ });
740
+
741
+ test("logs synthetic session-end event for non-completed agent", async () => {
742
+ const session = makeSession({ state: "working" });
743
+ saveSession(session);
744
+
745
+ await cleanCommand({ agent: "test-builder" });
746
+
747
+ const eventsDbPath = join(agentplateDir, "events.db");
748
+ const eventStore = createEventStore(eventsDbPath);
749
+ const events = eventStore.getByAgent("test-builder");
750
+ eventStore.close();
751
+
752
+ const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
753
+ expect(sessionEndEvents).toHaveLength(1);
754
+ const data = JSON.parse(sessionEndEvents[0]?.data ?? "{}");
755
+ expect(data.reason).toContain("clean --agent");
756
+ });
757
+
758
+ test("does not log session-end event for already-completed agent", async () => {
759
+ const session = makeSession({ state: "completed" });
760
+ saveSession(session);
761
+
762
+ await cleanCommand({ agent: "test-builder" });
763
+
764
+ const eventsDbPath = join(agentplateDir, "events.db");
765
+ if (existsSync(eventsDbPath)) {
766
+ const eventStore = createEventStore(eventsDbPath);
767
+ const events = eventStore.getByAgent("test-builder");
768
+ eventStore.close();
769
+ const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
770
+ expect(sessionEndEvents).toHaveLength(0);
771
+ }
772
+ });
773
+
774
+ test("--agent + --json returns JSON with agent result", async () => {
775
+ const session = makeSession({ state: "working" });
776
+ saveSession(session);
777
+
778
+ await cleanCommand({ agent: "test-builder", json: true });
779
+
780
+ const result = JSON.parse(stdoutOutput);
781
+ expect(result).toHaveProperty("agent");
782
+ expect(result.agent).toHaveProperty("agentName", "test-builder");
783
+ expect(result.agent).toHaveProperty("markedCompleted");
784
+ });
785
+
786
+ test("handles missing agent/logs directories gracefully", async () => {
787
+ const session = makeSession({ state: "completed" });
788
+ saveSession(session);
789
+
790
+ // No agent or logs dirs — should not error
791
+ await cleanCommand({ agent: "test-builder" });
792
+ expect(stdoutOutput).toContain("Agent cleaned");
793
+ });
794
+ });
795
+
796
+ // fs utility tests (wipeSqliteDb, resetJsonFile, clearDirectory, deleteFile)
797
+ // moved to src/utils/fs.test.ts