@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,1351 @@
1
+ /**
2
+ * CLI command: ap sling <task-id>
3
+ *
4
+ * CRITICAL PATH. Orchestrates a full agent spawn:
5
+ * 1. Load config + manifest
6
+ * 2. Validate (depth limit, hierarchy)
7
+ * 3. Load manifest + validate capability
8
+ * 4. Resolve or create run_id (current-run.txt)
9
+ * 5. Check name uniqueness + concurrency limit
10
+ * 6. Validate task exists
11
+ * 7. Create worktree
12
+ * 8. Generate + write overlay CLAUDE.md
13
+ * 9. Deploy hooks config
14
+ * 10. Claim task issue
15
+ * 11. Create agent identity
16
+ * 12. Create tmux session running claude
17
+ * 13. Record session in SessionStore + increment run agent count
18
+ * 14. Return AgentSession
19
+ */
20
+
21
+ import { mkdir } from "node:fs/promises";
22
+ import { join, resolve } from "node:path";
23
+ import { buildInitialHeadlessPrompt, formatMailSection } from "../agents/headless-prompt.ts";
24
+ import { createIdentity, loadIdentity } from "../agents/identity.ts";
25
+ import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
26
+ import { writeOverlay } from "../agents/overlay.ts";
27
+ import { runTurn } from "../agents/turn-runner.ts";
28
+ import { loadConfig } from "../config.ts";
29
+ import { AgentError, HierarchyError, ValidationError } from "../errors.ts";
30
+ import { inferDomain } from "../insights/analyzer.ts";
31
+ import { jsonOutput } from "../json.ts";
32
+ import { createLoamClient } from "../loam/client.ts";
33
+ import { printSuccess } from "../logging/color.ts";
34
+ import { createMailClient } from "../mail/client.ts";
35
+ import { createMailStore } from "../mail/store.ts";
36
+ import { getRuntime } from "../runtimes/registry.ts";
37
+ import { openSessionStore } from "../sessions/compat.ts";
38
+ import { createRunStore } from "../sessions/store.ts";
39
+ import type { TrackerIssue } from "../tracker/factory.ts";
40
+ import { createTrackerClient, resolveBackend, trackerCliName } from "../tracker/factory.ts";
41
+ import { createTrellisClient } from "../trellis/client.ts";
42
+ import type { AgentplateConfig, AgentSession, OverlayConfig } from "../types.ts";
43
+ import { createWorktree, rollbackWorktree } from "../worktree/manager.ts";
44
+ import {
45
+ capturePaneContent,
46
+ checkSessionState,
47
+ createSession,
48
+ ensureTmuxAvailable,
49
+ isSessionAlive,
50
+ killSession,
51
+ sanitizeTmuxName,
52
+ sendKeys,
53
+ waitForTuiReady,
54
+ } from "../worktree/tmux.ts";
55
+
56
+ /**
57
+ * Calculate how many milliseconds to sleep before spawning a new agent,
58
+ * based on the configured stagger delay and when the most recent active
59
+ * session was started.
60
+ *
61
+ * Returns 0 if no sleep is needed (no active sessions, delay is 0, or
62
+ * enough time has already elapsed).
63
+ *
64
+ * @param staggerDelayMs - The configured minimum delay between spawns
65
+ * @param activeSessions - Currently active (non-zombie) sessions
66
+ * @param now - Current timestamp in ms (defaults to Date.now(), injectable for testing)
67
+ */
68
+ export function calculateStaggerDelay(
69
+ staggerDelayMs: number,
70
+ activeSessions: ReadonlyArray<{ startedAt: string }>,
71
+ now: number = Date.now(),
72
+ ): number {
73
+ if (staggerDelayMs <= 0 || activeSessions.length === 0) {
74
+ return 0;
75
+ }
76
+
77
+ const mostRecent = activeSessions.reduce((latest, s) => {
78
+ return new Date(s.startedAt).getTime() > new Date(latest.startedAt).getTime() ? s : latest;
79
+ });
80
+ const elapsed = now - new Date(mostRecent.startedAt).getTime();
81
+ const remaining = staggerDelayMs - elapsed;
82
+ return remaining > 0 ? remaining : 0;
83
+ }
84
+
85
+ /**
86
+ * Generate a unique agent name from capability and taskId.
87
+ * Base: capability-taskId. If that collides with takenNames,
88
+ * appends -2, -3, etc. up to 100. Falls back to -Date.now() for guaranteed uniqueness.
89
+ */
90
+ export function generateAgentName(
91
+ capability: string,
92
+ taskId: string,
93
+ takenNames: readonly string[],
94
+ ): string {
95
+ const base = `${capability}-${taskId}`;
96
+ if (!takenNames.includes(base)) {
97
+ return base;
98
+ }
99
+ for (let i = 2; i <= 100; i++) {
100
+ const candidate = `${base}-${i}`;
101
+ if (!takenNames.includes(candidate)) {
102
+ return candidate;
103
+ }
104
+ }
105
+ return `${base}-${Date.now()}`;
106
+ }
107
+
108
+ /**
109
+ * Check if the current process is running as root (UID 0).
110
+ * Returns true if running as root, false otherwise.
111
+ * Returns false on platforms that don't support getuid (e.g., Windows).
112
+ *
113
+ * The getuid parameter is injectable for testability without mocking process.getuid.
114
+ */
115
+ export function isRunningAsRoot(getuid: (() => number) | undefined = process.getuid): boolean {
116
+ return getuid?.() === 0;
117
+ }
118
+
119
+ /**
120
+ * Infer loam domains from a list of file paths.
121
+ * Returns unique domains sorted alphabetically, falling back to
122
+ * configured defaults if no domains could be inferred.
123
+ */
124
+ export function inferDomainsFromFiles(
125
+ files: readonly string[],
126
+ configDomains: readonly string[],
127
+ ): string[] {
128
+ const inferred = new Set<string>();
129
+ for (const file of files) {
130
+ const domain = inferDomain(file);
131
+ if (domain !== null) {
132
+ inferred.add(domain);
133
+ }
134
+ }
135
+ if (inferred.size === 0) {
136
+ return [...configDomains];
137
+ }
138
+ return [...inferred].sort();
139
+ }
140
+
141
+ export interface SlingOptions {
142
+ capability?: string;
143
+ name?: string;
144
+ spec?: string;
145
+ files?: string;
146
+ parent?: string;
147
+ depth?: string;
148
+ skipScout?: boolean;
149
+ skipTaskCheck?: boolean;
150
+ forceHierarchy?: boolean;
151
+ json?: boolean;
152
+ maxAgents?: string;
153
+ skipReview?: boolean;
154
+ dispatchMaxAgents?: string;
155
+ runtime?: string;
156
+ noScoutCheck?: boolean;
157
+ baseBranch?: string;
158
+ profile?: string;
159
+ headless?: boolean;
160
+ recover?: boolean;
161
+ /**
162
+ * Comma-separated list of sibling agent names dispatched in parallel that
163
+ * may share file scope with this agent (agentplate-f76a). Plumbed through
164
+ * to `OverlayConfig.siblings` so the overlay renders rebase-before-merge_ready
165
+ * guidance.
166
+ */
167
+ siblings?: string;
168
+ }
169
+
170
+ /**
171
+ * Parse the `--siblings <names>` argument into a normalized string array.
172
+ * Trims whitespace, drops empty entries. Empty / undefined input → `[]`.
173
+ *
174
+ * Exported for unit-testing.
175
+ */
176
+ export function parseSiblings(raw: string | undefined): string[] {
177
+ if (!raw) return [];
178
+ return raw
179
+ .split(",")
180
+ .map((s) => s.trim())
181
+ .filter((s) => s.length > 0);
182
+ }
183
+
184
+ const WORKABLE_STATUSES = ["open", "in_progress"] as const;
185
+
186
+ /**
187
+ * Decide whether a task with the given tracker status can accept a fresh
188
+ * sling. Normal dispatch requires an `open` or `in_progress` task; passing
189
+ * `recover` accepts any status so a coordinator can re-dispatch against a
190
+ * task whose previous owner exited (e.g. closed by a dead lead). (agentplate-629f)
191
+ */
192
+ export function isTaskWorkable(status: string, recover: boolean): boolean {
193
+ if (recover) return true;
194
+ return (WORKABLE_STATUSES as readonly string[]).includes(status);
195
+ }
196
+
197
+ /**
198
+ * Resolve the effective `parentAgent` for a sling invocation, preserving the
199
+ * prior session's link on a re-spawn (`--recover`) when `--parent` was not
200
+ * explicitly passed.
201
+ *
202
+ * Pre-fix, sling always read `opts.parent ?? null` and upserted that into the
203
+ * session row, overwriting the prior `parent_agent` with null whenever a
204
+ * coordinator/lead invoked `ap sling --recover --name <existing>` without
205
+ * threading `--parent`. The runner then read `parentAgent === null` and
206
+ * skipped its in-band `worker_died` notify on a resumed-turn parser stall —
207
+ * the lead waited forever on a signal that never came (agentplate-de3c).
208
+ *
209
+ * Resolution rules:
210
+ * - **Explicit caller intent wins.** If `opts.parent` is defined (including
211
+ * an empty string), use it verbatim. The caller may legitimately want to
212
+ * change or clear the parent on re-spawn.
213
+ * - **Caller silence preserves linkage.** If `opts.parent` is undefined and
214
+ * a prior session row exists with a non-null `parentAgent`, fall back to
215
+ * the prior value. Otherwise return null.
216
+ *
217
+ * Pure function so the regression test in `sling.test.ts` can assert behavior
218
+ * without spinning up the full sling command pipeline.
219
+ */
220
+ export function resolveParentAgent(
221
+ optsParent: string | undefined,
222
+ existingSession: { parentAgent: string | null } | null,
223
+ ): string | null {
224
+ if (optsParent !== undefined) {
225
+ return optsParent;
226
+ }
227
+ return existingSession?.parentAgent ?? null;
228
+ }
229
+
230
+ export interface AutoDispatchOptions {
231
+ agentName: string;
232
+ taskId: string;
233
+ capability: string;
234
+ specPath: string | null;
235
+ parentAgent: string | null;
236
+ /**
237
+ * The agent who invoked `ap sling` (from `AGENTPLATE_AGENT_NAME` env var);
238
+ * takes precedence over `parentAgent` for the mail `from` field, since
239
+ * `--parent` describes the new agent's hierarchical parent, not the slinger.
240
+ */
241
+ slingerName: string | null;
242
+ instructionPath: string;
243
+ }
244
+
245
+ /**
246
+ * Build a structured auto-dispatch mail message for a newly slung agent.
247
+ *
248
+ * Sending this mail before creating the tmux session ensures it exists
249
+ * in the DB when SessionStart fires, eliminating the race where dispatch
250
+ * mail arrives after the agent boots and sits idle forever.
251
+ */
252
+ export function buildAutoDispatch(opts: AutoDispatchOptions): {
253
+ from: string;
254
+ to: string;
255
+ subject: string;
256
+ body: string;
257
+ } {
258
+ const from = opts.slingerName ?? opts.parentAgent ?? "orchestrator";
259
+ const specLine = opts.specPath
260
+ ? `Spec file: ${opts.specPath}`
261
+ : "No spec file provided. Check your overlay for task details.";
262
+ const body = [
263
+ `You have been assigned task ${opts.taskId} as a ${opts.capability} agent.`,
264
+ specLine,
265
+ `Read your overlay at ${opts.instructionPath} and begin immediately.`,
266
+ ].join(" ");
267
+
268
+ return {
269
+ from,
270
+ to: opts.agentName,
271
+ subject: `Dispatch: ${opts.taskId}`,
272
+ body,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Options for building the structured startup beacon.
278
+ */
279
+ export interface BeaconOptions {
280
+ agentName: string;
281
+ capability: string;
282
+ taskId: string;
283
+ parentAgent: string | null;
284
+ depth: number;
285
+ instructionPath: string;
286
+ }
287
+
288
+ /**
289
+ * Build a structured startup beacon for an agent.
290
+ *
291
+ * The beacon is the first user message sent to a Claude Code agent via
292
+ * tmux send-keys. It provides identity context and a numbered startup
293
+ * protocol so the agent knows exactly what to do on boot.
294
+ *
295
+ * Format:
296
+ * [AGENTPLATE] <agent-name> (<capability>) <ISO timestamp> task:<task-id>
297
+ * Depth: <n> | Parent: <parent-name|none>
298
+ * Startup protocol:
299
+ * 1. Read your assignment in .claude/CLAUDE.md
300
+ * 2. Load expertise: loam prime
301
+ * 3. Check mail: ap mail check --agent <name>
302
+ * 4. Begin working on task <task-id>
303
+ */
304
+ export function buildBeacon(opts: BeaconOptions): string {
305
+ const timestamp = new Date().toISOString();
306
+ const parent = opts.parentAgent ?? "none";
307
+ const parts = [
308
+ `[AGENTPLATE] ${opts.agentName} (${opts.capability}) ${timestamp} task:${opts.taskId}`,
309
+ `Depth: ${opts.depth} | Parent: ${parent}`,
310
+ `Startup: read ${opts.instructionPath}, run loam prime, check mail (ap mail check --agent ${opts.agentName}), then begin task ${opts.taskId}`,
311
+ ];
312
+ return parts.join(" — ");
313
+ }
314
+
315
+ /**
316
+ * Check if a parent agent has spawned any scouts.
317
+ * Returns true if the parent has at least one scout child in the session history.
318
+ */
319
+ export function parentHasScouts(
320
+ sessions: ReadonlyArray<{ parentAgent: string | null; capability: string }>,
321
+ parentAgent: string,
322
+ ): boolean {
323
+ return sessions.some((s) => s.parentAgent === parentAgent && s.capability === "scout");
324
+ }
325
+
326
+ /**
327
+ * Determine whether to emit the scout-before-build warning.
328
+ *
329
+ * Returns true when all of the following hold:
330
+ * - The incoming capability is "builder" (only builders trigger the check)
331
+ * - A parent agent is set (orphaned builders don't trigger it)
332
+ * - The parent has not yet spawned any scouts
333
+ * - noScoutCheck is false (caller has not suppressed the warning)
334
+ * - skipScout is false (the lead is not intentionally running without scouts)
335
+ *
336
+ * Extracted from slingCommand for testability (agentplate-6eyw).
337
+ *
338
+ * @param capability - The requested agent capability
339
+ * @param parentAgent - The --parent flag value (null = coordinator/human)
340
+ * @param sessions - All sessions (not just active) for parentHasScouts query
341
+ * @param noScoutCheck - True when --no-scout-check flag is set
342
+ * @param skipScout - True when --skip-scout flag is set (lead opted out of scouting)
343
+ */
344
+ export function shouldShowScoutWarning(
345
+ capability: string,
346
+ parentAgent: string | null,
347
+ sessions: ReadonlyArray<{ parentAgent: string | null; capability: string }>,
348
+ noScoutCheck: boolean,
349
+ skipScout: boolean,
350
+ ): boolean {
351
+ if (capability !== "builder") return false;
352
+ if (parentAgent === null) return false;
353
+ if (noScoutCheck) return false;
354
+ if (skipScout) return false;
355
+ return !parentHasScouts(sessions, parentAgent);
356
+ }
357
+
358
+ /**
359
+ * Resolve which canonical repo directories should be writable to an
360
+ * interactive agent runtime in addition to its worktree sandbox.
361
+ *
362
+ * All interactive agents need `.agentplate` so they can access shared mail,
363
+ * metrics, and session state. Only `lead` agents need canonical `.git`
364
+ * because they can spawn child worktrees from inside the runtime.
365
+ *
366
+ * @param projectRoot - Absolute path to the canonical repository root
367
+ * @param capability - Capability being launched
368
+ */
369
+ export function getSharedWritableDirs(projectRoot: string, capability: string): string[] {
370
+ const sharedWritableDirs = [join(projectRoot, ".agentplate")];
371
+
372
+ if (capability === "lead") {
373
+ sharedWritableDirs.push(join(projectRoot, ".git"));
374
+ }
375
+
376
+ return sharedWritableDirs;
377
+ }
378
+
379
+ /**
380
+ * Check if any active agent is already working on the given task ID.
381
+ * Returns the agent name if locked, or null if the task is free.
382
+ *
383
+ * @param activeSessions - Currently active (non-zombie) sessions
384
+ * @param taskId - The task ID to check for concurrent work
385
+ */
386
+ export function checkTaskLock(
387
+ activeSessions: ReadonlyArray<{ agentName: string; taskId: string }>,
388
+ taskId: string,
389
+ ): string | null {
390
+ const existing = activeSessions.find((s) => s.taskId === taskId);
391
+ return existing?.agentName ?? null;
392
+ }
393
+
394
+ /**
395
+ * Check if an active lead agent is already assigned to the given task ID.
396
+ * Returns the lead agent name if found, or null if no active lead exists.
397
+ *
398
+ * This prevents the duplicate-lead anti-pattern where two leads run
399
+ * simultaneously on the same bead, causing duplicate work streams and
400
+ * wasted tokens (agentplate-gktc postmortem).
401
+ *
402
+ * Only checks sessions with capability "lead". Builder/scout children
403
+ * working the same bead (via parent delegation) do not trigger this check.
404
+ *
405
+ * @param activeSessions - Currently active (non-zombie, non-completed) sessions
406
+ * @param taskId - The task ID to check for an existing lead
407
+ */
408
+ export function checkDuplicateLead(
409
+ activeSessions: ReadonlyArray<{ agentName: string; taskId: string; capability: string }>,
410
+ taskId: string,
411
+ ): string | null {
412
+ const existing = activeSessions.find((s) => s.taskId === taskId && s.capability === "lead");
413
+ return existing?.agentName ?? null;
414
+ }
415
+
416
+ /**
417
+ * Check if spawning another agent would exceed the per-run session limit.
418
+ * Returns true if the limit is reached. A limit of 0 means unlimited.
419
+ *
420
+ * @param maxSessionsPerRun - Config limit (0 = unlimited)
421
+ * @param currentRunAgentCount - Number of agents already spawned in this run
422
+ */
423
+ export function checkRunSessionLimit(
424
+ maxSessionsPerRun: number,
425
+ currentRunAgentCount: number,
426
+ ): boolean {
427
+ if (maxSessionsPerRun <= 0) return false;
428
+ return currentRunAgentCount >= maxSessionsPerRun;
429
+ }
430
+
431
+ /**
432
+ * Check if a parent agent has reached its per-lead child ceiling.
433
+ * Returns true if the limit is reached. A limit of 0 means unlimited.
434
+ *
435
+ * @param activeSessions - Currently active (non-zombie) sessions
436
+ * @param parentAgent - The parent agent name to count children for
437
+ * @param maxAgentsPerLead - Config or CLI limit (0 = unlimited)
438
+ */
439
+ export function checkParentAgentLimit(
440
+ activeSessions: ReadonlyArray<{ parentAgent: string | null }>,
441
+ parentAgent: string,
442
+ maxAgentsPerLead: number,
443
+ ): boolean {
444
+ if (maxAgentsPerLead <= 0) return false;
445
+ const count = activeSessions.filter((s) => s.parentAgent === parentAgent).length;
446
+ return count >= maxAgentsPerLead;
447
+ }
448
+
449
+ /**
450
+ * Validate hierarchy constraints for direct coordinator/human spawns.
451
+ *
452
+ * When parentAgent is null, the caller is the coordinator or a human.
453
+ * Direct spawns are allowed for "lead", "scout", and "builder".
454
+ * Other capabilities (reviewer, merger, etc.) must be spawned by a lead
455
+ * that passes --parent.
456
+ *
457
+ * @param parentAgent - The --parent flag value (null = coordinator/human)
458
+ * @param capability - The requested agent capability
459
+ * @param name - The agent name (for error context)
460
+ * @param depth - The requested hierarchy depth
461
+ * @param forceHierarchy - If true, bypass the check (for debugging)
462
+ * @throws HierarchyError if the constraint is violated
463
+ */
464
+ export function validateHierarchy(
465
+ parentAgent: string | null,
466
+ capability: string,
467
+ name: string,
468
+ _depth: number,
469
+ forceHierarchy: boolean,
470
+ ): void {
471
+ if (forceHierarchy) {
472
+ return;
473
+ }
474
+
475
+ const directSpawnCapabilities = ["lead", "scout", "builder"];
476
+ if (parentAgent === null && !directSpawnCapabilities.includes(capability)) {
477
+ throw new HierarchyError(
478
+ `Coordinator cannot spawn "${capability}" directly. Only lead, scout, and builder are allowed without --parent. Use a lead as intermediary, or pass --force-hierarchy to bypass.`,
479
+ { agentName: name, requestedCapability: capability },
480
+ );
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Extract loam record IDs and their domains from loam prime output text.
486
+ * Parses the markdown structure produced by lm prime: domain headings
487
+ * (## <name>) followed by record lines containing (mx-XXXXXX) identifiers.
488
+ * @param primeText - The output text from lm prime
489
+ * @returns Array of {id, domain} pairs. Deduplicated.
490
+ */
491
+ export function extractLoamRecordIds(primeText: string): Array<{ id: string; domain: string }> {
492
+ const results: Array<{ id: string; domain: string }> = [];
493
+ const seen = new Set<string>();
494
+ let currentDomain = "";
495
+
496
+ for (const line of primeText.split("\n")) {
497
+ const domainMatch = line.match(/^## ([\w-]+)/);
498
+ if (domainMatch) {
499
+ currentDomain = domainMatch[1] ?? "";
500
+ continue;
501
+ }
502
+ if (currentDomain) {
503
+ const idRegex = /\(mx-([a-f0-9]+)\)/g;
504
+ let match = idRegex.exec(line);
505
+ while (match !== null) {
506
+ const shortId = match[1] ?? "";
507
+ if (shortId) {
508
+ const key = `${currentDomain}:mx-${shortId}`;
509
+ if (!seen.has(key)) {
510
+ seen.add(key);
511
+ results.push({ id: `mx-${shortId}`, domain: currentDomain });
512
+ }
513
+ }
514
+ match = idRegex.exec(line);
515
+ }
516
+ }
517
+ }
518
+ return results;
519
+ }
520
+
521
+ /**
522
+ * Get the current git branch name for the repo at the given path.
523
+ *
524
+ * Returns null if in detached HEAD state, the directory is not a git repo,
525
+ * or git exits non-zero.
526
+ *
527
+ * @param repoRoot - Absolute path to the git repository root
528
+ */
529
+ export async function getCurrentBranch(repoRoot: string): Promise<string | null> {
530
+ const proc = Bun.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
531
+ cwd: repoRoot,
532
+ stdout: "pipe",
533
+ stderr: "pipe",
534
+ });
535
+ const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
536
+ if (exitCode !== 0) return null;
537
+ const branch = stdout.trim();
538
+ // "HEAD" is returned when in detached HEAD state
539
+ if (branch === "HEAD" || branch === "") return null;
540
+ return branch;
541
+ }
542
+
543
+ /**
544
+ * Resolve whether to use the headless spawn path for a given runtime + flags + config.
545
+ *
546
+ * Precedence (highest first):
547
+ * 1. runtime.headless === true (statically headless runtimes always use headless)
548
+ * 2. Explicit --headless / --no-headless flag (boolean | undefined from commander)
549
+ * 3. config.runtime.claudeHeadlessByDefault (only applies when runtime.id === "claude")
550
+ * 4. Default: false (tmux)
551
+ *
552
+ * Throws ValidationError when --headless is explicitly true but the runtime has no
553
+ * buildDirectSpawn implementation.
554
+ */
555
+ export function resolveUseHeadless(
556
+ runtime: { id: string; headless?: boolean; buildDirectSpawn?: unknown },
557
+ flag: boolean | undefined,
558
+ config: AgentplateConfig,
559
+ ): boolean {
560
+ if (runtime.headless === true) return true;
561
+
562
+ if (flag === true) {
563
+ if (typeof runtime.buildDirectSpawn !== "function") {
564
+ throw new ValidationError(
565
+ `--headless requires a runtime with headless support. Runtime "${runtime.id}" does not implement buildDirectSpawn.`,
566
+ { field: "headless", value: true },
567
+ );
568
+ }
569
+ return true;
570
+ }
571
+ if (flag === false) return false;
572
+
573
+ if (runtime.id === "claude" && config.runtime?.claudeHeadlessByDefault === true) {
574
+ if (typeof runtime.buildDirectSpawn !== "function") return false;
575
+ return true;
576
+ }
577
+
578
+ return false;
579
+ }
580
+
581
+ /**
582
+ * Entry point for `ap sling <task-id> [flags]`.
583
+ *
584
+ * @param taskId - The task ID to assign to the agent
585
+ * @param opts - Command options
586
+ */
587
+ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<void> {
588
+ if (!taskId) {
589
+ throw new ValidationError("Task ID is required: ap sling <task-id>", {
590
+ field: "taskId",
591
+ });
592
+ }
593
+
594
+ const capability = opts.capability ?? "builder";
595
+ const rawName = opts.name?.trim() ?? "";
596
+ const nameWasAutoGenerated = rawName.length === 0;
597
+ let name = nameWasAutoGenerated ? `${capability}-${taskId}` : rawName;
598
+ const specPath = opts.spec ?? null;
599
+ const filesRaw = opts.files;
600
+ // Reassigned later when re-spawning an existing agent to preserve the prior
601
+ // row's parentAgent — see agentplate-de3c at the existingSession lookup below.
602
+ let parentAgent = opts.parent ?? null;
603
+ const depthStr = opts.depth;
604
+ const depth = depthStr !== undefined ? Number.parseInt(depthStr, 10) : 0;
605
+ const forceHierarchy = opts.forceHierarchy ?? false;
606
+ const skipScout = opts.skipScout ?? false;
607
+ const skipTaskCheck = opts.skipTaskCheck ?? false;
608
+ const recover = opts.recover ?? false;
609
+
610
+ if (Number.isNaN(depth) || depth < 0) {
611
+ throw new ValidationError("--depth must be a non-negative integer", {
612
+ field: "depth",
613
+ value: depthStr,
614
+ });
615
+ }
616
+
617
+ if (isRunningAsRoot()) {
618
+ throw new AgentError(
619
+ "Cannot spawn agents as root (UID 0). The claude CLI rejects --permission-mode bypassPermissions when run as root, causing the tmux session to die immediately. Run agentplate as a non-root user.",
620
+ { agentName: name },
621
+ );
622
+ }
623
+
624
+ if (opts.maxAgents !== undefined) {
625
+ const parsed = Number.parseInt(opts.maxAgents, 10);
626
+ if (Number.isNaN(parsed) || parsed < 0) {
627
+ throw new ValidationError("--max-agents must be a non-negative integer", {
628
+ field: "maxAgents",
629
+ value: opts.maxAgents,
630
+ });
631
+ }
632
+ }
633
+
634
+ if (opts.dispatchMaxAgents !== undefined) {
635
+ const parsed = Number.parseInt(opts.dispatchMaxAgents, 10);
636
+ if (Number.isNaN(parsed) || parsed < 0) {
637
+ throw new ValidationError("--dispatch-max-agents must be a non-negative integer", {
638
+ field: "dispatchMaxAgents",
639
+ value: opts.dispatchMaxAgents,
640
+ });
641
+ }
642
+ }
643
+
644
+ // Warn if --skip-scout is used for a non-lead capability (harmless but confusing)
645
+ if (skipScout && capability !== "lead") {
646
+ process.stderr.write(
647
+ `Warning: --skip-scout is only meaningful for leads. Ignoring for "${capability}" agent "${name}".\n`,
648
+ );
649
+ }
650
+
651
+ if (skipTaskCheck && !parentAgent) {
652
+ process.stderr.write(
653
+ `Warning: --skip-task-check without --parent is unusual. This flag is designed for leads spawning builders with worktree-created issues.\n`,
654
+ );
655
+ }
656
+
657
+ // Validate that spec file exists if provided, and resolve to absolute path
658
+ // so agents in worktrees can access it (worktrees don't have .agentplate/)
659
+ let absoluteSpecPath: string | null = null;
660
+ if (specPath !== null) {
661
+ absoluteSpecPath = resolve(specPath);
662
+ const specFile = Bun.file(absoluteSpecPath);
663
+ const specExists = await specFile.exists();
664
+ if (!specExists) {
665
+ throw new ValidationError(`Spec file not found: ${specPath}`, {
666
+ field: "spec",
667
+ value: specPath,
668
+ });
669
+ }
670
+ }
671
+
672
+ const fileScope = filesRaw
673
+ ? filesRaw
674
+ .split(",")
675
+ .map((f) => f.trim())
676
+ .filter((f) => f.length > 0)
677
+ : [];
678
+
679
+ const siblings = parseSiblings(opts.siblings);
680
+
681
+ // 1. Load config
682
+ const cwd = process.cwd();
683
+ const config = await loadConfig(cwd);
684
+ const resolvedBackend = await resolveBackend(config.taskTracker.backend, config.project.root);
685
+
686
+ // 2. Validate depth limit
687
+ // Hierarchy: orchestrator(0) -> lead(1) -> specialist(2)
688
+ // With maxDepth=2, depth=2 is the deepest allowed leaf, so reject only depth > maxDepth
689
+ if (depth > config.agents.maxDepth) {
690
+ throw new AgentError(
691
+ `Depth limit exceeded: depth ${depth} > maxDepth ${config.agents.maxDepth}`,
692
+ { agentName: name },
693
+ );
694
+ }
695
+
696
+ // 2b. Validate hierarchy: coordinator (no --parent) can only spawn leads
697
+ validateHierarchy(parentAgent, capability, name, depth, forceHierarchy);
698
+
699
+ // 3. Load manifest and validate capability
700
+ const manifestLoader = createManifestLoader(
701
+ join(config.project.root, config.agents.manifestPath),
702
+ join(config.project.root, config.agents.baseDir),
703
+ );
704
+ const manifest = await manifestLoader.load();
705
+
706
+ const agentDef = manifest.agents[capability];
707
+ if (!agentDef) {
708
+ throw new AgentError(
709
+ `Unknown capability "${capability}". Available: ${Object.keys(manifest.agents).join(", ")}`,
710
+ { agentName: name, capability },
711
+ );
712
+ }
713
+
714
+ // 4. Resolve or create run_id for this spawn
715
+ const agentplateDir = join(config.project.root, ".agentplate");
716
+ const currentRunPath = join(agentplateDir, "current-run.txt");
717
+
718
+ // 5. Check name uniqueness and concurrency limit against active sessions
719
+ // (Session store opened here so we can also use it for parent run ID inheritance in step 4.)
720
+ const { store } = openSessionStore(agentplateDir);
721
+ try {
722
+ // 4a. Resolve run ID: inherit from parent → current-run.txt fallback → create new.
723
+ // Parent inheritance ensures child agents belong to the same run as their coordinator.
724
+ const runId = await (async (): Promise<string> => {
725
+ if (parentAgent) {
726
+ const parentSession = store.getByName(parentAgent);
727
+ if (parentSession?.runId) {
728
+ return parentSession.runId;
729
+ }
730
+ }
731
+
732
+ // Fallback: read current-run.txt (backward compat with single-coordinator setups).
733
+ const currentRunFile = Bun.file(currentRunPath);
734
+ if (await currentRunFile.exists()) {
735
+ const text = (await currentRunFile.text()).trim();
736
+ if (text) return text;
737
+ }
738
+
739
+ // Create a new run if none exists.
740
+ const newRunId = `run-${new Date().toISOString().replace(/[:.]/g, "-")}`;
741
+ const runStore = createRunStore(join(agentplateDir, "sessions.db"));
742
+ try {
743
+ runStore.createRun({
744
+ id: newRunId,
745
+ startedAt: new Date().toISOString(),
746
+ coordinatorSessionId: null,
747
+ coordinatorName: null,
748
+ status: "active",
749
+ });
750
+ } finally {
751
+ runStore.close();
752
+ }
753
+ await Bun.write(currentRunPath, newRunId);
754
+ return newRunId;
755
+ })();
756
+
757
+ // 4b. Check per-run session limit
758
+ if (config.agents.maxSessionsPerRun > 0) {
759
+ const runCheckStore = createRunStore(join(agentplateDir, "sessions.db"));
760
+ try {
761
+ const run = runCheckStore.getRun(runId);
762
+ if (run && checkRunSessionLimit(config.agents.maxSessionsPerRun, run.agentCount)) {
763
+ throw new AgentError(
764
+ `Run session limit reached: ${run.agentCount}/${config.agents.maxSessionsPerRun} agents spawned in run "${runId}". ` +
765
+ `Increase agents.maxSessionsPerRun in config.yaml or start a new run.`,
766
+ { agentName: name },
767
+ );
768
+ }
769
+ } finally {
770
+ runCheckStore.close();
771
+ }
772
+ }
773
+
774
+ const activeSessions = store.getActive();
775
+ if (activeSessions.length >= config.agents.maxConcurrent) {
776
+ throw new AgentError(
777
+ `Max concurrent agent limit reached: ${activeSessions.length}/${config.agents.maxConcurrent} active agents`,
778
+ { agentName: name },
779
+ );
780
+ }
781
+
782
+ // Track the prior session row when re-spawning against an existing agent
783
+ // name so downstream code can preserve linkage (parentAgent, claudeSessionId)
784
+ // that the upsert would otherwise erase. Auto-generated names are unique
785
+ // so there is never a prior row to preserve.
786
+ let existingSession: AgentSession | null = null;
787
+ if (nameWasAutoGenerated) {
788
+ const takenNames = activeSessions.map((s) => s.agentName);
789
+ name = generateAgentName(capability, taskId, takenNames);
790
+ } else {
791
+ existingSession = store.getByName(name);
792
+ if (
793
+ existingSession &&
794
+ existingSession.state !== "zombie" &&
795
+ existingSession.state !== "completed"
796
+ ) {
797
+ throw new AgentError(
798
+ `Agent name "${name}" is already in use (state: ${existingSession.state})`,
799
+ {
800
+ agentName: name,
801
+ },
802
+ );
803
+ }
804
+ }
805
+
806
+ // Preserve the prior session's parentAgent on re-spawn when --parent was
807
+ // not explicitly passed (agentplate-de3c). See `resolveParentAgent` for the
808
+ // full rationale and resolution rules.
809
+ parentAgent = resolveParentAgent(opts.parent, existingSession);
810
+
811
+ // 5d. Task-level locking: prevent concurrent agents on the same task ID.
812
+ // Exception: the parent agent may delegate its own task to a child.
813
+ const lockHolder = checkTaskLock(activeSessions, taskId);
814
+ if (lockHolder !== null && lockHolder !== parentAgent) {
815
+ throw new AgentError(
816
+ `Task "${taskId}" is already being worked by agent "${lockHolder}". ` +
817
+ `Concurrent work on the same task causes duplicate issues and wasted tokens.`,
818
+ { agentName: name },
819
+ );
820
+ }
821
+
822
+ // 5b. Enforce stagger delay between agent spawns
823
+ const staggerMs = calculateStaggerDelay(config.agents.staggerDelayMs, activeSessions);
824
+ if (staggerMs > 0) {
825
+ await Bun.sleep(staggerMs);
826
+ }
827
+
828
+ // 5e. Enforce per-lead agent ceiling when spawning under a parent
829
+ if (parentAgent !== null) {
830
+ const maxPerLead =
831
+ opts.maxAgents !== undefined
832
+ ? Number.parseInt(opts.maxAgents, 10)
833
+ : config.agents.maxAgentsPerLead;
834
+ if (checkParentAgentLimit(activeSessions, parentAgent, maxPerLead)) {
835
+ const currentCount = activeSessions.filter((s) => s.parentAgent === parentAgent).length;
836
+ throw new AgentError(
837
+ `Per-lead agent limit reached: "${parentAgent}" has ${currentCount}/${maxPerLead} active children. ` +
838
+ `Increase agents.maxAgentsPerLead in config.yaml or pass --max-agents <n>.`,
839
+ { agentName: name },
840
+ );
841
+ }
842
+ }
843
+
844
+ // 5c. Structural enforcement: warn when a lead spawns a builder without prior scouts.
845
+ // This is a non-blocking warning — it does not prevent the spawn, but surfaces
846
+ // the scout-skip pattern so agents and operators can see it happening.
847
+ // Use --no-scout-check to suppress this warning when intentionally skipping scouts.
848
+ if (
849
+ shouldShowScoutWarning(
850
+ capability,
851
+ parentAgent,
852
+ store.getAll(),
853
+ opts.noScoutCheck ?? false,
854
+ skipScout,
855
+ )
856
+ ) {
857
+ process.stderr.write(
858
+ `Warning: "${parentAgent}" is spawning builder "${name}" without having spawned any scouts.\n`,
859
+ );
860
+ process.stderr.write(
861
+ " Leads should spawn scouts in Phase 1 before building. See agents/lead.md.\n",
862
+ );
863
+ }
864
+
865
+ // 6. Validate task exists and is in a workable state (if tracker enabled)
866
+ const tracker = createTrackerClient(resolvedBackend, config.project.root);
867
+ if (config.taskTracker.enabled && !skipTaskCheck) {
868
+ let issue: TrackerIssue;
869
+ try {
870
+ issue = await tracker.show(taskId);
871
+ } catch (err) {
872
+ throw new AgentError(`Task "${taskId}" not found or inaccessible`, {
873
+ agentName: name,
874
+ cause: err instanceof Error ? err : undefined,
875
+ });
876
+ }
877
+
878
+ if (!isTaskWorkable(issue.status, recover)) {
879
+ throw new ValidationError(
880
+ `Task "${taskId}" is not workable (status: ${issue.status}). Only open or in_progress issues can be assigned. Pass --recover to re-dispatch against a closed task.`,
881
+ { field: "taskId", value: taskId },
882
+ );
883
+ }
884
+ if (recover && !(WORKABLE_STATUSES as readonly string[]).includes(issue.status)) {
885
+ process.stderr.write(
886
+ `Warning: --recover dispatching against task "${taskId}" with status "${issue.status}". Previous owner may have exited unexpectedly.\n`,
887
+ );
888
+ }
889
+ }
890
+
891
+ // 7. Create worktree
892
+ const worktreeBaseDir = join(config.project.root, config.worktrees.baseDir);
893
+ await mkdir(worktreeBaseDir, { recursive: true });
894
+
895
+ // Resolve base branch: --base-branch flag > current HEAD > config.project.canonicalBranch
896
+ const baseBranch =
897
+ opts.baseBranch ??
898
+ (await getCurrentBranch(config.project.root)) ??
899
+ config.project.canonicalBranch;
900
+
901
+ const { path: worktreePath, branch: branchName } = await createWorktree({
902
+ repoRoot: config.project.root,
903
+ baseDir: worktreeBaseDir,
904
+ agentName: name,
905
+ baseBranch,
906
+ taskId: taskId,
907
+ });
908
+
909
+ try {
910
+ // 8. Generate + write overlay CLAUDE.md
911
+ const agentDefPath = join(config.project.root, config.agents.baseDir, agentDef.file);
912
+ const baseDefinition = await Bun.file(agentDefPath).text();
913
+
914
+ // 8a. Fetch file-scoped loam expertise if loam is enabled and files are provided
915
+ let loamExpertise: string | undefined;
916
+ if (config.loam.enabled && fileScope.length > 0) {
917
+ try {
918
+ const loam = createLoamClient(config.project.root);
919
+ loamExpertise = await loam.prime(undefined, undefined, {
920
+ files: fileScope,
921
+ sortByScore: true,
922
+ });
923
+ } catch {
924
+ // Non-fatal: loam expertise is supplementary context
925
+ loamExpertise = undefined;
926
+ }
927
+ }
928
+
929
+ // 8b. Resolve trellis profile if specified
930
+ const profileName =
931
+ opts.profile ?? process.env.AGENTPLATE_PROFILE ?? config.project.defaultProfile;
932
+ let profileContent: string | undefined;
933
+ if (profileName) {
934
+ try {
935
+ const trellis = createTrellisClient(config.project.root);
936
+ const rendered = await trellis.render(profileName);
937
+ if (rendered.success && rendered.sections.length > 0) {
938
+ profileContent = rendered.sections.map((s) => s.body).join("\n\n");
939
+ }
940
+ } catch {
941
+ // Non-fatal: trellis may not be installed or profile may not exist
942
+ profileContent = undefined;
943
+ }
944
+ }
945
+
946
+ // Resolve runtime before overlayConfig so we can pass runtime.instructionPath
947
+ const runtime = getRuntime(opts.runtime, config, capability);
948
+
949
+ // Runtime-specific worktree preparation (e.g., Copilot folder trust)
950
+ if (runtime.prepareWorktree) {
951
+ await runtime.prepareWorktree(worktreePath);
952
+ }
953
+
954
+ const overlayConfig: OverlayConfig = {
955
+ agentName: name,
956
+ taskId: taskId,
957
+ specPath: absoluteSpecPath,
958
+ branchName,
959
+ worktreePath,
960
+ fileScope,
961
+ loamDomains: config.loam.enabled
962
+ ? inferDomainsFromFiles(fileScope, config.loam.domains)
963
+ : [],
964
+ parentAgent: parentAgent,
965
+ depth,
966
+ canSpawn: agentDef.canSpawn,
967
+ capability,
968
+ baseDefinition,
969
+ profileContent,
970
+ loamExpertise,
971
+ skipScout: skipScout && capability === "lead",
972
+ skipReview: opts.skipReview === true && capability === "lead",
973
+ maxAgentsOverride:
974
+ opts.dispatchMaxAgents !== undefined
975
+ ? Number.parseInt(opts.dispatchMaxAgents, 10)
976
+ : undefined,
977
+ qualityGates: config.project.qualityGates,
978
+ trackerCli: trackerCliName(resolvedBackend),
979
+ trackerName: resolvedBackend,
980
+ instructionPath: runtime.instructionPath,
981
+ siblings,
982
+ };
983
+
984
+ await writeOverlay(worktreePath, overlayConfig, config.project.root, runtime.instructionPath);
985
+
986
+ // 9. Resolve runtime + model (needed for deployConfig, spawn, and beacon)
987
+ const resolvedModel = resolveModel(config, manifest, capability, agentDef.model);
988
+
989
+ // 9a. Resolve headless mode before deployConfig so hooks can be skipped for headless agents.
990
+ // resolveUseHeadless is also used at 11c for spawn routing — hoisted here to share the value.
991
+ const useHeadless = resolveUseHeadless(runtime, opts.headless, config);
992
+
993
+ // 9b. Deploy hooks config (capability-specific guards). In headless mode we deploy
994
+ // a PreToolUse-only subset (security guards) — agentplate-e24b. Headless Claude Code
995
+ // dispatches settings.local.json hooks, so dropping them would leave destructive
996
+ // commands unblocked.
997
+ await runtime.deployConfig(worktreePath, undefined, {
998
+ agentName: name,
999
+ capability,
1000
+ worktreePath,
1001
+ qualityGates: config.project.qualityGates,
1002
+ isHeadless: useHeadless,
1003
+ });
1004
+
1005
+ // 9b. Send auto-dispatch mail so it exists when SessionStart hook fires.
1006
+ // This eliminates the race where coordinator sends dispatch AFTER agent boots.
1007
+ const slingerName = process.env.AGENTPLATE_AGENT_NAME?.trim() || null;
1008
+ const dispatch = buildAutoDispatch({
1009
+ agentName: name,
1010
+ taskId,
1011
+ capability,
1012
+ specPath: absoluteSpecPath,
1013
+ parentAgent,
1014
+ slingerName,
1015
+ instructionPath: runtime.instructionPath,
1016
+ });
1017
+ const mailStore = createMailStore(join(agentplateDir, "mail.db"));
1018
+ try {
1019
+ const mailClient = createMailClient(mailStore);
1020
+ mailClient.send({
1021
+ from: dispatch.from,
1022
+ to: dispatch.to,
1023
+ subject: dispatch.subject,
1024
+ body: dispatch.body,
1025
+ type: "dispatch",
1026
+ priority: "normal",
1027
+ });
1028
+ } finally {
1029
+ mailStore.close();
1030
+ }
1031
+
1032
+ // 10. Claim tracker issue
1033
+ if (config.taskTracker.enabled && !skipTaskCheck) {
1034
+ try {
1035
+ await tracker.claim(taskId);
1036
+ } catch {
1037
+ // Non-fatal: issue may already be claimed
1038
+ }
1039
+ }
1040
+
1041
+ // 11. Create agent identity (if new)
1042
+ const identityBaseDir = join(config.project.root, ".agentplate", "agents");
1043
+ const existingIdentity = await loadIdentity(identityBaseDir, name);
1044
+ if (!existingIdentity) {
1045
+ await createIdentity(identityBaseDir, {
1046
+ name,
1047
+ capability,
1048
+ created: new Date().toISOString(),
1049
+ sessionsCompleted: 0,
1050
+ expertiseDomains: config.loam.enabled ? config.loam.domains : [],
1051
+ recentTasks: [],
1052
+ });
1053
+ }
1054
+
1055
+ // 11b. Save applied loam record IDs for session-end outcome tracking.
1056
+ // Written to .agentplate/agents/{name}/applied-records.json so log.ts
1057
+ // can append outcomes when the session completes.
1058
+ if (loamExpertise) {
1059
+ const appliedRecords = extractLoamRecordIds(loamExpertise);
1060
+ if (appliedRecords.length > 0) {
1061
+ const appliedRecordsPath = join(identityBaseDir, name, "applied-records.json");
1062
+ const appliedData = { taskId, agentName: name, capability, records: appliedRecords };
1063
+ try {
1064
+ await Bun.write(appliedRecordsPath, `${JSON.stringify(appliedData, null, "\t")}\n`);
1065
+ } catch {
1066
+ // Non-fatal: outcome tracking is supplementary context
1067
+ }
1068
+ }
1069
+ }
1070
+
1071
+ // 11c. Spawn: headless runtimes bypass tmux entirely; tmux path is unchanged.
1072
+ // useHeadless was resolved at step 9a (hoisted so deployConfig can skip hooks for headless).
1073
+ if (useHeadless && runtime.buildDirectSpawn) {
1074
+ // Phase 3 spawn-per-turn: headless agents have NO long-lived process.
1075
+ // sling builds the initial prompt, upserts the session row in
1076
+ // "booting", then drives the first user turn synchronously through
1077
+ // `runTurn`. The runner spawns claude with `--resume` (when a prior
1078
+ // session id exists), writes the prompt to a real stdin pipe, drains
1079
+ // stream-json, captures session id, transitions state to "working"
1080
+ // (or "completed" if terminal mail observed), and exits. No persistent
1081
+ // process remains after this returns; subsequent turns are driven by
1082
+ // `ap serve` (mail) or `ap nudge`.
1083
+ // `existingSession` was captured during the name-collision check (above).
1084
+ // Re-using it here keeps re-spawn linkage (parentAgent + claudeSessionId)
1085
+ // resolved from the same row.
1086
+ const priorClaudeSessionId = existingSession?.claudeSessionId ?? null;
1087
+
1088
+ // Build the initial prompt (loam expertise + pending mail + beacon)
1089
+ // as the first user turn.
1090
+ const pendingMailStore = createMailStore(join(agentplateDir, "mail.db"));
1091
+ let initialPrompt: string;
1092
+ try {
1093
+ const pendingMailClient = createMailClient(pendingMailStore);
1094
+ const pendingMessages = pendingMailClient.check(name);
1095
+ const mailSection = formatMailSection(pendingMessages);
1096
+ const beacon = buildBeacon({
1097
+ agentName: name,
1098
+ capability,
1099
+ taskId,
1100
+ parentAgent,
1101
+ depth,
1102
+ instructionPath: runtime.instructionPath,
1103
+ });
1104
+ initialPrompt = buildInitialHeadlessPrompt(
1105
+ loamExpertise,
1106
+ mailSection || undefined,
1107
+ beacon,
1108
+ );
1109
+ } finally {
1110
+ pendingMailStore.close();
1111
+ }
1112
+
1113
+ // 13. Record session BEFORE runTurn so the runner reads it under its
1114
+ // lock. pid is null — there is no persistent process; the runner
1115
+ // publishes a per-turn PID via .agentplate/agents/<name>/turn.pid for
1116
+ // the duration of each turn. Carry priorClaudeSessionId (mx-5c5ae6).
1117
+ const session: AgentSession = {
1118
+ id: `session-${Date.now()}-${name}`,
1119
+ agentName: name,
1120
+ capability,
1121
+ worktreePath,
1122
+ branchName,
1123
+ taskId: taskId,
1124
+ tmuxSession: "",
1125
+ state: "booting",
1126
+ pid: null,
1127
+ parentAgent: parentAgent,
1128
+ depth,
1129
+ runId,
1130
+ startedAt: new Date().toISOString(),
1131
+ lastActivity: new Date().toISOString(),
1132
+ escalationLevel: 0,
1133
+ stalledSince: null,
1134
+ transcriptPath: null,
1135
+ ...(priorClaudeSessionId !== null ? { claudeSessionId: priorClaudeSessionId } : {}),
1136
+ };
1137
+ store.upsert(session);
1138
+
1139
+ // Drive the first user turn synchronously. runTurn manages spawn,
1140
+ // stdin write+EOF, event drain, session_id capture, terminal-mail
1141
+ // detection, and state transition.
1142
+ const turnResult = await runTurn({
1143
+ agentName: name,
1144
+ capability,
1145
+ agentplateDir,
1146
+ worktreePath,
1147
+ projectRoot: config.project.root,
1148
+ taskId,
1149
+ userTurnNdjson: initialPrompt,
1150
+ runtime,
1151
+ resolvedModel,
1152
+ runId,
1153
+ mailDbPath: join(agentplateDir, "mail.db"),
1154
+ eventsDbPath: join(agentplateDir, "events.db"),
1155
+ sessionsDbPath: join(agentplateDir, "sessions.db"),
1156
+ });
1157
+
1158
+ // 14. Output result (headless)
1159
+ if (opts.json ?? false) {
1160
+ jsonOutput("sling", {
1161
+ agentName: name,
1162
+ capability,
1163
+ taskId,
1164
+ branch: branchName,
1165
+ worktree: worktreePath,
1166
+ tmuxSession: "",
1167
+ pid: null,
1168
+ initialTurnFinalState: turnResult.finalState,
1169
+ claudeSessionId: turnResult.newSessionId,
1170
+ });
1171
+ } else {
1172
+ printSuccess("Agent launched (headless, spawn-per-turn)", name);
1173
+ process.stdout.write(` Task: ${taskId}\n`);
1174
+ process.stdout.write(` Branch: ${branchName}\n`);
1175
+ process.stdout.write(` Worktree: ${worktreePath}\n`);
1176
+ process.stdout.write(` First-turn state: ${turnResult.finalState}\n`);
1177
+ if (turnResult.newSessionId) {
1178
+ process.stdout.write(` Claude session id: ${turnResult.newSessionId}\n`);
1179
+ }
1180
+ }
1181
+ } else {
1182
+ // 11c. Preflight: verify tmux is available before attempting session creation
1183
+ await ensureTmuxAvailable();
1184
+
1185
+ // 12. Create tmux session running claude in interactive mode
1186
+ const tmuxSessionName = `agentplate-${sanitizeTmuxName(config.project.name)}-${name}`;
1187
+ const spawnCmd = runtime.buildSpawnCommand({
1188
+ model: resolvedModel.model,
1189
+ permissionMode: "bypass",
1190
+ cwd: worktreePath,
1191
+ sharedWritableDirs: getSharedWritableDirs(config.project.root, capability),
1192
+ env: {
1193
+ ...runtime.buildEnv(resolvedModel),
1194
+ AGENTPLATE_AGENT_NAME: name,
1195
+ AGENTPLATE_WORKTREE_PATH: worktreePath,
1196
+ AGENTPLATE_TASK_ID: taskId,
1197
+ AGENTPLATE_PROJECT_ROOT: config.project.root,
1198
+ },
1199
+ });
1200
+ const pid = await createSession(tmuxSessionName, worktreePath, spawnCmd, {
1201
+ ...runtime.buildEnv(resolvedModel),
1202
+ AGENTPLATE_AGENT_NAME: name,
1203
+ AGENTPLATE_WORKTREE_PATH: worktreePath,
1204
+ AGENTPLATE_TASK_ID: taskId,
1205
+ AGENTPLATE_PROJECT_ROOT: config.project.root,
1206
+ });
1207
+
1208
+ // 13. Record session BEFORE sending the beacon so that hook-triggered
1209
+ // updateLastActivity() can find the entry and transition booting->working.
1210
+ // Without this, a race exists: hooks fire before the session is persisted,
1211
+ // leaving the agent stuck in "booting" (agentplate-036f).
1212
+ const session: AgentSession = {
1213
+ id: `session-${Date.now()}-${name}`,
1214
+ agentName: name,
1215
+ capability,
1216
+ worktreePath,
1217
+ branchName,
1218
+ taskId: taskId,
1219
+ tmuxSession: tmuxSessionName,
1220
+ state: "booting",
1221
+ pid,
1222
+ parentAgent: parentAgent,
1223
+ depth,
1224
+ runId,
1225
+ startedAt: new Date().toISOString(),
1226
+ lastActivity: new Date().toISOString(),
1227
+ escalationLevel: 0,
1228
+ stalledSince: null,
1229
+ transcriptPath: null,
1230
+ };
1231
+
1232
+ store.upsert(session);
1233
+
1234
+ // 13b. Give slow shells time to finish initializing before polling for TUI readiness.
1235
+ const shellDelay = config.runtime?.shellInitDelayMs ?? 0;
1236
+ if (shellDelay > 0) {
1237
+ await Bun.sleep(shellDelay);
1238
+ }
1239
+
1240
+ // Wait for Claude Code TUI to render before sending input.
1241
+ // Polling capture-pane is more reliable than a fixed sleep because
1242
+ // TUI init time varies by machine load and model state.
1243
+ const tuiReady = await waitForTuiReady(tmuxSessionName, (content) =>
1244
+ runtime.detectReady(content),
1245
+ );
1246
+ if (!tuiReady) {
1247
+ const alive = await isSessionAlive(tmuxSessionName);
1248
+ // Mark as zombie (not completed) so the watchdog detects this failed
1249
+ // startup. 'completed' is a terminal success state that the watchdog
1250
+ // skips entirely (agentplate-c40e).
1251
+ store.updateState(name, "zombie");
1252
+
1253
+ if (alive) {
1254
+ await killSession(tmuxSessionName);
1255
+ throw new AgentError(
1256
+ `Agent tmux session "${tmuxSessionName}" did not become ready during startup. The runtime may still be waiting on an interactive dialog or initializing too slowly.`,
1257
+ { agentName: name },
1258
+ );
1259
+ }
1260
+
1261
+ const sessionState = await checkSessionState(tmuxSessionName);
1262
+ const detail =
1263
+ sessionState === "no_server"
1264
+ ? "The tmux server is no longer running. It may have crashed or been killed externally."
1265
+ : "The agent process may have crashed or exited immediately before the TUI became ready.";
1266
+ throw new AgentError(
1267
+ `Agent tmux session "${tmuxSessionName}" died during startup. ${detail}`,
1268
+ { agentName: name },
1269
+ );
1270
+ }
1271
+ // Buffer for the input handler to attach after initial render
1272
+ await Bun.sleep(1_000);
1273
+
1274
+ const beacon = buildBeacon({
1275
+ agentName: name,
1276
+ capability,
1277
+ taskId,
1278
+ parentAgent,
1279
+ depth,
1280
+ instructionPath: runtime.instructionPath,
1281
+ });
1282
+ await sendKeys(tmuxSessionName, beacon);
1283
+
1284
+ // 13c. Follow-up Enters with increasing delays to ensure submission.
1285
+ // Claude Code's TUI may consume early Enters during late initialization
1286
+ // (agentplate-yhv6). An Enter on an empty input line is harmless.
1287
+ for (const delay of [1_000, 2_000, 3_000, 5_000]) {
1288
+ await Bun.sleep(delay);
1289
+ await sendKeys(tmuxSessionName, "");
1290
+ }
1291
+
1292
+ // 13d. Verify beacon was received — if pane still shows the welcome
1293
+ // screen (detectReady returns "ready"), resend the beacon. Claude Code's TUI
1294
+ // sometimes consumes the Enter keystroke during late initialization, swallowing
1295
+ // the beacon text entirely (agentplate-3271).
1296
+ //
1297
+ // Skipped for runtimes that return false from requiresBeaconVerification().
1298
+ // Pi's TUI idle and processing states are indistinguishable via detectReady
1299
+ // (both show "pi v..." header and the token-usage status bar), so the loop
1300
+ // would incorrectly conclude the beacon was not received and spam duplicate
1301
+ // startup messages.
1302
+ const needsVerification =
1303
+ !runtime.requiresBeaconVerification || runtime.requiresBeaconVerification();
1304
+ if (needsVerification) {
1305
+ const verifyAttempts = 5;
1306
+ for (let v = 0; v < verifyAttempts; v++) {
1307
+ await Bun.sleep(2_000);
1308
+ const paneContent = await capturePaneContent(tmuxSessionName);
1309
+ if (paneContent) {
1310
+ const readyState = runtime.detectReady(paneContent);
1311
+ if (readyState.phase !== "ready") {
1312
+ break; // Agent is processing — beacon was received
1313
+ }
1314
+ }
1315
+ // Still at welcome/idle screen — resend beacon
1316
+ await sendKeys(tmuxSessionName, beacon);
1317
+ await Bun.sleep(1_000);
1318
+ await sendKeys(tmuxSessionName, ""); // Follow-up Enter
1319
+ }
1320
+ }
1321
+
1322
+ // 14. Output result
1323
+ const output = {
1324
+ agentName: name,
1325
+ capability,
1326
+ taskId,
1327
+ branch: branchName,
1328
+ worktree: worktreePath,
1329
+ tmuxSession: tmuxSessionName,
1330
+ pid,
1331
+ };
1332
+
1333
+ if (opts.json ?? false) {
1334
+ jsonOutput("sling", output);
1335
+ } else {
1336
+ printSuccess("Agent launched", name);
1337
+ process.stdout.write(` Task: ${taskId}\n`);
1338
+ process.stdout.write(` Branch: ${branchName}\n`);
1339
+ process.stdout.write(` Worktree: ${worktreePath}\n`);
1340
+ process.stdout.write(` Tmux: ${tmuxSessionName}\n`);
1341
+ process.stdout.write(` PID: ${pid}\n`);
1342
+ }
1343
+ }
1344
+ } catch (err) {
1345
+ await rollbackWorktree(config.project.root, worktreePath, branchName);
1346
+ throw err;
1347
+ }
1348
+ } finally {
1349
+ store.close();
1350
+ }
1351
+ }