@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,1297 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, realpathSync } from "node:fs";
3
+ import { mkdir, mkdtemp } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { ValidationError } from "../errors.ts";
7
+ import { createSessionStore } from "../sessions/store.ts";
8
+ import { cleanupTempDir, commitFile, createTempGitRepo, runGitInDir } from "../test-helpers.ts";
9
+ import type { AgentSession } from "../types.ts";
10
+ import { createWorktree } from "../worktree/manager.ts";
11
+ import { checkLiveChildren, worktreeCommand } from "./worktree.ts";
12
+
13
+ /**
14
+ * Tests for `agentplate worktree` command.
15
+ *
16
+ * Uses real git worktrees in temp repos to test list and clean subcommands.
17
+ * Captures process.stdout.write to verify output formatting.
18
+ */
19
+
20
+ describe("worktreeCommand", () => {
21
+ let chunks: string[];
22
+ let originalWrite: typeof process.stdout.write;
23
+ let tempDir: string;
24
+ let originalCwd: string;
25
+
26
+ beforeEach(async () => {
27
+ // Spy on stdout
28
+ chunks = [];
29
+ originalWrite = process.stdout.write;
30
+ process.stdout.write = ((chunk: string) => {
31
+ chunks.push(chunk);
32
+ return true;
33
+ }) as typeof process.stdout.write;
34
+
35
+ // Create temp git repo with .agentplate/config.yaml structure
36
+ tempDir = await createTempGitRepo();
37
+ // Normalize tempDir to resolve macOS /var -> /private/var symlink
38
+ tempDir = realpathSync(tempDir);
39
+ const agentplateDir = join(tempDir, ".agentplate");
40
+ await mkdir(agentplateDir, { recursive: true });
41
+ await Bun.write(
42
+ join(agentplateDir, "config.yaml"),
43
+ `project:\n name: test\n root: ${tempDir}\n canonicalBranch: main\n`,
44
+ );
45
+
46
+ // Change to temp dir so loadConfig() works
47
+ originalCwd = process.cwd();
48
+ process.chdir(tempDir);
49
+ });
50
+
51
+ afterEach(async () => {
52
+ process.stdout.write = originalWrite;
53
+ process.chdir(originalCwd);
54
+ await cleanupTempDir(tempDir);
55
+ });
56
+
57
+ function output(): string {
58
+ return chunks.join("");
59
+ }
60
+
61
+ /**
62
+ * Helper to create an AgentSession with sensible defaults.
63
+ * Uses FAKE tmux session names to avoid real tmux calls during tests.
64
+ */
65
+ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
66
+ return {
67
+ id: "session-test",
68
+ agentName: "test-agent",
69
+ capability: "builder",
70
+ worktreePath: join(tempDir, ".agentplate", "worktrees", "test-agent"),
71
+ branchName: "agentplate/test-agent/task-1",
72
+ taskId: "task-1",
73
+ tmuxSession: "agentplate-test-agent-fake", // FAKE tmux session name
74
+ state: "working",
75
+ pid: 12345,
76
+ parentAgent: null,
77
+ depth: 0,
78
+ runId: null,
79
+ startedAt: new Date().toISOString(),
80
+ lastActivity: new Date().toISOString(),
81
+ escalationLevel: 0,
82
+ stalledSince: null,
83
+ transcriptPath: null,
84
+ ...overrides,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Helper to write sessions to SessionStore (sessions.db) in the temp repo.
90
+ */
91
+ function writeSessionsToStore(sessions: AgentSession[]): void {
92
+ const dbPath = join(tempDir, ".agentplate", "sessions.db");
93
+ const store = createSessionStore(dbPath);
94
+ for (const session of sessions) {
95
+ store.upsert(session);
96
+ }
97
+ store.close();
98
+ }
99
+
100
+ describe("help flags", () => {
101
+ test("--help shows help text", async () => {
102
+ await worktreeCommand(["--help"]);
103
+ const out = output();
104
+
105
+ expect(out).toContain("worktree");
106
+ expect(out).toContain("list");
107
+ expect(out).toContain("clean");
108
+ });
109
+
110
+ test("-h shows help text", async () => {
111
+ await worktreeCommand(["-h"]);
112
+ const out = output();
113
+
114
+ expect(out).toContain("worktree");
115
+ expect(out).toContain("list");
116
+ });
117
+ });
118
+
119
+ describe("validation", () => {
120
+ test("unknown subcommand throws ValidationError", async () => {
121
+ await expect(worktreeCommand(["unknown"])).rejects.toThrow(ValidationError);
122
+ });
123
+
124
+ test("empty args shows help text", async () => {
125
+ await worktreeCommand([]);
126
+ const out = output();
127
+ expect(out).toContain("worktree");
128
+ });
129
+ });
130
+
131
+ describe("worktree list", () => {
132
+ test("no agentplate worktrees returns empty message", async () => {
133
+ await worktreeCommand(["list"]);
134
+ const out = output();
135
+
136
+ expect(out).toContain("No agent worktrees found");
137
+ });
138
+
139
+ test("with agentplate worktrees lists them with agent info", async () => {
140
+ // Create a real git worktree with agentplate/ prefix branch
141
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
142
+ await mkdir(worktreesDir, { recursive: true });
143
+
144
+ const worktreePath = join(worktreesDir, "test-agent");
145
+ await runGitInDir(tempDir, [
146
+ "worktree",
147
+ "add",
148
+ worktreePath,
149
+ "-b",
150
+ "agentplate/test-agent/task-1",
151
+ ]);
152
+
153
+ // Write sessions.db to associate worktree with agent
154
+ writeSessionsToStore([
155
+ {
156
+ id: "session-1",
157
+ agentName: "test-agent",
158
+ capability: "builder",
159
+ worktreePath,
160
+ branchName: "agentplate/test-agent/task-1",
161
+ taskId: "task-1",
162
+ tmuxSession: "agentplate-test-agent",
163
+ state: "working",
164
+ pid: 12345,
165
+ parentAgent: null,
166
+ depth: 0,
167
+ runId: null,
168
+ startedAt: new Date().toISOString(),
169
+ lastActivity: new Date().toISOString(),
170
+ escalationLevel: 0,
171
+ stalledSince: null,
172
+ transcriptPath: null,
173
+ },
174
+ ]);
175
+
176
+ await worktreeCommand(["list"]);
177
+ const out = output();
178
+
179
+ expect(out).toContain("Agent worktrees: 1");
180
+ expect(out).toContain("agentplate/test-agent/task-1");
181
+ expect(out).toContain("Agent: test-agent");
182
+ expect(out).toContain("State: working");
183
+ expect(out).toContain("Task: task-1");
184
+ expect(out).toContain(`Path: ${worktreePath}`);
185
+ });
186
+
187
+ test("--json flag outputs valid JSON array", async () => {
188
+ // Create a real git worktree
189
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
190
+ await mkdir(worktreesDir, { recursive: true });
191
+
192
+ const worktreePath = join(worktreesDir, "test-agent");
193
+ await runGitInDir(tempDir, [
194
+ "worktree",
195
+ "add",
196
+ worktreePath,
197
+ "-b",
198
+ "agentplate/test-agent/task-1",
199
+ ]);
200
+
201
+ // Write sessions.db
202
+ writeSessionsToStore([
203
+ {
204
+ id: "session-1",
205
+ agentName: "test-agent",
206
+ capability: "builder",
207
+ worktreePath,
208
+ branchName: "agentplate/test-agent/task-1",
209
+ taskId: "task-1",
210
+ tmuxSession: "agentplate-test-agent",
211
+ state: "working",
212
+ pid: 12345,
213
+ parentAgent: null,
214
+ depth: 0,
215
+ runId: null,
216
+ startedAt: new Date().toISOString(),
217
+ lastActivity: new Date().toISOString(),
218
+ escalationLevel: 0,
219
+ stalledSince: null,
220
+ transcriptPath: null,
221
+ },
222
+ ]);
223
+
224
+ await worktreeCommand(["list", "--json"]);
225
+ const out = output();
226
+
227
+ const parsed = JSON.parse(out.trim()) as {
228
+ success: boolean;
229
+ command: string;
230
+ worktrees: Array<{
231
+ path: string;
232
+ branch: string;
233
+ head: string;
234
+ agentName: string | null;
235
+ state: string | null;
236
+ taskId: string | null;
237
+ }>;
238
+ };
239
+
240
+ expect(parsed.success).toBe(true);
241
+ expect(parsed.command).toBe("worktree list");
242
+ expect(parsed.worktrees).toHaveLength(1);
243
+ expect(parsed.worktrees[0]?.path).toBe(worktreePath);
244
+ expect(parsed.worktrees[0]?.branch).toBe("agentplate/test-agent/task-1");
245
+ expect(parsed.worktrees[0]?.agentName).toBe("test-agent");
246
+ expect(parsed.worktrees[0]?.state).toBe("working");
247
+ expect(parsed.worktrees[0]?.taskId).toBe("task-1");
248
+ });
249
+
250
+ test("worktrees without sessions show unknown state", async () => {
251
+ // Create a worktree but no sessions.db entry
252
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
253
+ await mkdir(worktreesDir, { recursive: true });
254
+
255
+ const worktreePath = join(worktreesDir, "orphan-agent");
256
+ await runGitInDir(tempDir, [
257
+ "worktree",
258
+ "add",
259
+ worktreePath,
260
+ "-b",
261
+ "agentplate/orphan-agent/task-2",
262
+ ]);
263
+
264
+ await worktreeCommand(["list"]);
265
+ const out = output();
266
+
267
+ expect(out).toContain("agentplate/orphan-agent/task-2");
268
+ expect(out).toContain("Agent: ?");
269
+ expect(out).toContain("State: unknown");
270
+ expect(out).toContain("Task: ?");
271
+ });
272
+ });
273
+
274
+ describe("worktree clean", () => {
275
+ test("no agentplate worktrees returns empty message", async () => {
276
+ await worktreeCommand(["clean"]);
277
+ const out = output();
278
+
279
+ expect(out).toContain("No worktrees to clean");
280
+ });
281
+
282
+ test("with completed agent worktree removes it and reports count", async () => {
283
+ // Create a real git worktree
284
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
285
+ await mkdir(worktreesDir, { recursive: true });
286
+
287
+ const worktreePath = join(worktreesDir, "completed-agent");
288
+ await runGitInDir(tempDir, [
289
+ "worktree",
290
+ "add",
291
+ worktreePath,
292
+ "-b",
293
+ "agentplate/completed-agent/task-done",
294
+ ]);
295
+
296
+ // Write sessions.db with completed state
297
+ writeSessionsToStore([
298
+ {
299
+ id: "session-1",
300
+ agentName: "completed-agent",
301
+ capability: "builder",
302
+ worktreePath,
303
+ branchName: "agentplate/completed-agent/task-done",
304
+ taskId: "task-done",
305
+ tmuxSession: "agentplate-completed-agent",
306
+ state: "completed",
307
+ pid: 12345,
308
+ parentAgent: null,
309
+ depth: 0,
310
+ runId: null,
311
+ startedAt: new Date().toISOString(),
312
+ lastActivity: new Date().toISOString(),
313
+ escalationLevel: 0,
314
+ stalledSince: null,
315
+ transcriptPath: null,
316
+ },
317
+ ]);
318
+
319
+ await worktreeCommand(["clean"]);
320
+ const out = output();
321
+
322
+ expect(out).toContain("Removed");
323
+ expect(out).toContain("agentplate/completed-agent/task-done");
324
+ expect(out).toContain("Cleaned 1 worktree");
325
+
326
+ // Verify the worktree directory is gone
327
+ const worktreeExists = await Bun.file(worktreePath).exists();
328
+ expect(worktreeExists).toBe(false);
329
+
330
+ // Verify the branch is deleted
331
+ const branchListProc = Bun.spawn(
332
+ ["git", "branch", "--list", "agentplate/completed-agent/*"],
333
+ {
334
+ cwd: tempDir,
335
+ stdout: "pipe",
336
+ },
337
+ );
338
+ const branchList = await new Response(branchListProc.stdout).text();
339
+ expect(branchList.trim()).toBe("");
340
+ });
341
+
342
+ test("--json flag returns JSON with cleaned/failed/pruned arrays", async () => {
343
+ // Create a completed worktree
344
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
345
+ await mkdir(worktreesDir, { recursive: true });
346
+
347
+ const worktreePath = join(worktreesDir, "done-agent");
348
+ await runGitInDir(tempDir, [
349
+ "worktree",
350
+ "add",
351
+ worktreePath,
352
+ "-b",
353
+ "agentplate/done-agent/task-x",
354
+ ]);
355
+
356
+ writeSessionsToStore([
357
+ {
358
+ id: "session-1",
359
+ agentName: "done-agent",
360
+ capability: "builder",
361
+ worktreePath,
362
+ branchName: "agentplate/done-agent/task-x",
363
+ taskId: "task-x",
364
+ tmuxSession: "agentplate-done-agent",
365
+ state: "completed",
366
+ pid: 12345,
367
+ parentAgent: null,
368
+ depth: 0,
369
+ runId: null,
370
+ startedAt: new Date().toISOString(),
371
+ lastActivity: new Date().toISOString(),
372
+ escalationLevel: 0,
373
+ stalledSince: null,
374
+ transcriptPath: null,
375
+ },
376
+ ]);
377
+
378
+ await worktreeCommand(["clean", "--json"]);
379
+ const out = output();
380
+
381
+ const parsed = JSON.parse(out.trim()) as {
382
+ cleaned: string[];
383
+ failed: string[];
384
+ pruned: number;
385
+ };
386
+
387
+ expect(parsed.cleaned).toEqual(["agentplate/done-agent/task-x"]);
388
+ expect(parsed.failed).toEqual([]);
389
+ expect(parsed.pruned).toBe(1); // The zombie session was pruned
390
+ });
391
+
392
+ test("zombie sessions whose worktree paths no longer exist get pruned from sessions.db", async () => {
393
+ // Create sessions.db with a zombie entry whose worktree doesn't exist
394
+ const nonExistentPath = join(tempDir, ".agentplate", "worktrees", "ghost-agent");
395
+ writeSessionsToStore([
396
+ {
397
+ id: "session-ghost",
398
+ agentName: "ghost-agent",
399
+ capability: "builder",
400
+ worktreePath: nonExistentPath,
401
+ branchName: "agentplate/ghost-agent/task-ghost",
402
+ taskId: "task-ghost",
403
+ tmuxSession: "agentplate-ghost-agent",
404
+ state: "zombie",
405
+ pid: null,
406
+ parentAgent: null,
407
+ depth: 0,
408
+ runId: null,
409
+ startedAt: new Date().toISOString(),
410
+ lastActivity: new Date().toISOString(),
411
+ escalationLevel: 0,
412
+ stalledSince: null,
413
+ transcriptPath: null,
414
+ },
415
+ ]);
416
+
417
+ await worktreeCommand(["clean", "--json"]);
418
+ const out = output();
419
+
420
+ const parsed = JSON.parse(out.trim()) as {
421
+ cleaned: string[];
422
+ failed: string[];
423
+ pruned: number;
424
+ };
425
+
426
+ expect(parsed.pruned).toBe(1);
427
+
428
+ // Verify sessions.db no longer contains the zombie
429
+ const dbPath = join(tempDir, ".agentplate", "sessions.db");
430
+ const store = createSessionStore(dbPath);
431
+ const updatedSessions = store.getAll();
432
+ store.close();
433
+ expect(updatedSessions).toHaveLength(0);
434
+ });
435
+
436
+ test("stalled agents are cleaned like working agents (not by default)", async () => {
437
+ // Create a worktree with stalled state
438
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
439
+ await mkdir(worktreesDir, { recursive: true });
440
+
441
+ const worktreePath = join(worktreesDir, "stalled-agent");
442
+ await runGitInDir(tempDir, [
443
+ "worktree",
444
+ "add",
445
+ worktreePath,
446
+ "-b",
447
+ "agentplate/stalled-agent/task-stuck",
448
+ ]);
449
+
450
+ writeSessionsToStore([
451
+ {
452
+ id: "session-1",
453
+ agentName: "stalled-agent",
454
+ capability: "builder",
455
+ worktreePath,
456
+ branchName: "agentplate/stalled-agent/task-stuck",
457
+ taskId: "task-stuck",
458
+ tmuxSession: "agentplate-stalled-agent",
459
+ state: "stalled",
460
+ pid: 12345,
461
+ parentAgent: null,
462
+ depth: 0,
463
+ runId: null,
464
+ startedAt: new Date().toISOString(),
465
+ lastActivity: new Date().toISOString(),
466
+ escalationLevel: 0,
467
+ stalledSince: new Date().toISOString(),
468
+ transcriptPath: null,
469
+ },
470
+ ]);
471
+
472
+ await worktreeCommand(["clean"]);
473
+ const out = output();
474
+
475
+ // Stalled agents should not be cleaned by default (only completed/zombie are cleaned)
476
+ expect(out).toContain("No worktrees to clean");
477
+ });
478
+
479
+ test("--completed flag only cleans completed agents", async () => {
480
+ // Create two worktrees using createWorktree
481
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
482
+ await mkdir(worktreesDir, { recursive: true });
483
+
484
+ const { path: completedPath } = await createWorktree({
485
+ repoRoot: tempDir,
486
+ baseDir: worktreesDir,
487
+ agentName: "completed-agent",
488
+ baseBranch: "main",
489
+ taskId: "task-done",
490
+ });
491
+
492
+ const { path: workingPath } = await createWorktree({
493
+ repoRoot: tempDir,
494
+ baseDir: worktreesDir,
495
+ agentName: "working-agent",
496
+ baseBranch: "main",
497
+ taskId: "task-wip",
498
+ });
499
+
500
+ // Write sessions.db with both agents
501
+ writeSessionsToStore([
502
+ makeSession({
503
+ id: "session-1",
504
+ agentName: "completed-agent",
505
+ worktreePath: completedPath,
506
+ branchName: "agentplate/completed-agent/task-done",
507
+ taskId: "task-done",
508
+ tmuxSession: "agentplate-completed-agent-fake",
509
+ state: "completed",
510
+ }),
511
+ makeSession({
512
+ id: "session-2",
513
+ agentName: "working-agent",
514
+ worktreePath: workingPath,
515
+ branchName: "agentplate/working-agent/task-wip",
516
+ taskId: "task-wip",
517
+ tmuxSession: "agentplate-working-agent-fake",
518
+ state: "working",
519
+ pid: 12346,
520
+ }),
521
+ ]);
522
+
523
+ await worktreeCommand(["clean", "--completed"]);
524
+ const out = output();
525
+
526
+ expect(out).toContain("Cleaned 1 worktree");
527
+
528
+ // Verify only the completed worktree is removed
529
+ expect(existsSync(completedPath)).toBe(false);
530
+ expect(existsSync(workingPath)).toBe(true);
531
+ });
532
+
533
+ test("--all flag cleans all worktrees regardless of state", async () => {
534
+ // Create three worktrees with different states
535
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
536
+ await mkdir(worktreesDir, { recursive: true });
537
+
538
+ const { path: completedPath } = await createWorktree({
539
+ repoRoot: tempDir,
540
+ baseDir: worktreesDir,
541
+ agentName: "completed-agent",
542
+ baseBranch: "main",
543
+ taskId: "task-done",
544
+ });
545
+
546
+ const { path: workingPath } = await createWorktree({
547
+ repoRoot: tempDir,
548
+ baseDir: worktreesDir,
549
+ agentName: "working-agent",
550
+ baseBranch: "main",
551
+ taskId: "task-wip",
552
+ });
553
+
554
+ const { path: stalledPath } = await createWorktree({
555
+ repoRoot: tempDir,
556
+ baseDir: worktreesDir,
557
+ agentName: "stalled-agent",
558
+ baseBranch: "main",
559
+ taskId: "task-stuck",
560
+ });
561
+
562
+ // Write sessions with different states
563
+ writeSessionsToStore([
564
+ makeSession({
565
+ id: "session-1",
566
+ agentName: "completed-agent",
567
+ worktreePath: completedPath,
568
+ branchName: "agentplate/completed-agent/task-done",
569
+ taskId: "task-done",
570
+ state: "completed",
571
+ }),
572
+ makeSession({
573
+ id: "session-2",
574
+ agentName: "working-agent",
575
+ worktreePath: workingPath,
576
+ branchName: "agentplate/working-agent/task-wip",
577
+ taskId: "task-wip",
578
+ state: "working",
579
+ }),
580
+ makeSession({
581
+ id: "session-3",
582
+ agentName: "stalled-agent",
583
+ worktreePath: stalledPath,
584
+ branchName: "agentplate/stalled-agent/task-stuck",
585
+ taskId: "task-stuck",
586
+ state: "stalled",
587
+ }),
588
+ ]);
589
+
590
+ await worktreeCommand(["clean", "--all"]);
591
+ const out = output();
592
+
593
+ expect(out).toContain("Cleaned 3 worktrees");
594
+
595
+ // Verify all worktrees are removed
596
+ expect(existsSync(completedPath)).toBe(false);
597
+ expect(existsSync(workingPath)).toBe(false);
598
+ expect(existsSync(stalledPath)).toBe(false);
599
+ });
600
+
601
+ test("multiple completed worktrees reports correct count", async () => {
602
+ // Create two completed worktrees
603
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
604
+ await mkdir(worktreesDir, { recursive: true });
605
+
606
+ const path1 = join(worktreesDir, "agent-1");
607
+ await runGitInDir(tempDir, ["worktree", "add", path1, "-b", "agentplate/agent-1/task-1"]);
608
+
609
+ const path2 = join(worktreesDir, "agent-2");
610
+ await runGitInDir(tempDir, ["worktree", "add", path2, "-b", "agentplate/agent-2/task-2"]);
611
+
612
+ writeSessionsToStore([
613
+ {
614
+ id: "session-1",
615
+ agentName: "agent-1",
616
+ capability: "builder",
617
+ worktreePath: path1,
618
+ branchName: "agentplate/agent-1/task-1",
619
+ taskId: "task-1",
620
+ tmuxSession: "agentplate-agent-1",
621
+ state: "completed",
622
+ pid: 12345,
623
+ parentAgent: null,
624
+ depth: 0,
625
+ runId: null,
626
+ startedAt: new Date().toISOString(),
627
+ lastActivity: new Date().toISOString(),
628
+ escalationLevel: 0,
629
+ stalledSince: null,
630
+ transcriptPath: null,
631
+ },
632
+ {
633
+ id: "session-2",
634
+ agentName: "agent-2",
635
+ capability: "builder",
636
+ worktreePath: path2,
637
+ branchName: "agentplate/agent-2/task-2",
638
+ taskId: "task-2",
639
+ tmuxSession: "agentplate-agent-2",
640
+ state: "completed",
641
+ pid: 12346,
642
+ parentAgent: null,
643
+ depth: 0,
644
+ runId: null,
645
+ startedAt: new Date().toISOString(),
646
+ lastActivity: new Date().toISOString(),
647
+ escalationLevel: 0,
648
+ stalledSince: null,
649
+ transcriptPath: null,
650
+ },
651
+ ]);
652
+
653
+ await worktreeCommand(["clean"]);
654
+ const out = output();
655
+
656
+ expect(out).toContain("Cleaned 2 worktrees");
657
+ });
658
+
659
+ test("without --force, skips worktrees with unmerged branches and prints warning", async () => {
660
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
661
+ await mkdir(worktreesDir, { recursive: true });
662
+
663
+ const { path: wtPath } = await createWorktree({
664
+ repoRoot: tempDir,
665
+ baseDir: worktreesDir,
666
+ agentName: "unmerged-agent",
667
+ baseBranch: "main",
668
+ taskId: "task-unmerged",
669
+ });
670
+
671
+ // Add an unmerged commit
672
+ await commitFile(wtPath, "work.ts", "export const y = 2;", "unmerged work");
673
+
674
+ writeSessionsToStore([
675
+ makeSession({
676
+ id: "session-u",
677
+ agentName: "unmerged-agent",
678
+ worktreePath: wtPath,
679
+ branchName: "agentplate/unmerged-agent/task-unmerged",
680
+ taskId: "task-unmerged",
681
+ state: "completed",
682
+ }),
683
+ ]);
684
+
685
+ await worktreeCommand(["clean"]);
686
+ const out = output();
687
+
688
+ // Worktree should NOT have been removed
689
+ expect(existsSync(wtPath)).toBe(true);
690
+ // Warning should be printed
691
+ expect(out).toContain("Skipped 1 worktree");
692
+ expect(out).toContain("agentplate/unmerged-agent/task-unmerged");
693
+ expect(out).toContain("--force");
694
+ });
695
+
696
+ test("with --force, deletes worktrees with unmerged branches", async () => {
697
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
698
+ await mkdir(worktreesDir, { recursive: true });
699
+
700
+ const { path: wtPath } = await createWorktree({
701
+ repoRoot: tempDir,
702
+ baseDir: worktreesDir,
703
+ agentName: "unmerged-agent",
704
+ baseBranch: "main",
705
+ taskId: "task-force",
706
+ });
707
+
708
+ // Add an unmerged commit
709
+ await commitFile(wtPath, "work.ts", "export const y = 2;", "unmerged work");
710
+
711
+ writeSessionsToStore([
712
+ makeSession({
713
+ id: "session-f",
714
+ agentName: "unmerged-agent",
715
+ worktreePath: wtPath,
716
+ branchName: "agentplate/unmerged-agent/task-force",
717
+ taskId: "task-force",
718
+ state: "completed",
719
+ }),
720
+ ]);
721
+
722
+ await worktreeCommand(["clean", "--force"]);
723
+ const out = output();
724
+
725
+ // Worktree should be removed
726
+ expect(existsSync(wtPath)).toBe(false);
727
+ expect(out).toContain("agentplate/unmerged-agent/task-force");
728
+ });
729
+
730
+ test("without --force, removes worktrees whose branches ARE merged", async () => {
731
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
732
+ await mkdir(worktreesDir, { recursive: true });
733
+
734
+ const { path: wtPath, branch } = await createWorktree({
735
+ repoRoot: tempDir,
736
+ baseDir: worktreesDir,
737
+ agentName: "merged-agent",
738
+ baseBranch: "main",
739
+ taskId: "task-merged",
740
+ });
741
+
742
+ // Add a commit and merge it into main
743
+ await commitFile(wtPath, "work.ts", "export const z = 3;", "work to merge");
744
+ await runGitInDir(tempDir, ["merge", "--no-ff", branch, "-m", "merge feature"]);
745
+
746
+ writeSessionsToStore([
747
+ makeSession({
748
+ id: "session-m",
749
+ agentName: "merged-agent",
750
+ worktreePath: wtPath,
751
+ branchName: branch,
752
+ taskId: "task-merged",
753
+ state: "completed",
754
+ }),
755
+ ]);
756
+
757
+ await worktreeCommand(["clean"]);
758
+ const out = output();
759
+
760
+ // Merged worktree should be cleaned
761
+ expect(existsSync(wtPath)).toBe(false);
762
+ expect(out).toContain("Cleaned 1 worktree");
763
+ });
764
+
765
+ test("--json output includes skipped array for unmerged branches", async () => {
766
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
767
+ await mkdir(worktreesDir, { recursive: true });
768
+
769
+ const { path: wtPath } = await createWorktree({
770
+ repoRoot: tempDir,
771
+ baseDir: worktreesDir,
772
+ agentName: "unmerged-json-agent",
773
+ baseBranch: "main",
774
+ taskId: "task-json",
775
+ });
776
+
777
+ // Add an unmerged commit
778
+ await commitFile(wtPath, "work.ts", "export const w = 4;", "unmerged work");
779
+
780
+ writeSessionsToStore([
781
+ makeSession({
782
+ id: "session-j",
783
+ agentName: "unmerged-json-agent",
784
+ worktreePath: wtPath,
785
+ branchName: "agentplate/unmerged-json-agent/task-json",
786
+ taskId: "task-json",
787
+ state: "completed",
788
+ }),
789
+ ]);
790
+
791
+ await worktreeCommand(["clean", "--json"]);
792
+ const out = output();
793
+
794
+ const parsed = JSON.parse(out.trim()) as {
795
+ cleaned: string[];
796
+ failed: string[];
797
+ skipped: string[];
798
+ pruned: number;
799
+ mailPurged: number;
800
+ };
801
+
802
+ expect(parsed.cleaned).toEqual([]);
803
+ expect(parsed.skipped).toEqual(["agentplate/unmerged-json-agent/task-json"]);
804
+ });
805
+
806
+ test("lead worktree with .sprout/ changes preserves them to canonical before cleanup", async () => {
807
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
808
+ await mkdir(worktreesDir, { recursive: true });
809
+
810
+ const { path: wtPath, branch } = await createWorktree({
811
+ repoRoot: tempDir,
812
+ baseDir: worktreesDir,
813
+ agentName: "lead-with-sprout",
814
+ baseBranch: "main",
815
+ taskId: "task-lead-sprout",
816
+ });
817
+
818
+ // Commit a .sprout/ file in the lead worktree
819
+ await commitFile(
820
+ wtPath,
821
+ ".sprout/issues/test-issue.yaml",
822
+ "id: test-issue\ntitle: Test Issue\nstatus: open\n",
823
+ "sprout: add test issue",
824
+ );
825
+
826
+ writeSessionsToStore([
827
+ makeSession({
828
+ id: "session-lead-sprout",
829
+ agentName: "lead-with-sprout",
830
+ capability: "lead",
831
+ worktreePath: wtPath,
832
+ branchName: branch,
833
+ taskId: "task-lead-sprout",
834
+ state: "completed",
835
+ }),
836
+ ]);
837
+
838
+ await worktreeCommand(["clean"]);
839
+ const out = output();
840
+
841
+ // The worktree should be removed
842
+ expect(existsSync(wtPath)).toBe(false);
843
+
844
+ // The .sprout/ changes should have been preserved to main
845
+ const showProc = Bun.spawn(["git", "show", "main:.sprout/issues/test-issue.yaml"], {
846
+ cwd: tempDir,
847
+ stdout: "pipe",
848
+ stderr: "pipe",
849
+ });
850
+ const showOut = await new Response(showProc.stdout).text();
851
+ const showExit = await showProc.exited;
852
+ expect(showExit).toBe(0);
853
+ expect(showOut).toContain("test-issue");
854
+
855
+ // Output should mention preservation
856
+ expect(out).toContain("Preserved .sprout/");
857
+ });
858
+
859
+ test("lead worktree without .sprout/ changes cleans normally", async () => {
860
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
861
+ await mkdir(worktreesDir, { recursive: true });
862
+
863
+ const { path: wtPath, branch } = await createWorktree({
864
+ repoRoot: tempDir,
865
+ baseDir: worktreesDir,
866
+ agentName: "lead-no-sprout",
867
+ baseBranch: "main",
868
+ taskId: "task-lead-no-sprout",
869
+ });
870
+
871
+ // Commit a non-.sprout/ file
872
+ await commitFile(wtPath, "src/work.ts", "export const x = 1;", "non-sprout work");
873
+
874
+ writeSessionsToStore([
875
+ makeSession({
876
+ id: "session-lead-no-sprout",
877
+ agentName: "lead-no-sprout",
878
+ capability: "lead",
879
+ worktreePath: wtPath,
880
+ branchName: branch,
881
+ taskId: "task-lead-no-sprout",
882
+ state: "completed",
883
+ }),
884
+ ]);
885
+
886
+ await worktreeCommand(["clean"]);
887
+ const out = output();
888
+
889
+ // Worktree should be removed
890
+ expect(existsSync(wtPath)).toBe(false);
891
+ // Output should NOT mention .sprout/ preservation
892
+ expect(out).not.toContain("Preserved .sprout/");
893
+ // Should still report as cleaned
894
+ expect(out).toContain("Cleaned 1 worktree");
895
+ });
896
+
897
+ test("lead worktrees are cleaned without --force even with unmerged non-sprout changes", async () => {
898
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
899
+ await mkdir(worktreesDir, { recursive: true });
900
+
901
+ const { path: wtPath, branch } = await createWorktree({
902
+ repoRoot: tempDir,
903
+ baseDir: worktreesDir,
904
+ agentName: "lead-unmerged",
905
+ baseBranch: "main",
906
+ taskId: "task-lead-unmerged",
907
+ });
908
+
909
+ // Add unmerged non-.sprout/ commit
910
+ await commitFile(wtPath, "src/lead-work.ts", "export const y = 2;", "unmerged lead work");
911
+
912
+ writeSessionsToStore([
913
+ makeSession({
914
+ id: "session-lead-unmerged",
915
+ agentName: "lead-unmerged",
916
+ capability: "lead",
917
+ worktreePath: wtPath,
918
+ branchName: branch,
919
+ taskId: "task-lead-unmerged",
920
+ state: "completed",
921
+ }),
922
+ ]);
923
+
924
+ // Run clean WITHOUT --force — leads bypass merge check
925
+ await worktreeCommand(["clean"]);
926
+ const out = output();
927
+
928
+ // Lead worktree SHOULD be removed (not skipped)
929
+ expect(existsSync(wtPath)).toBe(false);
930
+ expect(out).toContain("Cleaned 1 worktree");
931
+ expect(out).not.toContain("Skipped");
932
+ });
933
+
934
+ test("--json output includes sproutPreserved array", async () => {
935
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
936
+ await mkdir(worktreesDir, { recursive: true });
937
+
938
+ const { path: wtPath, branch } = await createWorktree({
939
+ repoRoot: tempDir,
940
+ baseDir: worktreesDir,
941
+ agentName: "lead-sprout-json",
942
+ baseBranch: "main",
943
+ taskId: "task-sprout-json",
944
+ });
945
+
946
+ // Commit a .sprout/ file in the lead worktree
947
+ await commitFile(
948
+ wtPath,
949
+ ".sprout/issues/json-issue.yaml",
950
+ "id: json-issue\ntitle: JSON Issue\nstatus: open\n",
951
+ "sprout: add json issue",
952
+ );
953
+
954
+ writeSessionsToStore([
955
+ makeSession({
956
+ id: "session-lead-json",
957
+ agentName: "lead-sprout-json",
958
+ capability: "lead",
959
+ worktreePath: wtPath,
960
+ branchName: branch,
961
+ taskId: "task-sprout-json",
962
+ state: "completed",
963
+ }),
964
+ ]);
965
+
966
+ await worktreeCommand(["clean", "--json"]);
967
+ const out = output();
968
+
969
+ const parsed = JSON.parse(out.trim()) as {
970
+ cleaned: string[];
971
+ failed: string[];
972
+ skipped: string[];
973
+ pruned: number;
974
+ mailPurged: number;
975
+ sproutPreserved: string[];
976
+ };
977
+
978
+ expect(parsed.cleaned).toContain(branch);
979
+ expect(parsed.sproutPreserved).toContain(branch);
980
+ });
981
+
982
+ describe("live-children guard", () => {
983
+ /**
984
+ * Write sessions into a nested .agentplate/sessions.db inside a worktree.
985
+ * Simulates a lead worktree that has spawned builder children.
986
+ */
987
+ function writeNestedSessions(worktreePath: string, sessions: AgentSession[]): void {
988
+ const nestedAgentplate = join(worktreePath, ".agentplate");
989
+ mkdirSync(nestedAgentplate, { recursive: true });
990
+ const store = createSessionStore(join(nestedAgentplate, "sessions.db"));
991
+ for (const s of sessions) {
992
+ store.upsert(s);
993
+ }
994
+ store.close();
995
+ }
996
+
997
+ test("clean skipped when live children present (no --force)", async () => {
998
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
999
+ await mkdir(worktreesDir, { recursive: true });
1000
+
1001
+ const { path: wtPath } = await createWorktree({
1002
+ repoRoot: tempDir,
1003
+ baseDir: worktreesDir,
1004
+ agentName: "lead-with-children",
1005
+ baseBranch: "main",
1006
+ taskId: "task-lead",
1007
+ });
1008
+
1009
+ // Parent session is completed
1010
+ writeSessionsToStore([
1011
+ makeSession({
1012
+ id: "session-lead",
1013
+ agentName: "lead-with-children",
1014
+ capability: "lead",
1015
+ worktreePath: wtPath,
1016
+ branchName: "agentplate/lead-with-children/task-lead",
1017
+ taskId: "task-lead",
1018
+ state: "completed",
1019
+ }),
1020
+ ]);
1021
+
1022
+ // Nested session with process.pid (guaranteed alive)
1023
+ writeNestedSessions(wtPath, [
1024
+ {
1025
+ id: "nested-builder",
1026
+ agentName: "nested-builder",
1027
+ capability: "builder",
1028
+ worktreePath: join(wtPath, ".agentplate", "worktrees", "nested-builder"),
1029
+ branchName: "agentplate/nested-builder/task-child",
1030
+ taskId: "task-child",
1031
+ tmuxSession: "agentplate-nested-builder-fake",
1032
+ state: "working",
1033
+ pid: process.pid, // current process — guaranteed alive
1034
+ parentAgent: "lead-with-children",
1035
+ depth: 2,
1036
+ runId: null,
1037
+ startedAt: new Date().toISOString(),
1038
+ lastActivity: new Date().toISOString(),
1039
+ escalationLevel: 0,
1040
+ stalledSince: null,
1041
+ transcriptPath: null,
1042
+ },
1043
+ ]);
1044
+
1045
+ await worktreeCommand(["clean", "--completed", "--json"]);
1046
+ const out = output();
1047
+
1048
+ const parsed = JSON.parse(out.trim()) as {
1049
+ cleaned: string[];
1050
+ blockedByChildren: string[];
1051
+ };
1052
+
1053
+ expect(parsed.cleaned).toEqual([]);
1054
+ expect(parsed.blockedByChildren).toContain("agentplate/lead-with-children/task-lead");
1055
+ // Worktree still exists
1056
+ expect(existsSync(wtPath)).toBe(true);
1057
+ });
1058
+
1059
+ test("clean proceeds when nested sessions are dead (pid unreachable)", async () => {
1060
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
1061
+ await mkdir(worktreesDir, { recursive: true });
1062
+
1063
+ const { path: wtPath } = await createWorktree({
1064
+ repoRoot: tempDir,
1065
+ baseDir: worktreesDir,
1066
+ agentName: "lead-dead-children",
1067
+ baseBranch: "main",
1068
+ taskId: "task-dead",
1069
+ });
1070
+
1071
+ writeSessionsToStore([
1072
+ makeSession({
1073
+ id: "session-lead-dead",
1074
+ agentName: "lead-dead-children",
1075
+ capability: "lead",
1076
+ worktreePath: wtPath,
1077
+ branchName: "agentplate/lead-dead-children/task-dead",
1078
+ taskId: "task-dead",
1079
+ state: "completed",
1080
+ }),
1081
+ ]);
1082
+
1083
+ // Nested session with a dead pid (extremely high, will not exist)
1084
+ writeNestedSessions(wtPath, [
1085
+ {
1086
+ id: "nested-dead",
1087
+ agentName: "nested-dead",
1088
+ capability: "builder",
1089
+ worktreePath: join(wtPath, ".agentplate", "worktrees", "nested-dead"),
1090
+ branchName: "agentplate/nested-dead/task-dead-child",
1091
+ taskId: "task-dead-child",
1092
+ tmuxSession: "agentplate-nested-dead-fake",
1093
+ state: "working",
1094
+ pid: 999999999, // dead pid
1095
+ parentAgent: "lead-dead-children",
1096
+ depth: 2,
1097
+ runId: null,
1098
+ startedAt: new Date().toISOString(),
1099
+ lastActivity: new Date().toISOString(),
1100
+ escalationLevel: 0,
1101
+ stalledSince: null,
1102
+ transcriptPath: null,
1103
+ },
1104
+ ]);
1105
+
1106
+ await worktreeCommand(["clean", "--completed", "--json"]);
1107
+ const out = output();
1108
+
1109
+ const parsed = JSON.parse(out.trim()) as {
1110
+ cleaned: string[];
1111
+ blockedByChildren: string[];
1112
+ };
1113
+
1114
+ expect(parsed.cleaned).toContain("agentplate/lead-dead-children/task-dead");
1115
+ expect(parsed.blockedByChildren).toEqual([]);
1116
+ expect(existsSync(wtPath)).toBe(false);
1117
+ });
1118
+
1119
+ test("--force removes worktree even with live children", async () => {
1120
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
1121
+ await mkdir(worktreesDir, { recursive: true });
1122
+
1123
+ const { path: wtPath } = await createWorktree({
1124
+ repoRoot: tempDir,
1125
+ baseDir: worktreesDir,
1126
+ agentName: "lead-force",
1127
+ baseBranch: "main",
1128
+ taskId: "task-force-children",
1129
+ });
1130
+
1131
+ writeSessionsToStore([
1132
+ makeSession({
1133
+ id: "session-lead-force",
1134
+ agentName: "lead-force",
1135
+ capability: "lead",
1136
+ worktreePath: wtPath,
1137
+ branchName: "agentplate/lead-force/task-force-children",
1138
+ taskId: "task-force-children",
1139
+ state: "completed",
1140
+ }),
1141
+ ]);
1142
+
1143
+ // Use a dead pid — avoids actually killing any live process,
1144
+ // but still exercises the --force code path.
1145
+ writeNestedSessions(wtPath, [
1146
+ {
1147
+ id: "nested-force",
1148
+ agentName: "nested-force",
1149
+ capability: "builder",
1150
+ worktreePath: join(wtPath, ".agentplate", "worktrees", "nested-force"),
1151
+ branchName: "agentplate/nested-force/task-force-child",
1152
+ taskId: "task-force-child",
1153
+ tmuxSession: "agentplate-nested-force-fake",
1154
+ state: "working",
1155
+ pid: 999999999, // dead pid, safe to kill
1156
+ parentAgent: "lead-force",
1157
+ depth: 2,
1158
+ runId: null,
1159
+ startedAt: new Date().toISOString(),
1160
+ lastActivity: new Date().toISOString(),
1161
+ escalationLevel: 0,
1162
+ stalledSince: null,
1163
+ transcriptPath: null,
1164
+ },
1165
+ ]);
1166
+
1167
+ await worktreeCommand(["clean", "--force", "--json"]);
1168
+ const out = output();
1169
+
1170
+ const parsed = JSON.parse(out.trim()) as {
1171
+ cleaned: string[];
1172
+ blockedByChildren: string[];
1173
+ };
1174
+
1175
+ // Should be cleaned (not blocked) even though nested sessions existed
1176
+ expect(parsed.cleaned).toContain("agentplate/lead-force/task-force-children");
1177
+ expect(existsSync(wtPath)).toBe(false);
1178
+ });
1179
+
1180
+ test("no nested .agentplate — treated as no live children, clean proceeds", async () => {
1181
+ const worktreesDir = join(tempDir, ".agentplate", "worktrees");
1182
+ await mkdir(worktreesDir, { recursive: true });
1183
+
1184
+ const { path: wtPath } = await createWorktree({
1185
+ repoRoot: tempDir,
1186
+ baseDir: worktreesDir,
1187
+ agentName: "lead-no-nested",
1188
+ baseBranch: "main",
1189
+ taskId: "task-no-nested",
1190
+ });
1191
+
1192
+ writeSessionsToStore([
1193
+ makeSession({
1194
+ id: "session-lead-no-nested",
1195
+ agentName: "lead-no-nested",
1196
+ capability: "lead",
1197
+ worktreePath: wtPath,
1198
+ branchName: "agentplate/lead-no-nested/task-no-nested",
1199
+ taskId: "task-no-nested",
1200
+ state: "completed",
1201
+ }),
1202
+ ]);
1203
+
1204
+ // No nested .agentplate/ directory written
1205
+
1206
+ await worktreeCommand(["clean", "--completed", "--json"]);
1207
+ const out = output();
1208
+
1209
+ const parsed = JSON.parse(out.trim()) as {
1210
+ cleaned: string[];
1211
+ blockedByChildren: string[];
1212
+ };
1213
+
1214
+ expect(parsed.cleaned).toContain("agentplate/lead-no-nested/task-no-nested");
1215
+ expect(parsed.blockedByChildren).toEqual([]);
1216
+ expect(existsSync(wtPath)).toBe(false);
1217
+ });
1218
+ });
1219
+ });
1220
+ });
1221
+
1222
+ describe("checkLiveChildren", () => {
1223
+ let tempDir: string;
1224
+
1225
+ beforeEach(async () => {
1226
+ tempDir = await mkdtemp(join(tmpdir(), "agentplate-checkchildren-"));
1227
+ });
1228
+
1229
+ afterEach(async () => {
1230
+ await cleanupTempDir(tempDir);
1231
+ });
1232
+
1233
+ test("returns empty array when no nested .agentplate/sessions.db", async () => {
1234
+ const result = await checkLiveChildren(tempDir);
1235
+ expect(result).toEqual([]);
1236
+ });
1237
+
1238
+ test("returns empty array when all sessions are completed", async () => {
1239
+ const nestedAgentplate = join(tempDir, ".agentplate");
1240
+ mkdirSync(nestedAgentplate, { recursive: true });
1241
+ const store = createSessionStore(join(nestedAgentplate, "sessions.db"));
1242
+ store.upsert({
1243
+ id: "s1",
1244
+ agentName: "done-agent",
1245
+ capability: "builder",
1246
+ worktreePath: "/fake/wt",
1247
+ branchName: "agentplate/done/task",
1248
+ taskId: "task",
1249
+ tmuxSession: "",
1250
+ state: "completed",
1251
+ pid: process.pid,
1252
+ parentAgent: null,
1253
+ depth: 2,
1254
+ runId: null,
1255
+ startedAt: new Date().toISOString(),
1256
+ lastActivity: new Date().toISOString(),
1257
+ escalationLevel: 0,
1258
+ stalledSince: null,
1259
+ transcriptPath: null,
1260
+ });
1261
+ store.close();
1262
+
1263
+ const result = await checkLiveChildren(tempDir);
1264
+ expect(result).toEqual([]);
1265
+ });
1266
+
1267
+ test("returns live children when working session with alive pid exists", async () => {
1268
+ const nestedAgentplate = join(tempDir, ".agentplate");
1269
+ mkdirSync(nestedAgentplate, { recursive: true });
1270
+ const store = createSessionStore(join(nestedAgentplate, "sessions.db"));
1271
+ store.upsert({
1272
+ id: "s1",
1273
+ agentName: "live-agent",
1274
+ capability: "builder",
1275
+ worktreePath: "/fake/wt",
1276
+ branchName: "agentplate/live/task",
1277
+ taskId: "task",
1278
+ tmuxSession: "",
1279
+ state: "working",
1280
+ pid: process.pid, // current process — alive
1281
+ parentAgent: null,
1282
+ depth: 2,
1283
+ runId: null,
1284
+ startedAt: new Date().toISOString(),
1285
+ lastActivity: new Date().toISOString(),
1286
+ escalationLevel: 0,
1287
+ stalledSince: null,
1288
+ transcriptPath: null,
1289
+ });
1290
+ store.close();
1291
+
1292
+ const result = await checkLiveChildren(tempDir);
1293
+ expect(result).toHaveLength(1);
1294
+ expect(result[0]?.agentName).toBe("live-agent");
1295
+ expect(result[0]?.pid).toBe(process.pid);
1296
+ });
1297
+ });