@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,978 @@
1
+ /**
2
+ * Tests for MetricsStore (SQLite-backed session metrics storage).
3
+ *
4
+ * Uses real bun:sqlite with temp files. No mocks.
5
+ * Philosophy: "never mock what you can use for real" (mx-252b16).
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
+ import { mkdtemp } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { cleanupTempDir } from "../test-helpers.ts";
13
+ import type { SessionMetrics } from "../types.ts";
14
+ import { createMetricsStore, type MetricsStore } from "./store.ts";
15
+
16
+ let tempDir: string;
17
+ let dbPath: string;
18
+ let store: MetricsStore;
19
+
20
+ beforeEach(async () => {
21
+ tempDir = await mkdtemp(join(tmpdir(), "agentplate-metrics-test-"));
22
+ dbPath = join(tempDir, "metrics.db");
23
+ store = createMetricsStore(dbPath);
24
+ });
25
+
26
+ afterEach(async () => {
27
+ store.close();
28
+ await cleanupTempDir(tempDir);
29
+ });
30
+
31
+ /** Helper to create a SessionMetrics object with optional overrides. */
32
+ function makeSession(overrides: Partial<SessionMetrics> = {}): SessionMetrics {
33
+ return {
34
+ agentName: "test-agent",
35
+ taskId: "test-task-123",
36
+ capability: "builder",
37
+ startedAt: new Date("2026-01-01T00:00:00Z").toISOString(),
38
+ completedAt: new Date("2026-01-01T00:05:00Z").toISOString(),
39
+ durationMs: 300_000,
40
+ exitCode: 0,
41
+ mergeResult: "auto-resolve",
42
+ parentAgent: "coordinator",
43
+ inputTokens: 0,
44
+ outputTokens: 0,
45
+ cacheReadTokens: 0,
46
+ cacheCreationTokens: 0,
47
+ estimatedCostUsd: null,
48
+ modelUsed: null,
49
+ runId: null,
50
+ ...overrides,
51
+ };
52
+ }
53
+
54
+ // === recordSession ===
55
+
56
+ describe("recordSession", () => {
57
+ test("inserts a session and retrieves it via getRecentSessions", () => {
58
+ const session = makeSession();
59
+ store.recordSession(session);
60
+
61
+ const retrieved = store.getRecentSessions(10);
62
+ expect(retrieved).toHaveLength(1);
63
+ expect(retrieved[0]).toEqual(session);
64
+ });
65
+
66
+ test("INSERT OR REPLACE: same (agent_name, task_id) key overwrites previous row", () => {
67
+ const session1 = makeSession({ durationMs: 100_000 });
68
+ const session2 = makeSession({ durationMs: 200_000 });
69
+
70
+ store.recordSession(session1);
71
+ store.recordSession(session2);
72
+
73
+ const retrieved = store.getRecentSessions(10);
74
+ expect(retrieved).toHaveLength(1);
75
+ expect(retrieved[0]?.durationMs).toBe(200_000);
76
+ });
77
+
78
+ test("all fields roundtrip correctly (camelCase TS → snake_case SQLite → camelCase TS)", () => {
79
+ const session = makeSession({
80
+ agentName: "special-agent",
81
+ taskId: "task-xyz",
82
+ capability: "reviewer",
83
+ startedAt: "2026-02-01T12:00:00Z",
84
+ completedAt: "2026-02-01T12:30:00Z",
85
+ durationMs: 1_800_000,
86
+ exitCode: 42,
87
+ mergeResult: "ai-resolve",
88
+ parentAgent: "lead-agent",
89
+ });
90
+
91
+ store.recordSession(session);
92
+ const retrieved = store.getRecentSessions(10);
93
+
94
+ expect(retrieved).toHaveLength(1);
95
+ expect(retrieved[0]).toEqual(session);
96
+ });
97
+
98
+ test("null fields (completedAt, exitCode, mergeResult, parentAgent) stored and retrieved as null", () => {
99
+ const session = makeSession({
100
+ completedAt: null,
101
+ exitCode: null,
102
+ mergeResult: null,
103
+ parentAgent: null,
104
+ });
105
+
106
+ store.recordSession(session);
107
+ const retrieved = store.getRecentSessions(10);
108
+
109
+ expect(retrieved).toHaveLength(1);
110
+ expect(retrieved[0]?.completedAt).toBeNull();
111
+ expect(retrieved[0]?.exitCode).toBeNull();
112
+ expect(retrieved[0]?.mergeResult).toBeNull();
113
+ expect(retrieved[0]?.parentAgent).toBeNull();
114
+ });
115
+ });
116
+
117
+ // === getRecentSessions ===
118
+
119
+ describe("getRecentSessions", () => {
120
+ test("returns sessions ordered by started_at DESC (most recent first)", () => {
121
+ const session1 = makeSession({
122
+ taskId: "task-1",
123
+ startedAt: "2026-01-01T10:00:00Z",
124
+ });
125
+ const session2 = makeSession({
126
+ taskId: "task-2",
127
+ startedAt: "2026-01-01T12:00:00Z",
128
+ });
129
+ const session3 = makeSession({
130
+ taskId: "task-3",
131
+ startedAt: "2026-01-01T11:00:00Z",
132
+ });
133
+
134
+ store.recordSession(session1);
135
+ store.recordSession(session2);
136
+ store.recordSession(session3);
137
+
138
+ const retrieved = store.getRecentSessions(10);
139
+ expect(retrieved).toHaveLength(3);
140
+ expect(retrieved[0]?.taskId).toBe("task-2"); // most recent
141
+ expect(retrieved[1]?.taskId).toBe("task-3");
142
+ expect(retrieved[2]?.taskId).toBe("task-1"); // oldest
143
+ });
144
+
145
+ test("default limit is 20", () => {
146
+ // Insert 25 sessions
147
+ for (let i = 0; i < 25; i++) {
148
+ store.recordSession(
149
+ makeSession({
150
+ taskId: `task-${i}`,
151
+ startedAt: new Date(Date.now() + i * 1000).toISOString(),
152
+ }),
153
+ );
154
+ }
155
+
156
+ const retrieved = store.getRecentSessions();
157
+ expect(retrieved).toHaveLength(20);
158
+ });
159
+
160
+ test("custom limit works (e.g., limit=2 returns only 2)", () => {
161
+ store.recordSession(makeSession({ taskId: "task-1" }));
162
+ store.recordSession(makeSession({ taskId: "task-2" }));
163
+ store.recordSession(makeSession({ taskId: "task-3" }));
164
+
165
+ const retrieved = store.getRecentSessions(2);
166
+ expect(retrieved).toHaveLength(2);
167
+ });
168
+
169
+ test("empty DB returns empty array", () => {
170
+ const retrieved = store.getRecentSessions(10);
171
+ expect(retrieved).toEqual([]);
172
+ });
173
+ });
174
+
175
+ // === getSessionsByAgent ===
176
+
177
+ describe("getSessionsByAgent", () => {
178
+ test("filters by agent name correctly", () => {
179
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-1" }));
180
+ store.recordSession(makeSession({ agentName: "agent-b", taskId: "task-2" }));
181
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-3" }));
182
+
183
+ const retrieved = store.getSessionsByAgent("agent-a");
184
+ expect(retrieved).toHaveLength(2);
185
+ expect(retrieved[0]?.agentName).toBe("agent-a");
186
+ expect(retrieved[1]?.agentName).toBe("agent-a");
187
+ });
188
+
189
+ test("returns empty array for unknown agent", () => {
190
+ store.recordSession(makeSession({ agentName: "known-agent" }));
191
+
192
+ const retrieved = store.getSessionsByAgent("unknown-agent");
193
+ expect(retrieved).toEqual([]);
194
+ });
195
+
196
+ test("multiple sessions for same agent all returned, ordered by started_at DESC", () => {
197
+ store.recordSession(
198
+ makeSession({
199
+ agentName: "agent-x",
200
+ taskId: "task-1",
201
+ startedAt: "2026-01-01T10:00:00Z",
202
+ }),
203
+ );
204
+ store.recordSession(
205
+ makeSession({
206
+ agentName: "agent-x",
207
+ taskId: "task-2",
208
+ startedAt: "2026-01-01T12:00:00Z",
209
+ }),
210
+ );
211
+ store.recordSession(
212
+ makeSession({
213
+ agentName: "agent-x",
214
+ taskId: "task-3",
215
+ startedAt: "2026-01-01T11:00:00Z",
216
+ }),
217
+ );
218
+
219
+ const retrieved = store.getSessionsByAgent("agent-x");
220
+ expect(retrieved).toHaveLength(3);
221
+ expect(retrieved[0]?.taskId).toBe("task-2"); // most recent
222
+ expect(retrieved[1]?.taskId).toBe("task-3");
223
+ expect(retrieved[2]?.taskId).toBe("task-1"); // oldest
224
+ });
225
+ });
226
+
227
+ // === getSessionsByTask ===
228
+
229
+ describe("getSessionsByTask", () => {
230
+ test("returns sessions matching task_id", () => {
231
+ store.recordSession(makeSession({ agentName: "agent-1", taskId: "task-A" }));
232
+ store.recordSession(makeSession({ agentName: "agent-2", taskId: "task-A" }));
233
+ store.recordSession(makeSession({ agentName: "agent-3", taskId: "task-B" }));
234
+
235
+ const sessions = store.getSessionsByTask("task-A");
236
+ expect(sessions).toHaveLength(2);
237
+ expect(sessions.every((s) => s.taskId === "task-A")).toBe(true);
238
+ });
239
+
240
+ test("returns empty array for unknown task_id", () => {
241
+ store.recordSession(makeSession({ agentName: "agent-1", taskId: "task-A" }));
242
+
243
+ expect(store.getSessionsByTask("nonexistent")).toEqual([]);
244
+ });
245
+
246
+ test("returns sessions ordered by started_at DESC", () => {
247
+ store.recordSession(
248
+ makeSession({ agentName: "agent-1", taskId: "task-X", startedAt: "2026-01-01T10:00:00Z" }),
249
+ );
250
+ store.recordSession(
251
+ makeSession({ agentName: "agent-2", taskId: "task-X", startedAt: "2026-01-01T12:00:00Z" }),
252
+ );
253
+ store.recordSession(
254
+ makeSession({ agentName: "agent-3", taskId: "task-X", startedAt: "2026-01-01T11:00:00Z" }),
255
+ );
256
+
257
+ const sessions = store.getSessionsByTask("task-X");
258
+ expect(sessions).toHaveLength(3);
259
+ expect(sessions[0]?.startedAt).toBe("2026-01-01T12:00:00Z"); // most recent first
260
+ expect(sessions[1]?.startedAt).toBe("2026-01-01T11:00:00Z");
261
+ expect(sessions[2]?.startedAt).toBe("2026-01-01T10:00:00Z");
262
+ });
263
+ });
264
+
265
+ // === getAverageDuration ===
266
+
267
+ describe("getAverageDuration", () => {
268
+ test("average across all completed sessions (completedAt IS NOT NULL)", () => {
269
+ store.recordSession(makeSession({ taskId: "task-1", durationMs: 100_000 }));
270
+ store.recordSession(makeSession({ taskId: "task-2", durationMs: 200_000 }));
271
+ store.recordSession(makeSession({ taskId: "task-3", durationMs: 300_000 }));
272
+
273
+ const avg = store.getAverageDuration();
274
+ expect(avg).toBe(200_000);
275
+ });
276
+
277
+ test("average filtered by capability", () => {
278
+ store.recordSession(
279
+ makeSession({ taskId: "task-1", capability: "builder", durationMs: 100_000 }),
280
+ );
281
+ store.recordSession(makeSession({ taskId: "task-2", capability: "scout", durationMs: 50_000 }));
282
+ store.recordSession(
283
+ makeSession({ taskId: "task-3", capability: "builder", durationMs: 200_000 }),
284
+ );
285
+
286
+ const avgBuilder = store.getAverageDuration("builder");
287
+ const avgScout = store.getAverageDuration("scout");
288
+
289
+ expect(avgBuilder).toBe(150_000);
290
+ expect(avgScout).toBe(50_000);
291
+ });
292
+
293
+ test("returns 0 when no completed sessions exist", () => {
294
+ const avg = store.getAverageDuration();
295
+ expect(avg).toBe(0);
296
+ });
297
+
298
+ test("sessions with completedAt=null are excluded from average", () => {
299
+ store.recordSession(makeSession({ taskId: "task-1", durationMs: 100_000, completedAt: null }));
300
+ store.recordSession(makeSession({ taskId: "task-2", durationMs: 200_000 }));
301
+ store.recordSession(makeSession({ taskId: "task-3", durationMs: 300_000 }));
302
+
303
+ const avg = store.getAverageDuration();
304
+ expect(avg).toBe(250_000); // (200_000 + 300_000) / 2
305
+ });
306
+
307
+ test("single session returns that session's duration", () => {
308
+ store.recordSession(makeSession({ durationMs: 123_456 }));
309
+
310
+ const avg = store.getAverageDuration();
311
+ expect(avg).toBe(123_456);
312
+ });
313
+ });
314
+
315
+ // === token fields ===
316
+
317
+ describe("token fields", () => {
318
+ test("token data roundtrips correctly", () => {
319
+ const session = makeSession({
320
+ inputTokens: 15_000,
321
+ outputTokens: 3_000,
322
+ cacheReadTokens: 100_000,
323
+ cacheCreationTokens: 10_000,
324
+ estimatedCostUsd: 1.23,
325
+ modelUsed: "claude-opus-4-6",
326
+ });
327
+
328
+ store.recordSession(session);
329
+ const retrieved = store.getRecentSessions(10);
330
+
331
+ expect(retrieved).toHaveLength(1);
332
+ expect(retrieved[0]?.inputTokens).toBe(15_000);
333
+ expect(retrieved[0]?.outputTokens).toBe(3_000);
334
+ expect(retrieved[0]?.cacheReadTokens).toBe(100_000);
335
+ expect(retrieved[0]?.cacheCreationTokens).toBe(10_000);
336
+ expect(retrieved[0]?.estimatedCostUsd).toBeCloseTo(1.23, 2);
337
+ expect(retrieved[0]?.modelUsed).toBe("claude-opus-4-6");
338
+ });
339
+
340
+ test("token fields default to 0 and cost/model default to null", () => {
341
+ const session = makeSession();
342
+
343
+ store.recordSession(session);
344
+ const retrieved = store.getRecentSessions(10);
345
+
346
+ expect(retrieved).toHaveLength(1);
347
+ expect(retrieved[0]?.inputTokens).toBe(0);
348
+ expect(retrieved[0]?.outputTokens).toBe(0);
349
+ expect(retrieved[0]?.cacheReadTokens).toBe(0);
350
+ expect(retrieved[0]?.cacheCreationTokens).toBe(0);
351
+ expect(retrieved[0]?.estimatedCostUsd).toBeNull();
352
+ expect(retrieved[0]?.modelUsed).toBeNull();
353
+ });
354
+
355
+ test("migration adds token columns to existing table without them", () => {
356
+ // Close the current store which has the new schema
357
+ store.close();
358
+
359
+ // Create a DB with the old schema (no token columns)
360
+ const { Database } = require("bun:sqlite");
361
+ const oldDb = new Database(dbPath);
362
+ oldDb.exec("DROP TABLE IF EXISTS sessions");
363
+ oldDb.exec(`
364
+ CREATE TABLE sessions (
365
+ agent_name TEXT NOT NULL,
366
+ bead_id TEXT NOT NULL,
367
+ capability TEXT NOT NULL,
368
+ started_at TEXT NOT NULL,
369
+ completed_at TEXT,
370
+ duration_ms INTEGER NOT NULL DEFAULT 0,
371
+ exit_code INTEGER,
372
+ merge_result TEXT,
373
+ parent_agent TEXT,
374
+ PRIMARY KEY (agent_name, bead_id)
375
+ )
376
+ `);
377
+ // Insert a row with old schema
378
+ oldDb.exec(`
379
+ INSERT INTO sessions (agent_name, bead_id, capability, started_at, duration_ms)
380
+ VALUES ('old-agent', 'old-task', 'builder', '2026-01-01T00:00:00Z', 100000)
381
+ `);
382
+ oldDb.close();
383
+
384
+ // Re-open with createMetricsStore which should migrate
385
+ store = createMetricsStore(dbPath);
386
+
387
+ // The old row should still be readable with token defaults
388
+ const sessions = store.getRecentSessions(10);
389
+ expect(sessions).toHaveLength(1);
390
+ expect(sessions[0]?.agentName).toBe("old-agent");
391
+ expect(sessions[0]?.inputTokens).toBe(0);
392
+ expect(sessions[0]?.outputTokens).toBe(0);
393
+ expect(sessions[0]?.estimatedCostUsd).toBeNull();
394
+ expect(sessions[0]?.modelUsed).toBeNull();
395
+
396
+ // New rows with token data should work
397
+ store.recordSession(
398
+ makeSession({
399
+ agentName: "new-agent",
400
+ taskId: "new-task",
401
+ inputTokens: 5000,
402
+ outputTokens: 1000,
403
+ estimatedCostUsd: 0.42,
404
+ modelUsed: "claude-sonnet-4-20250514",
405
+ }),
406
+ );
407
+
408
+ const newSessions = store.getSessionsByAgent("new-agent");
409
+ expect(newSessions).toHaveLength(1);
410
+ expect(newSessions[0]?.inputTokens).toBe(5000);
411
+ expect(newSessions[0]?.estimatedCostUsd).toBeCloseTo(0.42, 2);
412
+ });
413
+ });
414
+
415
+ // === getSessionsByRun ===
416
+
417
+ describe("getSessionsByRun", () => {
418
+ test("returns sessions matching run_id", () => {
419
+ store.recordSession(makeSession({ agentName: "a1", taskId: "t1", runId: "run-001" }));
420
+ store.recordSession(makeSession({ agentName: "a2", taskId: "t2", runId: "run-001" }));
421
+ store.recordSession(makeSession({ agentName: "a3", taskId: "t3", runId: "run-002" }));
422
+
423
+ const sessions = store.getSessionsByRun("run-001");
424
+ expect(sessions).toHaveLength(2);
425
+ expect(sessions.every((s) => s.runId === "run-001")).toBe(true);
426
+ });
427
+
428
+ test("returns empty array for unknown run_id", () => {
429
+ store.recordSession(makeSession({ agentName: "a1", taskId: "t1", runId: "run-001" }));
430
+ expect(store.getSessionsByRun("run-nonexistent")).toEqual([]);
431
+ });
432
+
433
+ test("sessions with null run_id are not returned", () => {
434
+ store.recordSession(makeSession({ agentName: "a1", taskId: "t1", runId: null }));
435
+ store.recordSession(makeSession({ agentName: "a2", taskId: "t2", runId: "run-001" }));
436
+ expect(store.getSessionsByRun("run-001")).toHaveLength(1);
437
+ });
438
+ });
439
+
440
+ // === countSessions ===
441
+
442
+ describe("countSessions", () => {
443
+ test("returns 0 for empty database", () => {
444
+ expect(store.countSessions()).toBe(0);
445
+ });
446
+
447
+ test("returns total count of sessions", () => {
448
+ store.recordSession(makeSession({ agentName: "a1", taskId: "t1" }));
449
+ store.recordSession(makeSession({ agentName: "a2", taskId: "t2" }));
450
+ store.recordSession(makeSession({ agentName: "a3", taskId: "t3" }));
451
+
452
+ expect(store.countSessions()).toBe(3);
453
+ });
454
+
455
+ test("returns accurate count beyond getRecentSessions default limit", () => {
456
+ // Insert 25 sessions (more than the default limit of 20)
457
+ for (let i = 0; i < 25; i++) {
458
+ store.recordSession(
459
+ makeSession({
460
+ agentName: `agent-${i}`,
461
+ taskId: `task-${i}`,
462
+ startedAt: new Date(Date.now() + i * 1000).toISOString(),
463
+ }),
464
+ );
465
+ }
466
+
467
+ // getRecentSessions is capped at 20 by default
468
+ expect(store.getRecentSessions().length).toBe(20);
469
+ // countSessions returns the true total without a cap
470
+ expect(store.countSessions()).toBe(25);
471
+ });
472
+
473
+ test("count updates after purge", () => {
474
+ store.recordSession(makeSession({ agentName: "a1", taskId: "t1" }));
475
+ store.recordSession(makeSession({ agentName: "a2", taskId: "t2" }));
476
+ expect(store.countSessions()).toBe(2);
477
+
478
+ store.purge({ agent: "a1" });
479
+ expect(store.countSessions()).toBe(1);
480
+
481
+ store.purge({ all: true });
482
+ expect(store.countSessions()).toBe(0);
483
+ });
484
+ });
485
+
486
+ // === purge ===
487
+
488
+ describe("purge", () => {
489
+ test("purge all deletes everything and returns count", () => {
490
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-1" }));
491
+ store.recordSession(makeSession({ agentName: "agent-b", taskId: "task-2" }));
492
+ store.recordSession(makeSession({ agentName: "agent-c", taskId: "task-3" }));
493
+
494
+ const count = store.purge({ all: true });
495
+ expect(count).toBe(3);
496
+ expect(store.getRecentSessions(10)).toEqual([]);
497
+ });
498
+
499
+ test("purge by agent deletes only that agent's records", () => {
500
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-1" }));
501
+ store.recordSession(makeSession({ agentName: "agent-b", taskId: "task-2" }));
502
+ store.recordSession(makeSession({ agentName: "agent-a", taskId: "task-3" }));
503
+
504
+ const count = store.purge({ agent: "agent-a" });
505
+ expect(count).toBe(2);
506
+
507
+ const remaining = store.getRecentSessions(10);
508
+ expect(remaining).toHaveLength(1);
509
+ expect(remaining[0]?.agentName).toBe("agent-b");
510
+ });
511
+
512
+ test("purge on empty DB returns 0", () => {
513
+ const count = store.purge({ all: true });
514
+ expect(count).toBe(0);
515
+ });
516
+
517
+ test("purge with no options returns 0 without deleting", () => {
518
+ store.recordSession(makeSession({ taskId: "task-1" }));
519
+
520
+ const count = store.purge({});
521
+ expect(count).toBe(0);
522
+ expect(store.getRecentSessions(10)).toHaveLength(1);
523
+ });
524
+ });
525
+
526
+ // === token snapshots ===
527
+
528
+ describe("token snapshots", () => {
529
+ test("recordSnapshot inserts and can be retrieved via getLatestSnapshots", () => {
530
+ const snapshot = {
531
+ agentName: "test-agent",
532
+ inputTokens: 1000,
533
+ outputTokens: 500,
534
+ cacheReadTokens: 200,
535
+ cacheCreationTokens: 100,
536
+ estimatedCostUsd: 0.15,
537
+ modelUsed: "claude-sonnet-4-5",
538
+ runId: null,
539
+ createdAt: new Date().toISOString(),
540
+ };
541
+
542
+ store.recordSnapshot(snapshot);
543
+
544
+ const snapshots = store.getLatestSnapshots();
545
+ expect(snapshots).toHaveLength(1);
546
+ expect(snapshots[0]?.agentName).toBe("test-agent");
547
+ expect(snapshots[0]?.inputTokens).toBe(1000);
548
+ expect(snapshots[0]?.outputTokens).toBe(500);
549
+ expect(snapshots[0]?.estimatedCostUsd).toBeCloseTo(0.15, 2);
550
+ });
551
+
552
+ test("getLatestSnapshots returns one row per agent (the most recent)", () => {
553
+ const now = Date.now();
554
+ store.recordSnapshot({
555
+ agentName: "agent-a",
556
+ inputTokens: 100,
557
+ outputTokens: 50,
558
+ cacheReadTokens: 0,
559
+ cacheCreationTokens: 0,
560
+ estimatedCostUsd: 0.01,
561
+ modelUsed: "claude-sonnet-4-5",
562
+ runId: null,
563
+ createdAt: new Date(now - 60_000).toISOString(), // 1 min ago
564
+ });
565
+
566
+ store.recordSnapshot({
567
+ agentName: "agent-a",
568
+ inputTokens: 200,
569
+ outputTokens: 100,
570
+ cacheReadTokens: 0,
571
+ cacheCreationTokens: 0,
572
+ estimatedCostUsd: 0.02,
573
+ modelUsed: "claude-sonnet-4-5",
574
+ runId: null,
575
+ createdAt: new Date(now).toISOString(), // now (most recent)
576
+ });
577
+
578
+ store.recordSnapshot({
579
+ agentName: "agent-b",
580
+ inputTokens: 300,
581
+ outputTokens: 150,
582
+ cacheReadTokens: 0,
583
+ cacheCreationTokens: 0,
584
+ estimatedCostUsd: 0.03,
585
+ modelUsed: "claude-sonnet-4-5",
586
+ runId: null,
587
+ createdAt: new Date(now - 30_000).toISOString(), // 30s ago
588
+ });
589
+
590
+ const snapshots = store.getLatestSnapshots();
591
+ expect(snapshots).toHaveLength(2); // one per agent
592
+
593
+ const agentASnapshot = snapshots.find((s) => s.agentName === "agent-a");
594
+ const agentBSnapshot = snapshots.find((s) => s.agentName === "agent-b");
595
+
596
+ expect(agentASnapshot?.inputTokens).toBe(200); // most recent for agent-a
597
+ expect(agentBSnapshot?.inputTokens).toBe(300);
598
+ });
599
+
600
+ test("getLatestSnapshotTime returns the most recent timestamp for an agent", () => {
601
+ const now = Date.now();
602
+ const time1 = new Date(now - 60_000).toISOString();
603
+ const time2 = new Date(now).toISOString();
604
+
605
+ store.recordSnapshot({
606
+ agentName: "test-agent",
607
+ inputTokens: 100,
608
+ outputTokens: 50,
609
+ cacheReadTokens: 0,
610
+ cacheCreationTokens: 0,
611
+ estimatedCostUsd: null,
612
+ modelUsed: null,
613
+ runId: null,
614
+ createdAt: time1,
615
+ });
616
+
617
+ store.recordSnapshot({
618
+ agentName: "test-agent",
619
+ inputTokens: 200,
620
+ outputTokens: 100,
621
+ cacheReadTokens: 0,
622
+ cacheCreationTokens: 0,
623
+ estimatedCostUsd: null,
624
+ modelUsed: null,
625
+ runId: null,
626
+ createdAt: time2,
627
+ });
628
+
629
+ const latestTime = store.getLatestSnapshotTime("test-agent");
630
+ expect(latestTime).toBe(time2);
631
+ });
632
+
633
+ test("getLatestSnapshotTime returns null for unknown agent", () => {
634
+ const latestTime = store.getLatestSnapshotTime("unknown-agent");
635
+ expect(latestTime).toBeNull();
636
+ });
637
+
638
+ test("purgeSnapshots with all=true deletes everything", () => {
639
+ store.recordSnapshot({
640
+ agentName: "agent-a",
641
+ inputTokens: 100,
642
+ outputTokens: 50,
643
+ cacheReadTokens: 0,
644
+ cacheCreationTokens: 0,
645
+ estimatedCostUsd: null,
646
+ modelUsed: null,
647
+ runId: null,
648
+ createdAt: new Date().toISOString(),
649
+ });
650
+
651
+ store.recordSnapshot({
652
+ agentName: "agent-b",
653
+ inputTokens: 200,
654
+ outputTokens: 100,
655
+ cacheReadTokens: 0,
656
+ cacheCreationTokens: 0,
657
+ estimatedCostUsd: null,
658
+ modelUsed: null,
659
+ runId: null,
660
+ createdAt: new Date().toISOString(),
661
+ });
662
+
663
+ const count = store.purgeSnapshots({ all: true });
664
+ expect(count).toBe(2);
665
+ expect(store.getLatestSnapshots()).toEqual([]);
666
+ });
667
+
668
+ test("purgeSnapshots with agent filter deletes only that agent", () => {
669
+ store.recordSnapshot({
670
+ agentName: "agent-a",
671
+ inputTokens: 100,
672
+ outputTokens: 50,
673
+ cacheReadTokens: 0,
674
+ cacheCreationTokens: 0,
675
+ estimatedCostUsd: null,
676
+ modelUsed: null,
677
+ runId: null,
678
+ createdAt: new Date().toISOString(),
679
+ });
680
+
681
+ store.recordSnapshot({
682
+ agentName: "agent-b",
683
+ inputTokens: 200,
684
+ outputTokens: 100,
685
+ cacheReadTokens: 0,
686
+ cacheCreationTokens: 0,
687
+ estimatedCostUsd: null,
688
+ modelUsed: null,
689
+ runId: null,
690
+ createdAt: new Date().toISOString(),
691
+ });
692
+
693
+ const count = store.purgeSnapshots({ agent: "agent-a" });
694
+ expect(count).toBe(1);
695
+
696
+ const remaining = store.getLatestSnapshots();
697
+ expect(remaining).toHaveLength(1);
698
+ expect(remaining[0]?.agentName).toBe("agent-b");
699
+ });
700
+
701
+ test("purgeSnapshots with olderThanMs deletes old snapshots", () => {
702
+ const now = Date.now();
703
+ store.recordSnapshot({
704
+ agentName: "agent-a",
705
+ inputTokens: 100,
706
+ outputTokens: 50,
707
+ cacheReadTokens: 0,
708
+ cacheCreationTokens: 0,
709
+ estimatedCostUsd: null,
710
+ modelUsed: null,
711
+ runId: null,
712
+ createdAt: new Date(now - 120_000).toISOString(), // 2 min ago
713
+ });
714
+
715
+ store.recordSnapshot({
716
+ agentName: "agent-b",
717
+ inputTokens: 200,
718
+ outputTokens: 100,
719
+ cacheReadTokens: 0,
720
+ cacheCreationTokens: 0,
721
+ estimatedCostUsd: null,
722
+ modelUsed: null,
723
+ runId: null,
724
+ createdAt: new Date(now - 10_000).toISOString(), // 10s ago (recent)
725
+ });
726
+
727
+ const count = store.purgeSnapshots({ olderThanMs: 60_000 }); // delete older than 1 min
728
+ expect(count).toBe(1); // only the 2-min-old one
729
+
730
+ const remaining = store.getLatestSnapshots();
731
+ expect(remaining).toHaveLength(1);
732
+ expect(remaining[0]?.agentName).toBe("agent-b");
733
+ });
734
+
735
+ test("table creation is idempotent (re-opening store does not fail)", () => {
736
+ store.recordSnapshot({
737
+ agentName: "test-agent",
738
+ inputTokens: 100,
739
+ outputTokens: 50,
740
+ cacheReadTokens: 0,
741
+ cacheCreationTokens: 0,
742
+ estimatedCostUsd: null,
743
+ modelUsed: null,
744
+ runId: null,
745
+ createdAt: new Date().toISOString(),
746
+ });
747
+
748
+ store.close();
749
+
750
+ // Re-open and verify data persists
751
+ store = createMetricsStore(dbPath);
752
+ const snapshots = store.getLatestSnapshots();
753
+ expect(snapshots).toHaveLength(1);
754
+ expect(snapshots[0]?.agentName).toBe("test-agent");
755
+ });
756
+
757
+ test("runId roundtrips correctly through snapshot record and retrieval", () => {
758
+ const now = Date.now();
759
+ store.recordSnapshot({
760
+ agentName: "agent-a",
761
+ inputTokens: 100,
762
+ outputTokens: 50,
763
+ cacheReadTokens: 0,
764
+ cacheCreationTokens: 0,
765
+ estimatedCostUsd: null,
766
+ modelUsed: null,
767
+ runId: "run-abc",
768
+ createdAt: new Date(now).toISOString(),
769
+ });
770
+
771
+ store.recordSnapshot({
772
+ agentName: "agent-b",
773
+ inputTokens: 200,
774
+ outputTokens: 100,
775
+ cacheReadTokens: 0,
776
+ cacheCreationTokens: 0,
777
+ estimatedCostUsd: null,
778
+ modelUsed: null,
779
+ runId: null,
780
+ createdAt: new Date(now).toISOString(),
781
+ });
782
+
783
+ const snapshots = store.getLatestSnapshots();
784
+ const agentA = snapshots.find((s) => s.agentName === "agent-a");
785
+ const agentB = snapshots.find((s) => s.agentName === "agent-b");
786
+
787
+ expect(agentA?.runId).toBe("run-abc");
788
+ expect(agentB?.runId).toBeNull();
789
+ });
790
+
791
+ test("getLatestSnapshots(runId) returns only snapshots matching that run", () => {
792
+ const now = Date.now();
793
+ store.recordSnapshot({
794
+ agentName: "agent-a",
795
+ inputTokens: 100,
796
+ outputTokens: 50,
797
+ cacheReadTokens: 0,
798
+ cacheCreationTokens: 0,
799
+ estimatedCostUsd: null,
800
+ modelUsed: null,
801
+ runId: "run-001",
802
+ createdAt: new Date(now).toISOString(),
803
+ });
804
+
805
+ store.recordSnapshot({
806
+ agentName: "agent-b",
807
+ inputTokens: 200,
808
+ outputTokens: 100,
809
+ cacheReadTokens: 0,
810
+ cacheCreationTokens: 0,
811
+ estimatedCostUsd: null,
812
+ modelUsed: null,
813
+ runId: "run-001",
814
+ createdAt: new Date(now).toISOString(),
815
+ });
816
+
817
+ store.recordSnapshot({
818
+ agentName: "agent-c",
819
+ inputTokens: 300,
820
+ outputTokens: 150,
821
+ cacheReadTokens: 0,
822
+ cacheCreationTokens: 0,
823
+ estimatedCostUsd: null,
824
+ modelUsed: null,
825
+ runId: "run-002",
826
+ createdAt: new Date(now).toISOString(),
827
+ });
828
+
829
+ const run001Snapshots = store.getLatestSnapshots("run-001");
830
+ expect(run001Snapshots).toHaveLength(2);
831
+ expect(run001Snapshots.every((s) => s.runId === "run-001")).toBe(true);
832
+
833
+ const run002Snapshots = store.getLatestSnapshots("run-002");
834
+ expect(run002Snapshots).toHaveLength(1);
835
+ expect(run002Snapshots[0]?.agentName).toBe("agent-c");
836
+ });
837
+
838
+ test("getLatestSnapshots(runId) returns empty array for unknown run", () => {
839
+ store.recordSnapshot({
840
+ agentName: "agent-a",
841
+ inputTokens: 100,
842
+ outputTokens: 50,
843
+ cacheReadTokens: 0,
844
+ cacheCreationTokens: 0,
845
+ estimatedCostUsd: null,
846
+ modelUsed: null,
847
+ runId: "run-001",
848
+ createdAt: new Date().toISOString(),
849
+ });
850
+
851
+ const snapshots = store.getLatestSnapshots("run-nonexistent");
852
+ expect(snapshots).toEqual([]);
853
+ });
854
+
855
+ test("getLatestSnapshots(runId) excludes snapshots with null run_id", () => {
856
+ const now = Date.now();
857
+ store.recordSnapshot({
858
+ agentName: "agent-a",
859
+ inputTokens: 100,
860
+ outputTokens: 50,
861
+ cacheReadTokens: 0,
862
+ cacheCreationTokens: 0,
863
+ estimatedCostUsd: null,
864
+ modelUsed: null,
865
+ runId: null, // no run
866
+ createdAt: new Date(now).toISOString(),
867
+ });
868
+
869
+ store.recordSnapshot({
870
+ agentName: "agent-b",
871
+ inputTokens: 200,
872
+ outputTokens: 100,
873
+ cacheReadTokens: 0,
874
+ cacheCreationTokens: 0,
875
+ estimatedCostUsd: null,
876
+ modelUsed: null,
877
+ runId: "run-001",
878
+ createdAt: new Date(now).toISOString(),
879
+ });
880
+
881
+ const run001Snapshots = store.getLatestSnapshots("run-001");
882
+ expect(run001Snapshots).toHaveLength(1);
883
+ expect(run001Snapshots[0]?.agentName).toBe("agent-b");
884
+ });
885
+
886
+ test("getLatestSnapshots(runId) returns latest per agent within the run", () => {
887
+ const now = Date.now();
888
+ // Two snapshots for agent-a in run-001: should only get the latest
889
+ store.recordSnapshot({
890
+ agentName: "agent-a",
891
+ inputTokens: 100,
892
+ outputTokens: 50,
893
+ cacheReadTokens: 0,
894
+ cacheCreationTokens: 0,
895
+ estimatedCostUsd: null,
896
+ modelUsed: null,
897
+ runId: "run-001",
898
+ createdAt: new Date(now - 30_000).toISOString(), // older
899
+ });
900
+
901
+ store.recordSnapshot({
902
+ agentName: "agent-a",
903
+ inputTokens: 500,
904
+ outputTokens: 250,
905
+ cacheReadTokens: 0,
906
+ cacheCreationTokens: 0,
907
+ estimatedCostUsd: null,
908
+ modelUsed: null,
909
+ runId: "run-001",
910
+ createdAt: new Date(now).toISOString(), // latest
911
+ });
912
+
913
+ const snapshots = store.getLatestSnapshots("run-001");
914
+ expect(snapshots).toHaveLength(1);
915
+ expect(snapshots[0]?.inputTokens).toBe(500); // most recent
916
+ });
917
+
918
+ test("migration adds run_id to existing token_snapshots table", () => {
919
+ store.close();
920
+
921
+ // Create a DB with old token_snapshots schema (no run_id column)
922
+ const { Database } = require("bun:sqlite");
923
+ const oldDb = new Database(dbPath);
924
+ oldDb.exec("DROP TABLE IF EXISTS token_snapshots");
925
+ oldDb.exec(`
926
+ CREATE TABLE token_snapshots (
927
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
928
+ agent_name TEXT NOT NULL,
929
+ input_tokens INTEGER NOT NULL DEFAULT 0,
930
+ output_tokens INTEGER NOT NULL DEFAULT 0,
931
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
932
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
933
+ estimated_cost_usd REAL,
934
+ model_used TEXT,
935
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
936
+ )
937
+ `);
938
+ oldDb.exec(`
939
+ INSERT INTO token_snapshots (agent_name, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, created_at)
940
+ VALUES ('old-agent', 100, 50, 0, 0, '2026-01-01T00:00:00.000Z')
941
+ `);
942
+ oldDb.close();
943
+
944
+ // Re-open with createMetricsStore which should migrate
945
+ store = createMetricsStore(dbPath);
946
+
947
+ // Old row should be readable with null run_id
948
+ const snapshots = store.getLatestSnapshots();
949
+ expect(snapshots).toHaveLength(1);
950
+ expect(snapshots[0]?.agentName).toBe("old-agent");
951
+ expect(snapshots[0]?.runId).toBeNull();
952
+
953
+ // New rows with run_id should work
954
+ store.recordSnapshot({
955
+ agentName: "new-agent",
956
+ inputTokens: 200,
957
+ outputTokens: 100,
958
+ cacheReadTokens: 0,
959
+ cacheCreationTokens: 0,
960
+ estimatedCostUsd: null,
961
+ modelUsed: null,
962
+ runId: "run-xyz",
963
+ createdAt: new Date().toISOString(),
964
+ });
965
+
966
+ const newSnapshots = store.getLatestSnapshots("run-xyz");
967
+ expect(newSnapshots).toHaveLength(1);
968
+ expect(newSnapshots[0]?.runId).toBe("run-xyz");
969
+ });
970
+ });
971
+
972
+ // === close ===
973
+
974
+ describe("close", () => {
975
+ test("calling close does not throw", () => {
976
+ expect(() => store.close()).not.toThrow();
977
+ });
978
+ });