@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,721 @@
1
+ /**
2
+ * Tmux session management for agentplate agent workers.
3
+ *
4
+ * All operations use Bun.spawn to call the tmux CLI directly.
5
+ * Session naming convention: `agentplate-{projectName}-{agentName}`.
6
+ * The project name prefix prevents cross-project tmux session collisions
7
+ * and enables project-scoped cleanup (agentplate-pcef).
8
+ */
9
+
10
+ import { dirname, resolve } from "node:path";
11
+ import { AgentError } from "../errors.ts";
12
+ import type { ReadyState } from "../runtimes/types.ts";
13
+
14
+ /**
15
+ * Dedicated tmux server socket name for agent session isolation.
16
+ *
17
+ * All agentplate agent sessions use `tmux -L agentplate` so they run on a
18
+ * separate server from the user's personal tmux. This prevents user tmux
19
+ * config (themes, plugins, keybindings) from interfering with agent spawn.
20
+ * See GitHub #93.
21
+ */
22
+ export const TMUX_SOCKET = "agentplate";
23
+
24
+ /**
25
+ * Sanitize a name component for use in tmux session names.
26
+ *
27
+ * Tmux interprets dots (.) as session.window.pane separators and colons (:)
28
+ * as session:window separators in target strings (`-t`). If a project name
29
+ * contains these characters (e.g., "consulting.hgoudat.com"), the session
30
+ * is created fine but subsequent lookups via `-t` parse the dots as delimiters
31
+ * and fail to find the session. Replace both with underscores.
32
+ */
33
+ export function sanitizeTmuxName(name: string): string {
34
+ return name.replace(/[.:]/g, "_");
35
+ }
36
+
37
+ /**
38
+ * Build a tmux command array with the dedicated server socket.
39
+ * All agent session operations should use this to ensure isolation.
40
+ */
41
+ function tmuxCmd(...args: string[]): string[] {
42
+ return ["tmux", "-L", TMUX_SOCKET, ...args];
43
+ }
44
+
45
+ /**
46
+ * Detect the directory containing the agentplate binary.
47
+ *
48
+ * Tries `which ap` first (the short alias), then falls back to
49
+ * `which agentplate` (the original name). Both are registered in
50
+ * package.json bin, but depending on how the tool was installed
51
+ * (bun link, npm link, global install), only one may be on PATH.
52
+ *
53
+ * Returns null if detection fails.
54
+ */
55
+ async function detectAgentplateBinDir(): Promise<string | null> {
56
+ // Try both command names — the alias migration may leave only one resolvable
57
+ for (const cmdName of ["ap", "agentplate"]) {
58
+ try {
59
+ const proc = Bun.spawn(["which", cmdName], {
60
+ stdout: "pipe",
61
+ stderr: "pipe",
62
+ });
63
+ const exitCode = await proc.exited;
64
+ if (exitCode === 0) {
65
+ const binPath = (await new Response(proc.stdout).text()).trim();
66
+ if (binPath.length > 0) {
67
+ return dirname(resolve(binPath));
68
+ }
69
+ }
70
+ } catch {
71
+ // which not available or command not on PATH — try next
72
+ }
73
+ }
74
+
75
+ // Fallback: if process.argv[1] points to agentplate's own entry point (src/index.ts),
76
+ // derive the bin dir from the bun binary that's running it
77
+ const scriptPath = process.argv[1];
78
+ if (scriptPath?.includes("agentplate")) {
79
+ const bunPath = process.argv[0];
80
+ if (bunPath) {
81
+ return dirname(resolve(bunPath));
82
+ }
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ /**
89
+ * Run a shell command and capture its output.
90
+ */
91
+ async function runCommand(
92
+ cmd: string[],
93
+ cwd?: string,
94
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
95
+ const proc = Bun.spawn(cmd, {
96
+ cwd,
97
+ stdout: "pipe",
98
+ stderr: "pipe",
99
+ });
100
+ const stdout = await new Response(proc.stdout).text();
101
+ const stderr = await new Response(proc.stderr).text();
102
+ const exitCode = await proc.exited;
103
+ return { stdout, stderr, exitCode };
104
+ }
105
+
106
+ /**
107
+ * Create a new detached tmux session running the given command.
108
+ *
109
+ * @param name - Session name (e.g., "agentplate-myproject-auth-login")
110
+ * @param cwd - Working directory for the session
111
+ * @param command - Command to execute inside the session
112
+ * @param env - Optional environment variables to export in the session
113
+ * @returns The PID of the tmux server process for this session
114
+ * @throws AgentError if tmux is not installed or session creation fails
115
+ */
116
+ export async function createSession(
117
+ name: string,
118
+ cwd: string,
119
+ command: string,
120
+ env?: Record<string, string>,
121
+ maxRetries = 3,
122
+ ): Promise<number> {
123
+ // Build environment exports for the tmux session
124
+ const exports: string[] = [];
125
+
126
+ // Ensure PATH includes the agentplate binary directory
127
+ // so that hooks calling `agentplate` inside the session can find it
128
+ const agentplateBinDir = await detectAgentplateBinDir();
129
+ if (agentplateBinDir) {
130
+ exports.push(`export PATH="${agentplateBinDir}:$PATH"`);
131
+ }
132
+
133
+ // Clear Claude Code nesting guard so child agents can start.
134
+ // Claude Code >=2.1.66 sets CLAUDECODE=1 and refuses to launch when it's present.
135
+ // Agentplate's agent spawning is intentional, not accidental nesting.
136
+ exports.push("unset CLAUDECODE CLAUDE_CODE_SSE_PORT CLAUDE_CODE_ENTRYPOINT");
137
+
138
+ // Add any additional environment variables
139
+ if (env) {
140
+ for (const [key, value] of Object.entries(env)) {
141
+ exports.push(`export ${key}="${value}"`);
142
+ }
143
+ }
144
+
145
+ // Build the startup script using bash syntax (export/unset).
146
+ // Then wrap it in `/bin/bash -c '...'` so it always runs in bash,
147
+ // regardless of the user's $SHELL. Without this, tmux uses the user's
148
+ // default shell (e.g. fish), which rejects bash export/unset syntax and
149
+ // causes the session to die instantly. Single-quote wrapping with escaped
150
+ // single quotes prevents any intermediate shell from expanding variables
151
+ // before bash receives them. (GitHub #86)
152
+ //
153
+ // The `exec` prefix replaces the bash wrapper with the spawned command
154
+ // so there is no separate wrapper PID to orphan if the tmux server dies
155
+ // externally. Without exec, bash receives SIGHUP on tmux teardown but its
156
+ // claude child gets reparented to init and continues running. With exec,
157
+ // the wrapper IS the command — SIGHUP is delivered directly to claude.
158
+ // (agentplate-505d)
159
+ const startupScript =
160
+ exports.length > 0 ? `${exports.join(" && ")} && exec ${command}` : `exec ${command}`;
161
+ const wrappedCommand = `/bin/bash -c '${startupScript.replace(/'/g, "'\\''")}'`;
162
+
163
+ const { exitCode, stderr } = await runCommand(
164
+ tmuxCmd("new-session", "-d", "-s", name, "-c", cwd, wrappedCommand),
165
+ cwd,
166
+ );
167
+
168
+ if (exitCode !== 0) {
169
+ throw new AgentError(`Failed to create tmux session "${name}": ${stderr.trim()}`, {
170
+ agentName: name,
171
+ });
172
+ }
173
+
174
+ // Retrieve the actual PID of the process running inside the tmux pane.
175
+ // Retry up to maxRetries times with backoff for WSL2 race conditions where
176
+ // the session exists but the pane hasn't been registered yet (#73).
177
+ let pidResult: { stdout: string; stderr: string; exitCode: number } | undefined;
178
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
179
+ pidResult = await runCommand(tmuxCmd("list-panes", "-t", name, "-F", "#{pane_pid}"));
180
+ if (pidResult.exitCode === 0) break;
181
+ await Bun.sleep(250 * (attempt + 1));
182
+ }
183
+
184
+ if (!pidResult || pidResult.exitCode !== 0) {
185
+ throw new AgentError(
186
+ `Created tmux session "${name}" but failed to retrieve PID: ${pidResult?.stderr.trim() ?? "unknown error"}`,
187
+ { agentName: name },
188
+ );
189
+ }
190
+
191
+ const pidStr = pidResult.stdout.trim().split("\n")[0];
192
+ if (pidStr) {
193
+ const pid = Number.parseInt(pidStr, 10);
194
+ if (!Number.isNaN(pid)) {
195
+ return pid;
196
+ }
197
+ }
198
+
199
+ throw new AgentError(`Created tmux session "${name}" but could not find its pane PID`, {
200
+ agentName: name,
201
+ });
202
+ }
203
+
204
+ /**
205
+ * List all active tmux sessions.
206
+ *
207
+ * @returns Array of session name/pid pairs
208
+ * @throws AgentError if tmux is not installed
209
+ */
210
+ export async function listSessions(): Promise<Array<{ name: string; pid: number }>> {
211
+ const { exitCode, stdout, stderr } = await runCommand(
212
+ tmuxCmd("list-sessions", "-F", "#{session_name}:#{pid}"),
213
+ );
214
+
215
+ // Exit code 1 with "no server running" means no sessions exist — not an error
216
+ if (exitCode !== 0) {
217
+ if (stderr.includes("no server running") || stderr.includes("no sessions")) {
218
+ return [];
219
+ }
220
+ throw new AgentError(`Failed to list tmux sessions: ${stderr.trim()}`);
221
+ }
222
+
223
+ const sessions: Array<{ name: string; pid: number }> = [];
224
+ const lines = stdout.trim().split("\n");
225
+
226
+ for (const line of lines) {
227
+ if (line.trim() === "") continue;
228
+ const sepIndex = line.indexOf(":");
229
+ if (sepIndex === -1) continue;
230
+
231
+ const name = line.slice(0, sepIndex);
232
+ const pidStr = line.slice(sepIndex + 1);
233
+ if (name && pidStr) {
234
+ const pid = Number.parseInt(pidStr, 10);
235
+ if (!Number.isNaN(pid)) {
236
+ sessions.push({ name, pid });
237
+ }
238
+ }
239
+ }
240
+
241
+ return sessions;
242
+ }
243
+
244
+ /**
245
+ * Grace period (ms) between SIGTERM and SIGKILL during process cleanup.
246
+ */
247
+ const KILL_GRACE_PERIOD_MS = 2000;
248
+
249
+ /**
250
+ * Get the pane PID for a tmux session.
251
+ *
252
+ * @param name - Tmux session name
253
+ * @returns The PID of the process running in the session's pane, or null if
254
+ * the session doesn't exist or the PID can't be determined
255
+ */
256
+ export async function getPanePid(name: string): Promise<number | null> {
257
+ const { exitCode, stdout } = await runCommand(
258
+ tmuxCmd("display-message", "-p", "-t", name, "#{pane_pid}"),
259
+ );
260
+
261
+ if (exitCode !== 0) {
262
+ return null;
263
+ }
264
+
265
+ const pidStr = stdout.trim();
266
+ if (pidStr.length === 0) {
267
+ return null;
268
+ }
269
+
270
+ const pid = Number.parseInt(pidStr, 10);
271
+ return Number.isNaN(pid) ? null : pid;
272
+ }
273
+
274
+ /**
275
+ * Recursively collect all descendant PIDs of a given process.
276
+ *
277
+ * Uses `pgrep -P <pid>` to find direct children, then recurses into each child.
278
+ * Returns PIDs in depth-first order (deepest descendants first), which is the
279
+ * correct order for sending signals — kill children before parents so processes
280
+ * don't get reparented to init (PID 1).
281
+ *
282
+ * @param pid - The root process PID to walk from
283
+ * @returns Array of descendant PIDs, deepest-first
284
+ */
285
+ export async function getDescendantPids(pid: number): Promise<number[]> {
286
+ const { exitCode, stdout } = await runCommand(["pgrep", "-P", String(pid)]);
287
+
288
+ // pgrep exits 1 when no children found — not an error
289
+ if (exitCode !== 0 || stdout.trim().length === 0) {
290
+ return [];
291
+ }
292
+
293
+ const childPids: number[] = [];
294
+ for (const line of stdout.trim().split("\n")) {
295
+ const childPid = Number.parseInt(line.trim(), 10);
296
+ if (!Number.isNaN(childPid)) {
297
+ childPids.push(childPid);
298
+ }
299
+ }
300
+
301
+ // Recurse into each child to get their descendants first (depth-first)
302
+ const allDescendants: number[] = [];
303
+ for (const childPid of childPids) {
304
+ const grandchildren = await getDescendantPids(childPid);
305
+ allDescendants.push(...grandchildren);
306
+ }
307
+
308
+ // Append the direct children after their descendants (deepest-first order)
309
+ allDescendants.push(...childPids);
310
+
311
+ return allDescendants;
312
+ }
313
+
314
+ /**
315
+ * Check if a process is still alive.
316
+ *
317
+ * @param pid - Process ID to check
318
+ * @returns true if the process exists, false otherwise
319
+ */
320
+ export function isProcessAlive(pid: number): boolean {
321
+ try {
322
+ // signal 0 doesn't send a signal but checks if the process exists
323
+ process.kill(pid, 0);
324
+ return true;
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Kill a process tree: SIGTERM deepest-first, wait grace period, SIGKILL survivors.
332
+ *
333
+ * Follows gastown's KillSessionWithProcesses pattern:
334
+ * 1. Walk descendant tree from the root PID
335
+ * 2. Send SIGTERM to all descendants (deepest-first so children die before parents)
336
+ * 3. Wait a grace period for processes to clean up
337
+ * 4. Send SIGKILL to any survivors
338
+ *
339
+ * Handles edge cases:
340
+ * - Already-dead processes (ESRCH) — silently ignored
341
+ * - Reparented processes (PPID=1) — caught in the initial tree walk
342
+ * - Permission errors — silently ignored (process belongs to another user)
343
+ *
344
+ * @param rootPid - The root PID whose descendants should be killed
345
+ * @param gracePeriodMs - Time to wait between SIGTERM and SIGKILL (default 2000ms)
346
+ */
347
+ export async function killProcessTree(
348
+ rootPid: number,
349
+ gracePeriodMs: number = KILL_GRACE_PERIOD_MS,
350
+ ): Promise<void> {
351
+ const descendants = await getDescendantPids(rootPid);
352
+
353
+ if (descendants.length === 0) {
354
+ // No descendants — just try to kill the root process
355
+ sendSignal(rootPid, "SIGTERM");
356
+ return;
357
+ }
358
+
359
+ // Phase 1: SIGTERM all descendants (deepest-first, then root)
360
+ for (const pid of descendants) {
361
+ sendSignal(pid, "SIGTERM");
362
+ }
363
+ sendSignal(rootPid, "SIGTERM");
364
+
365
+ // Phase 2: Wait grace period for processes to clean up
366
+ await Bun.sleep(gracePeriodMs);
367
+
368
+ // Phase 3: SIGKILL any survivors (same order: deepest-first, then root)
369
+ for (const pid of descendants) {
370
+ if (isProcessAlive(pid)) {
371
+ sendSignal(pid, "SIGKILL");
372
+ }
373
+ }
374
+ if (isProcessAlive(rootPid)) {
375
+ sendSignal(rootPid, "SIGKILL");
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Send a signal to a process, ignoring errors for already-dead or inaccessible processes.
381
+ *
382
+ * @param pid - Process ID to signal
383
+ * @param signal - Signal name (e.g., "SIGTERM", "SIGKILL")
384
+ */
385
+ function sendSignal(pid: number, signal: "SIGTERM" | "SIGKILL"): void {
386
+ try {
387
+ process.kill(pid, signal);
388
+ } catch {
389
+ // Process already dead (ESRCH), permission denied (EPERM), or invalid PID — all OK
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Kill a tmux session by name, with proper process tree cleanup.
395
+ *
396
+ * Before killing the tmux session, walks the descendant process tree from the
397
+ * pane PID, sends SIGTERM to all descendants (deepest-first), waits a grace
398
+ * period, then sends SIGKILL to survivors. This ensures child processes
399
+ * (git, bun test, biome, etc.) are properly cleaned up rather than being
400
+ * orphaned or reparented to init.
401
+ *
402
+ * @param name - Session name to kill
403
+ * @throws AgentError if the tmux session cannot be killed (process cleanup
404
+ * failures are silently handled since the goal is best-effort cleanup)
405
+ */
406
+ export async function killSession(name: string): Promise<void> {
407
+ // Defense in depth: an empty session name passed to `tmux -t` is prefix-matched
408
+ // against every session in the server, wildcard-killing the entire agentplate
409
+ // swarm (agentplate-74ce). Reject empty names at the boundary so a regression in
410
+ // any caller surfaces loudly instead of silently nuking the tmux server.
411
+ if (name === "") {
412
+ throw new AgentError(
413
+ "killSession called with empty session name (would wildcard-kill all tmux sessions due to prefix matching)",
414
+ { agentName: name },
415
+ );
416
+ }
417
+
418
+ // Step 1: Get the pane PID before killing the tmux session
419
+ const panePid = await getPanePid(name);
420
+
421
+ // Step 2: If we have a pane PID, walk and kill the process tree
422
+ if (panePid !== null) {
423
+ await killProcessTree(panePid);
424
+ }
425
+
426
+ // Step 3: Kill the tmux session itself
427
+ const { exitCode, stderr } = await runCommand(tmuxCmd("kill-session", "-t", name));
428
+
429
+ if (exitCode !== 0) {
430
+ // If the session is already gone (e.g., died during process cleanup), that's fine
431
+ if (stderr.includes("session not found") || stderr.includes("can't find session")) {
432
+ return;
433
+ }
434
+ throw new AgentError(`Failed to kill tmux session "${name}": ${stderr.trim()}`, {
435
+ agentName: name,
436
+ });
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Detect the current tmux session name.
442
+ *
443
+ * Returns the session name if running inside tmux, null otherwise.
444
+ * Used by `agentplate prime` to register the orchestrator's tmux session
445
+ * so agents can nudge the orchestrator when they have results.
446
+ */
447
+ export async function getCurrentSessionName(): Promise<string | null> {
448
+ if (!process.env.TMUX) {
449
+ return null;
450
+ }
451
+ const { exitCode, stdout } = await runCommand([
452
+ "tmux",
453
+ "display-message",
454
+ "-p",
455
+ "#{session_name}",
456
+ ]);
457
+ if (exitCode !== 0) {
458
+ return null;
459
+ }
460
+ const name = stdout.trim();
461
+ return name.length > 0 ? name : null;
462
+ }
463
+
464
+ /**
465
+ * Check whether a tmux session is still alive.
466
+ *
467
+ * @param name - Session name to check
468
+ * @returns true if the session exists, false otherwise
469
+ */
470
+ export async function isSessionAlive(name: string): Promise<boolean> {
471
+ // Defense in depth: an empty `-t` argument is prefix-matched against every
472
+ // session, so `has-session` would return true whenever any agentplate session
473
+ // exists. Treat empty as "not alive" without contacting tmux (agentplate-74ce).
474
+ if (name === "") {
475
+ return false;
476
+ }
477
+ const { exitCode } = await runCommand(tmuxCmd("has-session", "-t", name));
478
+ return exitCode === 0;
479
+ }
480
+
481
+ /**
482
+ * Detailed session state for distinguishing failure modes.
483
+ *
484
+ * - `"alive"` -- tmux session exists and is reachable.
485
+ * - `"dead"` -- tmux server is running but the session does not exist.
486
+ * - `"no_server"` -- tmux server is not running at all.
487
+ */
488
+ export type SessionState = "alive" | "dead" | "no_server";
489
+
490
+ /**
491
+ * Check tmux session state with detailed failure mode reporting.
492
+ *
493
+ * Unlike `isSessionAlive()` which returns a simple boolean, this function
494
+ * distinguishes between three states:
495
+ * - `"alive"`: session exists -- the agent may still be running.
496
+ * - `"dead"`: tmux server is running but session is gone -- agent exited or was killed.
497
+ * - `"no_server"`: tmux server itself is not running -- all sessions are gone.
498
+ *
499
+ * Callers can use this to provide targeted error messages and decide whether
500
+ * stale session records should be cleaned up vs flagged as errors.
501
+ *
502
+ * @param name - Session name to check
503
+ * @returns The session state
504
+ */
505
+ export async function checkSessionState(name: string): Promise<SessionState> {
506
+ const { exitCode, stderr } = await runCommand(tmuxCmd("has-session", "-t", name));
507
+ if (exitCode === 0) return "alive";
508
+ if (stderr.includes("no server running") || stderr.includes("no sessions")) {
509
+ return "no_server";
510
+ }
511
+ return "dead";
512
+ }
513
+
514
+ /**
515
+ * Capture the visible content of a tmux session's pane.
516
+ *
517
+ * @param name - Session name to capture from
518
+ * @param lines - Number of history lines to capture (default 50)
519
+ * @returns The trimmed pane content, or null if capture fails
520
+ */
521
+ export async function capturePaneContent(name: string, lines = 50): Promise<string | null> {
522
+ const { exitCode, stdout } = await runCommand(
523
+ tmuxCmd("capture-pane", "-t", name, "-p", "-S", `-${lines}`),
524
+ );
525
+ if (exitCode !== 0) {
526
+ return null;
527
+ }
528
+ const content = stdout.trim();
529
+ return content.length > 0 ? content : null;
530
+ }
531
+
532
+ /**
533
+ * Wait for a tmux session's TUI to become ready for input.
534
+ *
535
+ * Delegates all readiness detection to the provided `detectReady` callback,
536
+ * making this function runtime-agnostic. The callback inspects pane content
537
+ * and returns a ReadyState phase: "loading" (keep waiting), "dialog" (send
538
+ * the requested action, then continue), or "ready" (return true).
539
+ *
540
+ * Dialog actions that type raw text (for example Claude Code's `type:2`
541
+ * bypass confirmation) are retried if the same dialog is still visible on
542
+ * later polls. This avoids one-shot startup flakes when tmux or the TUI drops
543
+ * the first keypress during initialization.
544
+ *
545
+ * @param name - Tmux session name to poll
546
+ * @param detectReady - Callback that inspects pane content and returns ReadyState
547
+ * @param timeoutMs - Maximum time to wait before giving up (default 30s)
548
+ * @param pollIntervalMs - Time between polls (default 500ms)
549
+ * @returns true once detectReady returns { phase: "ready" }, false on timeout or dead session
550
+ */
551
+ export async function waitForTuiReady(
552
+ name: string,
553
+ detectReady: (paneContent: string) => ReadyState,
554
+ timeoutMs = 30_000,
555
+ pollIntervalMs = 500,
556
+ ): Promise<boolean> {
557
+ const maxAttempts = Math.ceil(timeoutMs / pollIntervalMs);
558
+ const handledDialogs = new Map<string, number>();
559
+ const typedDialogRetryPolls = Math.max(2, Math.ceil(1_000 / pollIntervalMs));
560
+
561
+ for (let i = 0; i < maxAttempts; i++) {
562
+ const content = await capturePaneContent(name);
563
+ if (content !== null) {
564
+ const state = detectReady(content);
565
+
566
+ if (state.phase === "dialog") {
567
+ const lastHandledAttempt = handledDialogs.get(state.action);
568
+ const shouldRetryTypedDialog =
569
+ state.action.startsWith("type:") &&
570
+ lastHandledAttempt !== undefined &&
571
+ i - lastHandledAttempt >= typedDialogRetryPolls;
572
+ const shouldHandleDialog = lastHandledAttempt === undefined || shouldRetryTypedDialog;
573
+
574
+ if (shouldHandleDialog) {
575
+ await handleDialogAction(name, state.action, pollIntervalMs);
576
+ handledDialogs.set(state.action, i);
577
+ await Bun.sleep(pollIntervalMs);
578
+ continue;
579
+ }
580
+ }
581
+
582
+ if (state.phase === "ready") {
583
+ return true;
584
+ }
585
+ }
586
+
587
+ const alive = await isSessionAlive(name);
588
+ if (!alive) {
589
+ return false;
590
+ }
591
+ await Bun.sleep(pollIntervalMs);
592
+ }
593
+ return false;
594
+ }
595
+
596
+ /**
597
+ * Verify that tmux is installed and executable.
598
+ * Throws AgentError with a clear message if tmux is not available.
599
+ */
600
+ export async function ensureTmuxAvailable(): Promise<void> {
601
+ const { exitCode } = await runCommand(["tmux", "-V"]);
602
+ if (exitCode !== 0) {
603
+ throw new AgentError(
604
+ "tmux is not installed or not on PATH. Install tmux to use agentplate agent orchestration.",
605
+ );
606
+ }
607
+ }
608
+
609
+ /**
610
+ * Send keys to a tmux session, with retry for WSL2 pane registration race.
611
+ *
612
+ * On WSL2, tmux occasionally reports "can't find pane" immediately after session
613
+ * creation even though the session exists. This is a timing issue where the pane
614
+ * hasn't been fully registered yet. We retry with backoff to handle this.
615
+ *
616
+ * @param name - Session name to send keys to
617
+ * @param keys - The keys/text to send
618
+ * @param maxRetries - Maximum retry attempts for transient pane errors (default 3)
619
+ * @throws AgentError if the session does not exist or send fails after retries
620
+ */
621
+ export async function sendKeys(name: string, keys: string, maxRetries = 3): Promise<void> {
622
+ // Flatten newlines to spaces — multiline text via tmux send-keys causes
623
+ // Claude Code's TUI to receive embedded Enter keystrokes which prevent
624
+ // the final "Enter" from triggering message submission (agentplate-y2ob).
625
+ const flatKeys = keys.replace(/\n/g, " ");
626
+
627
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
628
+ const { exitCode, stderr } = await runCommand(
629
+ tmuxCmd("send-keys", "-t", name, flatKeys, "Enter"),
630
+ );
631
+
632
+ if (exitCode === 0) {
633
+ return;
634
+ }
635
+
636
+ const trimmedStderr = stderr.trim();
637
+
638
+ if (trimmedStderr.includes("no server running")) {
639
+ throw new AgentError(
640
+ `Tmux server is not running (cannot reach session "${name}"). This often happens when running as root (UID 0) or when tmux crashed. Original error: ${trimmedStderr}`,
641
+ { agentName: name },
642
+ );
643
+ }
644
+
645
+ // "can't find pane" is a transient race condition on WSL2 — the session
646
+ // exists but the pane hasn't been fully registered yet. Retry with backoff.
647
+ if (trimmedStderr.includes("can't find pane") || trimmedStderr.includes("cant find pane")) {
648
+ if (attempt < maxRetries) {
649
+ const delayMs = 250 * (attempt + 1);
650
+ await Bun.sleep(delayMs);
651
+ continue;
652
+ }
653
+ // Exhausted retries — report as pane-specific error
654
+ throw new AgentError(
655
+ `Tmux pane for session "${name}" not found after ${maxRetries + 1} attempts. On WSL2, this can indicate a tmux startup race condition. Try increasing the retry count or adding a delay after session creation.`,
656
+ { agentName: name },
657
+ );
658
+ }
659
+
660
+ if (
661
+ trimmedStderr.includes("session not found") ||
662
+ trimmedStderr.includes("can't find session") ||
663
+ trimmedStderr.includes("cant find session")
664
+ ) {
665
+ throw new AgentError(
666
+ `Tmux session "${name}" does not exist. The agent may have crashed or been killed before receiving input.`,
667
+ { agentName: name },
668
+ );
669
+ }
670
+
671
+ throw new AgentError(`Failed to send keys to tmux session "${name}": ${trimmedStderr}`, {
672
+ agentName: name,
673
+ });
674
+ }
675
+ }
676
+
677
+ async function sendRawKeys(name: string, keys: string): Promise<void> {
678
+ const flatKeys = keys.replace(/\n/g, " ");
679
+ const { exitCode, stderr } = await runCommand(tmuxCmd("send-keys", "-t", name, flatKeys));
680
+
681
+ if (exitCode !== 0) {
682
+ const trimmedStderr = stderr.trim();
683
+
684
+ if (trimmedStderr.includes("no server running")) {
685
+ throw new AgentError(
686
+ `Tmux server is not running (cannot reach session "${name}"). This often happens when running as root (UID 0) or when tmux crashed. Original error: ${trimmedStderr}`,
687
+ { agentName: name },
688
+ );
689
+ }
690
+
691
+ if (
692
+ trimmedStderr.includes("session not found") ||
693
+ trimmedStderr.includes("can't find session") ||
694
+ trimmedStderr.includes("cant find session")
695
+ ) {
696
+ throw new AgentError(
697
+ `Tmux session "${name}" does not exist. The agent may have crashed or been killed before receiving input.`,
698
+ { agentName: name },
699
+ );
700
+ }
701
+
702
+ throw new AgentError(`Failed to send keys to tmux session "${name}": ${trimmedStderr}`, {
703
+ agentName: name,
704
+ });
705
+ }
706
+ }
707
+
708
+ async function handleDialogAction(
709
+ name: string,
710
+ action: string,
711
+ pollIntervalMs: number,
712
+ ): Promise<void> {
713
+ if (action.startsWith("type:")) {
714
+ await sendRawKeys(name, action.slice("type:".length));
715
+ await Bun.sleep(Math.min(pollIntervalMs, 250));
716
+ await sendKeys(name, "");
717
+ return;
718
+ }
719
+
720
+ await sendKeys(name, action === "Enter" ? "" : action);
721
+ }