@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,958 @@
1
+ /**
2
+ * CLI command: ap log <event> --agent <name> [--stdin]
3
+ *
4
+ * Called by Pre/PostToolUse and Stop hooks.
5
+ * Events: tool-start, tool-end, session-end.
6
+ * Writes to .agentplate/logs/{agent-name}/{session-timestamp}/.
7
+ *
8
+ * When --stdin is passed, reads one line of JSON from stdin containing the full
9
+ * hook payload (tool_name, tool_input, transcript_path, session_id, etc.)
10
+ * and writes structured events to the EventStore for observability.
11
+ */
12
+
13
+ import { join } from "node:path";
14
+ import { Command } from "commander";
15
+ import { isStopHookPersistentCapability } from "../agents/capabilities.ts";
16
+ import { updateIdentity } from "../agents/identity.ts";
17
+ import { loadConfig } from "../config.ts";
18
+ import { ValidationError } from "../errors.ts";
19
+ import { createEventStore } from "../events/store.ts";
20
+ import { filterToolArgs } from "../events/tool-filter.ts";
21
+ import { analyzeSessionInsights } from "../insights/analyzer.ts";
22
+ import { hasWorkToVerify, runQualityGates } from "../insights/quality-gates.ts";
23
+ import { createLoamClient, type LoamClient } from "../loam/client.ts";
24
+ import { createLogger } from "../logging/logger.ts";
25
+ import { createMailClient } from "../mail/client.ts";
26
+ import { createMailStore } from "../mail/store.ts";
27
+ import { estimateCost } from "../metrics/pricing.ts";
28
+ import { createMetricsStore } from "../metrics/store.ts";
29
+ import { parseTranscriptUsage } from "../metrics/transcript.ts";
30
+ import { openSessionStore } from "../sessions/compat.ts";
31
+ import { createRunStore } from "../sessions/store.ts";
32
+ import type { AgentSession } from "../types.ts";
33
+
34
+ /**
35
+ * Get or create a session timestamp directory for the agent.
36
+ * Uses a file-based marker to track the current session directory.
37
+ */
38
+ async function getSessionDir(logsBase: string, agentName: string): Promise<string> {
39
+ const agentLogsDir = join(logsBase, agentName);
40
+ const markerPath = join(agentLogsDir, ".current-session");
41
+
42
+ const markerFile = Bun.file(markerPath);
43
+ if (await markerFile.exists()) {
44
+ const sessionDir = (await markerFile.text()).trim();
45
+ if (sessionDir.length > 0) {
46
+ return sessionDir;
47
+ }
48
+ }
49
+
50
+ // Create a new session directory
51
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
52
+ const sessionDir = join(agentLogsDir, timestamp);
53
+ const { mkdir } = await import("node:fs/promises");
54
+ await mkdir(sessionDir, { recursive: true });
55
+ await Bun.write(markerPath, sessionDir);
56
+ return sessionDir;
57
+ }
58
+
59
+ /**
60
+ * Update the lastActivity timestamp for an agent in the SessionStore.
61
+ * Non-fatal: silently ignores errors to avoid breaking hook execution.
62
+ */
63
+ function updateLastActivity(projectRoot: string, agentName: string): void {
64
+ try {
65
+ const agentplateDir = join(projectRoot, ".agentplate");
66
+ const { store } = openSessionStore(agentplateDir);
67
+ try {
68
+ const session = store.getByName(agentName);
69
+ if (session) {
70
+ store.updateLastActivity(agentName);
71
+ // Tool-use observed: try booting → working. Matrix-guarded so a
72
+ // zombie classification (set by watchdog) is NOT silently revived
73
+ // here — that revival was a contributor to the schizophrenic
74
+ // state=zombie + tool-use-active symptom in agentplate-a993.
75
+ if (session.state === "booting") {
76
+ store.tryTransitionState(agentName, "working");
77
+ }
78
+ }
79
+ } finally {
80
+ store.close();
81
+ }
82
+ } catch {
83
+ // Non-fatal: don't break logging if session update fails
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Maximum retry attempts for the session-end transition.
89
+ *
90
+ * The Stop hook is the only signal that turns sessions.db state from
91
+ * "working" to "completed" for headless legacy paths and tmux sessions.
92
+ * If it loses that signal due to a transient SQLite contention error
93
+ * (e.g. "database is locked" while the watchdog ticks against the same
94
+ * file), the row stays in "working" forever and the watchdog later
95
+ * promotes it to "zombie". Retrying with exponential backoff lets brief
96
+ * lock contention resolve before we give up. (agentplate-e74b)
97
+ */
98
+ const TRANSITION_MAX_ATTEMPTS = 5;
99
+ const TRANSITION_BACKOFF_BASE_MS = 50;
100
+
101
+ /**
102
+ * One attempt at the session-end state transition.
103
+ *
104
+ * Throws on transient failures (e.g. SQLite "database is locked") so the
105
+ * caller can retry. The body is the original logic from
106
+ * `transitionToCompleted`.
107
+ */
108
+ function transitionToCompletedOnce(projectRoot: string, agentName: string): void {
109
+ const agentplateDir = join(projectRoot, ".agentplate");
110
+ const { store } = openSessionStore(agentplateDir);
111
+ try {
112
+ const session = store.getByName(agentName);
113
+ if (session && isStopHookPersistentCapability(session.capability)) {
114
+ // Check if a persistent top-level agent self-exited by verifying the run
115
+ // is already completed.
116
+ // If `ap run complete` was called before session-end, the run status is 'completed'
117
+ // and we should transition the persistent session to completed too.
118
+ if (
119
+ (session.capability === "coordinator" || session.capability === "orchestrator") &&
120
+ session.runId
121
+ ) {
122
+ const runStore = createRunStore(join(agentplateDir, "sessions.db"));
123
+ try {
124
+ const run = runStore.getRun(session.runId);
125
+ if (run && run.status === "completed") {
126
+ // Self-exit: the persistent agent called ap run complete before session ended
127
+ store.updateState(agentName, "completed");
128
+ store.updateLastActivity(agentName);
129
+ return;
130
+ }
131
+ } finally {
132
+ runStore.close();
133
+ }
134
+ }
135
+ // Normal persistent agent: only update activity, don't mark completed
136
+ store.updateLastActivity(agentName);
137
+ return;
138
+ }
139
+ store.updateState(agentName, "completed");
140
+ store.updateLastActivity(agentName);
141
+ } finally {
142
+ store.close();
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Best-effort: log a session-end hook failure to events.db so it surfaces in
148
+ * `ap errors` and trace timelines. Swallows secondary errors (events.db may
149
+ * also be locked when the primary write failed).
150
+ */
151
+ async function logHookFailure(
152
+ projectRoot: string,
153
+ agentName: string,
154
+ hookName: string,
155
+ error: unknown,
156
+ attempts: number,
157
+ ): Promise<void> {
158
+ try {
159
+ const eventsDbPath = join(projectRoot, ".agentplate", "events.db");
160
+ const eventStore = createEventStore(eventsDbPath);
161
+ try {
162
+ eventStore.insert({
163
+ runId: null,
164
+ agentName,
165
+ sessionId: null,
166
+ eventType: "error",
167
+ toolName: null,
168
+ toolArgs: null,
169
+ toolDurationMs: null,
170
+ level: "error",
171
+ data: JSON.stringify({
172
+ hook: hookName,
173
+ attempts,
174
+ message: error instanceof Error ? error.message : String(error),
175
+ }),
176
+ });
177
+ } finally {
178
+ eventStore.close();
179
+ }
180
+ } catch {
181
+ // Non-fatal: events.db may also be unavailable when the primary write failed.
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Transition agent state to 'completed' in the SessionStore.
187
+ * Called when session-end event fires.
188
+ *
189
+ * Retries on transient SQLite contention with exponential backoff
190
+ * (50/100/200/400/800ms). On persistent failure, records an `error` event
191
+ * to events.db so the missed signal shows up in observability tooling and
192
+ * the watchdog's stale-but-tmux-dead fallback can recognize it.
193
+ * (agentplate-e74b)
194
+ *
195
+ * Skips the transition for capabilities in `STOP_HOOK_PERSISTENT_CAPABILITIES`
196
+ * (coordinator, orchestrator, monitor, lead) whose Stop hook fires every model
197
+ * turn rather than once at true session end. See
198
+ * `src/agents/capabilities.ts` for the full rationale and consumer list.
199
+ *
200
+ * Non-fatal: silently ignores errors to avoid breaking hook execution.
201
+ */
202
+ async function transitionToCompleted(projectRoot: string, agentName: string): Promise<void> {
203
+ let lastError: unknown;
204
+ for (let attempt = 0; attempt < TRANSITION_MAX_ATTEMPTS; attempt++) {
205
+ try {
206
+ transitionToCompletedOnce(projectRoot, agentName);
207
+ return;
208
+ } catch (err) {
209
+ lastError = err;
210
+ if (attempt < TRANSITION_MAX_ATTEMPTS - 1) {
211
+ await Bun.sleep(TRANSITION_BACKOFF_BASE_MS * 2 ** attempt);
212
+ }
213
+ }
214
+ }
215
+
216
+ // All retries failed — surface the missed signal via events.db.
217
+ await logHookFailure(
218
+ projectRoot,
219
+ agentName,
220
+ "session-end:transitionToCompleted",
221
+ lastError,
222
+ TRANSITION_MAX_ATTEMPTS,
223
+ );
224
+ }
225
+
226
+ /**
227
+ * Look up an agent's session record.
228
+ * Returns null if not found.
229
+ */
230
+ function getAgentSession(projectRoot: string, agentName: string): AgentSession | null {
231
+ try {
232
+ const agentplateDir = join(projectRoot, ".agentplate");
233
+ const { store } = openSessionStore(agentplateDir);
234
+ try {
235
+ return store.getByName(agentName);
236
+ } finally {
237
+ store.close();
238
+ }
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Read one line of JSON from stdin. Returns parsed object or null on failure.
246
+ * Used when --stdin flag is present to receive hook payload from Claude Code.
247
+ *
248
+ * Reads ALL chunks from stdin to handle large payloads that exceed a single buffer.
249
+ */
250
+ async function readStdinJson(): Promise<Record<string, unknown> | null> {
251
+ try {
252
+ const reader = Bun.stdin.stream().getReader();
253
+ const chunks: Uint8Array[] = [];
254
+ while (true) {
255
+ const { value, done } = await reader.read();
256
+ if (done) break;
257
+ if (value) chunks.push(value);
258
+ }
259
+ reader.releaseLock();
260
+ if (chunks.length === 0) return null;
261
+ // Concatenate all chunks
262
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
263
+ const combined = new Uint8Array(totalLength);
264
+ let offset = 0;
265
+ for (const chunk of chunks) {
266
+ combined.set(chunk, offset);
267
+ offset += chunk.length;
268
+ }
269
+ const text = new TextDecoder().decode(combined).trim();
270
+ if (text.length === 0) return null;
271
+ return JSON.parse(text) as Record<string, unknown>;
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Resolve the path to a Claude Code transcript JSONL file.
279
+ * Tries direct construction first, then searches all project directories.
280
+ * Caches the found path for faster subsequent lookups.
281
+ */
282
+ async function resolveTranscriptPath(
283
+ projectRoot: string,
284
+ sessionId: string,
285
+ logsBase: string,
286
+ agentName: string,
287
+ ): Promise<string | null> {
288
+ // Check SessionStore for a runtime-provided transcript path
289
+ try {
290
+ const { store } = openSessionStore(join(projectRoot, ".agentplate"));
291
+ try {
292
+ const session = store.getByName(agentName);
293
+ if (session?.transcriptPath) {
294
+ if (await Bun.file(session.transcriptPath).exists()) {
295
+ return session.transcriptPath;
296
+ }
297
+ }
298
+ } finally {
299
+ store.close();
300
+ }
301
+ } catch {
302
+ // Non-fatal: fall through to legacy resolution
303
+ }
304
+
305
+ // Check cached path first
306
+ const cachePath = join(logsBase, agentName, ".transcript-path");
307
+ const cacheFile = Bun.file(cachePath);
308
+ if (await cacheFile.exists()) {
309
+ const cached = (await cacheFile.text()).trim();
310
+ if (cached.length > 0 && (await Bun.file(cached).exists())) {
311
+ return cached;
312
+ }
313
+ }
314
+
315
+ const homeDir = process.env.HOME ?? "";
316
+ const claudeProjectsDir = join(homeDir, ".claude", "projects");
317
+
318
+ // Try direct construction from project root
319
+ const projectKey = projectRoot.replace(/\//g, "-");
320
+ const directPath = join(claudeProjectsDir, projectKey, `${sessionId}.jsonl`);
321
+ if (await Bun.file(directPath).exists()) {
322
+ await Bun.write(cachePath, directPath);
323
+ // Save discovered path to SessionStore for future lookups
324
+ try {
325
+ const { store: writeStore } = openSessionStore(join(projectRoot, ".agentplate"));
326
+ try {
327
+ writeStore.updateTranscriptPath(agentName, directPath);
328
+ } finally {
329
+ writeStore.close();
330
+ }
331
+ } catch {
332
+ // Non-fatal: cache write failure should not break transcript resolution
333
+ }
334
+ return directPath;
335
+ }
336
+
337
+ // Search all project directories for the session file
338
+ const { readdir } = await import("node:fs/promises");
339
+ try {
340
+ const projects = await readdir(claudeProjectsDir);
341
+ for (const project of projects) {
342
+ const candidate = join(claudeProjectsDir, project, `${sessionId}.jsonl`);
343
+ if (await Bun.file(candidate).exists()) {
344
+ await Bun.write(cachePath, candidate);
345
+ // Save discovered path to SessionStore for future lookups
346
+ try {
347
+ const { store: writeStore } = openSessionStore(join(projectRoot, ".agentplate"));
348
+ try {
349
+ writeStore.updateTranscriptPath(agentName, candidate);
350
+ } finally {
351
+ writeStore.close();
352
+ }
353
+ } catch {
354
+ // Non-fatal: cache write failure should not break transcript resolution
355
+ }
356
+ return candidate;
357
+ }
358
+ }
359
+ } catch {
360
+ // Claude projects dir may not exist
361
+ }
362
+
363
+ return null;
364
+ }
365
+
366
+ /**
367
+ * Auto-record expertise from loam learn results.
368
+ * Called during session-end for non-persistent agents.
369
+ * Records a reference entry for each suggested domain at the canonical root,
370
+ * then sends a slim notification mail to the parent agent.
371
+ *
372
+ * @returns List of successfully recorded domains
373
+ */
374
+ export async function autoRecordExpertise(params: {
375
+ loamClient: LoamClient;
376
+ agentName: string;
377
+ capability: string;
378
+ taskId: string | null;
379
+ mailDbPath: string;
380
+ parentAgent: string | null;
381
+ projectRoot: string;
382
+ sessionStartedAt: string;
383
+ outcomeStatus?: "success" | "partial" | "failure";
384
+ }): Promise<string[]> {
385
+ const learnResult = await params.loamClient.learn({ since: "HEAD~1" });
386
+ if (learnResult.suggestedDomains.length === 0) {
387
+ return [];
388
+ }
389
+
390
+ const recordedDomains: string[] = [];
391
+ const filesList = learnResult.changedFiles.join(", ");
392
+
393
+ for (const domain of learnResult.suggestedDomains) {
394
+ try {
395
+ await params.loamClient.record(domain, {
396
+ type: "reference",
397
+ description: `${params.capability} agent ${params.agentName} completed work in this domain. Files: ${filesList}`,
398
+ tags: ["auto-session-end", params.capability],
399
+ evidenceBead: params.taskId ?? undefined,
400
+ outcomeStatus: params.outcomeStatus,
401
+ outcomeAgent: params.agentName,
402
+ });
403
+ recordedDomains.push(domain);
404
+ } catch {
405
+ // Non-fatal per domain: skip failed records
406
+ }
407
+ }
408
+
409
+ // Analyze session events for deeper insights (tool usage, file edits, errors)
410
+ let insightSummary = "";
411
+ try {
412
+ const eventsDbPath = join(params.projectRoot, ".agentplate", "events.db");
413
+ const eventStore = createEventStore(eventsDbPath);
414
+
415
+ const events = eventStore.getByAgent(params.agentName, {
416
+ since: params.sessionStartedAt,
417
+ });
418
+ const toolStats = eventStore.getToolStats({
419
+ agentName: params.agentName,
420
+ since: params.sessionStartedAt,
421
+ });
422
+
423
+ eventStore.close();
424
+
425
+ const analysis = analyzeSessionInsights({
426
+ events,
427
+ toolStats,
428
+ agentName: params.agentName,
429
+ capability: params.capability,
430
+ domains: learnResult.suggestedDomains,
431
+ });
432
+
433
+ // Record each insight to loam
434
+ for (const insight of analysis.insights) {
435
+ try {
436
+ await params.loamClient.record(insight.domain, {
437
+ type: insight.type,
438
+ description: insight.description,
439
+ tags: insight.tags,
440
+ evidenceBead: params.taskId ?? undefined,
441
+ outcomeStatus: params.outcomeStatus,
442
+ outcomeAgent: params.agentName,
443
+ });
444
+ if (!recordedDomains.includes(insight.domain)) {
445
+ recordedDomains.push(insight.domain);
446
+ }
447
+ } catch {
448
+ // Non-fatal per insight: skip failed records
449
+ }
450
+ }
451
+
452
+ // Build insight summary for mail
453
+ if (analysis.insights.length > 0) {
454
+ const insightTypes = new Map<string, number>();
455
+ for (const insight of analysis.insights) {
456
+ const count = insightTypes.get(insight.type) ?? 0;
457
+ insightTypes.set(insight.type, count + 1);
458
+ }
459
+ const typeCounts = Array.from(insightTypes.entries())
460
+ .map(([type, count]) => `${count} ${type}`)
461
+ .join(", ");
462
+ insightSummary = `\n\nAuto-insights: ${typeCounts} (${analysis.toolProfile.totalToolCalls} tool calls, ${analysis.fileProfile.totalEdits} edits)`;
463
+ }
464
+ } catch {
465
+ // Non-fatal: insight analysis should not break session-end handling
466
+ }
467
+
468
+ if (recordedDomains.length > 0) {
469
+ const mailStore = createMailStore(params.mailDbPath);
470
+ const mailClient = createMailClient(mailStore);
471
+ const recipient = params.parentAgent ?? "orchestrator";
472
+ const domainsList = recordedDomains.join(", ");
473
+ mailClient.send({
474
+ from: params.agentName,
475
+ to: recipient,
476
+ subject: `loam: auto-recorded insights in ${domainsList}`,
477
+ body: `Session completed. Auto-recorded expertise in: ${domainsList}.\n\nChanged files: ${filesList}${insightSummary}`,
478
+ type: "status",
479
+ priority: "low",
480
+ });
481
+ mailClient.close();
482
+ }
483
+
484
+ return recordedDomains;
485
+ }
486
+
487
+ interface AppliedRecordsData {
488
+ taskId: string | null;
489
+ agentName: string;
490
+ capability: string;
491
+ records: Array<{ id: string; domain: string }>;
492
+ }
493
+
494
+ /**
495
+ * Append outcome entries to the loam records that were applied when this agent was spawned.
496
+ *
497
+ * At spawn time, sling.ts writes .agentplate/agents/{name}/applied-records.json listing
498
+ * the mx-* IDs from the prime output. At session-end, this function reads that file,
499
+ * appends a "success" outcome to each record, and deletes the file.
500
+ *
501
+ * @returns Number of records successfully updated.
502
+ */
503
+ export async function appendOutcomeToAppliedRecords(params: {
504
+ loamClient: LoamClient;
505
+ agentName: string;
506
+ capability: string;
507
+ taskId: string | null;
508
+ projectRoot: string;
509
+ outcomeStatus?: "success" | "partial" | "failure";
510
+ }): Promise<number> {
511
+ const appliedRecordsPath = join(
512
+ params.projectRoot,
513
+ ".agentplate",
514
+ "agents",
515
+ params.agentName,
516
+ "applied-records.json",
517
+ );
518
+ const appliedFile = Bun.file(appliedRecordsPath);
519
+ if (!(await appliedFile.exists())) return 0;
520
+
521
+ let data: AppliedRecordsData;
522
+ try {
523
+ data = JSON.parse(await appliedFile.text()) as AppliedRecordsData;
524
+ } catch {
525
+ return 0;
526
+ }
527
+
528
+ const { records } = data;
529
+ if (!records || records.length === 0) return 0;
530
+
531
+ const taskSuffix = params.taskId ? ` for task ${params.taskId}` : "";
532
+ const status: "success" | "partial" | "failure" = params.outcomeStatus ?? "success";
533
+ const gateNote = params.outcomeStatus ? ` Quality gates: ${params.outcomeStatus}.` : "";
534
+ const outcome = {
535
+ status,
536
+ agent: params.agentName,
537
+ notes: `Applied by ${params.capability} agent ${params.agentName}${taskSuffix}. Session completed.${gateNote}`,
538
+ };
539
+
540
+ let appended = 0;
541
+ for (const { id, domain } of records) {
542
+ try {
543
+ await params.loamClient.appendOutcome(domain, id, outcome);
544
+ appended++;
545
+ } catch {
546
+ // Non-fatal per record
547
+ }
548
+ }
549
+
550
+ try {
551
+ const { unlink } = await import("node:fs/promises");
552
+ await unlink(appliedRecordsPath);
553
+ } catch {
554
+ // Non-fatal: file may already be gone
555
+ }
556
+
557
+ return appended;
558
+ }
559
+
560
+ /**
561
+ * Core implementation for the log command.
562
+ */
563
+ async function runLog(opts: {
564
+ event: string;
565
+ agent: string;
566
+ toolName: string;
567
+ transcript: string | undefined;
568
+ stdin: boolean;
569
+ }): Promise<void> {
570
+ const validEvents = ["tool-start", "tool-end", "session-end"];
571
+ if (!validEvents.includes(opts.event)) {
572
+ throw new ValidationError(`Invalid event "${opts.event}". Valid: ${validEvents.join(", ")}`, {
573
+ field: "event",
574
+ value: opts.event,
575
+ });
576
+ }
577
+
578
+ // Read stdin payload if --stdin flag is set
579
+ let stdinPayload: Record<string, unknown> | null = null;
580
+ if (opts.stdin) {
581
+ stdinPayload = await readStdinJson();
582
+ }
583
+
584
+ // Extract fields from stdin payload (preferred) or fall back to flags
585
+ const toolName =
586
+ typeof stdinPayload?.tool_name === "string" ? stdinPayload.tool_name : opts.toolName;
587
+ const toolInput =
588
+ stdinPayload?.tool_input !== undefined &&
589
+ stdinPayload?.tool_input !== null &&
590
+ typeof stdinPayload.tool_input === "object"
591
+ ? (stdinPayload.tool_input as Record<string, unknown>)
592
+ : null;
593
+ const sessionId = typeof stdinPayload?.session_id === "string" ? stdinPayload.session_id : null;
594
+ const transcriptPath =
595
+ typeof stdinPayload?.transcript_path === "string"
596
+ ? stdinPayload.transcript_path
597
+ : opts.transcript;
598
+
599
+ const cwd = process.cwd();
600
+ const config = await loadConfig(cwd);
601
+ const logsBase = join(config.project.root, ".agentplate", "logs");
602
+ const sessionDir = await getSessionDir(logsBase, opts.agent);
603
+
604
+ const logger = createLogger({
605
+ logDir: sessionDir,
606
+ agentName: opts.agent,
607
+ verbose: config.logging.verbose,
608
+ redactSecrets: config.logging.redactSecrets,
609
+ });
610
+
611
+ switch (opts.event) {
612
+ case "tool-start": {
613
+ // Backward compatibility: always write to per-agent log files
614
+ logger.toolStart(toolName, toolInput ?? {});
615
+ updateLastActivity(config.project.root, opts.agent);
616
+
617
+ // Always write to EventStore for structured observability
618
+ // (works for both Claude Code --stdin and Pi runtime --tool-name agents)
619
+ try {
620
+ const eventsDbPath = join(config.project.root, ".agentplate", "events.db");
621
+ const eventStore = createEventStore(eventsDbPath);
622
+ const filtered = toolInput
623
+ ? filterToolArgs(toolName, toolInput)
624
+ : { args: {}, summary: toolName };
625
+ eventStore.insert({
626
+ runId: null,
627
+ agentName: opts.agent,
628
+ sessionId,
629
+ eventType: "tool_start",
630
+ toolName,
631
+ toolArgs: JSON.stringify(filtered.args),
632
+ toolDurationMs: null,
633
+ level: "info",
634
+ data: JSON.stringify({ summary: filtered.summary }),
635
+ });
636
+ eventStore.close();
637
+ } catch {
638
+ // Non-fatal: EventStore write should not break hook execution
639
+ }
640
+ break;
641
+ }
642
+ case "tool-end": {
643
+ // Backward compatibility: always write to per-agent log files
644
+ logger.toolEnd(toolName, 0);
645
+ updateLastActivity(config.project.root, opts.agent);
646
+
647
+ // Always write to EventStore for structured observability
648
+ // (works for both Claude Code --stdin and Pi runtime --tool-name agents)
649
+ try {
650
+ const eventsDbPath = join(config.project.root, ".agentplate", "events.db");
651
+ const eventStore = createEventStore(eventsDbPath);
652
+ const filtered = toolInput
653
+ ? filterToolArgs(toolName, toolInput)
654
+ : { args: {}, summary: toolName };
655
+ eventStore.insert({
656
+ runId: null,
657
+ agentName: opts.agent,
658
+ sessionId,
659
+ eventType: "tool_end",
660
+ toolName,
661
+ toolArgs: JSON.stringify(filtered.args),
662
+ toolDurationMs: null,
663
+ level: "info",
664
+ data: JSON.stringify({ summary: filtered.summary }),
665
+ });
666
+ const correlation = eventStore.correlateToolEnd(opts.agent, toolName);
667
+ if (correlation) {
668
+ logger.toolEnd(toolName, correlation.durationMs);
669
+ }
670
+ eventStore.close();
671
+ } catch {
672
+ // Non-fatal: EventStore write should not break hook execution
673
+ }
674
+
675
+ // Throttled token snapshot recording (requires sessionId from --stdin; skipped for Pi agents)
676
+ if (sessionId) {
677
+ try {
678
+ // Throttle check
679
+ const snapshotMarkerPath = join(logsBase, opts.agent, ".last-snapshot");
680
+ const SNAPSHOT_INTERVAL_MS = 30_000;
681
+ const snapshotMarkerFile = Bun.file(snapshotMarkerPath);
682
+ let shouldSnapshot = true;
683
+
684
+ if (await snapshotMarkerFile.exists()) {
685
+ const lastTs = Number.parseInt(await snapshotMarkerFile.text(), 10);
686
+ if (!Number.isNaN(lastTs) && Date.now() - lastTs < SNAPSHOT_INTERVAL_MS) {
687
+ shouldSnapshot = false;
688
+ }
689
+ }
690
+
691
+ if (shouldSnapshot) {
692
+ const resolvedTranscriptPath = await resolveTranscriptPath(
693
+ config.project.root,
694
+ sessionId,
695
+ logsBase,
696
+ opts.agent,
697
+ );
698
+ if (resolvedTranscriptPath) {
699
+ const usage = await parseTranscriptUsage(resolvedTranscriptPath);
700
+ const cost = estimateCost(usage);
701
+ const metricsDbPath = join(config.project.root, ".agentplate", "metrics.db");
702
+ const metricsStore = createMetricsStore(metricsDbPath);
703
+ const agentSession = getAgentSession(config.project.root, opts.agent);
704
+ metricsStore.recordSnapshot({
705
+ agentName: opts.agent,
706
+ inputTokens: usage.inputTokens,
707
+ outputTokens: usage.outputTokens,
708
+ cacheReadTokens: usage.cacheReadTokens,
709
+ cacheCreationTokens: usage.cacheCreationTokens,
710
+ estimatedCostUsd: cost,
711
+ modelUsed: usage.modelUsed,
712
+ runId: agentSession?.runId ?? null,
713
+ createdAt: new Date().toISOString(),
714
+ });
715
+ metricsStore.close();
716
+ await Bun.write(snapshotMarkerPath, String(Date.now()));
717
+ }
718
+ }
719
+ } catch {
720
+ // Non-fatal: snapshot recording should not break tool-end handling
721
+ }
722
+ }
723
+ break;
724
+ }
725
+ case "session-end":
726
+ logger.info("session.end", { agentName: opts.agent });
727
+ // Transition agent state to completed (with retry/backoff and
728
+ // events.db fallback on persistent failure — agentplate-e74b).
729
+ await transitionToCompleted(config.project.root, opts.agent);
730
+ // Look up agent session for identity update and metrics recording
731
+ {
732
+ const agentSession = getAgentSession(config.project.root, opts.agent);
733
+ const taskId = agentSession?.taskId ?? null;
734
+
735
+ // Update agent identity with completed session
736
+ const identityBaseDir = join(config.project.root, ".agentplate", "agents");
737
+ try {
738
+ await updateIdentity(identityBaseDir, opts.agent, {
739
+ sessionsCompleted: 1,
740
+ completedTask: taskId ? { taskId, summary: `Completed task ${taskId}` } : undefined,
741
+ });
742
+ } catch {
743
+ // Non-fatal: identity may not exist for this agent
744
+ }
745
+
746
+ // Record session metrics (with optional token data from transcript)
747
+ if (agentSession) {
748
+ // NOTE: We intentionally do NOT auto-complete the run here for coordinator agents.
749
+ // The coordinator's Stop hook fires on every turn boundary, not just at true session exit,
750
+ // so auto-completing the run here would kill the session after the first turn.
751
+ // Run completion is handled by: `ap coordinator stop`, `ap run complete` (self-exit),
752
+ // or the watchdog daemon detecting a dead coordinator process.
753
+
754
+ try {
755
+ const metricsDbPath = join(config.project.root, ".agentplate", "metrics.db");
756
+ const metricsStore = createMetricsStore(metricsDbPath);
757
+ const now = new Date().toISOString();
758
+ const durationMs = new Date(now).getTime() - new Date(agentSession.startedAt).getTime();
759
+
760
+ // Parse token usage from transcript if path provided
761
+ let inputTokens = 0;
762
+ let outputTokens = 0;
763
+ let cacheReadTokens = 0;
764
+ let cacheCreationTokens = 0;
765
+ let estimatedCostUsd: number | null = null;
766
+ let modelUsed: string | null = null;
767
+
768
+ if (transcriptPath) {
769
+ try {
770
+ const usage = await parseTranscriptUsage(transcriptPath);
771
+ inputTokens = usage.inputTokens;
772
+ outputTokens = usage.outputTokens;
773
+ cacheReadTokens = usage.cacheReadTokens;
774
+ cacheCreationTokens = usage.cacheCreationTokens;
775
+ modelUsed = usage.modelUsed;
776
+ estimatedCostUsd = estimateCost(usage);
777
+ } catch {
778
+ // Non-fatal: transcript parsing should not break metrics
779
+ }
780
+ }
781
+
782
+ metricsStore.recordSession({
783
+ agentName: opts.agent,
784
+ taskId: agentSession.taskId,
785
+ capability: agentSession.capability,
786
+ startedAt: agentSession.startedAt,
787
+ completedAt: now,
788
+ durationMs,
789
+ exitCode: null,
790
+ mergeResult: null,
791
+ parentAgent: agentSession.parentAgent,
792
+ inputTokens,
793
+ outputTokens,
794
+ cacheReadTokens,
795
+ cacheCreationTokens,
796
+ estimatedCostUsd,
797
+ modelUsed,
798
+ runId: agentSession.runId,
799
+ });
800
+ metricsStore.close();
801
+ } catch {
802
+ // Non-fatal: metrics recording should not break session-end handling
803
+ }
804
+
805
+ // Resolve outcome status from quality-gate results, threaded into
806
+ // every session-end loam record write so confirmation scoring
807
+ // reflects whether tests/lint/typecheck actually passed.
808
+ let outcomeStatus: "success" | "partial" | "failure" | undefined;
809
+ if (!isStopHookPersistentCapability(agentSession.capability)) {
810
+ try {
811
+ let baseRef = "main";
812
+ const baseBranchPath = join(config.project.root, ".agentplate", "session-branch.txt");
813
+ const baseFile = Bun.file(baseBranchPath);
814
+ if (await baseFile.exists()) {
815
+ const txt = (await baseFile.text()).trim();
816
+ if (txt.length > 0) baseRef = txt;
817
+ }
818
+ const hasWork = await hasWorkToVerify(agentSession.worktreePath, baseRef);
819
+ if (hasWork) {
820
+ const gates = config.project.qualityGates ?? [];
821
+ const outcome = await runQualityGates(gates, agentSession.worktreePath);
822
+ if (outcome) outcomeStatus = outcome.status;
823
+ }
824
+ } catch {
825
+ // Non-fatal: outcome status is optional
826
+ }
827
+ }
828
+
829
+ // Auto-record expertise via loam learn + record (post-session).
830
+ // Skip persistent agents whose Stop hook fires every turn.
831
+ if (!isStopHookPersistentCapability(agentSession.capability)) {
832
+ try {
833
+ const loamClient = createLoamClient(config.project.root);
834
+ const mailDbPath = join(config.project.root, ".agentplate", "mail.db");
835
+ await autoRecordExpertise({
836
+ loamClient,
837
+ agentName: opts.agent,
838
+ capability: agentSession.capability,
839
+ taskId,
840
+ mailDbPath,
841
+ parentAgent: agentSession.parentAgent,
842
+ projectRoot: config.project.root,
843
+ sessionStartedAt: agentSession.startedAt,
844
+ outcomeStatus,
845
+ });
846
+ } catch {
847
+ // Non-fatal: loam learn/record should not break session-end handling
848
+ }
849
+ }
850
+
851
+ // Append outcomes to applied loam records (outcome feedback loop).
852
+ // Reads applied-records.json written by sling.ts at spawn time.
853
+ if (!isStopHookPersistentCapability(agentSession.capability)) {
854
+ try {
855
+ const loamClient = createLoamClient(config.project.root);
856
+ await appendOutcomeToAppliedRecords({
857
+ loamClient,
858
+ agentName: opts.agent,
859
+ capability: agentSession.capability,
860
+ taskId,
861
+ projectRoot: config.project.root,
862
+ outcomeStatus,
863
+ });
864
+ } catch {
865
+ // Non-fatal
866
+ }
867
+ }
868
+ }
869
+
870
+ // Always write session-end event to EventStore (not just when --stdin is used)
871
+ try {
872
+ const eventsDbPath = join(config.project.root, ".agentplate", "events.db");
873
+ const eventStore = createEventStore(eventsDbPath);
874
+ eventStore.insert({
875
+ runId: null,
876
+ agentName: opts.agent,
877
+ sessionId,
878
+ eventType: "session_end",
879
+ toolName: null,
880
+ toolArgs: null,
881
+ toolDurationMs: null,
882
+ level: "info",
883
+ data: transcriptPath ? JSON.stringify({ transcriptPath }) : null,
884
+ });
885
+ eventStore.close();
886
+ } catch {
887
+ // Non-fatal: EventStore write should not break session-end
888
+ }
889
+ }
890
+ // Clear the current session marker
891
+ {
892
+ const markerPath = join(logsBase, opts.agent, ".current-session");
893
+ try {
894
+ const { unlink } = await import("node:fs/promises");
895
+ await unlink(markerPath);
896
+ } catch {
897
+ // Marker may not exist
898
+ }
899
+ }
900
+ break;
901
+ }
902
+
903
+ logger.close();
904
+ }
905
+
906
+ export function createLogCommand(): Command {
907
+ return new Command("log")
908
+ .description("Log a hook event")
909
+ .argument("<event>", "Event type: tool-start, tool-end, session-end")
910
+ .option("--agent <name>", "Agent name (required)")
911
+ .option("--tool-name <name>", "Tool name (for tool-start/tool-end events, legacy)")
912
+ .option("--transcript <path>", "Path to Claude Code transcript JSONL (for session-end, legacy)")
913
+ .option("--stdin", "Read hook payload JSON from stdin (preferred)")
914
+ .option("--json", "Output as JSON")
915
+ .action(
916
+ async (
917
+ event: string,
918
+ opts: {
919
+ agent?: string;
920
+ toolName?: string;
921
+ transcript?: string;
922
+ stdin?: boolean;
923
+ json?: boolean;
924
+ },
925
+ ) => {
926
+ if (!opts.agent) {
927
+ throw new ValidationError("--agent is required for log command", { field: "agent" });
928
+ }
929
+ await runLog({
930
+ event,
931
+ agent: opts.agent,
932
+ toolName: opts.toolName ?? "unknown",
933
+ transcript: opts.transcript,
934
+ stdin: opts.stdin ?? false,
935
+ });
936
+ },
937
+ );
938
+ }
939
+
940
+ /**
941
+ * Entry point for `ap log <event> --agent <name>`.
942
+ */
943
+ export async function logCommand(args: string[]): Promise<void> {
944
+ const cmd = createLogCommand();
945
+ cmd.exitOverride();
946
+
947
+ try {
948
+ await cmd.parseAsync(args, { from: "user" });
949
+ } catch (err: unknown) {
950
+ if (err && typeof err === "object" && "code" in err) {
951
+ const code = (err as { code: string }).code;
952
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
953
+ return;
954
+ }
955
+ }
956
+ throw err;
957
+ }
958
+ }