@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,719 @@
1
+ /**
2
+ * Tests for the NDJSON event tailer (src/events/tailer.ts).
3
+ *
4
+ * Uses real filesystem (temp directories via mkdtemp) and real EventStore
5
+ * (bun:sqlite in-memory or temp file) per the project's "never mock what you
6
+ * can use for real" philosophy.
7
+ *
8
+ * The tailer uses setTimeout-based polling, which is exercised by letting
9
+ * timers fire naturally in async tests rather than using fake timers.
10
+ */
11
+
12
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
13
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { createSessionStore, type SessionStore } from "../sessions/store.ts";
17
+ import { cleanupTempDir } from "../test-helpers.ts";
18
+ import type { AgentSession, EventStore } from "../types.ts";
19
+ import { createEventStore } from "./store.ts";
20
+ import type { TailerHandle, TailerOptions } from "./tailer.ts";
21
+ import { findLatestStdoutLog, startEventTailer } from "./tailer.ts";
22
+
23
+ // === Helpers ===
24
+
25
+ /** Create a temp directory to use as a fake .agentplate/ root. */
26
+ async function createTempDir(): Promise<string> {
27
+ return mkdtemp(join(tmpdir(), "agentplate-tailer-test-"));
28
+ }
29
+
30
+ /**
31
+ * Create a fake agent log directory structure:
32
+ * <agentplateDir>/logs/<agentName>/<timestamp>/stdout.log
33
+ * Returns the path to stdout.log.
34
+ */
35
+ async function createAgentLogDir(
36
+ agentplateDir: string,
37
+ agentName: string,
38
+ timestamp = "2026-03-05T14-52-26-089Z",
39
+ ): Promise<string> {
40
+ const logDir = join(agentplateDir, "logs", agentName, timestamp);
41
+ await mkdir(logDir, { recursive: true });
42
+ const logPath = join(logDir, "stdout.log");
43
+ // Create an empty file so Bun.file().exists() returns true.
44
+ await writeFile(logPath, "");
45
+ return logPath;
46
+ }
47
+
48
+ /** Wait at most maxMs for a condition to become true, polling every pollMs. */
49
+ async function waitFor(
50
+ condition: () => boolean | Promise<boolean>,
51
+ maxMs = 3000,
52
+ pollMs = 50,
53
+ ): Promise<void> {
54
+ const deadline = Date.now() + maxMs;
55
+ while (Date.now() < deadline) {
56
+ if (await condition()) return;
57
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
58
+ }
59
+ throw new Error(`waitFor timed out after ${maxMs}ms`);
60
+ }
61
+
62
+ // === Tests ===
63
+
64
+ describe("findLatestStdoutLog", () => {
65
+ let tmpDir: string;
66
+
67
+ beforeEach(async () => {
68
+ tmpDir = await createTempDir();
69
+ });
70
+
71
+ afterEach(async () => {
72
+ await cleanupTempDir(tmpDir);
73
+ });
74
+
75
+ test("returns null when agent log directory does not exist", async () => {
76
+ const result = await findLatestStdoutLog(tmpDir, "no-such-agent");
77
+ expect(result).toBeNull();
78
+ });
79
+
80
+ test("returns null when agent log directory is empty", async () => {
81
+ const agentLogsDir = join(tmpDir, "logs", "my-agent");
82
+ await mkdir(agentLogsDir, { recursive: true });
83
+ const result = await findLatestStdoutLog(tmpDir, "my-agent");
84
+ expect(result).toBeNull();
85
+ });
86
+
87
+ test("returns null when latest dir has no stdout.log", async () => {
88
+ const logDir = join(tmpDir, "logs", "my-agent", "2026-03-05T14-52-26-089Z");
89
+ await mkdir(logDir, { recursive: true });
90
+ // Directory exists but no stdout.log inside.
91
+ const result = await findLatestStdoutLog(tmpDir, "my-agent");
92
+ expect(result).toBeNull();
93
+ });
94
+
95
+ test("returns path to stdout.log in the only session directory", async () => {
96
+ const logPath = await createAgentLogDir(tmpDir, "my-agent");
97
+ const result = await findLatestStdoutLog(tmpDir, "my-agent");
98
+ expect(result).toBe(logPath);
99
+ });
100
+
101
+ test("returns the lexicographically latest session directory", async () => {
102
+ // Create three session dirs — the last in sorted order should win.
103
+ await createAgentLogDir(tmpDir, "my-agent", "2026-03-04T10-00-00-000Z");
104
+ await createAgentLogDir(tmpDir, "my-agent", "2026-03-05T08-00-00-000Z");
105
+ const latest = await createAgentLogDir(tmpDir, "my-agent", "2026-03-05T14-52-26-089Z");
106
+ const result = await findLatestStdoutLog(tmpDir, "my-agent");
107
+ expect(result).toBe(latest);
108
+ });
109
+ });
110
+
111
+ describe("startEventTailer", () => {
112
+ let tmpDir: string;
113
+ let eventStore: EventStore;
114
+ let eventsDbPath: string;
115
+
116
+ beforeEach(async () => {
117
+ tmpDir = await createTempDir();
118
+ eventsDbPath = join(tmpDir, "events.db");
119
+ eventStore = createEventStore(eventsDbPath);
120
+ });
121
+
122
+ afterEach(async () => {
123
+ eventStore.close();
124
+ await cleanupTempDir(tmpDir);
125
+ });
126
+
127
+ test("stop() is idempotent — calling twice does not throw", async () => {
128
+ const logPath = await createAgentLogDir(tmpDir, "agent-a");
129
+ const handle = startEventTailer({
130
+ stdoutLogPath: logPath,
131
+ agentName: "agent-a",
132
+ runId: null,
133
+ eventsDbPath,
134
+ _eventStore: eventStore,
135
+ });
136
+ handle.stop();
137
+ handle.stop(); // Should not throw.
138
+ });
139
+
140
+ test("handle exposes agentName and logPath", async () => {
141
+ const logPath = await createAgentLogDir(tmpDir, "agent-b");
142
+ const handle = startEventTailer({
143
+ stdoutLogPath: logPath,
144
+ agentName: "agent-b",
145
+ runId: null,
146
+ eventsDbPath,
147
+ _eventStore: eventStore,
148
+ });
149
+ try {
150
+ expect(handle.agentName).toBe("agent-b");
151
+ expect(handle.logPath).toBe(logPath);
152
+ } finally {
153
+ handle.stop();
154
+ }
155
+ });
156
+
157
+ test("parses NDJSON lines and writes events to EventStore", async () => {
158
+ const logPath = await createAgentLogDir(tmpDir, "agent-c");
159
+
160
+ // Write some NDJSON events to the log file.
161
+ const lines = `${[
162
+ JSON.stringify({ type: "turn_start", timestamp: new Date().toISOString() }),
163
+ JSON.stringify({
164
+ type: "tool_start",
165
+ tool: "Read",
166
+ timestamp: new Date().toISOString(),
167
+ }),
168
+ JSON.stringify({
169
+ type: "tool_end",
170
+ tool: "Read",
171
+ duration_ms: 42,
172
+ timestamp: new Date().toISOString(),
173
+ }),
174
+ JSON.stringify({ type: "turn_end", timestamp: new Date().toISOString() }),
175
+ ].join("\n")}\n`;
176
+
177
+ await writeFile(logPath, lines);
178
+
179
+ const handle = startEventTailer({
180
+ stdoutLogPath: logPath,
181
+ agentName: "agent-c",
182
+ runId: "run-1",
183
+ eventsDbPath,
184
+ pollIntervalMs: 50,
185
+ _eventStore: eventStore,
186
+ });
187
+
188
+ try {
189
+ // Wait until all 4 events appear in the store.
190
+ await waitFor(() => {
191
+ const events = eventStore.getByAgent("agent-c");
192
+ return events.length >= 4;
193
+ });
194
+
195
+ const events = eventStore.getByAgent("agent-c");
196
+ const types = events.map((e) => e.eventType);
197
+ expect(types).toContain("turn_start");
198
+ expect(types).toContain("tool_start");
199
+ expect(types).toContain("tool_end");
200
+ expect(types).toContain("turn_end");
201
+
202
+ // Verify tool_end carries duration_ms.
203
+ const toolEnd = events.find((e) => e.eventType === "tool_end");
204
+ expect(toolEnd?.toolDurationMs).toBe(42);
205
+
206
+ // Verify tool_start carries toolName.
207
+ const toolStart = events.find((e) => e.eventType === "tool_start");
208
+ expect(toolStart?.toolName).toBe("Read");
209
+
210
+ // Verify runId propagation.
211
+ for (const event of events) {
212
+ expect(event.runId).toBe("run-1");
213
+ expect(event.agentName).toBe("agent-c");
214
+ }
215
+ } finally {
216
+ handle.stop();
217
+ }
218
+ });
219
+
220
+ test("tails new content appended after tailer starts", async () => {
221
+ const logPath = await createAgentLogDir(tmpDir, "agent-d");
222
+
223
+ // Start with an empty file.
224
+ const handle = startEventTailer({
225
+ stdoutLogPath: logPath,
226
+ agentName: "agent-d",
227
+ runId: null,
228
+ eventsDbPath,
229
+ pollIntervalMs: 50,
230
+ _eventStore: eventStore,
231
+ });
232
+
233
+ try {
234
+ // Append a first event.
235
+ const event1 = `${JSON.stringify({ type: "turn_start", timestamp: new Date().toISOString() })}\n`;
236
+ await writeFile(logPath, event1);
237
+
238
+ await waitFor(() => eventStore.getByAgent("agent-d").length >= 1);
239
+ expect(eventStore.getByAgent("agent-d")).toHaveLength(1);
240
+
241
+ // Append a second event to the same file (simulate ongoing output).
242
+ const event2 = `${JSON.stringify({ type: "turn_end", timestamp: new Date().toISOString() })}\n`;
243
+ // BunFile.size updates on disk; we must append to get new bytes.
244
+ const existing = await Bun.file(logPath).text();
245
+ await writeFile(logPath, existing + event2);
246
+
247
+ await waitFor(() => eventStore.getByAgent("agent-d").length >= 2);
248
+ expect(eventStore.getByAgent("agent-d")).toHaveLength(2);
249
+ } finally {
250
+ handle.stop();
251
+ }
252
+ });
253
+
254
+ test("silently skips malformed (non-JSON) lines", async () => {
255
+ const logPath = await createAgentLogDir(tmpDir, "agent-e");
256
+
257
+ const content = `${[
258
+ "not json at all",
259
+ JSON.stringify({ type: "result", timestamp: new Date().toISOString() }),
260
+ "{incomplete",
261
+ ].join("\n")}\n`;
262
+
263
+ await writeFile(logPath, content);
264
+
265
+ const handle = startEventTailer({
266
+ stdoutLogPath: logPath,
267
+ agentName: "agent-e",
268
+ runId: null,
269
+ eventsDbPath,
270
+ pollIntervalMs: 50,
271
+ _eventStore: eventStore,
272
+ });
273
+
274
+ try {
275
+ // Only the valid JSON line should appear.
276
+ await waitFor(() => eventStore.getByAgent("agent-e").length >= 1);
277
+ const events = eventStore.getByAgent("agent-e");
278
+ expect(events).toHaveLength(1);
279
+ expect(events[0]?.eventType).toBe("result");
280
+ } finally {
281
+ handle.stop();
282
+ }
283
+ });
284
+
285
+ test("maps error events to error level", async () => {
286
+ const logPath = await createAgentLogDir(tmpDir, "agent-f");
287
+
288
+ const content = `${JSON.stringify({ type: "error", message: "boom", timestamp: new Date().toISOString() })}\n`;
289
+ await writeFile(logPath, content);
290
+
291
+ const handle = startEventTailer({
292
+ stdoutLogPath: logPath,
293
+ agentName: "agent-f",
294
+ runId: null,
295
+ eventsDbPath,
296
+ pollIntervalMs: 50,
297
+ _eventStore: eventStore,
298
+ });
299
+
300
+ try {
301
+ await waitFor(() => eventStore.getByAgent("agent-f").length >= 1);
302
+ const events = eventStore.getByAgent("agent-f");
303
+ expect(events[0]?.level).toBe("error");
304
+ expect(events[0]?.eventType).toBe("error");
305
+ } finally {
306
+ handle.stop();
307
+ }
308
+ });
309
+
310
+ test("unknown event types map to 'custom'", async () => {
311
+ const logPath = await createAgentLogDir(tmpDir, "agent-g");
312
+
313
+ const content = `${JSON.stringify({ type: "some_future_type", timestamp: new Date().toISOString() })}\n`;
314
+ await writeFile(logPath, content);
315
+
316
+ const handle = startEventTailer({
317
+ stdoutLogPath: logPath,
318
+ agentName: "agent-g",
319
+ runId: null,
320
+ eventsDbPath,
321
+ pollIntervalMs: 50,
322
+ _eventStore: eventStore,
323
+ });
324
+
325
+ try {
326
+ await waitFor(() => eventStore.getByAgent("agent-g").length >= 1);
327
+ const events = eventStore.getByAgent("agent-g");
328
+ expect(events[0]?.eventType).toBe("custom");
329
+ } finally {
330
+ handle.stop();
331
+ }
332
+ });
333
+
334
+ test("returns no-op handle immediately when EventStore cannot be created", async () => {
335
+ const logPath = await createAgentLogDir(tmpDir, "agent-noop");
336
+
337
+ // Point to a path whose parent directory does not exist — createEventStore throws.
338
+ const badDbPath = join(tmpDir, "nonexistent-subdir", "events.db");
339
+
340
+ // Do NOT inject _eventStore so the real createEventStore path is exercised.
341
+ const handle = startEventTailer({
342
+ stdoutLogPath: logPath,
343
+ agentName: "agent-noop",
344
+ runId: null,
345
+ eventsDbPath: badDbPath,
346
+ pollIntervalMs: 50,
347
+ });
348
+
349
+ // No polling should have started — wait a couple intervals to confirm.
350
+ await new Promise((resolve) => setTimeout(resolve, 150));
351
+
352
+ // stop() on a no-op handle must not throw.
353
+ expect(() => handle.stop()).not.toThrow();
354
+ handle.stop(); // idempotent
355
+ expect(handle.agentName).toBe("agent-noop");
356
+ expect(handle.logPath).toBe(logPath);
357
+ });
358
+
359
+ test("does not crash when log file does not exist yet", async () => {
360
+ // Non-existent log path — tailer should silently poll without errors.
361
+ const logPath = join(tmpDir, "logs", "agent-h", "2026-03-05T00-00-00-000Z", "stdout.log");
362
+
363
+ const handle = startEventTailer({
364
+ stdoutLogPath: logPath,
365
+ agentName: "agent-h",
366
+ runId: null,
367
+ eventsDbPath,
368
+ pollIntervalMs: 50,
369
+ _eventStore: eventStore,
370
+ });
371
+
372
+ // Wait a couple poll cycles to ensure no crash.
373
+ await new Promise((resolve) => setTimeout(resolve, 150));
374
+ handle.stop();
375
+
376
+ // No events should have been written.
377
+ expect(eventStore.getByAgent("agent-h")).toHaveLength(0);
378
+ });
379
+ });
380
+
381
+ describe("daemon tailer integration", () => {
382
+ /**
383
+ * Verify that the daemon wires tailer start/stop correctly using DI.
384
+ * This test exercises the daemon's tailer management logic with injected
385
+ * mocks rather than actual polling — the tailer behaviour itself is tested
386
+ * above in the startEventTailer suite.
387
+ */
388
+ test("daemon starts a tailer for headless sessions and stops it when completed", async () => {
389
+ const tmpDir = await createTempDir();
390
+ const agentplateDir = join(tmpDir, ".agentplate");
391
+ await mkdir(agentplateDir, { recursive: true });
392
+
393
+ // Create a minimal log structure so findLatestStdoutLog succeeds.
394
+ const agentName = "headless-agent";
395
+ const logPath = await createAgentLogDir(agentplateDir, agentName);
396
+
397
+ // Use a registry we control.
398
+ const registry = new Map<string, { agentName: string; logPath: string; stop: () => void }>();
399
+ const stopped: string[] = [];
400
+
401
+ const tailerFactory = (opts: {
402
+ stdoutLogPath: string;
403
+ agentName: string;
404
+ runId: string | null;
405
+ eventsDbPath: string;
406
+ }) => {
407
+ const handle = {
408
+ agentName: opts.agentName,
409
+ logPath: opts.stdoutLogPath,
410
+ stop: () => {
411
+ stopped.push(opts.agentName);
412
+ registry.delete(opts.agentName);
413
+ },
414
+ };
415
+ return handle;
416
+ };
417
+
418
+ // Write a headless session.
419
+ const { createSessionStore } = await import("../sessions/store.ts");
420
+ const sessionStore = createSessionStore(join(agentplateDir, "sessions.db"));
421
+ sessionStore.upsert({
422
+ id: "sess-1",
423
+ agentName,
424
+ capability: "builder",
425
+ worktreePath: tmpDir,
426
+ branchName: "test-branch",
427
+ taskId: "task-1",
428
+ tmuxSession: "", // headless
429
+ state: "working",
430
+ pid: process.pid,
431
+ parentAgent: null,
432
+ depth: 0,
433
+ runId: null,
434
+ startedAt: new Date().toISOString(),
435
+ lastActivity: new Date().toISOString(),
436
+ escalationLevel: 0,
437
+ stalledSince: null,
438
+ transcriptPath: null,
439
+ });
440
+ sessionStore.close();
441
+
442
+ const { runDaemonTick } = await import("../watchdog/daemon.ts");
443
+
444
+ // First tick: should start a tailer for the headless session.
445
+ await runDaemonTick({
446
+ root: tmpDir,
447
+ staleThresholdMs: 300_000,
448
+ zombieThresholdMs: 600_000,
449
+ _tmux: { isSessionAlive: async () => false, killSession: async () => {} },
450
+ _triage: async () => "extend",
451
+ _nudge: async () => ({ delivered: false }),
452
+ _eventStore: null,
453
+ _recordFailure: async () => {},
454
+ _tailerRegistry: registry as unknown as Map<string, TailerHandle>,
455
+ _tailerFactory: tailerFactory as unknown as (opts: TailerOptions) => TailerHandle,
456
+ _findLatestStdoutLog: async () => logPath,
457
+ });
458
+
459
+ expect(registry.has(agentName)).toBe(true);
460
+ expect(stopped).toHaveLength(0);
461
+
462
+ // Mark session as completed.
463
+ const store2 = createSessionStore(join(agentplateDir, "sessions.db"));
464
+ store2.updateState(agentName, "completed");
465
+ store2.close();
466
+
467
+ // Second tick: completed session is skipped, tailer should be stopped.
468
+ await runDaemonTick({
469
+ root: tmpDir,
470
+ staleThresholdMs: 300_000,
471
+ zombieThresholdMs: 600_000,
472
+ _tmux: { isSessionAlive: async () => false, killSession: async () => {} },
473
+ _triage: async () => "extend",
474
+ _nudge: async () => ({ delivered: false }),
475
+ _eventStore: null,
476
+ _recordFailure: async () => {},
477
+ _tailerRegistry: registry as unknown as Map<string, TailerHandle>,
478
+ _tailerFactory: tailerFactory as unknown as (opts: TailerOptions) => TailerHandle,
479
+ _findLatestStdoutLog: async () => logPath,
480
+ });
481
+
482
+ expect(stopped).toContain(agentName);
483
+ expect(registry.has(agentName)).toBe(false);
484
+
485
+ await cleanupTempDir(tmpDir);
486
+ });
487
+ });
488
+
489
+ // === session_id capture (agentplate-7b8c Phase 1) ===
490
+
491
+ describe("startEventTailer session_id capture", () => {
492
+ let tmpDir: string;
493
+ let eventStore: EventStore;
494
+ let eventsDbPath: string;
495
+ let sessionStore: SessionStore;
496
+ let sessionsDbPath: string;
497
+
498
+ function makeSession(agentName: string): AgentSession {
499
+ const now = new Date().toISOString();
500
+ return {
501
+ id: `id-${agentName}`,
502
+ agentName,
503
+ capability: "builder",
504
+ worktreePath: "/tmp/wt",
505
+ branchName: "test-branch",
506
+ taskId: "task-1",
507
+ tmuxSession: "",
508
+ state: "working",
509
+ pid: 12345,
510
+ parentAgent: null,
511
+ depth: 0,
512
+ runId: null,
513
+ startedAt: now,
514
+ lastActivity: now,
515
+ escalationLevel: 0,
516
+ stalledSince: null,
517
+ transcriptPath: null,
518
+ };
519
+ }
520
+
521
+ beforeEach(async () => {
522
+ tmpDir = await createTempDir();
523
+ eventsDbPath = join(tmpDir, "events.db");
524
+ eventStore = createEventStore(eventsDbPath);
525
+ sessionsDbPath = join(tmpDir, "sessions.db");
526
+ sessionStore = createSessionStore(sessionsDbPath);
527
+ });
528
+
529
+ afterEach(async () => {
530
+ eventStore.close();
531
+ sessionStore.close();
532
+ await cleanupTempDir(tmpDir);
533
+ });
534
+
535
+ test("parses system event session_id and calls updateClaudeSessionId once", async () => {
536
+ const agentName = "agent-sid-1";
537
+ sessionStore.upsert(makeSession(agentName));
538
+ const logPath = await createAgentLogDir(tmpDir, agentName);
539
+
540
+ const sysLine = JSON.stringify({
541
+ type: "system",
542
+ subtype: "init",
543
+ session_id: "sess-first-pin",
544
+ timestamp: new Date().toISOString(),
545
+ });
546
+ await writeFile(logPath, `${sysLine}\n`);
547
+
548
+ const handle = startEventTailer({
549
+ stdoutLogPath: logPath,
550
+ agentName,
551
+ runId: null,
552
+ eventsDbPath,
553
+ pollIntervalMs: 50,
554
+ _eventStore: eventStore,
555
+ _sessionStore: sessionStore,
556
+ });
557
+
558
+ try {
559
+ await waitFor(() => sessionStore.getByName(agentName)?.claudeSessionId === "sess-first-pin");
560
+ expect(sessionStore.getByName(agentName)?.claudeSessionId).toBe("sess-first-pin");
561
+ } finally {
562
+ handle.stop();
563
+ }
564
+ });
565
+
566
+ test("ignores subsequent system events with the same session_id (single-fire)", async () => {
567
+ const agentName = "agent-sid-2";
568
+ sessionStore.upsert(makeSession(agentName));
569
+ const logPath = await createAgentLogDir(tmpDir, agentName);
570
+
571
+ // Three system events all carrying the same session_id.
572
+ const lines = [
573
+ JSON.stringify({
574
+ type: "system",
575
+ subtype: "init",
576
+ session_id: "sess-stable",
577
+ timestamp: new Date().toISOString(),
578
+ }),
579
+ JSON.stringify({
580
+ type: "system",
581
+ subtype: "ping",
582
+ session_id: "sess-stable",
583
+ timestamp: new Date().toISOString(),
584
+ }),
585
+ JSON.stringify({
586
+ type: "system",
587
+ subtype: "ping",
588
+ session_id: "sess-stable",
589
+ timestamp: new Date().toISOString(),
590
+ }),
591
+ ].join("\n");
592
+ await writeFile(logPath, `${lines}\n`);
593
+
594
+ // Wrap the SessionStore so we can count update calls without altering behaviour.
595
+ let updateCalls = 0;
596
+ const proxy: SessionStore = {
597
+ ...sessionStore,
598
+ upsert: (s) => sessionStore.upsert(s),
599
+ getByName: (n) => sessionStore.getByName(n),
600
+ getActive: () => sessionStore.getActive(),
601
+ getAll: () => sessionStore.getAll(),
602
+ count: () => sessionStore.count(),
603
+ getByRun: (r) => sessionStore.getByRun(r),
604
+ updateState: (n, s) => sessionStore.updateState(n, s),
605
+ updateLastActivity: (n) => sessionStore.updateLastActivity(n),
606
+ updateEscalation: (n, l, s) => sessionStore.updateEscalation(n, l, s),
607
+ updateTranscriptPath: (n, p) => sessionStore.updateTranscriptPath(n, p),
608
+ updateClaudeSessionId: (n, s) => {
609
+ updateCalls++;
610
+ sessionStore.updateClaudeSessionId(n, s);
611
+ },
612
+ remove: (n) => sessionStore.remove(n),
613
+ purge: (o) => sessionStore.purge(o),
614
+ close: () => {
615
+ /* owned by outer test */
616
+ },
617
+ };
618
+
619
+ const handle = startEventTailer({
620
+ stdoutLogPath: logPath,
621
+ agentName,
622
+ runId: null,
623
+ eventsDbPath,
624
+ pollIntervalMs: 50,
625
+ _eventStore: eventStore,
626
+ _sessionStore: proxy,
627
+ });
628
+
629
+ try {
630
+ // Wait until events.db has all three lines processed.
631
+ await waitFor(() => eventStore.getByAgent(agentName).length >= 3);
632
+ // Allow extra poll cycles to confirm no late updates sneak in.
633
+ await new Promise((resolve) => setTimeout(resolve, 150));
634
+ expect(updateCalls).toBe(1);
635
+ expect(sessionStore.getByName(agentName)?.claudeSessionId).toBe("sess-stable");
636
+ } finally {
637
+ handle.stop();
638
+ }
639
+ });
640
+
641
+ test("detects resume mismatch and invokes _onResumeMismatch DI hook (observed wins)", async () => {
642
+ const agentName = "agent-sid-3";
643
+ const session = makeSession(agentName);
644
+ session.claudeSessionId = "sess-requested-OLD";
645
+ sessionStore.upsert(session);
646
+ const logPath = await createAgentLogDir(tmpDir, agentName);
647
+
648
+ const sysLine = JSON.stringify({
649
+ type: "system",
650
+ subtype: "init",
651
+ session_id: "sess-observed-NEW",
652
+ timestamp: new Date().toISOString(),
653
+ });
654
+ await writeFile(logPath, `${sysLine}\n`);
655
+
656
+ const mismatches: Array<{ agent: string; requested: string; observed: string }> = [];
657
+ const handle = startEventTailer({
658
+ stdoutLogPath: logPath,
659
+ agentName,
660
+ runId: null,
661
+ eventsDbPath,
662
+ pollIntervalMs: 50,
663
+ _eventStore: eventStore,
664
+ _sessionStore: sessionStore,
665
+ _onResumeMismatch: (agent, requested, observed) =>
666
+ mismatches.push({ agent, requested, observed }),
667
+ });
668
+
669
+ try {
670
+ await waitFor(
671
+ () => sessionStore.getByName(agentName)?.claudeSessionId === "sess-observed-NEW",
672
+ );
673
+ expect(mismatches).toHaveLength(1);
674
+ expect(mismatches[0]).toEqual({
675
+ agent: agentName,
676
+ requested: "sess-requested-OLD",
677
+ observed: "sess-observed-NEW",
678
+ });
679
+ // observed wins — SessionStore is overwritten with the new id.
680
+ expect(sessionStore.getByName(agentName)?.claudeSessionId).toBe("sess-observed-NEW");
681
+ } finally {
682
+ handle.stop();
683
+ }
684
+ });
685
+
686
+ test("backward compat: tailer with no sessionsDbPath performs no SessionStore writes", async () => {
687
+ const agentName = "agent-sid-4";
688
+ sessionStore.upsert(makeSession(agentName));
689
+ const logPath = await createAgentLogDir(tmpDir, agentName);
690
+
691
+ const sysLine = JSON.stringify({
692
+ type: "system",
693
+ subtype: "init",
694
+ session_id: "sess-should-not-pin",
695
+ timestamp: new Date().toISOString(),
696
+ });
697
+ await writeFile(logPath, `${sysLine}\n`);
698
+
699
+ // No sessionsDbPath, no _sessionStore — tailer must still process events.
700
+ const handle = startEventTailer({
701
+ stdoutLogPath: logPath,
702
+ agentName,
703
+ runId: null,
704
+ eventsDbPath,
705
+ pollIntervalMs: 50,
706
+ _eventStore: eventStore,
707
+ });
708
+
709
+ try {
710
+ await waitFor(() => eventStore.getByAgent(agentName).length >= 1);
711
+ // Give the tailer extra time to confirm no late writes occur.
712
+ await new Promise((resolve) => setTimeout(resolve, 150));
713
+ // SessionStore must remain untouched.
714
+ expect(sessionStore.getByName(agentName)?.claudeSessionId ?? null).toBeNull();
715
+ } finally {
716
+ handle.stop();
717
+ }
718
+ });
719
+ });