@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,848 @@
1
+ /**
2
+ * CLI command: agentplate mail send/check/list/read/reply
3
+ *
4
+ * Parses CLI args via Commander.js and delegates to the mail client.
5
+ * Supports --inject for hook context injection, --json for machine output,
6
+ * and various filters for listing messages.
7
+ */
8
+
9
+ import { join } from "node:path";
10
+ import { Command, CommanderError } from "commander";
11
+ import { resolveProjectRoot } from "../config.ts";
12
+ import { MailError, ValidationError } from "../errors.ts";
13
+ import { createEventStore } from "../events/store.ts";
14
+ import { jsonOutput } from "../json.ts";
15
+ import { accent, printHint, printSuccess } from "../logging/color.ts";
16
+ import { isGroupAddress, resolveGroupAddress } from "../mail/broadcast.ts";
17
+ import { createMailClient } from "../mail/client.ts";
18
+ import { createMailStore } from "../mail/store.ts";
19
+ import { openSessionStore } from "../sessions/compat.ts";
20
+ import type { MailMessage, MailMessageType } from "../types.ts";
21
+ import { MAIL_MESSAGE_TYPES } from "../types.ts";
22
+
23
+ /**
24
+ * Protocol message types that require immediate recipient attention.
25
+ * These trigger auto-nudge regardless of priority level.
26
+ */
27
+ export const AUTO_NUDGE_TYPES: ReadonlySet<MailMessageType> = new Set([
28
+ "worker_done",
29
+ "merge_ready",
30
+ "error",
31
+ "escalation",
32
+ "merge_failed",
33
+ ]);
34
+
35
+ /**
36
+ * Check if a message type/priority combination should trigger a pending nudge.
37
+ * Exported for testability.
38
+ */
39
+ export function shouldAutoNudge(type: MailMessageType, priority: MailMessage["priority"]): boolean {
40
+ return priority === "urgent" || priority === "high" || AUTO_NUDGE_TYPES.has(type);
41
+ }
42
+
43
+ /**
44
+ * Check if a message type should trigger an immediate tmux dispatch nudge.
45
+ * Dispatch nudges target newly spawned agents at the welcome screen.
46
+ * Exported for testability.
47
+ */
48
+ export function isDispatchNudge(type: MailMessageType): boolean {
49
+ return type === "dispatch";
50
+ }
51
+
52
+ /** Format a single message for human-readable output. */
53
+ function formatMessage(msg: MailMessage): string {
54
+ const readMarker = msg.read ? " " : "*";
55
+ const priorityTag = msg.priority !== "normal" ? ` [${msg.priority.toUpperCase()}]` : "";
56
+ const lines: string[] = [
57
+ `${readMarker} ${accent(msg.id)} From: ${accent(msg.from)} → To: ${accent(msg.to)}${priorityTag}`,
58
+ ` Subject: ${msg.subject} (${msg.type})`,
59
+ ` ${msg.body}`,
60
+ ];
61
+ if (msg.payload !== null) {
62
+ lines.push(` Payload: ${msg.payload}`);
63
+ }
64
+ lines.push(` ${msg.createdAt}`);
65
+ return lines.join("\n");
66
+ }
67
+
68
+ /**
69
+ * Open a mail store connected to the project's mail.db.
70
+ * The cwd must already be resolved to the canonical project root.
71
+ */
72
+ function openStore(cwd: string) {
73
+ const dbPath = join(cwd, ".agentplate", "mail.db");
74
+ return createMailStore(dbPath);
75
+ }
76
+
77
+ // === Pending Nudge Markers ===
78
+ //
79
+ // Instead of sending tmux keys (which corrupt tool I/O), auto-nudge writes
80
+ // a JSON marker file per agent. The `mail check --inject` flow reads and
81
+ // clears these markers, prepending a priority banner to the injected output.
82
+
83
+ /** Directory where pending nudge markers are stored. */
84
+ function pendingNudgeDir(cwd: string): string {
85
+ return join(cwd, ".agentplate", "pending-nudges");
86
+ }
87
+
88
+ /** Shape of a pending nudge marker file. */
89
+ interface PendingNudge {
90
+ from: string;
91
+ reason: string;
92
+ subject: string;
93
+ messageId: string;
94
+ createdAt: string;
95
+ }
96
+
97
+ /**
98
+ * Write a pending nudge marker for an agent.
99
+ *
100
+ * Creates `.agentplate/pending-nudges/{agent}.json` so that the next
101
+ * `mail check --inject` call surfaces a priority banner for this message.
102
+ * Overwrites any existing marker (only the latest nudge matters).
103
+ */
104
+ async function writePendingNudge(
105
+ cwd: string,
106
+ agentName: string,
107
+ nudge: Omit<PendingNudge, "createdAt">,
108
+ ): Promise<void> {
109
+ const dir = pendingNudgeDir(cwd);
110
+ const { mkdir } = await import("node:fs/promises");
111
+ await mkdir(dir, { recursive: true });
112
+
113
+ const marker: PendingNudge = {
114
+ ...nudge,
115
+ createdAt: new Date().toISOString(),
116
+ };
117
+ const filePath = join(dir, `${agentName}.json`);
118
+ await Bun.write(filePath, `${JSON.stringify(marker, null, "\t")}\n`);
119
+ }
120
+
121
+ /**
122
+ * Read and clear pending nudge markers for an agent.
123
+ *
124
+ * Returns the pending nudge (if any) and removes the marker file.
125
+ * Called by `mail check --inject` to prepend a priority banner.
126
+ */
127
+ async function readAndClearPendingNudge(
128
+ cwd: string,
129
+ agentName: string,
130
+ ): Promise<PendingNudge | null> {
131
+ const filePath = join(pendingNudgeDir(cwd), `${agentName}.json`);
132
+ const file = Bun.file(filePath);
133
+ if (!(await file.exists())) {
134
+ return null;
135
+ }
136
+ try {
137
+ const text = await file.text();
138
+ const nudge = JSON.parse(text) as PendingNudge;
139
+ const { unlink } = await import("node:fs/promises");
140
+ await unlink(filePath);
141
+ return nudge;
142
+ } catch {
143
+ // Corrupt or race condition — clear it and move on
144
+ try {
145
+ const { unlink } = await import("node:fs/promises");
146
+ await unlink(filePath);
147
+ } catch {
148
+ // Already gone
149
+ }
150
+ return null;
151
+ }
152
+ }
153
+
154
+ // === Mail Check Debounce ===
155
+ //
156
+ // Prevents excessive mail checking by tracking the last check timestamp per agent.
157
+ // When --debounce flag is provided, mail check will skip if called within the
158
+ // debounce window.
159
+
160
+ /**
161
+ * Path to the mail check debounce state file.
162
+ */
163
+ function mailCheckStatePath(cwd: string): string {
164
+ return join(cwd, ".agentplate", "mail-check-state.json");
165
+ }
166
+
167
+ /**
168
+ * Check if a mail check for this agent is within the debounce window.
169
+ *
170
+ * @param cwd - Project root directory
171
+ * @param agentName - Agent name
172
+ * @param debounceMs - Debounce interval in milliseconds
173
+ * @returns true if the last check was within the debounce window
174
+ */
175
+ async function isMailCheckDebounced(
176
+ cwd: string,
177
+ agentName: string,
178
+ debounceMs: number,
179
+ ): Promise<boolean> {
180
+ const statePath = mailCheckStatePath(cwd);
181
+ const file = Bun.file(statePath);
182
+ if (!(await file.exists())) {
183
+ return false;
184
+ }
185
+ try {
186
+ const text = await file.text();
187
+ const state = JSON.parse(text) as Record<string, number>;
188
+ const lastCheck = state[agentName];
189
+ if (lastCheck === undefined) {
190
+ return false;
191
+ }
192
+ return Date.now() - lastCheck < debounceMs;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Record a mail check timestamp for debounce tracking.
200
+ *
201
+ * @param cwd - Project root directory
202
+ * @param agentName - Agent name
203
+ */
204
+ async function recordMailCheck(cwd: string, agentName: string): Promise<void> {
205
+ const statePath = mailCheckStatePath(cwd);
206
+ let state: Record<string, number> = {};
207
+ const file = Bun.file(statePath);
208
+ if (await file.exists()) {
209
+ try {
210
+ const text = await file.text();
211
+ state = JSON.parse(text) as Record<string, number>;
212
+ } catch {
213
+ // Corrupt state file — start fresh
214
+ }
215
+ }
216
+ state[agentName] = Date.now();
217
+ await Bun.write(statePath, `${JSON.stringify(state, null, "\t")}\n`);
218
+ }
219
+
220
+ /**
221
+ * Open a mail client connected to the project's mail.db.
222
+ * The cwd must already be resolved to the canonical project root.
223
+ */
224
+ function openClient(cwd: string) {
225
+ const store = openStore(cwd);
226
+ const client = createMailClient(store);
227
+ return client;
228
+ }
229
+
230
+ // === Typed option interfaces for each subcommand ===
231
+
232
+ interface SendOpts {
233
+ to: string;
234
+ subject: string;
235
+ body: string;
236
+ from?: string;
237
+ agent?: string;
238
+ type?: string;
239
+ priority?: string;
240
+ payload?: string;
241
+ json?: boolean;
242
+ }
243
+
244
+ interface CheckOpts {
245
+ agent?: string;
246
+ inject?: boolean;
247
+ json?: boolean;
248
+ debounce?: string;
249
+ }
250
+
251
+ interface ListOpts {
252
+ from?: string;
253
+ to?: string;
254
+ agent?: string;
255
+ unread?: boolean;
256
+ type?: string;
257
+ json?: boolean;
258
+ }
259
+
260
+ interface ReplyOpts {
261
+ body: string;
262
+ from?: string;
263
+ agent?: string;
264
+ json?: boolean;
265
+ }
266
+
267
+ interface PurgeOpts {
268
+ all?: boolean;
269
+ days?: string;
270
+ agent?: string;
271
+ json?: boolean;
272
+ }
273
+
274
+ /** agentplate mail send */
275
+ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
276
+ const { to, subject, body } = opts;
277
+ const from = opts.agent ?? opts.from ?? "orchestrator";
278
+ const rawPayload = opts.payload;
279
+ const VALID_PRIORITIES = ["low", "normal", "high", "urgent"] as const;
280
+
281
+ const rawType = opts.type ?? "status";
282
+ const rawPriority = opts.priority ?? "normal";
283
+
284
+ if (!MAIL_MESSAGE_TYPES.includes(rawType as MailMessage["type"])) {
285
+ throw new ValidationError(
286
+ `Invalid --type "${rawType}". Must be one of: ${MAIL_MESSAGE_TYPES.join(", ")}`,
287
+ { field: "type", value: rawType },
288
+ );
289
+ }
290
+ if (!VALID_PRIORITIES.includes(rawPriority as MailMessage["priority"])) {
291
+ throw new ValidationError(
292
+ `Invalid --priority "${rawPriority}". Must be one of: ${VALID_PRIORITIES.join(", ")}`,
293
+ { field: "priority", value: rawPriority },
294
+ );
295
+ }
296
+
297
+ const type = rawType as MailMessage["type"];
298
+ const priority = rawPriority as MailMessage["priority"];
299
+
300
+ // Validate JSON payload if provided
301
+ let payload: string | undefined;
302
+ if (rawPayload !== undefined) {
303
+ try {
304
+ JSON.parse(rawPayload);
305
+ payload = rawPayload;
306
+ } catch {
307
+ throw new ValidationError("--payload must be valid JSON", {
308
+ field: "payload",
309
+ value: rawPayload,
310
+ });
311
+ }
312
+ }
313
+
314
+ // Handle broadcast messages (group addresses)
315
+ if (isGroupAddress(to)) {
316
+ const agentplateDir = join(cwd, ".agentplate");
317
+ const { store: sessionStore } = openSessionStore(agentplateDir);
318
+
319
+ try {
320
+ const activeSessions = sessionStore.getActive();
321
+ const recipients = resolveGroupAddress(to, activeSessions, from);
322
+
323
+ const client = openClient(cwd);
324
+ const messageIds: string[] = [];
325
+
326
+ try {
327
+ // Fan out: send individual message to each recipient
328
+ for (const recipient of recipients) {
329
+ const id = client.send({ from, to: recipient, subject, body, type, priority, payload });
330
+ messageIds.push(id);
331
+
332
+ // Record mail_sent event for each individual message (fire-and-forget)
333
+ try {
334
+ const eventsDbPath = join(cwd, ".agentplate", "events.db");
335
+ const eventStore = createEventStore(eventsDbPath);
336
+ try {
337
+ let runId: string | null = null;
338
+ const runIdPath = join(cwd, ".agentplate", "current-run.txt");
339
+ const runIdFile = Bun.file(runIdPath);
340
+ if (await runIdFile.exists()) {
341
+ const text = await runIdFile.text();
342
+ const trimmed = text.trim();
343
+ if (trimmed.length > 0) {
344
+ runId = trimmed;
345
+ }
346
+ }
347
+ eventStore.insert({
348
+ runId,
349
+ agentName: from,
350
+ sessionId: null,
351
+ eventType: "mail_sent",
352
+ toolName: null,
353
+ toolArgs: null,
354
+ toolDurationMs: null,
355
+ level: "info",
356
+ data: JSON.stringify({
357
+ to: recipient,
358
+ subject,
359
+ type,
360
+ priority,
361
+ messageId: id,
362
+ broadcast: true,
363
+ }),
364
+ });
365
+ } finally {
366
+ eventStore.close();
367
+ }
368
+ } catch {
369
+ // Event recording failure is non-fatal
370
+ }
371
+
372
+ // Auto-nudge for each individual message
373
+ const shouldNudge =
374
+ priority === "urgent" || priority === "high" || AUTO_NUDGE_TYPES.has(type);
375
+ if (shouldNudge) {
376
+ const nudgeReason = AUTO_NUDGE_TYPES.has(type) ? type : `${priority} priority`;
377
+ await writePendingNudge(cwd, recipient, {
378
+ from,
379
+ reason: nudgeReason,
380
+ subject,
381
+ messageId: id,
382
+ });
383
+ }
384
+ }
385
+ } finally {
386
+ client.close();
387
+ }
388
+
389
+ // Output broadcast summary
390
+ if (opts.json) {
391
+ jsonOutput("mail send", { messageIds, recipientCount: recipients.length });
392
+ } else {
393
+ process.stdout.write(
394
+ `Broadcast sent to ${recipients.length} recipient${recipients.length === 1 ? "" : "s"} (${to})\n`,
395
+ );
396
+ for (let i = 0; i < recipients.length; i++) {
397
+ const recipient = recipients[i];
398
+ const msgId = messageIds[i];
399
+ process.stdout.write(` → ${accent(recipient)} (${accent(msgId)})\n`);
400
+ }
401
+ }
402
+
403
+ return; // Early return — broadcast handled
404
+ } finally {
405
+ sessionStore.close();
406
+ }
407
+ }
408
+
409
+ // Reject sends to agents in a terminal state (completed/zombie).
410
+ // `installMailInjectors` reaps the per-agent dispatch loop the moment a
411
+ // session lands in a terminal state (serve.ts:378), so any mail addressed
412
+ // after that point would sit unread forever with no way to surface it.
413
+ // Sessions with no row at all (orchestrator, coordinator, operator roles)
414
+ // fall through — we only know about agents tracked in SessionStore.
415
+ // Group addresses already skip terminal agents via `getActive()`.
416
+ {
417
+ const agentplateDir = join(cwd, ".agentplate");
418
+ const { store: sessionStore } = openSessionStore(agentplateDir);
419
+ try {
420
+ const recipient = sessionStore.getByName(to);
421
+ if (recipient && (recipient.state === "completed" || recipient.state === "zombie")) {
422
+ throw new MailError(
423
+ `Recipient "${to}" is in terminal state (${recipient.state}); message not sent. ` +
424
+ `The agent is no longer running, so this message would never be delivered.`,
425
+ { agentName: to },
426
+ );
427
+ }
428
+ } finally {
429
+ sessionStore.close();
430
+ }
431
+ }
432
+
433
+ // Single-recipient message (existing logic)
434
+ const client = openClient(cwd);
435
+ try {
436
+ const id = client.send({ from, to, subject, body, type, priority, payload });
437
+
438
+ // Record mail_sent event to EventStore (fire-and-forget)
439
+ try {
440
+ const eventsDbPath = join(cwd, ".agentplate", "events.db");
441
+ const eventStore = createEventStore(eventsDbPath);
442
+ try {
443
+ let runId: string | null = null;
444
+ const runIdPath = join(cwd, ".agentplate", "current-run.txt");
445
+ const runIdFile = Bun.file(runIdPath);
446
+ if (await runIdFile.exists()) {
447
+ const text = await runIdFile.text();
448
+ const trimmed = text.trim();
449
+ if (trimmed.length > 0) {
450
+ runId = trimmed;
451
+ }
452
+ }
453
+ eventStore.insert({
454
+ runId,
455
+ agentName: from,
456
+ sessionId: null,
457
+ eventType: "mail_sent",
458
+ toolName: null,
459
+ toolArgs: null,
460
+ toolDurationMs: null,
461
+ level: "info",
462
+ data: JSON.stringify({ to, subject, type, priority, messageId: id }),
463
+ });
464
+ } finally {
465
+ eventStore.close();
466
+ }
467
+ } catch {
468
+ // Event recording failure is non-fatal
469
+ }
470
+
471
+ if (opts.json) {
472
+ jsonOutput("mail send", { id });
473
+ } else {
474
+ printSuccess("Sent message", id);
475
+ }
476
+
477
+ // Auto-nudge: write a pending nudge marker instead of sending tmux keys.
478
+ // Direct tmux sendKeys during tool execution corrupts the agent's I/O,
479
+ // causing SIGKILL (exit 137) and "request interrupted" errors (agentplate-ii1o).
480
+ // The message is already in the DB — the UserPromptSubmit hook's
481
+ // `mail check --inject` will surface it on the next prompt cycle.
482
+ // The pending nudge marker ensures the message gets a priority banner.
483
+ const shouldNudge = priority === "urgent" || priority === "high" || AUTO_NUDGE_TYPES.has(type);
484
+ if (shouldNudge) {
485
+ const nudgeReason = AUTO_NUDGE_TYPES.has(type) ? type : `${priority} priority`;
486
+ await writePendingNudge(cwd, to, {
487
+ from,
488
+ reason: nudgeReason,
489
+ subject,
490
+ messageId: id,
491
+ });
492
+ if (!opts.json) {
493
+ process.stdout.write(
494
+ `Queued nudge for "${to}" (${nudgeReason}, delivered on next prompt)\n`,
495
+ );
496
+ }
497
+ }
498
+
499
+ // For dispatch messages, also send an immediate tmux nudge.
500
+ // Dispatch targets newly spawned agents that may be idle at the welcome
501
+ // screen where file-based nudges can't reach (no hook fires on idle agents).
502
+ // The I/O corruption concern (agentplate-ii1o) only applies during active
503
+ // tool execution — newly spawned agents are idle, so sendKeys is safe.
504
+ if (type === "dispatch") {
505
+ try {
506
+ const { nudgeAgent } = await import("./nudge.ts");
507
+ const nudgeMessage = `[DISPATCH] ${subject}: ${body.slice(0, 500)}`;
508
+ // Small delay to let the agent's TUI stabilize after sling
509
+ await Bun.sleep(3_000);
510
+ await nudgeAgent(cwd, to, nudgeMessage, true); // force=true to skip debounce
511
+ } catch {
512
+ // Non-fatal: the file-based nudge is the fallback
513
+ }
514
+ }
515
+
516
+ // Reviewer coverage check for merge_ready (advisory warning)
517
+ if (type === "merge_ready") {
518
+ try {
519
+ const agentplateDir = join(cwd, ".agentplate");
520
+ const { store: sessionStore } = openSessionStore(agentplateDir);
521
+ try {
522
+ const allSessions = sessionStore.getAll();
523
+ const myBuilders = allSessions.filter(
524
+ (s) => s.parentAgent === from && s.capability === "builder",
525
+ );
526
+ const myReviewers = allSessions.filter(
527
+ (s) => s.parentAgent === from && s.capability === "reviewer",
528
+ );
529
+ if (myBuilders.length > 0 && myReviewers.length === 0) {
530
+ process.stderr.write(
531
+ `\nWarning: merge_ready sent but NO reviewer sessions found for "${from}".\n` +
532
+ `${myBuilders.length} builder(s) completed without review. This violates the review-before-merge requirement.\n` +
533
+ `Spawn reviewers for each builder before merge. See REVIEW_SKIP in agents/lead.md.\n\n`,
534
+ );
535
+ } else if (myReviewers.length > 0 && myReviewers.length < myBuilders.length) {
536
+ process.stderr.write(
537
+ `\nNote: Only ${myReviewers.length} reviewer(s) for ${myBuilders.length} builder(s). Ensure all builder work is review-verified.\n\n`,
538
+ );
539
+ }
540
+ } finally {
541
+ sessionStore.close();
542
+ }
543
+ } catch {
544
+ // Reviewer check failure is non-fatal — do not block mail send
545
+ }
546
+ }
547
+ } finally {
548
+ client.close();
549
+ }
550
+ }
551
+
552
+ /** agentplate mail check */
553
+ async function handleCheck(opts: CheckOpts, cwd: string): Promise<void> {
554
+ const agent = opts.agent ?? "orchestrator";
555
+ const inject = opts.inject ?? false;
556
+ const json = opts.json ?? false;
557
+ const debounceFlag = opts.debounce;
558
+
559
+ // Parse debounce interval if provided
560
+ let debounceMs: number | undefined;
561
+ if (debounceFlag !== undefined) {
562
+ const parsed = Number.parseInt(debounceFlag, 10);
563
+ if (Number.isNaN(parsed) || parsed < 0) {
564
+ throw new ValidationError(
565
+ `--debounce must be a non-negative integer (milliseconds), got: ${debounceFlag}`,
566
+ { field: "debounce", value: debounceFlag },
567
+ );
568
+ }
569
+ debounceMs = parsed;
570
+ }
571
+
572
+ // Check debounce if enabled
573
+ if (debounceMs !== undefined) {
574
+ const debounced = await isMailCheckDebounced(cwd, agent, debounceMs);
575
+ if (debounced) {
576
+ // Silent skip — no output when debounced
577
+ return;
578
+ }
579
+ }
580
+
581
+ const client = openClient(cwd);
582
+ try {
583
+ if (inject) {
584
+ // Check for pending nudge markers (written by auto-nudge instead of tmux keys)
585
+ const pendingNudge = await readAndClearPendingNudge(cwd, agent);
586
+ const output = client.checkInject(agent);
587
+
588
+ // Prepend a priority banner if there's a pending nudge
589
+ if (pendingNudge) {
590
+ const banner = `PRIORITY: ${pendingNudge.reason} message from ${pendingNudge.from} — "${pendingNudge.subject}"\n\n`;
591
+ process.stdout.write(banner);
592
+ }
593
+
594
+ if (output.length > 0) {
595
+ process.stdout.write(output);
596
+ }
597
+ } else {
598
+ const messages = client.check(agent);
599
+
600
+ if (json) {
601
+ jsonOutput("mail check", { messages });
602
+ } else if (messages.length === 0) {
603
+ printHint("No new messages");
604
+ } else {
605
+ process.stdout.write(
606
+ `${messages.length} new message${messages.length === 1 ? "" : "s"}:\n\n`,
607
+ );
608
+ for (const msg of messages) {
609
+ process.stdout.write(`${formatMessage(msg)}\n\n`);
610
+ }
611
+ }
612
+ }
613
+
614
+ // Record this check for debounce tracking (only if debounce is enabled)
615
+ if (debounceMs !== undefined) {
616
+ await recordMailCheck(cwd, agent);
617
+ }
618
+ } finally {
619
+ client.close();
620
+ }
621
+ }
622
+
623
+ /** agentplate mail list */
624
+ function handleList(opts: ListOpts, cwd: string): void {
625
+ const from = opts.from;
626
+ // --to takes precedence over --agent (agent is an alias for recipient filtering)
627
+ const to = opts.to ?? opts.agent;
628
+ const unread = opts.unread ? true : undefined;
629
+ const json = opts.json ?? false;
630
+
631
+ let type: MailMessageType | undefined;
632
+ if (opts.type !== undefined) {
633
+ if (!MAIL_MESSAGE_TYPES.includes(opts.type as MailMessageType)) {
634
+ throw new ValidationError(
635
+ `Invalid --type "${opts.type}". Must be one of: ${MAIL_MESSAGE_TYPES.join(", ")}`,
636
+ { field: "type", value: opts.type },
637
+ );
638
+ }
639
+ type = opts.type as MailMessageType;
640
+ }
641
+
642
+ const client = openClient(cwd);
643
+ try {
644
+ const messages = client.list({ from, to, unread, type });
645
+
646
+ if (json) {
647
+ jsonOutput("mail list", { messages });
648
+ } else if (messages.length === 0) {
649
+ printHint("No messages found");
650
+ } else {
651
+ for (const msg of messages) {
652
+ process.stdout.write(`${formatMessage(msg)}\n\n`);
653
+ }
654
+ process.stdout.write(
655
+ `Total: ${messages.length} message${messages.length === 1 ? "" : "s"}\n`,
656
+ );
657
+ }
658
+ } finally {
659
+ client.close();
660
+ }
661
+ }
662
+
663
+ /** agentplate mail read */
664
+ function handleRead(id: string, cwd: string): void {
665
+ const client = openClient(cwd);
666
+ try {
667
+ const { alreadyRead } = client.markRead(id);
668
+ if (alreadyRead) {
669
+ printHint(`Message ${accent(id)} was already read`);
670
+ } else {
671
+ printSuccess("Marked as read", id);
672
+ }
673
+ } finally {
674
+ client.close();
675
+ }
676
+ }
677
+
678
+ /** agentplate mail reply */
679
+ function handleReply(id: string, opts: ReplyOpts, cwd: string): void {
680
+ const body = opts.body;
681
+ const from = opts.agent ?? opts.from ?? "orchestrator";
682
+
683
+ const client = openClient(cwd);
684
+ try {
685
+ const replyId = client.reply(id, body, from);
686
+
687
+ if (opts.json) {
688
+ jsonOutput("mail reply", { id: replyId });
689
+ } else {
690
+ printSuccess("Reply sent", replyId);
691
+ }
692
+ } finally {
693
+ client.close();
694
+ }
695
+ }
696
+
697
+ /** agentplate mail purge */
698
+ function handlePurge(opts: PurgeOpts, cwd: string): void {
699
+ const all = opts.all ?? false;
700
+ const daysStr = opts.days;
701
+ const agent = opts.agent;
702
+ const json = opts.json ?? false;
703
+
704
+ if (!all && daysStr === undefined && agent === undefined) {
705
+ throw new ValidationError(
706
+ "mail purge requires at least one filter: --all, --days <n>, or --agent <name>",
707
+ { field: "purge" },
708
+ );
709
+ }
710
+
711
+ let olderThanMs: number | undefined;
712
+ if (daysStr !== undefined) {
713
+ const days = Number.parseInt(daysStr, 10);
714
+ if (Number.isNaN(days) || days <= 0) {
715
+ throw new ValidationError("--days must be a positive integer", {
716
+ field: "days",
717
+ value: daysStr,
718
+ });
719
+ }
720
+ olderThanMs = days * 24 * 60 * 60 * 1000;
721
+ }
722
+
723
+ const store = openStore(cwd);
724
+ try {
725
+ const purged = store.purge({ all, olderThanMs, agent });
726
+
727
+ if (json) {
728
+ jsonOutput("mail purge", { purged });
729
+ } else {
730
+ printSuccess(`Purged ${purged} message(s)`);
731
+ }
732
+ } finally {
733
+ store.close();
734
+ }
735
+ }
736
+
737
+ /**
738
+ * Entry point for `agentplate mail <subcommand> [args...]`.
739
+ *
740
+ * Subcommands: send, check, list, read, reply, purge.
741
+ * Uses Commander.js for subcommand routing and option parsing.
742
+ */
743
+ export async function mailCommand(args: string[]): Promise<void> {
744
+ // Resolve the actual project root (handles git worktrees).
745
+ // Mail commands may run from agent worktrees via hooks, so we must
746
+ // resolve up to the main project root where .agentplate/mail.db lives.
747
+ const root = await resolveProjectRoot(process.cwd());
748
+
749
+ const program = new Command();
750
+ program.name("ap mail").description("Agent messaging system").exitOverride();
751
+
752
+ program
753
+ .command("send")
754
+ .description("Send a message")
755
+ .requiredOption("--to <agent>", "Recipient agent name")
756
+ .requiredOption("--subject <text>", "Message subject")
757
+ .requiredOption("--body <text>", "Message body")
758
+ .option("--from <name>", "Sender name")
759
+ .option("--agent <name>", "Alias for --from")
760
+ .option("--type <type>", "Message type", "status")
761
+ .option("--priority <level>", "Priority level", "normal")
762
+ .option("--payload <json>", "Structured JSON payload")
763
+ .option("--json", "Output as JSON")
764
+ .exitOverride()
765
+ .action(async (opts: SendOpts) => {
766
+ await handleSend(opts, root);
767
+ });
768
+
769
+ program
770
+ .command("check")
771
+ .description("Check inbox for one agent and mark unread as read (per-agent scope)")
772
+ .option("--agent <name>", "Agent name (default: orchestrator)")
773
+ .option("--inject", "Inject format for hook context")
774
+ .option("--json", "Output as JSON")
775
+ .option("--debounce <ms>", "Debounce interval in milliseconds")
776
+ .exitOverride()
777
+ .action(async (opts: CheckOpts) => {
778
+ await handleCheck(opts, root);
779
+ });
780
+
781
+ program
782
+ .command("list")
783
+ .description("List messages with filters (system-wide unless --to/--agent given)")
784
+ .option("--from <name>", "Filter by sender")
785
+ .option("--to <name>", "Filter by recipient (scopes to one agent)")
786
+ .option("--agent <name>", "Alias for --to (filter by recipient)")
787
+ .option("--unread", "Show only unread messages (does NOT mark them read)")
788
+ .option("--type <type>", "Filter by message type")
789
+ .option("--json", "Output as JSON")
790
+ .exitOverride()
791
+ .action((opts: ListOpts) => {
792
+ handleList(opts, root);
793
+ });
794
+
795
+ program
796
+ .command("read")
797
+ .description("Mark a message as read")
798
+ .argument("<message-id>", "Message ID")
799
+ .exitOverride()
800
+ .action((id: string) => {
801
+ handleRead(id, root);
802
+ });
803
+
804
+ program
805
+ .command("reply")
806
+ .description("Reply to a message")
807
+ .argument("<message-id>", "Message ID to reply to")
808
+ .requiredOption("--body <text>", "Reply body")
809
+ .option("--from <name>", "Sender name")
810
+ .option("--agent <name>", "Alias for --from")
811
+ .option("--json", "Output as JSON")
812
+ .exitOverride()
813
+ .action((id: string, opts: ReplyOpts) => {
814
+ handleReply(id, opts, root);
815
+ });
816
+
817
+ program
818
+ .command("purge")
819
+ .description("Delete old messages")
820
+ .option("--all", "Purge all messages")
821
+ .option("--days <n>", "Purge messages older than N days")
822
+ .option("--agent <name>", "Purge messages for specific agent")
823
+ .option("--json", "Output as JSON")
824
+ .exitOverride()
825
+ .action((opts: PurgeOpts) => {
826
+ handlePurge(opts, root);
827
+ });
828
+
829
+ try {
830
+ await program.parseAsync(["node", "agentplate-mail", ...args]);
831
+ } catch (err) {
832
+ // `exitOverride()` turns Commander's help paths into thrown
833
+ // CommanderErrors after the help text was already written to stdout.
834
+ // Swallow both the explicit `--help` path (commander.helpDisplayed,
835
+ // exitCode 0) and the missing-subcommand path (commander.help,
836
+ // exitCode 1) — the user got what they asked for.
837
+ if (
838
+ err instanceof CommanderError &&
839
+ (err.code === "commander.helpDisplayed" || err.code === "commander.help")
840
+ ) {
841
+ if (err.exitCode !== 0) {
842
+ process.exitCode = err.exitCode;
843
+ }
844
+ return;
845
+ }
846
+ throw err;
847
+ }
848
+ }