@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,1058 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { AgentError } from "../errors.ts";
6
+ import { cleanupTempDir } from "../test-helpers.ts";
7
+ import type { OverlayConfig, QualityGate } from "../types.ts";
8
+ import {
9
+ formatQualityGatesBash,
10
+ formatQualityGatesCapabilities,
11
+ formatQualityGatesInline,
12
+ formatQualityGatesSteps,
13
+ formatSiblings,
14
+ generateOverlay,
15
+ isCanonicalRoot,
16
+ writeOverlay,
17
+ } from "./overlay.ts";
18
+
19
+ const SAMPLE_BASE_DEFINITION = `# Builder Agent
20
+
21
+ You are a **builder agent** in the agentplate swarm system.
22
+
23
+ ## Role
24
+ Implement changes according to a spec.
25
+
26
+ ## Propulsion Principle
27
+ Read your assignment. Execute immediately.
28
+
29
+ ## Failure Modes
30
+ - FILE_SCOPE_VIOLATION
31
+ - SILENT_FAILURE
32
+ `;
33
+
34
+ /** Build a complete OverlayConfig with sensible defaults, overrideable by partial. */
35
+ function makeConfig(overrides?: Partial<OverlayConfig>): OverlayConfig {
36
+ return {
37
+ agentName: "test-builder",
38
+ taskId: "agentplate-abc",
39
+ specPath: ".agentplate/specs/agentplate-abc.md",
40
+ branchName: "agent/test-builder/agentplate-abc",
41
+ worktreePath: "/tmp/test-project/.agentplate/worktrees/test-builder",
42
+ fileScope: ["src/agents/manifest.ts", "src/agents/overlay.ts"],
43
+ loamDomains: ["typescript", "testing"],
44
+ parentAgent: "lead-alpha",
45
+ depth: 1,
46
+ canSpawn: false,
47
+ capability: "builder",
48
+ baseDefinition: SAMPLE_BASE_DEFINITION,
49
+ ...overrides,
50
+ };
51
+ }
52
+
53
+ describe("generateOverlay", () => {
54
+ test("output contains agent name", async () => {
55
+ const config = makeConfig({ agentName: "my-scout" });
56
+ const output = await generateOverlay(config);
57
+
58
+ expect(output).toContain("my-scout");
59
+ });
60
+
61
+ test("output contains task ID", async () => {
62
+ const config = makeConfig({ taskId: "agentplate-xyz" });
63
+ const output = await generateOverlay(config);
64
+
65
+ expect(output).toContain("agentplate-xyz");
66
+ });
67
+
68
+ test("output contains branch name", async () => {
69
+ const config = makeConfig({ branchName: "agent/scout/agentplate-xyz" });
70
+ const output = await generateOverlay(config);
71
+
72
+ expect(output).toContain("agent/scout/agentplate-xyz");
73
+ });
74
+
75
+ test("output contains parent agent name", async () => {
76
+ const config = makeConfig({ parentAgent: "lead-bravo" });
77
+ const output = await generateOverlay(config);
78
+
79
+ expect(output).toContain("lead-bravo");
80
+ });
81
+
82
+ test("output contains depth", async () => {
83
+ const config = makeConfig({ depth: 2 });
84
+ const output = await generateOverlay(config);
85
+
86
+ expect(output).toContain("2");
87
+ });
88
+
89
+ test("output contains spec path when provided", async () => {
90
+ const config = makeConfig({ specPath: ".agentplate/specs/my-task.md" });
91
+ const output = await generateOverlay(config);
92
+
93
+ expect(output).toContain(".agentplate/specs/my-task.md");
94
+ });
95
+
96
+ test("shows fallback text when specPath is null", async () => {
97
+ const config = makeConfig({ specPath: null });
98
+ const output = await generateOverlay(config);
99
+
100
+ expect(output).toContain("No spec file provided");
101
+ expect(output).not.toContain("{{SPEC_PATH}}");
102
+ });
103
+
104
+ test("includes 'Read your task spec' instruction when spec provided", async () => {
105
+ const config = makeConfig({ specPath: ".agentplate/specs/my-task.md" });
106
+ const output = await generateOverlay(config);
107
+
108
+ expect(output).toContain("Read your task spec at the path above");
109
+ });
110
+
111
+ test("does not include 'Read your task spec' instruction when specPath is null", async () => {
112
+ const config = makeConfig({ specPath: null });
113
+ const output = await generateOverlay(config);
114
+
115
+ expect(output).not.toContain("Read your task spec at the path above");
116
+ expect(output).toContain("No task spec was provided");
117
+ });
118
+
119
+ test("shows 'coordinator' when parentAgent is null", async () => {
120
+ const config = makeConfig({ parentAgent: null });
121
+ const output = await generateOverlay(config);
122
+
123
+ expect(output).toContain("coordinator");
124
+ });
125
+
126
+ test("file scope is formatted as markdown bullets", async () => {
127
+ const config = makeConfig({
128
+ fileScope: ["src/foo.ts", "src/bar.ts"],
129
+ });
130
+ const output = await generateOverlay(config);
131
+
132
+ expect(output).toContain("- `src/foo.ts`");
133
+ expect(output).toContain("- `src/bar.ts`");
134
+ });
135
+
136
+ test("empty file scope shows fallback text", async () => {
137
+ const config = makeConfig({ fileScope: [] });
138
+ const output = await generateOverlay(config);
139
+
140
+ expect(output).toContain("No file scope restrictions");
141
+ });
142
+
143
+ test("loam domains formatted as prime command", async () => {
144
+ const config = makeConfig({ loamDomains: ["typescript", "testing"] });
145
+ const output = await generateOverlay(config);
146
+
147
+ expect(output).toContain("lm prime typescript testing");
148
+ });
149
+
150
+ test("empty loam domains shows fallback text", async () => {
151
+ const config = makeConfig({ loamDomains: [] });
152
+ const output = await generateOverlay(config);
153
+
154
+ expect(output).toContain("No specific expertise domains configured");
155
+ });
156
+
157
+ test("canSpawn false says 'You may NOT spawn sub-workers'", async () => {
158
+ const config = makeConfig({ canSpawn: false });
159
+ const output = await generateOverlay(config);
160
+
161
+ expect(output).toContain("You may NOT spawn sub-workers");
162
+ });
163
+
164
+ test("canSpawn true includes sling example", async () => {
165
+ const config = makeConfig({
166
+ canSpawn: true,
167
+ agentName: "lead-alpha",
168
+ depth: 1,
169
+ });
170
+ const output = await generateOverlay(config);
171
+
172
+ expect(output).toContain("ap sling");
173
+ expect(output).toContain("--parent lead-alpha");
174
+ expect(output).toContain("--depth 2");
175
+ });
176
+
177
+ test("no unreplaced placeholders remain in output", async () => {
178
+ const config = makeConfig();
179
+ const output = await generateOverlay(config);
180
+
181
+ expect(output).not.toContain("{{");
182
+ expect(output).not.toContain("}}");
183
+ });
184
+
185
+ test("includes pre-loaded expertise when loamExpertise is provided", async () => {
186
+ const config = makeConfig({
187
+ loamExpertise: "## architecture\n- Pattern: use singleton for config loader",
188
+ });
189
+ const output = await generateOverlay(config);
190
+
191
+ expect(output).toContain("### Pre-loaded Expertise");
192
+ expect(output).toContain("automatically loaded at spawn time");
193
+ expect(output).toContain("## architecture");
194
+ expect(output).toContain("Pattern: use singleton for config loader");
195
+ });
196
+
197
+ test("omits expertise section when loamExpertise is undefined", async () => {
198
+ const config = makeConfig({ loamExpertise: undefined });
199
+ const output = await generateOverlay(config);
200
+
201
+ expect(output).not.toContain("### Pre-loaded Expertise");
202
+ expect(output).not.toContain("automatically loaded at spawn time");
203
+ });
204
+
205
+ test("omits expertise section when loamExpertise is empty string", async () => {
206
+ const config = makeConfig({ loamExpertise: "" });
207
+ const output = await generateOverlay(config);
208
+
209
+ expect(output).not.toContain("### Pre-loaded Expertise");
210
+ });
211
+
212
+ test("omits expertise section when loamExpertise is whitespace only", async () => {
213
+ const config = makeConfig({ loamExpertise: " \n\t \n " });
214
+ const output = await generateOverlay(config);
215
+
216
+ expect(output).not.toContain("### Pre-loaded Expertise");
217
+ });
218
+
219
+ test("builder capability includes full quality gates section", async () => {
220
+ const config = makeConfig({ capability: "builder" });
221
+ const output = await generateOverlay(config);
222
+
223
+ expect(output).toContain("Quality Gates");
224
+ expect(output).toContain("bun test");
225
+ expect(output).toContain("bun run lint");
226
+ expect(output).toContain("Commit");
227
+ });
228
+
229
+ test("lead capability includes full quality gates section", async () => {
230
+ const config = makeConfig({ capability: "lead" });
231
+ const output = await generateOverlay(config);
232
+
233
+ expect(output).toContain("Quality Gates");
234
+ expect(output).toContain("bun test");
235
+ expect(output).toContain("bun run lint");
236
+ });
237
+
238
+ test("merger capability includes full quality gates section", async () => {
239
+ const config = makeConfig({ capability: "merger" });
240
+ const output = await generateOverlay(config);
241
+
242
+ expect(output).toContain("Quality Gates");
243
+ expect(output).toContain("bun test");
244
+ });
245
+
246
+ test("scout capability gets read-only completion section instead of quality gates", async () => {
247
+ const config = makeConfig({ capability: "scout", agentName: "my-scout" });
248
+ const output = await generateOverlay(config);
249
+
250
+ expect(output).toContain("Completion");
251
+ expect(output).toContain("read-only agent");
252
+ expect(output).toContain("Do NOT commit");
253
+ expect(output).not.toContain("Quality Gates");
254
+ expect(output).not.toContain("bun test");
255
+ expect(output).not.toContain("bun run lint");
256
+ });
257
+
258
+ test("reviewer capability gets read-only completion section instead of quality gates", async () => {
259
+ const config = makeConfig({ capability: "reviewer", agentName: "my-reviewer" });
260
+ const output = await generateOverlay(config);
261
+
262
+ expect(output).toContain("Completion");
263
+ expect(output).toContain("read-only agent");
264
+ expect(output).toContain("Do NOT commit");
265
+ expect(output).not.toContain("Quality Gates");
266
+ expect(output).not.toContain("bun test");
267
+ expect(output).not.toContain("bun run lint");
268
+ });
269
+
270
+ test("scout completion section includes sr close and mail send", async () => {
271
+ const config = makeConfig({
272
+ capability: "scout",
273
+ agentName: "recon-1",
274
+ taskId: "agentplate-task1",
275
+ parentAgent: "lead-alpha",
276
+ });
277
+ const output = await generateOverlay(config);
278
+
279
+ expect(output).toContain("sr close agentplate-task1");
280
+ expect(output).toContain("ap mail send --to lead-alpha");
281
+ });
282
+
283
+ test("reviewer completion section uses coordinator when no parent", async () => {
284
+ const config = makeConfig({
285
+ capability: "reviewer",
286
+ parentAgent: null,
287
+ });
288
+ const output = await generateOverlay(config);
289
+
290
+ expect(output).toContain("--to coordinator");
291
+ });
292
+
293
+ test("output includes communication section with agent address", async () => {
294
+ const config = makeConfig({ agentName: "worker-42" });
295
+ const output = await generateOverlay(config);
296
+
297
+ expect(output).toContain("ap mail check --agent worker-42");
298
+ expect(output).toContain("ap mail send --to");
299
+ });
300
+
301
+ test("output includes base agent definition content (Layer 1)", async () => {
302
+ const config = makeConfig();
303
+ const output = await generateOverlay(config);
304
+
305
+ expect(output).toContain("# Builder Agent");
306
+ expect(output).toContain("Propulsion Principle");
307
+ expect(output).toContain("FILE_SCOPE_VIOLATION");
308
+ });
309
+
310
+ test("base definition appears before task assignment section", async () => {
311
+ const config = makeConfig();
312
+ const output = await generateOverlay(config);
313
+
314
+ const baseDefIndex = output.indexOf("# Builder Agent");
315
+ const assignmentIndex = output.indexOf("## Your Assignment");
316
+ expect(baseDefIndex).toBeGreaterThan(-1);
317
+ expect(assignmentIndex).toBeGreaterThan(-1);
318
+ expect(baseDefIndex).toBeLessThan(assignmentIndex);
319
+ });
320
+
321
+ test("output contains worktree path in assignment section", async () => {
322
+ const config = makeConfig({
323
+ worktreePath: "/project/.agentplate/worktrees/my-builder",
324
+ });
325
+ const output = await generateOverlay(config);
326
+
327
+ expect(output).toContain("/project/.agentplate/worktrees/my-builder");
328
+ expect(output).toContain("**Worktree:**");
329
+ });
330
+
331
+ test("output contains Working Directory section with worktree path", async () => {
332
+ const config = makeConfig({
333
+ worktreePath: "/tmp/worktrees/builder-1",
334
+ });
335
+ const output = await generateOverlay(config);
336
+
337
+ expect(output).toContain("## Working Directory");
338
+ expect(output).toContain("Your worktree root is: `/tmp/worktrees/builder-1`");
339
+ expect(output).toContain("PATH_BOUNDARY_VIOLATION");
340
+ });
341
+
342
+ test("file scope section references worktree root", async () => {
343
+ const config = makeConfig({
344
+ worktreePath: "/tmp/worktrees/builder-scope",
345
+ });
346
+ const output = await generateOverlay(config);
347
+
348
+ expect(output).toContain(
349
+ "These paths are relative to your worktree root: `/tmp/worktrees/builder-scope`",
350
+ );
351
+ });
352
+
353
+ test("builder constraints include worktree isolation", async () => {
354
+ const config = makeConfig({
355
+ capability: "builder",
356
+ worktreePath: "/tmp/worktrees/builder-constraints",
357
+ });
358
+ const output = await generateOverlay(config);
359
+
360
+ expect(output).toContain("WORKTREE ISOLATION");
361
+ expect(output).toContain("/tmp/worktrees/builder-constraints");
362
+ expect(output).toContain("NEVER write to the canonical repo root");
363
+ });
364
+
365
+ test("no unreplaced WORKTREE_PATH placeholders", async () => {
366
+ const config = makeConfig();
367
+ const output = await generateOverlay(config);
368
+
369
+ expect(output).not.toContain("{{WORKTREE_PATH}}");
370
+ });
371
+
372
+ test("builder with custom qualityGates uses them instead of defaults", async () => {
373
+ const gates: QualityGate[] = [
374
+ { name: "Test", command: "pytest", description: "all tests pass" },
375
+ { name: "Lint", command: "ruff check .", description: "no lint errors" },
376
+ ];
377
+ const config = makeConfig({ capability: "builder", qualityGates: gates });
378
+ const output = await generateOverlay(config);
379
+
380
+ expect(output).toContain("pytest");
381
+ expect(output).toContain("ruff check .");
382
+ expect(output).not.toContain("bun test");
383
+ expect(output).not.toContain("bun run lint");
384
+ expect(output).not.toContain("bun run typecheck");
385
+ });
386
+
387
+ test("builder with undefined qualityGates falls back to defaults", async () => {
388
+ const config = makeConfig({ capability: "builder", qualityGates: undefined });
389
+ const output = await generateOverlay(config);
390
+
391
+ expect(output).toContain("bun test");
392
+ expect(output).toContain("bun run lint");
393
+ expect(output).toContain("bun run typecheck");
394
+ });
395
+
396
+ test("builder with empty qualityGates array falls back to defaults", async () => {
397
+ const config = makeConfig({ capability: "builder", qualityGates: [] });
398
+ const output = await generateOverlay(config);
399
+
400
+ expect(output).toContain("bun test");
401
+ expect(output).toContain("bun run lint");
402
+ expect(output).toContain("bun run typecheck");
403
+ });
404
+
405
+ test("custom qualityGates are numbered correctly", async () => {
406
+ const gates: QualityGate[] = [
407
+ { name: "Build", command: "cargo build", description: "compilation succeeds" },
408
+ { name: "Test", command: "cargo test", description: "all tests pass" },
409
+ ];
410
+ const config = makeConfig({ capability: "builder", qualityGates: gates });
411
+ const output = await generateOverlay(config);
412
+
413
+ expect(output).toContain("1. **Build:**");
414
+ expect(output).toContain("2. **Test:**");
415
+ // Commit should be item 3
416
+ expect(output).toContain("3. **Commit:**");
417
+ });
418
+
419
+ test("scout capability ignores qualityGates (stays read-only)", async () => {
420
+ const gates: QualityGate[] = [
421
+ { name: "Test", command: "pytest", description: "all tests pass" },
422
+ ];
423
+ const config = makeConfig({ capability: "scout", qualityGates: gates });
424
+ const output = await generateOverlay(config);
425
+
426
+ expect(output).toContain("read-only agent");
427
+ expect(output).not.toContain("pytest");
428
+ expect(output).not.toContain("Quality Gates");
429
+ });
430
+
431
+ test("default trackerCli renders as sr in quality gates", async () => {
432
+ const config = makeConfig({ capability: "builder", taskId: "agentplate-task1" });
433
+ const output = await generateOverlay(config);
434
+
435
+ expect(output).toContain("sr close agentplate-task1");
436
+ });
437
+
438
+ test("custom trackerCli replaces sr in quality gates", async () => {
439
+ const config = makeConfig({
440
+ capability: "builder",
441
+ trackerCli: "sr",
442
+ taskId: "agentplate-test1",
443
+ });
444
+ const output = await generateOverlay(config);
445
+
446
+ expect(output).toContain("sr close agentplate-test1");
447
+ expect(output).not.toContain("bd close");
448
+ });
449
+
450
+ test("custom trackerCli replaces bd in constraints", async () => {
451
+ const config = makeConfig({
452
+ capability: "builder",
453
+ trackerCli: "sr",
454
+ });
455
+ const output = await generateOverlay(config);
456
+
457
+ expect(output).toContain("`sr close`");
458
+ });
459
+
460
+ test("custom trackerCli replaces bd in read-only completion section", async () => {
461
+ const config = makeConfig({
462
+ capability: "scout",
463
+ trackerCli: "sr",
464
+ taskId: "agentplate-test2",
465
+ });
466
+ const output = await generateOverlay(config);
467
+
468
+ expect(output).toContain("sr close agentplate-test2");
469
+ expect(output).not.toContain("bd close");
470
+ });
471
+
472
+ test("TRACKER_CLI in base definition is replaced", async () => {
473
+ const config = makeConfig({
474
+ trackerCli: "sr",
475
+ baseDefinition: "Run `{{TRACKER_CLI}} show` to check status.",
476
+ });
477
+ const output = await generateOverlay(config);
478
+
479
+ expect(output).toContain("Run `sr show` to check status.");
480
+ expect(output).not.toContain("{{TRACKER_CLI}}");
481
+ });
482
+
483
+ test("TRACKER_NAME in base definition is replaced", async () => {
484
+ const config = makeConfig({
485
+ trackerName: "sprout",
486
+ baseDefinition: "Close your {{TRACKER_NAME}} issue when done.",
487
+ });
488
+ const output = await generateOverlay(config);
489
+
490
+ expect(output).toContain("Close your sprout issue when done.");
491
+ expect(output).not.toContain("{{TRACKER_NAME}}");
492
+ });
493
+
494
+ test("defaults: no trackerCli/trackerName produces sr/sprout", async () => {
495
+ const config = makeConfig({ capability: "builder", taskId: "agentplate-back" });
496
+ const output = await generateOverlay(config);
497
+
498
+ expect(output).toContain("sr close agentplate-back");
499
+ });
500
+
501
+ test("dispatch overrides: skipReview injects SKIP REVIEW directive for leads", async () => {
502
+ const config = makeConfig({
503
+ capability: "lead",
504
+ skipReview: true,
505
+ canSpawn: true,
506
+ });
507
+ const output = await generateOverlay(config);
508
+
509
+ expect(output).toContain("Dispatch Overrides");
510
+ expect(output).toContain("SKIP REVIEW");
511
+ expect(output).toContain("Self-verify");
512
+ });
513
+
514
+ test("dispatch overrides: maxAgentsOverride injects MAX AGENTS directive for leads", async () => {
515
+ const config = makeConfig({
516
+ capability: "lead",
517
+ maxAgentsOverride: 3,
518
+ canSpawn: true,
519
+ });
520
+ const output = await generateOverlay(config);
521
+
522
+ expect(output).toContain("Dispatch Overrides");
523
+ expect(output).toContain("MAX AGENTS");
524
+ expect(output).toContain("3");
525
+ });
526
+
527
+ test("dispatch overrides: maxAgentsOverride of 1 directs the lead to spend the slot on a single builder", async () => {
528
+ const config = makeConfig({
529
+ capability: "lead",
530
+ maxAgentsOverride: 1,
531
+ canSpawn: true,
532
+ });
533
+ const output = await generateOverlay(config);
534
+
535
+ expect(output).toContain("MAX AGENTS");
536
+ expect(output).toContain("single builder");
537
+ expect(output).toContain("Leads cannot implement directly");
538
+ });
539
+
540
+ test("dispatch overrides: maxAgentsOverride of 2 enables compressed-mode guidance", async () => {
541
+ const config = makeConfig({
542
+ capability: "lead",
543
+ maxAgentsOverride: 2,
544
+ canSpawn: true,
545
+ });
546
+ const output = await generateOverlay(config);
547
+
548
+ expect(output).toContain("MAX AGENTS");
549
+ expect(output).toContain("compressed mode");
550
+ expect(output).toContain("Leads do not implement");
551
+ });
552
+
553
+ test("dispatch overrides: both skipReview and maxAgentsOverride together", async () => {
554
+ const config = makeConfig({
555
+ capability: "lead",
556
+ skipReview: true,
557
+ maxAgentsOverride: 4,
558
+ canSpawn: true,
559
+ });
560
+ const output = await generateOverlay(config);
561
+
562
+ expect(output).toContain("SKIP REVIEW");
563
+ expect(output).toContain("MAX AGENTS");
564
+ expect(output).toContain("4");
565
+ });
566
+
567
+ test("dispatch overrides: not injected for builder capability", async () => {
568
+ const config = makeConfig({
569
+ capability: "builder",
570
+ skipReview: true,
571
+ maxAgentsOverride: 3,
572
+ });
573
+ const output = await generateOverlay(config);
574
+
575
+ expect(output).not.toContain("Dispatch Overrides");
576
+ });
577
+
578
+ test("dispatch overrides: not injected when no overrides set", async () => {
579
+ const config = makeConfig({
580
+ capability: "lead",
581
+ canSpawn: true,
582
+ });
583
+ const output = await generateOverlay(config);
584
+
585
+ expect(output).not.toContain("Dispatch Overrides");
586
+ });
587
+
588
+ test("dispatch overrides: maxAgentsOverride of 0 is not injected", async () => {
589
+ const config = makeConfig({
590
+ capability: "lead",
591
+ maxAgentsOverride: 0,
592
+ canSpawn: true,
593
+ });
594
+ const output = await generateOverlay(config);
595
+
596
+ expect(output).not.toContain("MAX AGENTS");
597
+ });
598
+
599
+ test("no unreplaced DISPATCH_OVERRIDES placeholder", async () => {
600
+ const config = makeConfig();
601
+ const output = await generateOverlay(config);
602
+
603
+ expect(output).not.toContain("{{DISPATCH_OVERRIDES}}");
604
+ });
605
+ });
606
+
607
+ describe("writeOverlay", () => {
608
+ let tempDir: string;
609
+
610
+ beforeEach(async () => {
611
+ tempDir = await mkdtemp(join(tmpdir(), "agentplate-overlay-test-"));
612
+ });
613
+
614
+ afterEach(async () => {
615
+ await cleanupTempDir(tempDir);
616
+ });
617
+
618
+ test("creates .claude/CLAUDE.md in worktree directory", async () => {
619
+ const worktreePath = join(tempDir, "worktree");
620
+ const config = makeConfig();
621
+
622
+ await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
623
+
624
+ const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
625
+ const file = Bun.file(outputPath);
626
+ const exists = await file.exists();
627
+ expect(exists).toBe(true);
628
+ });
629
+
630
+ test("written file contains the overlay content", async () => {
631
+ const worktreePath = join(tempDir, "worktree");
632
+ const config = makeConfig({ agentName: "file-writer-test" });
633
+
634
+ await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
635
+
636
+ const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
637
+ const content = await Bun.file(outputPath).text();
638
+ expect(content).toContain("file-writer-test");
639
+ expect(content).toContain(config.taskId);
640
+ expect(content).toContain(config.branchName);
641
+ });
642
+
643
+ test("creates .claude directory even if worktree already exists", async () => {
644
+ const worktreePath = join(tempDir, "existing-worktree");
645
+ const { mkdir } = await import("node:fs/promises");
646
+ await mkdir(worktreePath, { recursive: true });
647
+
648
+ const config = makeConfig();
649
+ await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
650
+
651
+ const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
652
+ const exists = await Bun.file(outputPath).exists();
653
+ expect(exists).toBe(true);
654
+ });
655
+
656
+ test("overwrites existing CLAUDE.md if it already exists", async () => {
657
+ const worktreePath = join(tempDir, "worktree");
658
+ const claudeDir = join(worktreePath, ".claude");
659
+ const { mkdir } = await import("node:fs/promises");
660
+ await mkdir(claudeDir, { recursive: true });
661
+ await Bun.write(join(claudeDir, "CLAUDE.md"), "old content");
662
+
663
+ const config = makeConfig({ agentName: "new-agent" });
664
+ await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
665
+
666
+ const content = await Bun.file(join(claudeDir, "CLAUDE.md")).text();
667
+ expect(content).toContain("new-agent");
668
+ expect(content).not.toContain("old content");
669
+ });
670
+
671
+ test("writeOverlay content matches generateOverlay output", async () => {
672
+ const worktreePath = join(tempDir, "worktree");
673
+ const config = makeConfig();
674
+
675
+ const generated = await generateOverlay(config);
676
+ await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
677
+
678
+ const written = await Bun.file(join(worktreePath, ".claude", "CLAUDE.md")).text();
679
+ expect(written).toBe(generated);
680
+ });
681
+
682
+ test("throws AgentError when worktreePath is the canonical project root", async () => {
683
+ const fakeProjectRoot = join(tempDir, "project-root");
684
+ await mkdir(fakeProjectRoot, { recursive: true });
685
+
686
+ const config = makeConfig({ agentName: "rogue-agent" });
687
+
688
+ expect(async () => {
689
+ await writeOverlay(fakeProjectRoot, config, fakeProjectRoot);
690
+ }).toThrow(AgentError);
691
+ });
692
+
693
+ test("error message mentions canonical project root when guard triggers", async () => {
694
+ const fakeProjectRoot = join(tempDir, "project-root-msg");
695
+ await mkdir(fakeProjectRoot, { recursive: true });
696
+
697
+ const config = makeConfig({ agentName: "rogue-agent" });
698
+
699
+ try {
700
+ await writeOverlay(fakeProjectRoot, config, fakeProjectRoot);
701
+ expect.unreachable("should have thrown");
702
+ } catch (err) {
703
+ expect(err).toBeInstanceOf(AgentError);
704
+ const agentErr = err as AgentError;
705
+ expect(agentErr.message).toContain("canonical project root");
706
+ expect(agentErr.message).toContain(fakeProjectRoot);
707
+ expect(agentErr.agentName).toBe("rogue-agent");
708
+ }
709
+ });
710
+
711
+ test("does NOT throw when worktreePath is a proper worktree subdirectory", async () => {
712
+ const fakeProjectRoot = join(tempDir, "project-with-worktrees");
713
+ await mkdir(join(fakeProjectRoot, ".agentplate", "worktrees", "my-agent"), { recursive: true });
714
+
715
+ const worktreePath = join(fakeProjectRoot, ".agentplate", "worktrees", "my-agent");
716
+ const config = makeConfig();
717
+
718
+ // This should succeed — the worktree is not the canonical root
719
+ await writeOverlay(worktreePath, config, fakeProjectRoot);
720
+
721
+ const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
722
+ const exists = await Bun.file(outputPath).exists();
723
+ expect(exists).toBe(true);
724
+ });
725
+
726
+ test("does not write CLAUDE.md when guard rejects the path", async () => {
727
+ const fakeProjectRoot = join(tempDir, "project-no-write");
728
+ await mkdir(fakeProjectRoot, { recursive: true });
729
+
730
+ const config = makeConfig();
731
+
732
+ try {
733
+ await writeOverlay(fakeProjectRoot, config, fakeProjectRoot);
734
+ } catch {
735
+ // Expected
736
+ }
737
+
738
+ // Verify CLAUDE.md was NOT written
739
+ const claudeMdPath = join(fakeProjectRoot, ".claude", "CLAUDE.md");
740
+ const exists = await Bun.file(claudeMdPath).exists();
741
+ expect(exists).toBe(false);
742
+ });
743
+
744
+ test("succeeds for worktree with .agentplate/config.yaml (dogfooding scenario)", async () => {
745
+ // When dogfooding on agentplate's own repo, .agentplate/config.yaml is tracked
746
+ // in git. Every worktree checkout includes it. The old file-existence heuristic
747
+ // would incorrectly reject these worktrees. The path-comparison guard must allow
748
+ // writes because the worktree path differs from the canonical root (agentplate-p4st).
749
+ const fakeProjectRoot = join(tempDir, "agentplate-dogfood");
750
+ const worktreePath = join(fakeProjectRoot, ".agentplate", "worktrees", "dogfood-agent");
751
+ await mkdir(join(worktreePath, ".agentplate"), { recursive: true });
752
+ // Simulate tracked .agentplate/config.yaml appearing in the worktree checkout
753
+ await Bun.write(
754
+ join(worktreePath, ".agentplate", "config.yaml"),
755
+ "project:\n name: agentplate\n",
756
+ );
757
+
758
+ const config = makeConfig({ agentName: "dogfood-agent" });
759
+
760
+ // Must succeed — worktreePath !== fakeProjectRoot even though config.yaml exists
761
+ await writeOverlay(worktreePath, config, fakeProjectRoot);
762
+
763
+ const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
764
+ const exists = await Bun.file(outputPath).exists();
765
+ expect(exists).toBe(true);
766
+ });
767
+
768
+ test("writes to custom instruction path when provided", async () => {
769
+ const worktreePath = join(tempDir, "worktree");
770
+ const config = makeConfig();
771
+ await writeOverlay(worktreePath, config, "/nonexistent-canonical-root", "AGENTS.md");
772
+ const outputPath = join(worktreePath, "AGENTS.md");
773
+ expect(await Bun.file(outputPath).exists()).toBe(true);
774
+ expect(await Bun.file(join(worktreePath, ".claude", "CLAUDE.md")).exists()).toBe(false);
775
+ });
776
+
777
+ test("custom instruction path creates necessary subdirectories", async () => {
778
+ const worktreePath = join(tempDir, "worktree");
779
+ const config = makeConfig();
780
+ await writeOverlay(
781
+ worktreePath,
782
+ config,
783
+ "/nonexistent-canonical-root",
784
+ ".pi/instructions/AGENT.md",
785
+ );
786
+ expect(await Bun.file(join(worktreePath, ".pi", "instructions", "AGENT.md")).exists()).toBe(
787
+ true,
788
+ );
789
+ });
790
+ });
791
+
792
+ describe("isCanonicalRoot", () => {
793
+ test("returns true when dir matches canonicalRoot", () => {
794
+ expect(isCanonicalRoot("/projects/my-app", "/projects/my-app")).toBe(true);
795
+ });
796
+
797
+ test("returns true when paths resolve to the same location", () => {
798
+ expect(isCanonicalRoot("/projects/my-app/./", "/projects/my-app")).toBe(true);
799
+ });
800
+
801
+ test("returns false when dir differs from canonicalRoot", () => {
802
+ expect(
803
+ isCanonicalRoot("/projects/my-app/.agentplate/worktrees/agent-1", "/projects/my-app"),
804
+ ).toBe(false);
805
+ });
806
+
807
+ test("returns false for worktree even when it contains .agentplate/config.yaml (dogfooding)", () => {
808
+ // This is the core dogfooding scenario: the worktree has .agentplate/config.yaml
809
+ // because it's tracked in git, but the path is different from the canonical root.
810
+ const canonicalRoot = "/projects/agentplate";
811
+ const worktreePath = "/projects/agentplate/.agentplate/worktrees/dogfood-agent";
812
+ expect(isCanonicalRoot(worktreePath, canonicalRoot)).toBe(false);
813
+ });
814
+ });
815
+
816
+ describe("formatQualityGatesInline", () => {
817
+ test("formats default gates as inline backtick list", () => {
818
+ const result = formatQualityGatesInline(undefined);
819
+ expect(result).toBe("`bun test`, `bun run lint`, `bun run typecheck`");
820
+ });
821
+
822
+ test("formats custom gates as inline backtick list", () => {
823
+ const gates: QualityGate[] = [
824
+ { name: "Test", command: "pytest", description: "all tests pass" },
825
+ { name: "Lint", command: "ruff check .", description: "no lint errors" },
826
+ ];
827
+ const result = formatQualityGatesInline(gates);
828
+ expect(result).toBe("`pytest`, `ruff check .`");
829
+ });
830
+
831
+ test("falls back to defaults for empty array", () => {
832
+ const result = formatQualityGatesInline([]);
833
+ expect(result).toContain("`bun test`");
834
+ });
835
+ });
836
+
837
+ describe("formatQualityGatesSteps", () => {
838
+ test("formats default gates as numbered steps", () => {
839
+ const result = formatQualityGatesSteps(undefined);
840
+ expect(result).toContain("1. Run `bun test`");
841
+ expect(result).toContain("2. Run `bun run lint`");
842
+ expect(result).toContain("3. Run `bun run typecheck`");
843
+ });
844
+
845
+ test("formats custom gates as numbered steps", () => {
846
+ const gates: QualityGate[] = [
847
+ { name: "Build", command: "cargo build", description: "compilation succeeds" },
848
+ { name: "Test", command: "cargo test", description: "all tests pass" },
849
+ ];
850
+ const result = formatQualityGatesSteps(gates);
851
+ expect(result).toBe(
852
+ "1. Run `cargo build` -- compilation succeeds.\n2. Run `cargo test` -- all tests pass.",
853
+ );
854
+ });
855
+ });
856
+
857
+ describe("formatQualityGatesBash", () => {
858
+ test("formats as fenced bash block with aligned comments", () => {
859
+ const result = formatQualityGatesBash(undefined);
860
+ expect(result).toContain("```bash");
861
+ expect(result).toContain("bun test");
862
+ expect(result).toContain("bun run lint");
863
+ expect(result).toContain("bun run typecheck");
864
+ expect(result).toContain("```");
865
+ });
866
+
867
+ test("capitalizes first letter of description in comments", () => {
868
+ const gates: QualityGate[] = [
869
+ { name: "Test", command: "pytest", description: "all tests pass" },
870
+ ];
871
+ const result = formatQualityGatesBash(gates);
872
+ expect(result).toContain("# All tests pass");
873
+ });
874
+
875
+ test("custom gates produce correct bash block", () => {
876
+ const gates: QualityGate[] = [
877
+ { name: "Test", command: "npm test", description: "tests pass" },
878
+ { name: "Lint", command: "npm run lint", description: "lint clean" },
879
+ ];
880
+ const result = formatQualityGatesBash(gates);
881
+ expect(result).toContain("npm test");
882
+ expect(result).toContain("npm run lint");
883
+ expect(result).not.toContain("bun");
884
+ });
885
+ });
886
+
887
+ describe("formatQualityGatesCapabilities", () => {
888
+ test("formats as indented bullet list", () => {
889
+ const result = formatQualityGatesCapabilities(undefined);
890
+ expect(result).toContain(" - `bun test`");
891
+ expect(result).toContain(" - `bun run lint`");
892
+ expect(result).toContain(" - `bun run typecheck`");
893
+ });
894
+
895
+ test("custom gates produce correct capability bullets", () => {
896
+ const gates: QualityGate[] = [
897
+ { name: "Test", command: "pytest", description: "run tests" },
898
+ { name: "Type", command: "mypy .", description: "type check" },
899
+ ];
900
+ const result = formatQualityGatesCapabilities(gates);
901
+ expect(result).toBe(" - `pytest` (run tests)\n - `mypy .` (type check)");
902
+ });
903
+ });
904
+
905
+ describe("INSTRUCTION_PATH placeholder", () => {
906
+ test("defaults to .claude/CLAUDE.md when instructionPath is not set", async () => {
907
+ const config = makeConfig({
908
+ baseDefinition: "Read your overlay at {{INSTRUCTION_PATH}} in your worktree.",
909
+ });
910
+ const output = await generateOverlay(config);
911
+
912
+ expect(output).toContain("Read your overlay at .claude/CLAUDE.md in your worktree.");
913
+ expect(output).not.toContain("{{INSTRUCTION_PATH}}");
914
+ });
915
+
916
+ test("uses custom instructionPath when set", async () => {
917
+ const config = makeConfig({
918
+ instructionPath: "SAPLING.md",
919
+ baseDefinition: "Read your overlay at {{INSTRUCTION_PATH}} in your worktree.",
920
+ });
921
+ const output = await generateOverlay(config);
922
+
923
+ expect(output).toContain("Read your overlay at SAPLING.md in your worktree.");
924
+ expect(output).not.toContain("{{INSTRUCTION_PATH}}");
925
+ expect(output).not.toContain(".claude/CLAUDE.md");
926
+ });
927
+
928
+ test("INSTRUCTION_PATH in base definition replaced throughout (multiple occurrences)", async () => {
929
+ const config = makeConfig({
930
+ instructionPath: "AGENTS.md",
931
+ baseDefinition: "Step 1: read {{INSTRUCTION_PATH}}.\nContext is in {{INSTRUCTION_PATH}}.",
932
+ });
933
+ const output = await generateOverlay(config);
934
+
935
+ expect(output).not.toContain("{{INSTRUCTION_PATH}}");
936
+ expect(output.split("AGENTS.md").length - 1).toBeGreaterThanOrEqual(2);
937
+ });
938
+
939
+ test("no unreplaced INSTRUCTION_PATH placeholders in final output", async () => {
940
+ const config = makeConfig({ instructionPath: "SAPLING.md" });
941
+ const output = await generateOverlay(config);
942
+
943
+ expect(output).not.toContain("{{INSTRUCTION_PATH}}");
944
+ });
945
+ });
946
+
947
+ describe("quality gate placeholders in base definitions", () => {
948
+ test("QUALITY_GATE_INLINE in base definition gets replaced", async () => {
949
+ const config = makeConfig({
950
+ baseDefinition: "Run {{QUALITY_GATE_INLINE}} before closing.",
951
+ });
952
+ const output = await generateOverlay(config);
953
+ expect(output).toContain("`bun test`, `bun run lint`, `bun run typecheck`");
954
+ expect(output).not.toContain("{{QUALITY_GATE_INLINE}}");
955
+ });
956
+
957
+ test("QUALITY_GATE_STEPS in base definition gets replaced", async () => {
958
+ const config = makeConfig({
959
+ baseDefinition: "## Steps\n{{QUALITY_GATE_STEPS}}",
960
+ });
961
+ const output = await generateOverlay(config);
962
+ expect(output).toContain("1. Run `bun test`");
963
+ expect(output).not.toContain("{{QUALITY_GATE_STEPS}}");
964
+ });
965
+
966
+ test("QUALITY_GATE_BASH in base definition gets replaced", async () => {
967
+ const config = makeConfig({
968
+ baseDefinition: "## Workflow\n{{QUALITY_GATE_BASH}}",
969
+ });
970
+ const output = await generateOverlay(config);
971
+ expect(output).toContain("```bash");
972
+ expect(output).toContain("bun test");
973
+ expect(output).not.toContain("{{QUALITY_GATE_BASH}}");
974
+ });
975
+
976
+ test("QUALITY_GATE_CAPABILITIES in base definition gets replaced", async () => {
977
+ const config = makeConfig({
978
+ baseDefinition: "## Caps\n{{QUALITY_GATE_CAPABILITIES}}",
979
+ });
980
+ const output = await generateOverlay(config);
981
+ expect(output).toContain(" - `bun test`");
982
+ expect(output).not.toContain("{{QUALITY_GATE_CAPABILITIES}}");
983
+ });
984
+
985
+ test("custom quality gates in base definition get custom commands", async () => {
986
+ const gates: QualityGate[] = [
987
+ { name: "Test", command: "pytest", description: "all tests pass" },
988
+ { name: "Lint", command: "ruff check .", description: "no lint errors" },
989
+ ];
990
+ const config = makeConfig({
991
+ capability: "builder",
992
+ qualityGates: gates,
993
+ baseDefinition:
994
+ "Run {{QUALITY_GATE_INLINE}} before closing.\n{{QUALITY_GATE_BASH}}\n{{QUALITY_GATE_STEPS}}",
995
+ });
996
+ const output = await generateOverlay(config);
997
+ expect(output).toContain("`pytest`, `ruff check .`");
998
+ expect(output).toContain("pytest");
999
+ expect(output).toContain("ruff check .");
1000
+ expect(output).not.toContain("bun test");
1001
+ expect(output).not.toContain("{{QUALITY_GATE");
1002
+ });
1003
+ });
1004
+
1005
+ describe("formatSiblings (agentplate-f76a)", () => {
1006
+ test("empty siblings array → empty string", () => {
1007
+ const config = makeConfig({ siblings: [] });
1008
+ expect(formatSiblings(config)).toBe("");
1009
+ });
1010
+
1011
+ test("missing siblings field → empty string", () => {
1012
+ const config = makeConfig();
1013
+ expect(formatSiblings(config)).toBe("");
1014
+ });
1015
+
1016
+ test("one sibling → markdown with the name and rebase guidance", () => {
1017
+ const config = makeConfig({ siblings: ["sibling-a"] });
1018
+ const out = formatSiblings(config);
1019
+ expect(out).toContain("## Parallel Siblings");
1020
+ expect(out).toContain("- sibling-a");
1021
+ expect(out).toContain("git fetch origin main:main");
1022
+ expect(out).toContain("git rebase main");
1023
+ expect(out).toContain("merge_ready");
1024
+ });
1025
+
1026
+ test("multiple siblings render every name as a bullet", () => {
1027
+ const config = makeConfig({ siblings: ["sibling-a", "sibling-b", "sibling-c"] });
1028
+ const out = formatSiblings(config);
1029
+ expect(out).toContain("- sibling-a");
1030
+ expect(out).toContain("- sibling-b");
1031
+ expect(out).toContain("- sibling-c");
1032
+ });
1033
+ });
1034
+
1035
+ describe("generateOverlay siblings wiring (agentplate-f76a)", () => {
1036
+ test("siblings field renders Parallel Siblings section in overlay", async () => {
1037
+ const config = makeConfig({ siblings: ["sibling-a", "sibling-b"] });
1038
+ const output = await generateOverlay(config);
1039
+ expect(output).toContain("## Parallel Siblings");
1040
+ expect(output).toContain("- sibling-a");
1041
+ expect(output).toContain("- sibling-b");
1042
+ expect(output).toContain("git rebase main");
1043
+ expect(output).not.toContain("{{SIBLINGS}}");
1044
+ });
1045
+
1046
+ test("no siblings → overlay omits Parallel Siblings section", async () => {
1047
+ const config = makeConfig();
1048
+ const output = await generateOverlay(config);
1049
+ expect(output).not.toContain("## Parallel Siblings");
1050
+ expect(output).not.toContain("{{SIBLINGS}}");
1051
+ });
1052
+
1053
+ test("empty siblings array → overlay omits Parallel Siblings section", async () => {
1054
+ const config = makeConfig({ siblings: [] });
1055
+ const output = await generateOverlay(config);
1056
+ expect(output).not.toContain("## Parallel Siblings");
1057
+ });
1058
+ });