@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,954 @@
1
+ /**
2
+ * Tests for agentplate dashboard command.
3
+ *
4
+ * We only test help output and validation since the dashboard runs an infinite
5
+ * polling loop. The actual rendering cannot be tested without complex mocking
6
+ * of terminal state and multiple data sources.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
+ import { mkdir, mkdtemp } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { ValidationError } from "../errors.ts";
14
+ import { createEventStore } from "../events/store.ts";
15
+ import { color } from "../logging/color.ts";
16
+ import { createSessionStore } from "../sessions/store.ts";
17
+ import { cleanupTempDir } from "../test-helpers.ts";
18
+ import type { DashboardStores } from "./dashboard.ts";
19
+ import {
20
+ closeDashboardStores,
21
+ computeAgentPanelHeight,
22
+ createDashboardCommand,
23
+ dashboardCommand,
24
+ dimBox,
25
+ EventBuffer,
26
+ filterAgentsByRun,
27
+ horizontalLine,
28
+ openDashboardStores,
29
+ pad,
30
+ renderAgentPanel,
31
+ renderFeedPanel,
32
+ renderTasksPanel,
33
+ truncate,
34
+ } from "./dashboard.ts";
35
+
36
+ describe("dashboard ui subcommand", () => {
37
+ test("createDashboardCommand registers a 'ui' subcommand", () => {
38
+ const cmd = createDashboardCommand();
39
+ const names = cmd.commands.map((c) => c.name());
40
+ expect(names).toContain("ui");
41
+ });
42
+
43
+ test("ui --help prints help and does not start a server", async () => {
44
+ // dashboardCommand uses exitOverride; --help resolves without throwing.
45
+ await dashboardCommand(["ui", "--help"]);
46
+ });
47
+
48
+ test("ui --port with a non-numeric value throws ValidationError before serving", async () => {
49
+ await expect(dashboardCommand(["ui", "--port", "abc"])).rejects.toThrow(ValidationError);
50
+ });
51
+
52
+ test("ui --port out of range throws ValidationError", async () => {
53
+ await expect(dashboardCommand(["ui", "--port", "70000"])).rejects.toThrow(ValidationError);
54
+ });
55
+ });
56
+
57
+ describe("dashboardCommand", () => {
58
+ let chunks: string[];
59
+ let originalWrite: typeof process.stdout.write;
60
+ let tempDir: string;
61
+
62
+ beforeEach(async () => {
63
+ chunks = [];
64
+ originalWrite = process.stdout.write;
65
+ process.stdout.write = ((chunk: string) => {
66
+ chunks.push(chunk);
67
+ return true;
68
+ }) as typeof process.stdout.write;
69
+
70
+ tempDir = await mkdtemp(join(tmpdir(), "dashboard-test-"));
71
+ });
72
+
73
+ afterEach(async () => {
74
+ process.stdout.write = originalWrite;
75
+ await cleanupTempDir(tempDir);
76
+ });
77
+
78
+ function output(): string {
79
+ return chunks.join("");
80
+ }
81
+
82
+ test("--help flag prints help text", async () => {
83
+ await dashboardCommand(["--help"]);
84
+ const out = output();
85
+
86
+ expect(out).toContain("dashboard");
87
+ expect(out).toContain("--interval");
88
+ expect(out).toContain("Ctrl+C");
89
+ });
90
+
91
+ test("-h flag prints help text", async () => {
92
+ await dashboardCommand(["-h"]);
93
+ const out = output();
94
+
95
+ expect(out).toContain("dashboard");
96
+ expect(out).toContain("--interval");
97
+ expect(out).toContain("Ctrl+C");
98
+ });
99
+
100
+ test("--interval with non-numeric value throws ValidationError", async () => {
101
+ await expect(dashboardCommand(["--interval", "abc"])).rejects.toThrow(ValidationError);
102
+ });
103
+
104
+ test("--interval below 500 throws ValidationError", async () => {
105
+ await expect(dashboardCommand(["--interval", "499"])).rejects.toThrow(ValidationError);
106
+ });
107
+
108
+ test("--interval with NaN throws ValidationError", async () => {
109
+ await expect(dashboardCommand(["--interval", "not-a-number"])).rejects.toThrow(ValidationError);
110
+ });
111
+
112
+ test("--interval at exactly 500 passes validation", async () => {
113
+ // This test verifies that interval validation passes for the value 500.
114
+ // We chdir to a temp dir WITHOUT .agentplate/config.yaml so that loadConfig()
115
+ // throws BEFORE the infinite while loop starts. This proves validation passed
116
+ // (no ValidationError about interval) while preventing the loop from leaking.
117
+
118
+ const originalCwd = process.cwd();
119
+
120
+ try {
121
+ process.chdir(tempDir);
122
+ await dashboardCommand(["--interval", "500"]);
123
+ } catch (err) {
124
+ // If it's a ValidationError about interval, the test should fail
125
+ if (err instanceof ValidationError && err.field === "interval") {
126
+ throw new Error("Interval validation should have passed for value 500");
127
+ }
128
+ // Other errors (like from loadConfig) are expected - they occur after validation passed
129
+ } finally {
130
+ process.chdir(originalCwd);
131
+ }
132
+
133
+ // If we reach here without throwing a ValidationError about interval, validation passed
134
+ });
135
+
136
+ test("help text includes --all flag", async () => {
137
+ await dashboardCommand(["--help"]);
138
+ const out = output();
139
+
140
+ expect(out).toContain("--all");
141
+ });
142
+
143
+ test("help text describes current run scoping", async () => {
144
+ await dashboardCommand(["--help"]);
145
+ const out = output();
146
+
147
+ expect(out).toContain("current run");
148
+ });
149
+ });
150
+
151
+ describe("pad", () => {
152
+ test("zero width returns empty string", () => {
153
+ expect(pad("hello", 0)).toBe("");
154
+ });
155
+
156
+ test("negative width returns empty string", () => {
157
+ expect(pad("hello", -1)).toBe("");
158
+ });
159
+
160
+ test("truncates string longer than width", () => {
161
+ expect(pad("hello", 3)).toBe("hel");
162
+ });
163
+
164
+ test("pads string shorter than width with spaces", () => {
165
+ expect(pad("hi", 5)).toBe("hi ");
166
+ });
167
+ });
168
+
169
+ describe("truncate", () => {
170
+ test("zero maxLen returns empty string", () => {
171
+ expect(truncate("hello world", 0)).toBe("");
172
+ });
173
+
174
+ test("negative maxLen returns empty string", () => {
175
+ expect(truncate("hello world", -1)).toBe("");
176
+ });
177
+
178
+ test("truncates with ellipsis", () => {
179
+ expect(truncate("hello world", 5)).toBe("hell…");
180
+ });
181
+
182
+ test("string shorter than maxLen returned as-is", () => {
183
+ expect(truncate("hi", 10)).toBe("hi");
184
+ });
185
+ });
186
+
187
+ describe("horizontalLine", () => {
188
+ test("width 0 does not throw", () => {
189
+ expect(() => horizontalLine(0, "┌", "─", "┐")).not.toThrow();
190
+ });
191
+
192
+ test("width 1 does not throw", () => {
193
+ expect(() => horizontalLine(1, "┌", "─", "┐")).not.toThrow();
194
+ });
195
+
196
+ test("width 2 returns just connectors", () => {
197
+ expect(horizontalLine(2, "┌", "─", "┐")).toBe("┌┐");
198
+ });
199
+
200
+ test("width 4 returns connectors with fill", () => {
201
+ expect(horizontalLine(4, "┌", "─", "┐")).toBe("┌──┐");
202
+ });
203
+ });
204
+
205
+ describe("filterAgentsByRun", () => {
206
+ type Stub = { runId: string | null; name: string };
207
+
208
+ const coordinator: Stub = { runId: null, name: "coordinator" };
209
+ const builder1: Stub = { runId: "run-001", name: "builder-1" };
210
+ const builder2: Stub = { runId: "run-002", name: "builder-2" };
211
+ const agents = [coordinator, builder1, builder2];
212
+
213
+ test("no runId returns all agents", () => {
214
+ expect(filterAgentsByRun(agents, null)).toEqual(agents);
215
+ expect(filterAgentsByRun(agents, undefined)).toEqual(agents);
216
+ });
217
+
218
+ test("run-scoped includes matching runId agents", () => {
219
+ const result = filterAgentsByRun(agents, "run-001");
220
+ expect(result.map((a) => a.name)).toContain("builder-1");
221
+ });
222
+
223
+ test("run-scoped includes null-runId agents (coordinator)", () => {
224
+ const result = filterAgentsByRun(agents, "run-001");
225
+ expect(result.map((a) => a.name)).toContain("coordinator");
226
+ });
227
+
228
+ test("run-scoped excludes agents from other runs", () => {
229
+ const result = filterAgentsByRun(agents, "run-001");
230
+ expect(result.map((a) => a.name)).not.toContain("builder-2");
231
+ });
232
+
233
+ test("empty agents list returns empty", () => {
234
+ expect(filterAgentsByRun([], "run-001")).toEqual([]);
235
+ });
236
+ });
237
+
238
+ describe("dimBox", () => {
239
+ test("dimBox.vertical equals color.dim(│)", () => {
240
+ expect(dimBox.vertical).toBe(color.dim("│"));
241
+ });
242
+
243
+ test("dimBox.horizontal equals color.dim(─)", () => {
244
+ expect(dimBox.horizontal).toBe(color.dim("─"));
245
+ });
246
+
247
+ test("dimBox.tee equals color.dim(├)", () => {
248
+ expect(dimBox.tee).toBe(color.dim("├"));
249
+ });
250
+
251
+ test("dimBox.teeRight equals color.dim(┤)", () => {
252
+ expect(dimBox.teeRight).toBe(color.dim("┤"));
253
+ });
254
+
255
+ test("dimBox values equal color.dim() applied to their characters", () => {
256
+ // dimBox values are always equal to color.dim(char) regardless of whether
257
+ // Chalk emits ANSI codes (it may suppress them in non-TTY / NO_COLOR envs).
258
+ expect(dimBox.topLeft).toBe(color.dim("┌"));
259
+ expect(dimBox.topRight).toBe(color.dim("┐"));
260
+ expect(dimBox.bottomLeft).toBe(color.dim("└"));
261
+ expect(dimBox.bottomRight).toBe(color.dim("┘"));
262
+ expect(dimBox.cross).toBe(color.dim("┼"));
263
+ });
264
+ });
265
+
266
+ describe("computeAgentPanelHeight", () => {
267
+ test("0 agents: clamps to minimum 8", () => {
268
+ // max(8, min(floor(30*0.35)=10, 0+4)) = max(8, min(10,4)) = max(8,4) = 8
269
+ expect(computeAgentPanelHeight(30, 0)).toBe(8);
270
+ });
271
+
272
+ test("4 agents: still clamps to minimum 8", () => {
273
+ // max(8, min(10, 4+4)) = max(8, 8) = 8
274
+ expect(computeAgentPanelHeight(30, 4)).toBe(8);
275
+ });
276
+
277
+ test("20 agents with height 30: clamps to floor(height*0.35)", () => {
278
+ // max(8, min(floor(30*0.35)=10, 24)) = max(8,10) = 10
279
+ expect(computeAgentPanelHeight(30, 20)).toBe(10);
280
+ });
281
+
282
+ test("10 agents with height 30: grows with agent count", () => {
283
+ // max(8, min(10, 14)) = max(8,10) = 10
284
+ expect(computeAgentPanelHeight(30, 10)).toBe(10);
285
+ });
286
+
287
+ test("small height: respects 35% cap", () => {
288
+ // height=20: max(8, min(floor(20*0.35)=7, 24)) = max(8,7) = 8
289
+ expect(computeAgentPanelHeight(20, 20)).toBe(8);
290
+ });
291
+ });
292
+
293
+ // Helper to build a minimal DashboardData for panel tests
294
+ function makeDashboardData(
295
+ overrides: Partial<{
296
+ tasks: Array<{ id: string; title: string; priority: number; status: string; type: string }>;
297
+ recentEvents: Array<{
298
+ id: number;
299
+ agentName: string;
300
+ eventType: string;
301
+ level: string;
302
+ createdAt: string;
303
+ runId: null;
304
+ sessionId: null;
305
+ toolName: null;
306
+ toolArgs: null;
307
+ toolDurationMs: null;
308
+ data: null;
309
+ }>;
310
+ }> = {},
311
+ ) {
312
+ return {
313
+ currentRunId: null,
314
+ status: {
315
+ currentRunId: null,
316
+ agents: [],
317
+ worktrees: [],
318
+ tmuxSessions: [],
319
+ unreadMailCount: 0,
320
+ unreadMailScope: "orchestrator",
321
+ mergeQueueCount: 0,
322
+ recentMetricsCount: 0,
323
+ },
324
+ recentMail: [],
325
+ mergeQueue: [],
326
+ metrics: { totalSessions: 0, avgDuration: 0, byCapability: {} },
327
+ tasks: overrides.tasks ?? [],
328
+ recentEvents: (overrides.recentEvents as never[]) ?? [],
329
+ feedColorMap: new Map(),
330
+ };
331
+ }
332
+
333
+ describe("renderTasksPanel", () => {
334
+ test("renders task id in output", () => {
335
+ const data = makeDashboardData({
336
+ tasks: [{ id: "t1", title: "Test task", priority: 2, status: "open", type: "task" }],
337
+ });
338
+ const out = renderTasksPanel(data, 1, 80, 10, 1);
339
+ expect(out).toContain("t1");
340
+ });
341
+
342
+ test("renders task title in output", () => {
343
+ const data = makeDashboardData({
344
+ tasks: [{ id: "t1", title: "Test task", priority: 2, status: "open", type: "task" }],
345
+ });
346
+ const out = renderTasksPanel(data, 1, 80, 10, 1);
347
+ expect(out).toContain("Test task");
348
+ });
349
+
350
+ test("renders priority label in output", () => {
351
+ const data = makeDashboardData({
352
+ tasks: [{ id: "t1", title: "Test task", priority: 2, status: "open", type: "task" }],
353
+ });
354
+ const out = renderTasksPanel(data, 1, 80, 10, 1);
355
+ expect(out).toContain("P2");
356
+ });
357
+
358
+ test("shows 'No tracker data' when tasks list is empty", () => {
359
+ const data = makeDashboardData({ tasks: [] });
360
+ const out = renderTasksPanel(data, 1, 80, 10, 1);
361
+ expect(out).toContain("No tracker data");
362
+ });
363
+
364
+ test("renders Tasks header", () => {
365
+ const data = makeDashboardData({ tasks: [] });
366
+ const out = renderTasksPanel(data, 1, 80, 6, 1);
367
+ expect(out).toContain("Tasks");
368
+ });
369
+
370
+ test("renders multiple tasks", () => {
371
+ const data = makeDashboardData({
372
+ tasks: [
373
+ { id: "abc-001", title: "First task", priority: 1, status: "open", type: "task" },
374
+ { id: "abc-002", title: "Second task", priority: 3, status: "in_progress", type: "bug" },
375
+ ],
376
+ });
377
+ const out = renderTasksPanel(data, 1, 80, 10, 1);
378
+ expect(out).toContain("abc-001");
379
+ expect(out).toContain("abc-002");
380
+ });
381
+ });
382
+
383
+ describe("renderFeedPanel", () => {
384
+ test("shows 'No recent events' when recentEvents is empty", () => {
385
+ const data = makeDashboardData({ recentEvents: [] });
386
+ const out = renderFeedPanel(data, 1, 80, 8, 1);
387
+ expect(out).toContain("No recent events");
388
+ });
389
+
390
+ test("renders Feed header", () => {
391
+ const data = makeDashboardData({ recentEvents: [] });
392
+ const out = renderFeedPanel(data, 1, 80, 8, 1);
393
+ expect(out).toContain("Feed");
394
+ expect(out).toContain("(live)");
395
+ });
396
+
397
+ test("renders event agent name when events are present", () => {
398
+ const event = {
399
+ id: 1,
400
+ agentName: "test-agent",
401
+ eventType: "tool_end" as const,
402
+ level: "info" as const,
403
+ createdAt: new Date().toISOString(),
404
+ runId: null,
405
+ sessionId: null,
406
+ toolName: null,
407
+ toolArgs: null,
408
+ toolDurationMs: null,
409
+ data: null,
410
+ };
411
+ const data = makeDashboardData({ recentEvents: [event] });
412
+ // formatEventLine is a stub — returns "" — so output won't have agent name from it.
413
+ // But the panel itself should not throw and should render the border structure.
414
+ const out = renderFeedPanel(data, 1, 80, 8, 1);
415
+ // Panel renders without error and contains Feed header
416
+ expect(out).toContain("Feed");
417
+ // At least 1 row rendered (not the "No recent events" path)
418
+ expect(out).not.toContain("No recent events");
419
+ });
420
+ });
421
+
422
+ describe("renderAgentPanel", () => {
423
+ test("renders Agents header", () => {
424
+ const data = makeDashboardData({});
425
+ const out = renderAgentPanel(data, 100, 12, 3);
426
+ expect(out).toContain("Agents");
427
+ });
428
+
429
+ test("renders with dimmed border characters", () => {
430
+ const data = makeDashboardData({});
431
+ const out = renderAgentPanel(data, 100, 12, 3);
432
+ // dimBox.vertical is a dimmed ANSI string — present in output
433
+ expect(out).toContain(dimBox.vertical);
434
+ });
435
+
436
+ test("renders Live column header (not Tmux)", () => {
437
+ const data = makeDashboardData({});
438
+ const out = renderAgentPanel(data, 100, 12, 3);
439
+ expect(out).toContain("Live");
440
+ expect(out).not.toContain("Tmux");
441
+ });
442
+
443
+ test("shows green dot for headless agent with alive PID", () => {
444
+ const alivePid = process.pid; // own PID — guaranteed alive
445
+ const data = {
446
+ ...makeDashboardData({}),
447
+ status: {
448
+ currentRunId: null,
449
+ agents: [
450
+ {
451
+ id: "sess-h1",
452
+ agentName: "headless-worker",
453
+ capability: "builder",
454
+ worktreePath: "/tmp/wt/headless",
455
+ branchName: "agentplate/headless/task-1",
456
+ taskId: "task-h1",
457
+ tmuxSession: "", // headless
458
+ state: "working" as const,
459
+ pid: alivePid,
460
+ parentAgent: null,
461
+ depth: 0,
462
+ runId: null,
463
+ startedAt: new Date(Date.now() - 10_000).toISOString(),
464
+ lastActivity: new Date().toISOString(),
465
+ escalationLevel: 0,
466
+ stalledSince: null,
467
+ transcriptPath: null,
468
+ },
469
+ ],
470
+ worktrees: [],
471
+ tmuxSessions: [], // no tmux sessions
472
+ unreadMailCount: 0,
473
+ unreadMailScope: "orchestrator",
474
+ mergeQueueCount: 0,
475
+ recentMetricsCount: 0,
476
+ },
477
+ };
478
+ const out = renderAgentPanel(data, 100, 12, 3);
479
+ // Green ">" for alive headless agent
480
+ expect(out).toContain(">");
481
+ expect(out).toContain("headless-worker");
482
+ });
483
+
484
+ test("shows red dot for headless agent with dead PID", () => {
485
+ const deadPid = 2_147_483_647;
486
+ const data = {
487
+ ...makeDashboardData({}),
488
+ status: {
489
+ currentRunId: null,
490
+ agents: [
491
+ {
492
+ id: "sess-h2",
493
+ agentName: "dead-headless", // short enough to not be truncated
494
+ capability: "builder",
495
+ worktreePath: "/tmp/wt/dead-headless",
496
+ branchName: "agentplate/dead-headless/task-2",
497
+ taskId: "task-h2",
498
+ tmuxSession: "", // headless
499
+ state: "working" as const,
500
+ pid: deadPid,
501
+ parentAgent: null,
502
+ depth: 0,
503
+ runId: null,
504
+ startedAt: new Date(Date.now() - 10_000).toISOString(),
505
+ lastActivity: new Date().toISOString(),
506
+ escalationLevel: 0,
507
+ stalledSince: null,
508
+ transcriptPath: null,
509
+ },
510
+ ],
511
+ worktrees: [],
512
+ tmuxSessions: [],
513
+ unreadMailCount: 0,
514
+ unreadMailScope: "orchestrator",
515
+ mergeQueueCount: 0,
516
+ recentMetricsCount: 0,
517
+ },
518
+ };
519
+ const out = renderAgentPanel(data, 100, 12, 3);
520
+ expect(out).toContain("x");
521
+ expect(out).toContain("dead-headless");
522
+ });
523
+
524
+ test("renders mixed tmux + headless agents in same frame with correct liveness", () => {
525
+ const data = {
526
+ ...makeDashboardData({}),
527
+ status: {
528
+ currentRunId: null,
529
+ agents: [
530
+ {
531
+ id: "sess-tmux-1",
532
+ agentName: "pane-agent",
533
+ capability: "builder",
534
+ worktreePath: "/tmp/wt/pane-agent",
535
+ branchName: "agentplate/pane-agent/task-t1",
536
+ taskId: "task-t1",
537
+ tmuxSession: "agentplate-pane-agent",
538
+ state: "working" as const,
539
+ pid: 99999,
540
+ parentAgent: null,
541
+ depth: 0,
542
+ runId: null,
543
+ startedAt: new Date(Date.now() - 10_000).toISOString(),
544
+ lastActivity: new Date().toISOString(),
545
+ escalationLevel: 0,
546
+ stalledSince: null,
547
+ transcriptPath: null,
548
+ },
549
+ {
550
+ id: "sess-headless-1",
551
+ agentName: "live-headless",
552
+ capability: "builder",
553
+ worktreePath: "/tmp/wt/live-headless",
554
+ branchName: "agentplate/live-headless/task-h1",
555
+ taskId: "task-h1",
556
+ tmuxSession: "", // headless
557
+ state: "working" as const,
558
+ pid: process.pid, // own PID — guaranteed alive
559
+ parentAgent: null,
560
+ depth: 0,
561
+ runId: null,
562
+ startedAt: new Date(Date.now() - 10_000).toISOString(),
563
+ lastActivity: new Date().toISOString(),
564
+ escalationLevel: 0,
565
+ stalledSince: null,
566
+ transcriptPath: null,
567
+ },
568
+ ],
569
+ worktrees: [],
570
+ tmuxSessions: [{ name: "agentplate-pane-agent", pid: 99998 }],
571
+ unreadMailCount: 0,
572
+ unreadMailScope: "orchestrator",
573
+ mergeQueueCount: 0,
574
+ recentMetricsCount: 0,
575
+ },
576
+ };
577
+ const out = renderAgentPanel(data, 100, 12, 3);
578
+ expect(out).toContain("pane-agent");
579
+ expect(out).toContain("live-headless");
580
+ const aliveMarkers = (out.match(/>/g) ?? []).length;
581
+ expect(aliveMarkers).toBeGreaterThanOrEqual(2);
582
+ expect(out).not.toContain("x");
583
+ });
584
+
585
+ test("spawn-per-turn worker (no tmux, no pid) renders alive when state is non-terminal (agentplate-7a34)", () => {
586
+ // Repro: freshly slung headless lead has tmuxSession='' and pid=null.
587
+ // Previously fell into the tmux path → never matched → red "x" while
588
+ // ap feed showed live tool events from the same agent.
589
+ const data = {
590
+ ...makeDashboardData({}),
591
+ status: {
592
+ currentRunId: null,
593
+ agents: [
594
+ {
595
+ id: "sess-spt-1",
596
+ agentName: "freshly-slung",
597
+ capability: "lead",
598
+ worktreePath: "/tmp/wt/freshly-slung",
599
+ branchName: "agentplate/freshly-slung/task-l1",
600
+ taskId: "task-l1",
601
+ tmuxSession: "", // headless
602
+ state: "working" as const,
603
+ pid: null, // spawn-per-turn: no persistent process between turns
604
+ parentAgent: null,
605
+ depth: 0,
606
+ runId: null,
607
+ startedAt: new Date(Date.now() - 5_000).toISOString(),
608
+ lastActivity: new Date().toISOString(),
609
+ escalationLevel: 0,
610
+ stalledSince: null,
611
+ transcriptPath: null,
612
+ },
613
+ ],
614
+ worktrees: [],
615
+ tmuxSessions: [],
616
+ unreadMailCount: 0,
617
+ unreadMailScope: "orchestrator",
618
+ mergeQueueCount: 0,
619
+ recentMetricsCount: 0,
620
+ },
621
+ };
622
+ const out = renderAgentPanel(data, 100, 12, 3);
623
+ expect(out).toContain("freshly-slung");
624
+ // Green ">" — agent is logically alive between turns
625
+ expect(out).toContain(">");
626
+ // No red marker should be present (name 'freshly-slung' has no 'x')
627
+ expect(out).not.toContain("x");
628
+ });
629
+
630
+ test("spawn-per-turn worker in zombie state renders dead marker (agentplate-7a34)", () => {
631
+ const data = {
632
+ ...makeDashboardData({}),
633
+ status: {
634
+ currentRunId: null,
635
+ agents: [
636
+ {
637
+ id: "sess-spt-2",
638
+ agentName: "abandoned-spt",
639
+ capability: "builder",
640
+ worktreePath: "/tmp/wt/abandoned-spt",
641
+ branchName: "agentplate/abandoned-spt/task-a1",
642
+ taskId: "task-a1",
643
+ tmuxSession: "",
644
+ state: "zombie" as const,
645
+ pid: null,
646
+ parentAgent: null,
647
+ depth: 0,
648
+ runId: null,
649
+ startedAt: new Date(Date.now() - 600_000).toISOString(),
650
+ lastActivity: new Date(Date.now() - 600_000).toISOString(),
651
+ escalationLevel: 0,
652
+ stalledSince: null,
653
+ transcriptPath: null,
654
+ },
655
+ ],
656
+ worktrees: [],
657
+ tmuxSessions: [],
658
+ unreadMailCount: 0,
659
+ unreadMailScope: "orchestrator",
660
+ mergeQueueCount: 0,
661
+ recentMetricsCount: 0,
662
+ },
663
+ };
664
+ const out = renderAgentPanel(data, 100, 12, 3);
665
+ expect(out).toContain("abandoned-spt");
666
+ expect(out).toContain("x");
667
+ });
668
+
669
+ test("headless agent renders dead marker when tmux session list is non-empty", () => {
670
+ const deadPid = 2_147_483_647;
671
+ const data = {
672
+ ...makeDashboardData({}),
673
+ status: {
674
+ currentRunId: null,
675
+ agents: [
676
+ {
677
+ id: "sess-dead-headless-1",
678
+ agentName: "gone-headless",
679
+ capability: "builder",
680
+ worktreePath: "/tmp/wt/gone-headless",
681
+ branchName: "agentplate/gone-headless/task-g1",
682
+ taskId: "task-g1",
683
+ tmuxSession: "", // headless
684
+ state: "working" as const,
685
+ pid: deadPid,
686
+ parentAgent: null,
687
+ depth: 0,
688
+ runId: null,
689
+ startedAt: new Date(Date.now() - 10_000).toISOString(),
690
+ lastActivity: new Date().toISOString(),
691
+ escalationLevel: 0,
692
+ stalledSince: null,
693
+ transcriptPath: null,
694
+ },
695
+ ],
696
+ worktrees: [],
697
+ tmuxSessions: [{ name: "agentplate-other-tmux", pid: 11111 }],
698
+ unreadMailCount: 0,
699
+ unreadMailScope: "orchestrator",
700
+ mergeQueueCount: 0,
701
+ recentMetricsCount: 0,
702
+ },
703
+ };
704
+ const out = renderAgentPanel(data, 100, 12, 3);
705
+ expect(out).toContain("x");
706
+ expect(out).toContain("gone-headless");
707
+ });
708
+ });
709
+
710
+ describe("openDashboardStores", () => {
711
+ let tempDir: string;
712
+
713
+ beforeEach(async () => {
714
+ tempDir = await mkdtemp(join(tmpdir(), "dashboard-stores-test-"));
715
+ });
716
+
717
+ afterEach(async () => {
718
+ await cleanupTempDir(tempDir);
719
+ });
720
+
721
+ test("sessionStore is non-null when .agentplate/ has sessions.db", async () => {
722
+ const agentplateDir = join(tempDir, ".agentplate");
723
+ await mkdir(agentplateDir, { recursive: true });
724
+ const seeder = createSessionStore(join(agentplateDir, "sessions.db"));
725
+ seeder.close();
726
+
727
+ const stores = openDashboardStores(tempDir);
728
+ try {
729
+ expect(stores.sessionStore).not.toBeNull();
730
+ } finally {
731
+ closeDashboardStores(stores);
732
+ }
733
+ });
734
+
735
+ test("mailStore is null when mail.db does not exist", async () => {
736
+ const agentplateDir = join(tempDir, ".agentplate");
737
+ await mkdir(agentplateDir, { recursive: true });
738
+ const seeder = createSessionStore(join(agentplateDir, "sessions.db"));
739
+ seeder.close();
740
+
741
+ const stores = openDashboardStores(tempDir);
742
+ try {
743
+ expect(stores.mailStore).toBeNull();
744
+ } finally {
745
+ closeDashboardStores(stores);
746
+ }
747
+ });
748
+
749
+ test("mergeQueue is null when merge-queue.db does not exist", async () => {
750
+ const agentplateDir = join(tempDir, ".agentplate");
751
+ await mkdir(agentplateDir, { recursive: true });
752
+ const seeder = createSessionStore(join(agentplateDir, "sessions.db"));
753
+ seeder.close();
754
+
755
+ const stores = openDashboardStores(tempDir);
756
+ try {
757
+ expect(stores.mergeQueue).toBeNull();
758
+ } finally {
759
+ closeDashboardStores(stores);
760
+ }
761
+ });
762
+
763
+ test("metricsStore is null when metrics.db does not exist", async () => {
764
+ const agentplateDir = join(tempDir, ".agentplate");
765
+ await mkdir(agentplateDir, { recursive: true });
766
+ const seeder = createSessionStore(join(agentplateDir, "sessions.db"));
767
+ seeder.close();
768
+
769
+ const stores = openDashboardStores(tempDir);
770
+ try {
771
+ expect(stores.metricsStore).toBeNull();
772
+ } finally {
773
+ closeDashboardStores(stores);
774
+ }
775
+ });
776
+
777
+ test("eventStore is null when events.db does not exist", async () => {
778
+ const agentplateDir = join(tempDir, ".agentplate");
779
+ await mkdir(agentplateDir, { recursive: true });
780
+ const seeder = createSessionStore(join(agentplateDir, "sessions.db"));
781
+ seeder.close();
782
+
783
+ const stores = openDashboardStores(tempDir);
784
+ try {
785
+ expect(stores.eventStore).toBeNull();
786
+ } finally {
787
+ closeDashboardStores(stores);
788
+ }
789
+ });
790
+
791
+ test("eventStore is non-null when events.db exists", async () => {
792
+ const agentplateDir = join(tempDir, ".agentplate");
793
+ await mkdir(agentplateDir, { recursive: true });
794
+ const seeder = createSessionStore(join(agentplateDir, "sessions.db"));
795
+ seeder.close();
796
+
797
+ // Create events.db via createEventStore
798
+ const eventsDb = createEventStore(join(agentplateDir, "events.db"));
799
+ eventsDb.close();
800
+
801
+ const stores = openDashboardStores(tempDir);
802
+ try {
803
+ expect(stores.eventStore).not.toBeNull();
804
+ } finally {
805
+ closeDashboardStores(stores);
806
+ }
807
+ });
808
+ });
809
+
810
+ describe("closeDashboardStores", () => {
811
+ let tempDir: string;
812
+
813
+ beforeEach(async () => {
814
+ tempDir = await mkdtemp(join(tmpdir(), "dashboard-close-test-"));
815
+ });
816
+
817
+ afterEach(async () => {
818
+ await cleanupTempDir(tempDir);
819
+ });
820
+
821
+ test("closing stores does not throw", async () => {
822
+ const agentplateDir = join(tempDir, ".agentplate");
823
+ await mkdir(agentplateDir, { recursive: true });
824
+ const seeder = createSessionStore(join(agentplateDir, "sessions.db"));
825
+ seeder.close();
826
+
827
+ const stores = openDashboardStores(tempDir);
828
+ expect(() => closeDashboardStores(stores)).not.toThrow();
829
+ });
830
+
831
+ test("closing already-closed stores does not throw (best-effort)", async () => {
832
+ const agentplateDir = join(tempDir, ".agentplate");
833
+ await mkdir(agentplateDir, { recursive: true });
834
+ const seeder = createSessionStore(join(agentplateDir, "sessions.db"));
835
+ seeder.close();
836
+
837
+ const stores = openDashboardStores(tempDir);
838
+ closeDashboardStores(stores);
839
+ // Second close should not throw due to best-effort try/catch
840
+ expect(() => closeDashboardStores(stores)).not.toThrow();
841
+ });
842
+
843
+ test("closing stores with eventStore does not throw", async () => {
844
+ const agentplateDir = join(tempDir, ".agentplate");
845
+ await mkdir(agentplateDir, { recursive: true });
846
+ const seeder = createSessionStore(join(agentplateDir, "sessions.db"));
847
+ seeder.close();
848
+ const eventsDb = createEventStore(join(agentplateDir, "events.db"));
849
+ eventsDb.close();
850
+
851
+ const stores = openDashboardStores(tempDir);
852
+ expect(() => closeDashboardStores(stores)).not.toThrow();
853
+ });
854
+ });
855
+
856
+ describe("EventBuffer", () => {
857
+ let tempDir: string;
858
+
859
+ beforeEach(async () => {
860
+ tempDir = await mkdtemp(join(tmpdir(), "event-buffer-test-"));
861
+ });
862
+
863
+ afterEach(async () => {
864
+ await cleanupTempDir(tempDir);
865
+ });
866
+
867
+ function makeEvent(agentName: string) {
868
+ return {
869
+ agentName,
870
+ eventType: "tool_end" as const,
871
+ level: "info" as const,
872
+ runId: null,
873
+ sessionId: null,
874
+ toolName: null,
875
+ toolArgs: null,
876
+ toolDurationMs: null,
877
+ data: null,
878
+ };
879
+ }
880
+
881
+ test("starts empty", () => {
882
+ const buf = new EventBuffer();
883
+ expect(buf.size).toBe(0);
884
+ expect(buf.getEvents()).toEqual([]);
885
+ });
886
+
887
+ test("poll adds events from event store", async () => {
888
+ const agentplateDir = join(tempDir, ".agentplate");
889
+ await mkdir(agentplateDir, { recursive: true });
890
+ const store = createEventStore(join(agentplateDir, "events.db"));
891
+ store.insert(makeEvent("agent-a"));
892
+
893
+ const buf = new EventBuffer();
894
+ buf.poll(store);
895
+ expect(buf.size).toBe(1);
896
+ store.close();
897
+ });
898
+
899
+ test("deduplicates by lastSeenId (double poll returns same count)", async () => {
900
+ const agentplateDir = join(tempDir, ".agentplate");
901
+ await mkdir(agentplateDir, { recursive: true });
902
+ const store = createEventStore(join(agentplateDir, "events.db"));
903
+ store.insert(makeEvent("agent-a"));
904
+
905
+ const buf = new EventBuffer();
906
+ buf.poll(store);
907
+ buf.poll(store); // second poll should not duplicate
908
+ expect(buf.size).toBe(1);
909
+ store.close();
910
+ });
911
+
912
+ test("trims to maxSize keeping most recent events", async () => {
913
+ const agentplateDir = join(tempDir, ".agentplate");
914
+ await mkdir(agentplateDir, { recursive: true });
915
+ const store = createEventStore(join(agentplateDir, "events.db"));
916
+ for (let i = 0; i < 5; i++) {
917
+ store.insert(makeEvent(`agent-${i}`));
918
+ }
919
+
920
+ const buf = new EventBuffer(3);
921
+ buf.poll(store);
922
+ expect(buf.size).toBe(3);
923
+ store.close();
924
+ });
925
+
926
+ test("builds color map across polls", async () => {
927
+ const agentplateDir = join(tempDir, ".agentplate");
928
+ await mkdir(agentplateDir, { recursive: true });
929
+ const store = createEventStore(join(agentplateDir, "events.db"));
930
+ store.insert(makeEvent("agent-x"));
931
+
932
+ const buf = new EventBuffer();
933
+ buf.poll(store);
934
+ expect(buf.getColorMap().has("agent-x")).toBe(true);
935
+
936
+ store.insert(makeEvent("agent-y"));
937
+ buf.poll(store);
938
+ expect(buf.getColorMap().has("agent-x")).toBe(true);
939
+ expect(buf.getColorMap().has("agent-y")).toBe(true);
940
+ store.close();
941
+ });
942
+ });
943
+
944
+ // Type check: DashboardStores includes eventStore
945
+ test("DashboardStores type includes eventStore field", () => {
946
+ const stores: DashboardStores = {
947
+ sessionStore: null as never,
948
+ mailStore: null,
949
+ mergeQueue: null,
950
+ metricsStore: null,
951
+ eventStore: null,
952
+ };
953
+ expect(stores.eventStore).toBeNull();
954
+ });