@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,1011 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { cleanupTempDir, createTempGitRepo, runGitInDir } from "../test-helpers.ts";
5
+ import type { Spawner } from "./init.ts";
6
+ import { AGENTPLATE_GITIGNORE, AGENTPLATE_README, initCommand, resolveToolSet } from "./init.ts";
7
+
8
+ /**
9
+ * Tests for `agentplate init` -- agent definition deployment.
10
+ *
11
+ * Uses real temp git repos. Suppresses stdout to keep test output clean.
12
+ * process.cwd() is saved/restored because initCommand uses it to find the project root.
13
+ *
14
+ * Tests that don't exercise ecosystem bootstrap pass a no-op spawner via _spawner
15
+ * so they don't require lm/sr/tl CLIs to be installed (they aren't available in CI).
16
+ */
17
+
18
+ /** No-op spawner that treats all ecosystem tools as "not installed". */
19
+ const noopSpawner: Spawner = async () => ({ exitCode: 1, stdout: "", stderr: "not found" });
20
+
21
+ const AGENT_DEF_FILES = [
22
+ "scout.md",
23
+ "builder.md",
24
+ "reviewer.md",
25
+ "lead.md",
26
+ "merger.md",
27
+ "coordinator.md",
28
+ "monitor.md",
29
+ "orchestrator.md",
30
+ "ap-co-creation.md",
31
+ ];
32
+
33
+ /** Resolve the source agents directory (same logic as init.ts). */
34
+ const SOURCE_AGENTS_DIR = join(import.meta.dir, "..", "..", "agents");
35
+
36
+ describe("initCommand: agent-defs deployment", () => {
37
+ let tempDir: string;
38
+ let originalCwd: string;
39
+ let originalWrite: typeof process.stdout.write;
40
+
41
+ beforeEach(async () => {
42
+ tempDir = await createTempGitRepo();
43
+ originalCwd = process.cwd();
44
+ process.chdir(tempDir);
45
+
46
+ // Suppress stdout noise from initCommand
47
+ originalWrite = process.stdout.write;
48
+ process.stdout.write = (() => true) as typeof process.stdout.write;
49
+ });
50
+
51
+ afterEach(async () => {
52
+ process.chdir(originalCwd);
53
+ process.stdout.write = originalWrite;
54
+ await cleanupTempDir(tempDir);
55
+ });
56
+
57
+ test("creates .agentplate/agent-defs/ with all 9 agent definition files (supervisor deprecated)", async () => {
58
+ await initCommand({ _spawner: noopSpawner });
59
+
60
+ const agentDefsDir = join(tempDir, ".agentplate", "agent-defs");
61
+ const files = await readdir(agentDefsDir);
62
+ const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
63
+
64
+ expect(mdFiles).toEqual(AGENT_DEF_FILES.slice().sort());
65
+ });
66
+
67
+ test("copied files match source content", async () => {
68
+ await initCommand({ _spawner: noopSpawner });
69
+
70
+ for (const fileName of AGENT_DEF_FILES) {
71
+ const sourcePath = join(SOURCE_AGENTS_DIR, fileName);
72
+ const targetPath = join(tempDir, ".agentplate", "agent-defs", fileName);
73
+
74
+ const sourceContent = await Bun.file(sourcePath).text();
75
+ const targetContent = await Bun.file(targetPath).text();
76
+
77
+ expect(targetContent).toBe(sourceContent);
78
+ }
79
+ });
80
+
81
+ test("--force reinit overwrites existing agent def files", async () => {
82
+ // First init
83
+ await initCommand({ _spawner: noopSpawner });
84
+
85
+ // Tamper with one of the deployed files
86
+ const tamperPath = join(tempDir, ".agentplate", "agent-defs", "scout.md");
87
+ await Bun.write(tamperPath, "# tampered content\n");
88
+
89
+ // Verify tamper worked
90
+ const tampered = await Bun.file(tamperPath).text();
91
+ expect(tampered).toBe("# tampered content\n");
92
+
93
+ // Reinit with --force
94
+ await initCommand({ force: true, _spawner: noopSpawner });
95
+
96
+ // Verify the file was overwritten with the original source
97
+ const sourceContent = await Bun.file(join(SOURCE_AGENTS_DIR, "scout.md")).text();
98
+ const restored = await Bun.file(tamperPath).text();
99
+ expect(restored).toBe(sourceContent);
100
+ });
101
+
102
+ test("Stop hook includes loam learn command", async () => {
103
+ await initCommand({ _spawner: noopSpawner });
104
+
105
+ const hooksPath = join(tempDir, ".agentplate", "hooks.json");
106
+ const content = await Bun.file(hooksPath).text();
107
+ const parsed = JSON.parse(content);
108
+ const stopHooks = parsed.hooks.Stop[0].hooks;
109
+
110
+ expect(stopHooks.length).toBe(2);
111
+ expect(stopHooks[0].command).toContain("ap log session-end");
112
+ expect(stopHooks[1].command).toBe("loam learn");
113
+ });
114
+
115
+ test("PostToolUse hooks include Bash-matched loam diff hook", async () => {
116
+ await initCommand({ _spawner: noopSpawner });
117
+
118
+ const hooksPath = join(tempDir, ".agentplate", "hooks.json");
119
+ const content = await Bun.file(hooksPath).text();
120
+ const parsed = JSON.parse(content);
121
+ const postToolUseHooks = parsed.hooks.PostToolUse;
122
+
123
+ // Should have the generic tool-end logger plus the new Bash-specific hook
124
+ expect(postToolUseHooks.length).toBe(2);
125
+
126
+ const bashHookEntry = postToolUseHooks[1];
127
+ expect(bashHookEntry.matcher).toBe("Bash");
128
+ expect(bashHookEntry.hooks.length).toBe(1);
129
+
130
+ const command = bashHookEntry.hooks[0].command;
131
+ expect(command).toContain("git commit");
132
+ expect(command).toContain("loam diff HEAD~1");
133
+ });
134
+ });
135
+
136
+ describe("initCommand: .agentplate/.gitignore", () => {
137
+ let tempDir: string;
138
+ let originalCwd: string;
139
+ let originalWrite: typeof process.stdout.write;
140
+
141
+ beforeEach(async () => {
142
+ tempDir = await createTempGitRepo();
143
+ originalCwd = process.cwd();
144
+ process.chdir(tempDir);
145
+
146
+ // Suppress stdout noise from initCommand
147
+ originalWrite = process.stdout.write;
148
+ process.stdout.write = (() => true) as typeof process.stdout.write;
149
+ });
150
+
151
+ afterEach(async () => {
152
+ process.chdir(originalCwd);
153
+ process.stdout.write = originalWrite;
154
+ await cleanupTempDir(tempDir);
155
+ });
156
+
157
+ test("creates .agentplate/.gitignore with wildcard+whitelist model", async () => {
158
+ await initCommand({ _spawner: noopSpawner });
159
+
160
+ const gitignorePath = join(tempDir, ".agentplate", ".gitignore");
161
+ const content = await Bun.file(gitignorePath).text();
162
+
163
+ // Verify wildcard+whitelist pattern
164
+ expect(content).toContain("*\n");
165
+ expect(content).toContain("!.gitignore\n");
166
+ expect(content).toContain("!config.yaml\n");
167
+ expect(content).toContain("!agent-manifest.json\n");
168
+ expect(content).toContain("!hooks.json\n");
169
+ expect(content).toContain("!groups.json\n");
170
+ expect(content).toContain("!agent-defs/\n");
171
+ expect(content).toContain("!agent-defs/**\n");
172
+
173
+ // Verify it matches the exported constant
174
+ expect(content).toBe(AGENTPLATE_GITIGNORE);
175
+ });
176
+
177
+ test("gitignore is always written when init completes", async () => {
178
+ // Init should write gitignore
179
+ await initCommand({ _spawner: noopSpawner });
180
+
181
+ const gitignorePath = join(tempDir, ".agentplate", ".gitignore");
182
+ const content = await Bun.file(gitignorePath).text();
183
+
184
+ // Verify gitignore was written with correct content
185
+ expect(content).toBe(AGENTPLATE_GITIGNORE);
186
+
187
+ // Verify the file exists
188
+ const exists = await Bun.file(gitignorePath).exists();
189
+ expect(exists).toBe(true);
190
+ });
191
+
192
+ test("--force reinit overwrites stale .agentplate/.gitignore", async () => {
193
+ // First init
194
+ await initCommand({ _spawner: noopSpawner });
195
+
196
+ const gitignorePath = join(tempDir, ".agentplate", ".gitignore");
197
+
198
+ // Tamper with the gitignore file (simulate old deny-list format)
199
+ await Bun.write(gitignorePath, "# old format\nworktrees/\nlogs/\nmail.db\n");
200
+
201
+ // Verify tamper worked
202
+ const tampered = await Bun.file(gitignorePath).text();
203
+ expect(tampered).not.toContain("*\n");
204
+ expect(tampered).not.toContain("!.gitignore\n");
205
+
206
+ // Reinit with --force
207
+ await initCommand({ force: true, _spawner: noopSpawner });
208
+
209
+ // Verify the file was overwritten with the new wildcard+whitelist format
210
+ const restored = await Bun.file(gitignorePath).text();
211
+ expect(restored).toBe(AGENTPLATE_GITIGNORE);
212
+ expect(restored).toContain("*\n");
213
+ expect(restored).toContain("!.gitignore\n");
214
+ });
215
+
216
+ test("subsequent init without --force does not overwrite gitignore", async () => {
217
+ // First init
218
+ await initCommand({ _spawner: noopSpawner });
219
+
220
+ const gitignorePath = join(tempDir, ".agentplate", ".gitignore");
221
+
222
+ // Tamper with the gitignore file
223
+ await Bun.write(gitignorePath, "# custom content\n");
224
+
225
+ // Verify tamper worked
226
+ const tampered = await Bun.file(gitignorePath).text();
227
+ expect(tampered).toBe("# custom content\n");
228
+
229
+ // Second init without --force should return early (not overwrite)
230
+ await initCommand({ _spawner: noopSpawner });
231
+
232
+ // Verify the file was NOT overwritten (early return prevented it)
233
+ const afterSecondInit = await Bun.file(gitignorePath).text();
234
+ expect(afterSecondInit).toBe("# custom content\n");
235
+ });
236
+ });
237
+
238
+ describe("initCommand: .agentplate/README.md", () => {
239
+ let tempDir: string;
240
+ let originalCwd: string;
241
+ let originalWrite: typeof process.stdout.write;
242
+
243
+ beforeEach(async () => {
244
+ tempDir = await createTempGitRepo();
245
+ originalCwd = process.cwd();
246
+ process.chdir(tempDir);
247
+
248
+ // Suppress stdout noise from initCommand
249
+ originalWrite = process.stdout.write;
250
+ process.stdout.write = (() => true) as typeof process.stdout.write;
251
+ });
252
+
253
+ afterEach(async () => {
254
+ process.chdir(originalCwd);
255
+ process.stdout.write = originalWrite;
256
+ await cleanupTempDir(tempDir);
257
+ });
258
+
259
+ test("creates .agentplate/README.md with expected content", async () => {
260
+ await initCommand({ _spawner: noopSpawner });
261
+
262
+ const readmePath = join(tempDir, ".agentplate", "README.md");
263
+ const exists = await Bun.file(readmePath).exists();
264
+ expect(exists).toBe(true);
265
+
266
+ const content = await Bun.file(readmePath).text();
267
+ expect(content).toBe(AGENTPLATE_README);
268
+ });
269
+
270
+ test("README.md is whitelisted in gitignore", () => {
271
+ expect(AGENTPLATE_GITIGNORE).toContain("!README.md\n");
272
+ });
273
+
274
+ test("--force reinit overwrites README.md", async () => {
275
+ // First init
276
+ await initCommand({ _spawner: noopSpawner });
277
+
278
+ const readmePath = join(tempDir, ".agentplate", "README.md");
279
+
280
+ // Tamper with the README
281
+ await Bun.write(readmePath, "# tampered\n");
282
+ const tampered = await Bun.file(readmePath).text();
283
+ expect(tampered).toBe("# tampered\n");
284
+
285
+ // Reinit with --force
286
+ await initCommand({ force: true, _spawner: noopSpawner });
287
+
288
+ // Verify restored to canonical content
289
+ const restored = await Bun.file(readmePath).text();
290
+ expect(restored).toBe(AGENTPLATE_README);
291
+ });
292
+
293
+ test("subsequent init without --force does not overwrite README.md", async () => {
294
+ // First init
295
+ await initCommand({ _spawner: noopSpawner });
296
+
297
+ const readmePath = join(tempDir, ".agentplate", "README.md");
298
+
299
+ // Tamper with the README
300
+ await Bun.write(readmePath, "# custom content\n");
301
+ const tampered = await Bun.file(readmePath).text();
302
+ expect(tampered).toBe("# custom content\n");
303
+
304
+ // Second init without --force returns early
305
+ await initCommand({ _spawner: noopSpawner });
306
+
307
+ // Verify tampered content preserved (early return)
308
+ const afterSecondInit = await Bun.file(readmePath).text();
309
+ expect(afterSecondInit).toBe("# custom content\n");
310
+ });
311
+ });
312
+
313
+ describe("initCommand: canonical branch detection", () => {
314
+ let tempDir: string;
315
+ let originalCwd: string;
316
+ let originalWrite: typeof process.stdout.write;
317
+
318
+ beforeEach(async () => {
319
+ tempDir = await createTempGitRepo();
320
+ originalCwd = process.cwd();
321
+ // Remove origin remote so detectCanonicalBranch falls through to
322
+ // current-branch check (otherwise remote HEAD resolves to main regardless)
323
+ await runGitInDir(tempDir, ["remote", "remove", "origin"]);
324
+ process.chdir(tempDir);
325
+
326
+ // Suppress stdout noise from initCommand
327
+ originalWrite = process.stdout.write;
328
+ process.stdout.write = (() => true) as typeof process.stdout.write;
329
+ });
330
+
331
+ afterEach(async () => {
332
+ process.chdir(originalCwd);
333
+ process.stdout.write = originalWrite;
334
+ await cleanupTempDir(tempDir);
335
+ });
336
+
337
+ test("non-standard branch names are accepted as canonicalBranch", async () => {
338
+ // Switch to a non-standard branch name
339
+ await runGitInDir(tempDir, ["switch", "-c", "trunk"]);
340
+
341
+ await initCommand({ _spawner: noopSpawner });
342
+
343
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
344
+ const content = await Bun.file(configPath).text();
345
+ expect(content).toContain("canonicalBranch: trunk");
346
+ });
347
+
348
+ test("standard branch names (main) still work as canonicalBranch", async () => {
349
+ // createTempGitRepo defaults to main branch
350
+ await initCommand({ _spawner: noopSpawner });
351
+
352
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
353
+ const content = await Bun.file(configPath).text();
354
+ expect(content).toContain("canonicalBranch: main");
355
+ });
356
+
357
+ test("generated config opts into headless Claude by default (agentplate-caec)", async () => {
358
+ await initCommand({ _spawner: noopSpawner });
359
+
360
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
361
+ const content = await Bun.file(configPath).text();
362
+ expect(content).toContain("claudeHeadlessByDefault: true");
363
+ });
364
+ });
365
+
366
+ describe("initCommand: --yes flag", () => {
367
+ let tempDir: string;
368
+ let originalCwd: string;
369
+ let originalWrite: typeof process.stdout.write;
370
+
371
+ beforeEach(async () => {
372
+ tempDir = await createTempGitRepo();
373
+ originalCwd = process.cwd();
374
+ process.chdir(tempDir);
375
+
376
+ // Suppress stdout noise from initCommand
377
+ originalWrite = process.stdout.write;
378
+ process.stdout.write = (() => true) as typeof process.stdout.write;
379
+ });
380
+
381
+ afterEach(async () => {
382
+ process.chdir(originalCwd);
383
+ process.stdout.write = originalWrite;
384
+ await cleanupTempDir(tempDir);
385
+ });
386
+
387
+ test("--yes reinitializes when .agentplate/ already exists", async () => {
388
+ // First init
389
+ await initCommand({ _spawner: noopSpawner });
390
+
391
+ // Tamper with config to verify reinit happens
392
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
393
+ await Bun.write(configPath, "# tampered\n");
394
+
395
+ // Second init with --yes should reinitialize (not return early)
396
+ await initCommand({ yes: true, _spawner: noopSpawner });
397
+
398
+ // Verify config was regenerated (not the tampered content)
399
+ const content = await Bun.file(configPath).text();
400
+ expect(content).not.toBe("# tampered\n");
401
+ expect(content).toContain("# Agentplate configuration");
402
+ });
403
+
404
+ test("--yes works on fresh project (no .agentplate/ yet)", async () => {
405
+ await initCommand({ yes: true, _spawner: noopSpawner });
406
+
407
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
408
+ const exists = await Bun.file(configPath).exists();
409
+ expect(exists).toBe(true);
410
+
411
+ const content = await Bun.file(configPath).text();
412
+ expect(content).toContain("# Agentplate configuration");
413
+ });
414
+
415
+ test("--yes overwrites agent-defs on reinit", async () => {
416
+ // First init
417
+ await initCommand({ _spawner: noopSpawner });
418
+
419
+ // Tamper with an agent def
420
+ const scoutPath = join(tempDir, ".agentplate", "agent-defs", "scout.md");
421
+ await Bun.write(scoutPath, "TAMPERED CONTENT");
422
+
423
+ // Reinit with --yes should overwrite
424
+ await initCommand({ yes: true, _spawner: noopSpawner });
425
+
426
+ const restored = await Bun.file(scoutPath).text();
427
+ expect(restored).not.toBe("TAMPERED CONTENT");
428
+ });
429
+ });
430
+
431
+ describe("initCommand: --name flag", () => {
432
+ let tempDir: string;
433
+ let originalCwd: string;
434
+ let originalWrite: typeof process.stdout.write;
435
+
436
+ beforeEach(async () => {
437
+ tempDir = await createTempGitRepo();
438
+ originalCwd = process.cwd();
439
+ process.chdir(tempDir);
440
+
441
+ // Suppress stdout noise from initCommand
442
+ originalWrite = process.stdout.write;
443
+ process.stdout.write = (() => true) as typeof process.stdout.write;
444
+ });
445
+
446
+ afterEach(async () => {
447
+ process.chdir(originalCwd);
448
+ process.stdout.write = originalWrite;
449
+ await cleanupTempDir(tempDir);
450
+ });
451
+
452
+ test("--name overrides auto-detected project name", async () => {
453
+ await initCommand({ name: "custom-project", _spawner: noopSpawner });
454
+
455
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
456
+ const content = await Bun.file(configPath).text();
457
+ expect(content).toContain("name: custom-project");
458
+ });
459
+
460
+ test("--name combined with --yes works for fully non-interactive init", async () => {
461
+ await initCommand({ yes: true, name: "scripted-project", _spawner: noopSpawner });
462
+
463
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
464
+ const content = await Bun.file(configPath).text();
465
+ expect(content).toContain("name: scripted-project");
466
+ expect(content).toContain("# Agentplate configuration");
467
+ });
468
+ });
469
+
470
+ // ---- Ecosystem Bootstrap Tests ----
471
+
472
+ /**
473
+ * Build a Spawner that returns preset responses keyed by "arg0 arg1 ..." prefix.
474
+ * Records all calls for assertion.
475
+ */
476
+ function createMockSpawner(
477
+ responses: Record<string, { exitCode: number; stdout: string; stderr: string }>,
478
+ ): {
479
+ spawner: Spawner;
480
+ calls: string[][];
481
+ } {
482
+ const calls: string[][] = [];
483
+ const spawner: Spawner = async (args) => {
484
+ calls.push(args);
485
+ const key = args.join(" ");
486
+ // Longest prefix match
487
+ let bestMatch = "";
488
+ let bestResponse = { exitCode: 1, stdout: "", stderr: "not found" };
489
+ for (const [pattern, response] of Object.entries(responses)) {
490
+ if (key.startsWith(pattern) && pattern.length > bestMatch.length) {
491
+ bestMatch = pattern;
492
+ bestResponse = response;
493
+ }
494
+ }
495
+ return bestResponse;
496
+ };
497
+ return { spawner, calls };
498
+ }
499
+
500
+ describe("resolveToolSet", () => {
501
+ test("default (no opts) returns all three tools in order", () => {
502
+ const tools = resolveToolSet({});
503
+ expect(tools.map((t) => t.name)).toEqual(["loam", "sprout", "trellis"]);
504
+ });
505
+
506
+ test("--skip-loam removes loam", () => {
507
+ const tools = resolveToolSet({ skipLoam: true });
508
+ expect(tools.map((t) => t.name)).toEqual(["sprout", "trellis"]);
509
+ });
510
+
511
+ test("--skip-sprout removes sprout", () => {
512
+ const tools = resolveToolSet({ skipSprout: true });
513
+ expect(tools.map((t) => t.name)).toEqual(["loam", "trellis"]);
514
+ });
515
+
516
+ test("--skip-trellis removes trellis", () => {
517
+ const tools = resolveToolSet({ skipTrellis: true });
518
+ expect(tools.map((t) => t.name)).toEqual(["loam", "sprout"]);
519
+ });
520
+
521
+ test("multiple skip flags combine", () => {
522
+ const tools = resolveToolSet({ skipLoam: true, skipSprout: true });
523
+ expect(tools.map((t) => t.name)).toEqual(["trellis"]);
524
+ });
525
+
526
+ test("--tools overrides to specific tools", () => {
527
+ const tools = resolveToolSet({ tools: "loam,sprout" });
528
+ expect(tools.map((t) => t.name)).toEqual(["loam", "sprout"]);
529
+ });
530
+
531
+ test("--tools single tool", () => {
532
+ const tools = resolveToolSet({ tools: "trellis" });
533
+ expect(tools.map((t) => t.name)).toEqual(["trellis"]);
534
+ });
535
+
536
+ test("--tools with unknown name filters it out", () => {
537
+ const tools = resolveToolSet({ tools: "loam,unknown" });
538
+ expect(tools.map((t) => t.name)).toEqual(["loam"]);
539
+ });
540
+
541
+ test("--tools overrides skip flags", () => {
542
+ // --tools takes precedence over --skip-* flags
543
+ const tools = resolveToolSet({ tools: "loam", skipLoam: true });
544
+ expect(tools.map((t) => t.name)).toEqual(["loam"]);
545
+ });
546
+
547
+ test("all skip flags returns empty array", () => {
548
+ const tools = resolveToolSet({ skipLoam: true, skipSprout: true, skipTrellis: true });
549
+ expect(tools).toHaveLength(0);
550
+ });
551
+ });
552
+
553
+ describe("initCommand: ecosystem bootstrap", () => {
554
+ let tempDir: string;
555
+ let originalCwd: string;
556
+ let originalWrite: typeof process.stdout.write;
557
+
558
+ beforeEach(async () => {
559
+ tempDir = await createTempGitRepo();
560
+ originalCwd = process.cwd();
561
+ process.chdir(tempDir);
562
+ originalWrite = process.stdout.write;
563
+ process.stdout.write = (() => true) as typeof process.stdout.write;
564
+ });
565
+
566
+ afterEach(async () => {
567
+ process.chdir(originalCwd);
568
+ process.stdout.write = originalWrite;
569
+ await cleanupTempDir(tempDir);
570
+ });
571
+
572
+ test("all tools installed and init succeeds → status initialized", async () => {
573
+ const { spawner, calls } = createMockSpawner({
574
+ "lm --version": { exitCode: 0, stdout: "0.6.3", stderr: "" },
575
+ "lm init": { exitCode: 0, stdout: "initialized", stderr: "" },
576
+ "lm onboard": { exitCode: 0, stdout: "appended", stderr: "" },
577
+ "sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
578
+ "sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
579
+ "sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
580
+ "tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
581
+ "tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
582
+ "tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
583
+ });
584
+
585
+ await initCommand({ _spawner: spawner });
586
+
587
+ // All three init commands were called
588
+ expect(calls).toContainEqual(["lm", "init"]);
589
+ expect(calls).toContainEqual(["sr", "init"]);
590
+ expect(calls).toContainEqual(["tl", "init"]);
591
+
592
+ // All three onboard commands were called
593
+ expect(calls).toContainEqual(["lm", "onboard"]);
594
+ expect(calls).toContainEqual(["sr", "onboard"]);
595
+ expect(calls).toContainEqual(["tl", "onboard"]);
596
+ });
597
+
598
+ test("tool not installed → init and onboard not called", async () => {
599
+ const { spawner, calls } = createMockSpawner({
600
+ "lm --version": { exitCode: 1, stdout: "", stderr: "command not found" },
601
+ "sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
602
+ "sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
603
+ "sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
604
+ "tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
605
+ "tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
606
+ "tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
607
+ });
608
+
609
+ await initCommand({ _spawner: spawner });
610
+
611
+ // loam init should NOT have been called
612
+ expect(calls).not.toContainEqual(["lm", "init"]);
613
+ // sprout and trellis should still be called
614
+ expect(calls).toContainEqual(["sr", "init"]);
615
+ expect(calls).toContainEqual(["tl", "init"]);
616
+ });
617
+
618
+ test("tool init non-zero + dir exists → already_initialized", async () => {
619
+ // Create .loam/ directory to simulate existing loam init
620
+ const { mkdir } = await import("node:fs/promises");
621
+ await mkdir(join(tempDir, ".loam"), { recursive: true });
622
+
623
+ const { spawner } = createMockSpawner({
624
+ "lm --version": { exitCode: 0, stdout: "0.6.3", stderr: "" },
625
+ "lm init": { exitCode: 1, stdout: "", stderr: "already initialized" },
626
+ "lm onboard": { exitCode: 0, stdout: "appended", stderr: "" },
627
+ "sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
628
+ "sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
629
+ "sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
630
+ "tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
631
+ "tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
632
+ "tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
633
+ });
634
+
635
+ // Should not throw — already_initialized is not an error
636
+ await initCommand({ _spawner: spawner });
637
+ });
638
+
639
+ test("--skip-onboard skips onboard calls", async () => {
640
+ const { spawner, calls } = createMockSpawner({
641
+ "lm --version": { exitCode: 0, stdout: "0.6.3", stderr: "" },
642
+ "lm init": { exitCode: 0, stdout: "initialized", stderr: "" },
643
+ "sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
644
+ "sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
645
+ "tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
646
+ "tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
647
+ });
648
+
649
+ await initCommand({ skipOnboard: true, _spawner: spawner });
650
+
651
+ expect(calls).not.toContainEqual(["lm", "onboard"]);
652
+ expect(calls).not.toContainEqual(["sr", "onboard"]);
653
+ expect(calls).not.toContainEqual(["tl", "onboard"]);
654
+ });
655
+
656
+ test("--skip-loam skips loam entirely", async () => {
657
+ const { spawner, calls } = createMockSpawner({
658
+ "sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
659
+ "sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
660
+ "sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
661
+ "tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
662
+ "tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
663
+ "tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
664
+ });
665
+
666
+ await initCommand({ skipLoam: true, _spawner: spawner });
667
+
668
+ expect(calls.filter((c) => c[0] === "lm")).toHaveLength(0);
669
+ });
670
+
671
+ test("--json outputs JSON envelope with tools and onboard status", async () => {
672
+ const { spawner } = createMockSpawner({
673
+ "lm --version": { exitCode: 0, stdout: "0.6.3", stderr: "" },
674
+ "lm init": { exitCode: 0, stdout: "initialized", stderr: "" },
675
+ "lm onboard": { exitCode: 0, stdout: "appended", stderr: "" },
676
+ "sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
677
+ "sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
678
+ "sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
679
+ "tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
680
+ "tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
681
+ "tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
682
+ });
683
+
684
+ let capturedOutput = "";
685
+ const restoreWrite = process.stdout.write;
686
+ process.stdout.write = ((chunk: unknown) => {
687
+ capturedOutput += String(chunk);
688
+ return true;
689
+ }) as typeof process.stdout.write;
690
+
691
+ await initCommand({ json: true, _spawner: spawner });
692
+
693
+ process.stdout.write = restoreWrite;
694
+
695
+ // Find the JSON line (last line with JSON content)
696
+ const jsonLine = capturedOutput.split("\n").find((line) => line.startsWith('{"success":'));
697
+
698
+ expect(jsonLine).toBeDefined();
699
+ const parsed = JSON.parse(jsonLine ?? "{}") as Record<string, unknown>;
700
+ expect(parsed.success).toBe(true);
701
+ expect(parsed.command).toBe("init");
702
+ expect(parsed.tools).toBeDefined();
703
+ expect(parsed.onboard).toBeDefined();
704
+ expect(typeof parsed.gitattributes).toBe("boolean");
705
+
706
+ const tools = parsed.tools as Record<string, { status: string }>;
707
+ expect(tools.agentplate?.status).toBe("initialized");
708
+ expect(tools.loam?.status).toBe("initialized");
709
+ expect(tools.sprout?.status).toBe("initialized");
710
+ expect(tools.trellis?.status).toBe("initialized");
711
+ });
712
+ });
713
+
714
+ describe("initCommand: scaffold commit", () => {
715
+ let tempDir: string;
716
+ let originalCwd: string;
717
+ let originalWrite: typeof process.stdout.write;
718
+
719
+ beforeEach(async () => {
720
+ tempDir = await createTempGitRepo();
721
+ originalCwd = process.cwd();
722
+ process.chdir(tempDir);
723
+ originalWrite = process.stdout.write;
724
+ process.stdout.write = (() => true) as typeof process.stdout.write;
725
+ });
726
+
727
+ afterEach(async () => {
728
+ process.chdir(originalCwd);
729
+ process.stdout.write = originalWrite;
730
+ await cleanupTempDir(tempDir);
731
+ });
732
+
733
+ test("git commit is called with scaffold message when git add succeeds and changes are staged", async () => {
734
+ const calls: string[][] = [];
735
+ const spawner: import("./init.ts").Spawner = async (args) => {
736
+ calls.push(args);
737
+ const key = args.join(" ");
738
+ // Sibling tool calls: all "not installed"
739
+ if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
740
+ // git add: success
741
+ if (key.startsWith("git add")) return { exitCode: 0, stdout: "", stderr: "" };
742
+ // git diff --cached --quiet: exit 1 means changes are staged
743
+ if (key.startsWith("git diff --cached --quiet"))
744
+ return { exitCode: 1, stdout: "", stderr: "" };
745
+ // git commit: success
746
+ if (key.startsWith("git commit")) return { exitCode: 0, stdout: "", stderr: "" };
747
+ return { exitCode: 1, stdout: "", stderr: "not found" };
748
+ };
749
+
750
+ await initCommand({ _spawner: spawner });
751
+
752
+ expect(calls).toContainEqual([
753
+ "git",
754
+ "commit",
755
+ "-m",
756
+ "chore: initialize agentplate and ecosystem tools",
757
+ ]);
758
+ });
759
+
760
+ test("git commit is NOT called when git diff reports nothing staged (exit 0)", async () => {
761
+ const calls: string[][] = [];
762
+ const spawner: import("./init.ts").Spawner = async (args) => {
763
+ calls.push(args);
764
+ const key = args.join(" ");
765
+ if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
766
+ if (key.startsWith("git add")) return { exitCode: 0, stdout: "", stderr: "" };
767
+ // exit 0 = nothing staged
768
+ if (key.startsWith("git diff --cached --quiet"))
769
+ return { exitCode: 0, stdout: "", stderr: "" };
770
+ if (key.startsWith("git commit")) return { exitCode: 0, stdout: "", stderr: "" };
771
+ return { exitCode: 1, stdout: "", stderr: "not found" };
772
+ };
773
+
774
+ await initCommand({ _spawner: spawner });
775
+
776
+ const commitCalls = calls.filter((c) => c[0] === "git" && c[1] === "commit");
777
+ expect(commitCalls).toHaveLength(0);
778
+ });
779
+
780
+ test("git commit failure does not throw — init still succeeds", async () => {
781
+ const spawner: import("./init.ts").Spawner = async (args) => {
782
+ const key = args.join(" ");
783
+ if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
784
+ if (key.startsWith("git add")) return { exitCode: 0, stdout: "", stderr: "" };
785
+ if (key.startsWith("git diff --cached --quiet"))
786
+ return { exitCode: 1, stdout: "", stderr: "" };
787
+ // commit fails
788
+ if (key.startsWith("git commit"))
789
+ return { exitCode: 1, stdout: "", stderr: "nothing to commit" };
790
+ return { exitCode: 1, stdout: "", stderr: "not found" };
791
+ };
792
+
793
+ // Should not throw
794
+ await expect(initCommand({ _spawner: spawner })).resolves.toBeUndefined();
795
+
796
+ // .agentplate files should still be created
797
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
798
+ const exists = await Bun.file(configPath).exists();
799
+ expect(exists).toBe(true);
800
+ });
801
+
802
+ test("git add failure skips commit without throwing", async () => {
803
+ const calls: string[][] = [];
804
+ const spawner: import("./init.ts").Spawner = async (args) => {
805
+ calls.push(args);
806
+ const key = args.join(" ");
807
+ if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
808
+ // git add fails
809
+ if (key.startsWith("git add")) return { exitCode: 1, stdout: "", stderr: "git add failed" };
810
+ if (key.startsWith("git commit")) return { exitCode: 0, stdout: "", stderr: "" };
811
+ return { exitCode: 1, stdout: "", stderr: "not found" };
812
+ };
813
+
814
+ await expect(initCommand({ _spawner: spawner })).resolves.toBeUndefined();
815
+
816
+ // commit should NOT have been called since add failed
817
+ const commitCalls = calls.filter((c) => c[0] === "git" && c[1] === "commit");
818
+ expect(commitCalls).toHaveLength(0);
819
+ });
820
+
821
+ test("--json output includes scaffoldCommitted boolean", async () => {
822
+ const spawner: import("./init.ts").Spawner = async (args) => {
823
+ const key = args.join(" ");
824
+ if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
825
+ if (key.startsWith("git add")) return { exitCode: 0, stdout: "", stderr: "" };
826
+ if (key.startsWith("git diff --cached --quiet"))
827
+ return { exitCode: 1, stdout: "", stderr: "" };
828
+ if (key.startsWith("git commit")) return { exitCode: 0, stdout: "", stderr: "" };
829
+ return { exitCode: 1, stdout: "", stderr: "not found" };
830
+ };
831
+
832
+ let capturedOutput = "";
833
+ const restoreWrite = process.stdout.write;
834
+ process.stdout.write = ((chunk: unknown) => {
835
+ capturedOutput += String(chunk);
836
+ return true;
837
+ }) as typeof process.stdout.write;
838
+
839
+ await initCommand({ json: true, _spawner: spawner });
840
+
841
+ process.stdout.write = restoreWrite;
842
+
843
+ const jsonLine = capturedOutput.split("\n").find((line) => line.startsWith('{"success":'));
844
+ expect(jsonLine).toBeDefined();
845
+ const parsed = JSON.parse(jsonLine ?? "{}") as Record<string, unknown>;
846
+ expect(typeof parsed.scaffoldCommitted).toBe("boolean");
847
+ expect(parsed.scaffoldCommitted).toBe(true);
848
+ });
849
+ });
850
+
851
+ describe("initCommand: spawner error resilience", () => {
852
+ let tempDir: string;
853
+ let originalCwd: string;
854
+ let originalWrite: typeof process.stdout.write;
855
+
856
+ beforeEach(async () => {
857
+ tempDir = await createTempGitRepo();
858
+ originalCwd = process.cwd();
859
+ process.chdir(tempDir);
860
+ originalWrite = process.stdout.write;
861
+ process.stdout.write = (() => true) as typeof process.stdout.write;
862
+ });
863
+
864
+ afterEach(async () => {
865
+ process.chdir(originalCwd);
866
+ process.stdout.write = originalWrite;
867
+ await cleanupTempDir(tempDir);
868
+ });
869
+
870
+ test("spawner that throws ENOENT does not crash init — degrades gracefully", async () => {
871
+ const throwingSpawner: Spawner = async (args) => {
872
+ const key = args.join(" ");
873
+ // Allow git operations through (git add, git diff, git commit)
874
+ if (key.startsWith("git")) return { exitCode: 0, stdout: "", stderr: "" };
875
+ // Simulate ecosystem tool binary not found (ENOENT)
876
+ throw new Error(`spawn ENOENT: ${args[0]}: not found`);
877
+ };
878
+
879
+ // Should not throw — graceful degradation
880
+ await expect(initCommand({ _spawner: throwingSpawner })).resolves.toBeUndefined();
881
+
882
+ // Core .agentplate files should still be created
883
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
884
+ expect(await Bun.file(configPath).exists()).toBe(true);
885
+ });
886
+
887
+ test("throwing spawner causes all ecosystem tools to be skipped", async () => {
888
+ const calls: string[][] = [];
889
+ const throwingSpawner: Spawner = async (args) => {
890
+ calls.push(args);
891
+ const key = args.join(" ");
892
+ if (key.startsWith("git")) return { exitCode: 0, stdout: "", stderr: "" };
893
+ throw new Error("spawn ENOENT");
894
+ };
895
+
896
+ await initCommand({ _spawner: throwingSpawner });
897
+
898
+ // init and onboard should NOT be called when --version throws
899
+ expect(calls).not.toContainEqual(["lm", "init"]);
900
+ expect(calls).not.toContainEqual(["sr", "init"]);
901
+ expect(calls).not.toContainEqual(["tl", "init"]);
902
+ expect(calls).not.toContainEqual(["lm", "onboard"]);
903
+ expect(calls).not.toContainEqual(["sr", "onboard"]);
904
+ expect(calls).not.toContainEqual(["tl", "onboard"]);
905
+ });
906
+
907
+ test("spawner that throws only on init (not --version) still skips gracefully", async () => {
908
+ // --version succeeds (tool appears installed), but init itself throws
909
+ const throwingInitSpawner: Spawner = async (args) => {
910
+ const key = args.join(" ");
911
+ if (key.startsWith("git")) return { exitCode: 0, stdout: "", stderr: "" };
912
+ if (key.endsWith("--version")) return { exitCode: 0, stdout: "1.0.0", stderr: "" };
913
+ if (key.endsWith("onboard")) return { exitCode: 0, stdout: "", stderr: "" };
914
+ // init itself throws
915
+ throw new Error("spawn ENOENT on init");
916
+ };
917
+
918
+ await expect(initCommand({ _spawner: throwingInitSpawner })).resolves.toBeUndefined();
919
+
920
+ const configPath = join(tempDir, ".agentplate", "config.yaml");
921
+ expect(await Bun.file(configPath).exists()).toBe(true);
922
+ });
923
+ });
924
+
925
+ describe("initCommand: .gitattributes setup", () => {
926
+ let tempDir: string;
927
+ let originalCwd: string;
928
+ let originalWrite: typeof process.stdout.write;
929
+
930
+ beforeEach(async () => {
931
+ tempDir = await createTempGitRepo();
932
+ originalCwd = process.cwd();
933
+ process.chdir(tempDir);
934
+ originalWrite = process.stdout.write;
935
+ process.stdout.write = (() => true) as typeof process.stdout.write;
936
+ });
937
+
938
+ afterEach(async () => {
939
+ process.chdir(originalCwd);
940
+ process.stdout.write = originalWrite;
941
+ await cleanupTempDir(tempDir);
942
+ });
943
+
944
+ test("creates .gitattributes with merge=union entries", async () => {
945
+ // Use a spawner that skips all ecosystem tools so only gitattributes step runs
946
+ const { spawner } = createMockSpawner({});
947
+ await initCommand({ skipLoam: true, skipSprout: true, skipTrellis: true, _spawner: spawner });
948
+
949
+ const gitattrsPath = join(tempDir, ".gitattributes");
950
+ const exists = await Bun.file(gitattrsPath).exists();
951
+ expect(exists).toBe(true);
952
+
953
+ const content = await Bun.file(gitattrsPath).text();
954
+ expect(content).toContain(".loam/expertise/*.jsonl merge=union");
955
+ expect(content).toContain(".sprout/issues.jsonl merge=union");
956
+ });
957
+
958
+ test("does not duplicate entries on reinit with --force", async () => {
959
+ const { spawner } = createMockSpawner({});
960
+
961
+ // First init
962
+ await initCommand({ skipLoam: true, skipSprout: true, skipTrellis: true, _spawner: spawner });
963
+
964
+ // Second init with --force
965
+ await initCommand({
966
+ force: true,
967
+ skipLoam: true,
968
+ skipSprout: true,
969
+ skipTrellis: true,
970
+ _spawner: spawner,
971
+ });
972
+
973
+ const gitattrsPath = join(tempDir, ".gitattributes");
974
+ const content = await Bun.file(gitattrsPath).text();
975
+
976
+ // Count occurrences — should be exactly one each
977
+ const loamCount = (content.match(/\.loam\/expertise\/\*\.jsonl merge=union/g) ?? []).length;
978
+ const sproutCount = (content.match(/\.sprout\/issues\.jsonl merge=union/g) ?? []).length;
979
+ expect(loamCount).toBe(1);
980
+ expect(sproutCount).toBe(1);
981
+ });
982
+
983
+ test("preserves existing .gitattributes content", async () => {
984
+ // Pre-create .gitattributes with existing content
985
+ const existingContent = "*.lock binary\n*.png binary\n";
986
+ await Bun.write(join(tempDir, ".gitattributes"), existingContent);
987
+
988
+ const { spawner } = createMockSpawner({});
989
+ await initCommand({ skipLoam: true, skipSprout: true, skipTrellis: true, _spawner: spawner });
990
+
991
+ const content = await Bun.file(join(tempDir, ".gitattributes")).text();
992
+ expect(content).toContain("*.lock binary");
993
+ expect(content).toContain("*.png binary");
994
+ expect(content).toContain(".loam/expertise/*.jsonl merge=union");
995
+ expect(content).toContain(".sprout/issues.jsonl merge=union");
996
+ });
997
+
998
+ test("no-op when entries already present", async () => {
999
+ // Pre-create .gitattributes with the entries already
1000
+ const existingContent =
1001
+ ".loam/expertise/*.jsonl merge=union\n.sprout/issues.jsonl merge=union\n";
1002
+ await Bun.write(join(tempDir, ".gitattributes"), existingContent);
1003
+
1004
+ const { spawner } = createMockSpawner({});
1005
+ await initCommand({ skipLoam: true, skipSprout: true, skipTrellis: true, _spawner: spawner });
1006
+
1007
+ const content = await Bun.file(join(tempDir, ".gitattributes")).text();
1008
+ // Content should be unchanged
1009
+ expect(content).toBe(existingContent);
1010
+ });
1011
+ });