@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,654 @@
1
+ /**
2
+ * CLI command: ap serve [--port <n>] [--host <addr>]
3
+ *
4
+ * Starts an HTTP server backed by Bun.serve. Serves:
5
+ * - /healthz — JSON health envelope (always available)
6
+ * - /api/* — REST handlers registered via registerApiHandler()
7
+ * - /ws — WebSocket upgrade registered via registerWsHandler()
8
+ * - everything else — static files from ui/dist/ with SPA fallback to index.html
9
+ *
10
+ * Route registration is intentionally modular: future streams add REST/WebSocket
11
+ * support by calling the exported register*() helpers — no changes to this file needed.
12
+ */
13
+
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { Command } from "commander";
17
+ import { isPersistentCapability } from "../agents/capabilities.ts";
18
+ import {
19
+ startPersistentMailLoop,
20
+ startTurnRunnerMailLoop,
21
+ type TurnRunnerFn,
22
+ } from "../agents/headless-mail-injector.ts";
23
+ import { createManifestLoader } from "../agents/manifest.ts";
24
+ import { runTurn } from "../agents/turn-runner.ts";
25
+ import { buildRunTurnOptsFactory, isSpawnPerTurnAgent } from "../agents/turn-runner-dispatch.ts";
26
+ import { loadConfig } from "../config.ts";
27
+ import { ValidationError } from "../errors.ts";
28
+ import { apiJson, jsonError, jsonOutput } from "../json.ts";
29
+ import { printError, printSuccess } from "../logging/color.ts";
30
+ import { getConnection } from "../runtimes/connections.ts";
31
+ import { hasNudge } from "../runtimes/headless-connection.ts";
32
+ import { openSessionStore } from "../sessions/compat.ts";
33
+ import type { AgentManifest, AgentplateConfig } from "../types.ts";
34
+ import { ensureUiBuild } from "./serve/build.ts";
35
+ import { type DevServerHandle, startDevServer } from "./serve/dev.ts";
36
+ import { type RestApiDeps, registerRestApi } from "./serve/rest.ts";
37
+ import { serveStatic } from "./serve/static.ts";
38
+ import { installBroadcaster } from "./serve/ws.ts";
39
+
40
+ /**
41
+ * Default TCP port for `ap serve`. 8080 collides with Colima's SSH mux tunnel
42
+ * (and Tomcat/Jenkins/Docker dev proxies); the kernel routes to *:8080 vs.
43
+ * 127.0.0.1:8080 inconsistently, so users saw foreign error JSON instead of
44
+ * the agentplate UI (agentplate-eaba). 7321 is unassigned and easy to type.
45
+ */
46
+ export const DEFAULT_SERVE_PORT = 7321;
47
+
48
+ // === Extensible route registry ===
49
+
50
+ /** Handler for /api/* routes. Return null to fall through to the next handler. */
51
+ export type ApiHandler = (req: Request) => Response | Promise<Response> | null;
52
+
53
+ /** Handler for WebSocket upgrade on /ws. */
54
+ export type WsHandler = {
55
+ open?: (ws: ServerWebSocket) => void;
56
+ message?: (ws: ServerWebSocket, message: string | Buffer) => void;
57
+ close?: (ws: ServerWebSocket, code: number, reason: string) => void;
58
+ /** Return upgrade data (passed to ws.data) or null to reject with HTTP 400. */
59
+ getUpgradeData?: (req: Request) => unknown | null;
60
+ };
61
+
62
+ // ServerWebSocket is a Bun built-in — use the global type alias
63
+ type ServerWebSocket = import("bun").ServerWebSocket<unknown>;
64
+
65
+ const _apiHandlers: ApiHandler[] = [];
66
+ let _wsHandler: WsHandler | undefined;
67
+
68
+ /**
69
+ * Register an API route handler for requests under /api/*.
70
+ * Handlers are tried in registration order; first non-null response wins.
71
+ * Intended for use by future streams (REST endpoints, etc.).
72
+ */
73
+ export function registerApiHandler(handler: ApiHandler): void {
74
+ _apiHandlers.push(handler);
75
+ }
76
+
77
+ /**
78
+ * Register the WebSocket handler for /ws upgrades.
79
+ * Only one handler may be active; subsequent calls replace the previous one.
80
+ * Intended for use by the WebSocket broadcaster stream.
81
+ */
82
+ export function registerWsHandler(handler: WsHandler): void {
83
+ _wsHandler = handler;
84
+ }
85
+
86
+ /** Reset registered handlers (test isolation only). */
87
+ export function _resetHandlers(): void {
88
+ _apiHandlers.length = 0;
89
+ _wsHandler = undefined;
90
+ }
91
+
92
+ // === Core server logic ===
93
+
94
+ export interface ServeOptions {
95
+ port?: number;
96
+ host?: string;
97
+ json?: boolean;
98
+ /** When true, also start the Vite-style dev UI server (HMR, /api+/ws proxy). */
99
+ dev?: boolean;
100
+ /** Dev UI port. Ignored unless dev is true. Default 3000. */
101
+ devPort?: number;
102
+ /**
103
+ * Invoked once after the server is bound and the URL is known. Used by
104
+ * `ap dashboard ui` to open a browser. Awaited, so a slow callback delays
105
+ * nothing critical (the server is already accepting connections).
106
+ */
107
+ onReady?: (url: string) => void | Promise<void>;
108
+ }
109
+
110
+ /** Dependencies injectable for testing. */
111
+ export interface ServeDeps {
112
+ _loadConfig?: typeof loadConfig;
113
+ _existsSync?: typeof existsSync;
114
+ _readFile?: (path: string) => Promise<Uint8Array>;
115
+ /** REST store deps. Pass false to skip REST registration (test isolation). */
116
+ _restDeps?: RestApiDeps | false;
117
+ _ensureUiBuild?: typeof ensureUiBuild;
118
+ _startDevServer?: typeof startDevServer;
119
+ /** Skip the auto-build step entirely (test isolation). */
120
+ _skipAutoBuild?: boolean;
121
+ /**
122
+ * Override ui/dist resolution. Default prefers `<projectRoot>/ui/dist`,
123
+ * falling back to the prebuilt assets shipped inside the npm package. Tests
124
+ * pass a stub returning a non-existent path to force the 503 branch in
125
+ * serveStatic without depending on the dev repo's package layout.
126
+ */
127
+ _resolveUiDistPath?: (projectRoot: string) => string;
128
+ }
129
+
130
+ /**
131
+ * Resolve the directory containing built UI assets. Prefers the project's own
132
+ * `ui/dist` (so agentplate dev builds and project-local UI overrides win), and
133
+ * falls back to the prebuilt `ui/dist` shipped inside @ag-eco/agentplate-cli
134
+ * for production installs that have no `ui/` workspace (agentplate-916d).
135
+ */
136
+ export function resolveUiDistPath(
137
+ projectRoot: string,
138
+ _exists: typeof existsSync = existsSync,
139
+ ): string {
140
+ const projectDist = join(projectRoot, "ui", "dist");
141
+ if (_exists(projectDist)) return projectDist;
142
+ return new URL("../../ui/dist", import.meta.url).pathname;
143
+ }
144
+
145
+ /** Read the package version once at module load to avoid circular imports with index.ts. */
146
+ const _pkgVersion = (): string => {
147
+ try {
148
+ const raw = readFileSync(new URL("../../package.json", import.meta.url).pathname, "utf-8");
149
+ return (JSON.parse(raw) as { version: string }).version;
150
+ } catch {
151
+ return "unknown";
152
+ }
153
+ };
154
+ const SERVE_VERSION = _pkgVersion();
155
+
156
+ /**
157
+ * Build and return a Bun server instance without binding to process signals.
158
+ * Used by tests to control lifecycle directly.
159
+ */
160
+ export async function createServeServer(
161
+ opts: ServeOptions,
162
+ deps: ServeDeps = {},
163
+ ): Promise<ReturnType<typeof Bun.serve>> {
164
+ const _cfg = deps._loadConfig ?? loadConfig;
165
+ const _exists = deps._existsSync ?? existsSync;
166
+
167
+ const cwd = process.cwd();
168
+ const config = await _cfg(cwd);
169
+
170
+ const port = opts.port ?? DEFAULT_SERVE_PORT;
171
+ const hostname = opts.host ?? "127.0.0.1";
172
+ const _resolveUiDist = deps._resolveUiDistPath ?? resolveUiDistPath;
173
+ const uiDistPath = _resolveUiDist(config.project.root);
174
+ const startTime = performance.now();
175
+
176
+ // Register REST handlers before Bun.serve() — skip only for test isolation
177
+ if (deps._restDeps !== false) {
178
+ registerRestApi({ _projectRoot: config.project.root, ...(deps._restDeps ?? {}) });
179
+ }
180
+
181
+ const server = Bun.serve({
182
+ port,
183
+ hostname,
184
+ fetch: async (req: Request, srv: ReturnType<typeof Bun.serve>): Promise<Response> => {
185
+ const url = new URL(req.url);
186
+ const path = url.pathname;
187
+
188
+ // /healthz — always handled here
189
+ if (path === "/healthz") {
190
+ return apiJson({
191
+ status: "ok",
192
+ uptimeMs: Math.round(performance.now() - startTime),
193
+ version: SERVE_VERSION,
194
+ });
195
+ }
196
+
197
+ // /ws — WebSocket upgrade
198
+ if (path === "/ws") {
199
+ if (_wsHandler === undefined) {
200
+ return new Response(
201
+ JSON.stringify({ success: false, command: "serve", error: "WebSocket not available" }),
202
+ { status: 404, headers: { "Content-Type": "application/json" } },
203
+ );
204
+ }
205
+ const upgradeData = _wsHandler.getUpgradeData?.(req);
206
+ if (upgradeData === null) {
207
+ return new Response(
208
+ JSON.stringify({
209
+ success: false,
210
+ command: "serve",
211
+ error: "Missing run or agent query parameter",
212
+ }),
213
+ { status: 400, headers: { "Content-Type": "application/json" } },
214
+ );
215
+ }
216
+ const upgraded = srv.upgrade(req, { data: upgradeData });
217
+ if (upgraded) {
218
+ return new Response(null, { status: 101 });
219
+ }
220
+ return new Response(
221
+ JSON.stringify({ success: false, command: "serve", error: "WebSocket upgrade failed" }),
222
+ { status: 500, headers: { "Content-Type": "application/json" } },
223
+ );
224
+ }
225
+
226
+ // /api/* — delegated to registered API handlers
227
+ if (path.startsWith("/api/")) {
228
+ for (const handler of _apiHandlers) {
229
+ const res = await handler(req);
230
+ if (res !== null) {
231
+ return res;
232
+ }
233
+ }
234
+ return new Response(
235
+ JSON.stringify({ success: false, command: "serve", error: "Not found" }),
236
+ {
237
+ status: 404,
238
+ headers: { "Content-Type": "application/json" },
239
+ },
240
+ );
241
+ }
242
+
243
+ // Static files from ui/dist/ with SPA fallback and path-traversal guard
244
+ return serveStatic(path, uiDistPath, _exists);
245
+ },
246
+ websocket: {
247
+ open(ws) {
248
+ _wsHandler?.open?.(ws);
249
+ },
250
+ message(ws, message) {
251
+ _wsHandler?.message?.(ws, message as string | Buffer);
252
+ },
253
+ close(ws, code, reason) {
254
+ _wsHandler?.close?.(ws, code, reason);
255
+ },
256
+ },
257
+ });
258
+
259
+ return server;
260
+ }
261
+
262
+ /**
263
+ * Install per-agent mail injection loops, driven by filesystem discovery of
264
+ * stdin FIFOs.
265
+ *
266
+ * Replaces the UserPromptSubmit hook for headless Claude agents: each agent
267
+ * spawned by `ap sling` mkfifos a `{agentplateDir}/agents/{name}/stdin.fifo`,
268
+ * and `ap serve` watches that directory. For every FIFO it sees, the server
269
+ * starts a polling loop that opens the FIFO, writes any unread mail as a
270
+ * stream-json user turn, then closes. Loops are torn down when the FIFO file
271
+ * disappears (agent terminated + cleanup ran), when the writer reports
272
+ * "no-reader" (agent died but cleanup hasn't run), or on graceful shutdown.
273
+ *
274
+ * The cross-process design — file-on-disk vs in-memory registry — is essential
275
+ * because `ap sling` and `ap serve` are separate processes. The earlier
276
+ * connection-registry design only worked when serve and sling shared a process,
277
+ * which is never the case in production. See agentplate-41eb.
278
+ */
279
+ /** Optional spawn-per-turn dispatch context for `installMailInjectors`. */
280
+ export interface MailInjectorDispatchDeps {
281
+ config: AgentplateConfig;
282
+ manifest: AgentManifest;
283
+ /** Test injection: replaces `runTurn`. */
284
+ _runTurnFn?: TurnRunnerFn;
285
+ }
286
+
287
+ /**
288
+ * Attempt to start the spawn-per-turn dispatcher for one agent. Returns the
289
+ * stop function on success, or null when the agent is not eligible (capability
290
+ * gate, flag off, terminal state, missing session row, or runtime can't drive
291
+ * a direct spawn).
292
+ */
293
+ function tryInstallTurnRunnerLoop(
294
+ agentName: string,
295
+ mailDbPath: string,
296
+ agentplateDir: string,
297
+ dispatch: MailInjectorDispatchDeps,
298
+ ): (() => void) | null {
299
+ const { store } = openSessionStore(agentplateDir);
300
+ let session: ReturnType<typeof store.getByName>;
301
+ try {
302
+ session = store.getByName(agentName);
303
+ } finally {
304
+ store.close();
305
+ }
306
+ if (!session) return null;
307
+
308
+ let factory: ReturnType<typeof buildRunTurnOptsFactory>;
309
+ try {
310
+ factory = buildRunTurnOptsFactory({
311
+ session,
312
+ config: dispatch.config,
313
+ manifest: dispatch.manifest,
314
+ agentplateDir,
315
+ });
316
+ } catch {
317
+ return null;
318
+ }
319
+
320
+ if (!isSpawnPerTurnAgent(session, dispatch.config, factory.runtime)) return null;
321
+
322
+ const runTurnFn = dispatch._runTurnFn ?? runTurn;
323
+ // Per-tick liveness check: re-read SessionStore on every poll so that
324
+ // `ap stop` (which writes state=completed within ~milliseconds) is observed
325
+ // before the 5s rescan reaps the loop. Without this guard, the 2s tick
326
+ // could dispatch a fresh runTurn against the stopped agent during the
327
+ // rescan window (agentplate-eb7c).
328
+ const isAgentLive = (): boolean => {
329
+ const { store: liveStore } = openSessionStore(agentplateDir);
330
+ try {
331
+ const live = liveStore.getByName(agentName);
332
+ if (!live) return false;
333
+ return live.state !== "completed" && live.state !== "zombie";
334
+ } finally {
335
+ liveStore.close();
336
+ }
337
+ };
338
+ return startTurnRunnerMailLoop(
339
+ agentName,
340
+ factory.build,
341
+ runTurnFn,
342
+ mailDbPath,
343
+ undefined,
344
+ isAgentLive,
345
+ );
346
+ }
347
+
348
+ /**
349
+ * Attempt to start a persistent-capability mail loop for one agent. Returns
350
+ * the stop function on success, or null when the agent is not eligible —
351
+ * either it's not a persistent capability (coordinator/orchestrator/monitor),
352
+ * or no in-process headless connection is registered (tmux-mode persistent
353
+ * agents receive mail through `nudgeAgent` -> tmux send-keys, not stdin).
354
+ *
355
+ * This closes the mail-delivery gap for headless coordinators spawned by
356
+ * `ap serve` (agentplate-b03a). The spawn-per-turn loop only handles
357
+ * task-scoped capabilities; without this fallback, mail to the coordinator
358
+ * lands in `mail.db` but never reaches its stdin.
359
+ */
360
+ function tryInstallPersistentMailLoop(
361
+ agentName: string,
362
+ mailDbPath: string,
363
+ agentplateDir: string,
364
+ ): (() => void) | null {
365
+ const { store } = openSessionStore(agentplateDir);
366
+ let session: ReturnType<typeof store.getByName>;
367
+ try {
368
+ session = store.getByName(agentName);
369
+ } finally {
370
+ store.close();
371
+ }
372
+ if (!session) return null;
373
+ if (!isPersistentCapability(session.capability)) return null;
374
+
375
+ // Headless persistent agents have an empty tmuxSession AND a registered
376
+ // HeadlessClaudeConnection (set by spawnHeadlessAgent). Tmux-mode persistent
377
+ // agents have a non-empty tmuxSession and no registered connection — they
378
+ // receive mail through tmux send-keys via `ap nudge` and don't need this loop.
379
+ if (session.tmuxSession !== "") return null;
380
+
381
+ const conn = getConnection(agentName);
382
+ if (conn === undefined || !hasNudge(conn)) return null;
383
+
384
+ const isAgentLive = (): boolean => {
385
+ const { store: liveStore } = openSessionStore(agentplateDir);
386
+ try {
387
+ const live = liveStore.getByName(agentName);
388
+ if (!live) return false;
389
+ return live.state !== "completed" && live.state !== "zombie";
390
+ } finally {
391
+ liveStore.close();
392
+ }
393
+ };
394
+
395
+ return startPersistentMailLoop(agentName, mailDbPath, {
396
+ getConn: (name) => {
397
+ const c = getConnection(name);
398
+ return c !== undefined && hasNudge(c) ? c : undefined;
399
+ },
400
+ isAgentLive,
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Install per-agent mail injection loops driven by the spawn-per-turn engine.
406
+ *
407
+ * Discovers agents from SessionStore (rather than from a per-agent FIFO file
408
+ * — Phase 3 deletes the FIFO infrastructure). Sessions in non-terminal state
409
+ * with a task-scoped capability get a `runTurn`-driven mail dispatcher; loops
410
+ * auto-stop when the session transitions to `completed`/`zombie`.
411
+ *
412
+ * `dispatch` is required: under Phase 3 spawn-per-turn is the only mail
413
+ * injection mechanism for headless Claude. When called without dispatch (e.g.
414
+ * in tests that don't exercise mail), the function still returns a no-op stop.
415
+ */
416
+ export function installMailInjectors(
417
+ mailDbPath: string,
418
+ agentplateDir: string,
419
+ dispatch?: MailInjectorDispatchDeps,
420
+ ): () => void {
421
+ const activeLoops = new Map<string, () => void>();
422
+
423
+ const startLoopFor = (agentName: string): void => {
424
+ if (activeLoops.has(agentName)) return;
425
+ // Spawn-per-turn loop for task-scoped capabilities (builder/scout/...)
426
+ // only when manifest dispatch context is available.
427
+ const turnLoop = dispatch
428
+ ? tryInstallTurnRunnerLoop(agentName, mailDbPath, agentplateDir, dispatch)
429
+ : null;
430
+ // Persistent-capability loop for headless coordinator/orchestrator/monitor
431
+ // — runs even without `dispatch` since it doesn't need the manifest.
432
+ const persistentLoop =
433
+ turnLoop === null ? tryInstallPersistentMailLoop(agentName, mailDbPath, agentplateDir) : null;
434
+ const loop = turnLoop ?? persistentLoop;
435
+ if (loop === null) return;
436
+ activeLoops.set(agentName, () => {
437
+ loop();
438
+ activeLoops.delete(agentName);
439
+ });
440
+ };
441
+
442
+ const stopLoopFor = (agentName: string): void => {
443
+ activeLoops.get(agentName)?.();
444
+ };
445
+
446
+ // Discover non-terminal agents from SessionStore. Each rescan re-checks
447
+ // every agent's state so loops auto-stop on completed/zombie.
448
+ //
449
+ // Errors during scan are absorbed — when the project root has been torn
450
+ // down (e.g. test cleanup races a still-running rescan timer), opening
451
+ // the session store throws SQLiteError. Letting that propagate would
452
+ // surface as an unhandled "between tests" error and fail unrelated tests.
453
+ const scan = (): void => {
454
+ let sessions: ReturnType<ReturnType<typeof openSessionStore>["store"]["getAll"]>;
455
+ try {
456
+ const { store } = openSessionStore(agentplateDir);
457
+ try {
458
+ sessions = store.getAll();
459
+ } finally {
460
+ store.close();
461
+ }
462
+ } catch {
463
+ return;
464
+ }
465
+ const liveNames = new Set<string>();
466
+ for (const session of sessions) {
467
+ if (session.state === "completed" || session.state === "zombie") continue;
468
+ liveNames.add(session.agentName);
469
+ startLoopFor(session.agentName);
470
+ }
471
+ // Reap loops whose sessions transitioned to a terminal state.
472
+ for (const name of [...activeLoops.keys()]) {
473
+ if (!liveNames.has(name)) {
474
+ stopLoopFor(name);
475
+ }
476
+ }
477
+ };
478
+ scan();
479
+
480
+ const rescanTimer = setInterval(scan, 5000);
481
+
482
+ return function stopMailInjectors(): void {
483
+ clearInterval(rescanTimer);
484
+ for (const stop of [...activeLoops.values()]) stop();
485
+ activeLoops.clear();
486
+ };
487
+ }
488
+
489
+ /**
490
+ * Core implementation for `ap serve`. Starts the server and blocks until
491
+ * SIGINT/SIGTERM. Handles graceful shutdown.
492
+ */
493
+ export async function runServe(opts: ServeOptions, deps: ServeDeps = {}): Promise<void> {
494
+ const _cfg = deps._loadConfig ?? loadConfig;
495
+ const config = await _cfg(process.cwd());
496
+
497
+ const agentplateDir = join(config.project.root, ".agentplate");
498
+ const mailDbPath = join(agentplateDir, "mail.db");
499
+ const uiDir = join(config.project.root, "ui");
500
+
501
+ // Production mode: ensure ui/dist is current before binding the port.
502
+ // In dev mode, skip the prebuilt assets entirely — the dev server owns
503
+ // the UI surface and reads ui/src directly.
504
+ const _ensureUi = deps._ensureUiBuild ?? ensureUiBuild;
505
+ if (!opts.dev && deps._skipAutoBuild !== true) {
506
+ await _ensureUi({ uiDir });
507
+ }
508
+
509
+ // Install broadcaster before Bun.serve so handler is ready for the first request
510
+ const stopBroadcaster = installBroadcaster({
511
+ eventsDbPath: join(agentplateDir, "events.db"),
512
+ mailDbPath,
513
+ });
514
+
515
+ // Install per-agent mail injection loops (UserPromptSubmit hook equivalent
516
+ // for headless Claude agents). Discovers task-scoped agents from
517
+ // SessionStore and dispatches each batch of unread mail through `runTurn`,
518
+ // which spawns a fresh claude with --resume per turn (Phase 3 spawn-per-turn).
519
+ const manifestLoader = createManifestLoader(
520
+ join(config.project.root, config.agents.manifestPath),
521
+ join(config.project.root, config.agents.baseDir),
522
+ );
523
+ let manifest: AgentManifest | undefined;
524
+ try {
525
+ manifest = await manifestLoader.load();
526
+ } catch {
527
+ // Non-fatal: missing manifest just means the spawn-per-turn dispatcher
528
+ // stays disabled and every agent uses the legacy FIFO loop.
529
+ manifest = undefined;
530
+ }
531
+ const stopMailInjectors = installMailInjectors(
532
+ mailDbPath,
533
+ agentplateDir,
534
+ manifest ? { config, manifest } : undefined,
535
+ );
536
+
537
+ const server = await createServeServer(opts, deps);
538
+
539
+ let dev: DevServerHandle | undefined;
540
+ if (opts.dev) {
541
+ const _startDev = deps._startDevServer ?? startDevServer;
542
+ dev = await _startDev({
543
+ uiDir,
544
+ port: opts.devPort ?? 3000,
545
+ apiPort: server.port,
546
+ apiHost: server.hostname,
547
+ });
548
+ }
549
+
550
+ const useJson = opts.json ?? false;
551
+ const apiUrl = `http://${server.hostname}:${server.port}`;
552
+ if (useJson) {
553
+ jsonOutput("serve", {
554
+ status: "started",
555
+ port: server.port,
556
+ hostname: server.hostname,
557
+ url: apiUrl,
558
+ ...(dev ? { devUrl: `http://127.0.0.1:${dev.port}` } : {}),
559
+ });
560
+ } else {
561
+ printSuccess(`ap serve listening on ${apiUrl}`);
562
+ if (dev) {
563
+ printSuccess(`ap serve dev UI on http://127.0.0.1:${dev.port}`);
564
+ }
565
+ }
566
+
567
+ // Notify the caller now that the server is bound. `ap dashboard ui` uses
568
+ // this to open a browser. In dev mode prefer the HMR URL the operator
569
+ // actually browses; otherwise the API server also serves the SPA.
570
+ if (opts.onReady) {
571
+ const readyUrl = dev ? `http://127.0.0.1:${dev.port}` : apiUrl;
572
+ await opts.onReady(readyUrl);
573
+ }
574
+
575
+ // Graceful shutdown handler
576
+ const shutdown = (): void => {
577
+ if (!useJson) {
578
+ process.stdout.write("\nShutting down...\n");
579
+ }
580
+ // Stop the dev server first so the upstream WebSocket pump drains
581
+ // before we tear down the broadcaster + main server.
582
+ const stopDev = dev ? dev.stop() : Promise.resolve();
583
+ stopDev
584
+ .catch(() => {
585
+ // Best-effort stop — surface nothing on failure.
586
+ })
587
+ .finally(() => {
588
+ stopMailInjectors();
589
+ stopBroadcaster();
590
+ server.stop(true);
591
+ process.exit(0);
592
+ });
593
+ };
594
+
595
+ process.on("SIGINT", shutdown);
596
+ process.on("SIGTERM", shutdown);
597
+
598
+ // Block indefinitely — the server keeps the process alive via Bun's event loop
599
+ await new Promise<void>(() => {});
600
+ }
601
+
602
+ /**
603
+ * Create the Commander command for `ap serve`.
604
+ */
605
+ export function createServeCommand(): Command {
606
+ return new Command("serve")
607
+ .description("Start the HTTP server (static UI + /healthz + /api/* + /ws)")
608
+ .option("--port <n>", "TCP port to listen on", String(DEFAULT_SERVE_PORT))
609
+ .option("--host <addr>", "Host/address to bind", "127.0.0.1")
610
+ .option("--dev", "Also start the dev UI server with HMR + API/WS proxy")
611
+ .option("--dev-port <n>", "Dev UI port (only with --dev)", "3000")
612
+ .option("--json", "Output startup info as JSON")
613
+ .action(
614
+ async (opts: {
615
+ port?: string;
616
+ host?: string;
617
+ dev?: boolean;
618
+ devPort?: string;
619
+ json?: boolean;
620
+ }) => {
621
+ const port = opts.port !== undefined ? Number.parseInt(opts.port, 10) : DEFAULT_SERVE_PORT;
622
+ const devPort = opts.devPort !== undefined ? Number.parseInt(opts.devPort, 10) : 3000;
623
+ try {
624
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
625
+ throw new ValidationError(`Invalid port: ${opts.port ?? "undefined"}`, {
626
+ field: "port",
627
+ value: opts.port,
628
+ });
629
+ }
630
+ if (Number.isNaN(devPort) || devPort < 1 || devPort > 65535) {
631
+ throw new ValidationError(`Invalid dev port: ${opts.devPort ?? "undefined"}`, {
632
+ field: "devPort",
633
+ value: opts.devPort,
634
+ });
635
+ }
636
+ await runServe({
637
+ port,
638
+ host: opts.host ?? "127.0.0.1",
639
+ json: opts.json,
640
+ dev: opts.dev ?? false,
641
+ devPort,
642
+ });
643
+ } catch (err: unknown) {
644
+ const msg = err instanceof Error ? err.message : String(err);
645
+ if (opts.json) {
646
+ jsonError("serve", msg);
647
+ } else {
648
+ printError(`ap serve failed: ${msg}`);
649
+ }
650
+ process.exitCode = 1;
651
+ }
652
+ },
653
+ );
654
+ }