@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,490 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { DEFAULT_QUALITY_GATES } from "../config.ts";
4
+ import { AgentError } from "../errors.ts";
5
+ import type { OverlayConfig, QualityGate } from "../types.ts";
6
+ import { terminalMailTypesFor } from "./capabilities.ts";
7
+
8
+ /**
9
+ * Capability-specific completion-mail guidance for the dynamic overlay.
10
+ *
11
+ * Returns the terminal mail-type name and a one-line example fragment so the
12
+ * overlay can render: "ap mail send ... --type <terminalType> ...".
13
+ *
14
+ * Crucial: this MUST stay in sync with `terminalMailTypesFor()` — agentplate-1a4c
15
+ * found that overlay text saying `--type result` while the runner watched only
16
+ * for `worker_done` left worker sessions stuck in `working`.
17
+ */
18
+ function completionMailTypeFor(capability: string): string {
19
+ const types = terminalMailTypesFor(capability);
20
+ // `terminalMailTypesFor` returns the canonical type first
21
+ // (worker_done for workers, merged for mergers). Use that for prose;
22
+ // agents may also use the secondary types (`merge_failed`, etc.) where
23
+ // applicable per their base prompt.
24
+ return types[0] ?? "worker_done";
25
+ }
26
+
27
+ /**
28
+ * Resolve the path to the overlay template file.
29
+ * The template lives at `templates/overlay.md.tmpl` relative to the repo root.
30
+ */
31
+ function getTemplatePath(): string {
32
+ // src/agents/overlay.ts -> repo root is ../../
33
+ return join(dirname(import.meta.dir), "..", "templates", "overlay.md.tmpl");
34
+ }
35
+
36
+ /**
37
+ * Format the parallel-siblings section (agentplate-f76a). Returns empty string
38
+ * when no siblings are configured. When set, renders a markdown section that
39
+ * names each sibling and instructs the agent to rebase onto `main` BEFORE
40
+ * sending `merge_ready`. Reason: parallel leads branch off pre-merge `main`;
41
+ * whichever merges second carries a stale base and risks reverting sibling
42
+ * work (mx-c0c122 stale-base-revert).
43
+ *
44
+ * Exported for unit-testing.
45
+ */
46
+ export function formatSiblings(config: OverlayConfig): string {
47
+ const siblings = config.siblings;
48
+ if (!siblings || siblings.length === 0) return "";
49
+
50
+ const bullets = siblings.map((name) => `- ${name}`).join("\n");
51
+ return [
52
+ "## Parallel Siblings",
53
+ "",
54
+ "The coordinator has dispatched the following sibling agents in parallel that may share file scope with you:",
55
+ "",
56
+ bullets,
57
+ "",
58
+ "**CRITICAL**: rebase your branch onto the latest `main` BEFORE sending `merge_ready`, then re-run quality gates AFTER the rebase. Sibling work may have landed on `main` while you were working — sending `merge_ready` from a stale base risks reverting their changes (mx-c0c122 stale-base-revert).",
59
+ "",
60
+ "```bash",
61
+ "git fetch origin main:main",
62
+ "git rebase main",
63
+ "# re-run quality gates here, then signal merge_ready",
64
+ "```",
65
+ ].join("\n");
66
+ }
67
+
68
+ /**
69
+ * Format the file scope list as a markdown bullet list.
70
+ * Returns a human-readable fallback if no files are scoped.
71
+ */
72
+ function formatFileScope(fileScope: readonly string[]): string {
73
+ if (fileScope.length === 0) {
74
+ return "No file scope restrictions";
75
+ }
76
+ return fileScope.map((f) => `- \`${f}\``).join("\n");
77
+ }
78
+
79
+ /**
80
+ * Format loam domains as a `lm prime` command.
81
+ * Returns a human-readable fallback if no domains are configured.
82
+ */
83
+ function formatLoamDomains(domains: readonly string[]): string {
84
+ if (domains.length === 0) {
85
+ return "No specific expertise domains configured";
86
+ }
87
+ return `\`\`\`bash\nlm prime ${domains.join(" ")}\n\`\`\``;
88
+ }
89
+
90
+ /**
91
+ * Format profile content (Layer 2: deployment-specific WHAT KIND) for embedding in the overlay.
92
+ * Returns empty string if no profile was provided (omits the section entirely).
93
+ * When profile IS provided, renders it as-is — the caller (trellis) owns the formatting.
94
+ */
95
+ function formatProfile(profileContent: string | undefined): string {
96
+ if (!profileContent || profileContent.trim().length === 0) {
97
+ return "";
98
+ }
99
+ return profileContent;
100
+ }
101
+
102
+ /**
103
+ * Format pre-fetched loam expertise for embedding in the overlay.
104
+ * Returns empty string if no expertise was provided (omits the section entirely).
105
+ * When expertise IS provided, renders it under a 'Pre-loaded Expertise' heading
106
+ * with a brief intro explaining it was loaded at spawn time based on file scope.
107
+ */
108
+ function formatLoamExpertise(expertise: string | undefined): string {
109
+ if (!expertise || expertise.trim().length === 0) {
110
+ return "";
111
+ }
112
+ return [
113
+ "### Pre-loaded Expertise",
114
+ "",
115
+ "The following expertise was automatically loaded at spawn time based on your file scope:",
116
+ "",
117
+ expertise,
118
+ ].join("\n");
119
+ }
120
+
121
+ /** Capabilities that are read-only and should not get quality gates for commits/tests/lint. */
122
+ const READ_ONLY_CAPABILITIES = new Set(["scout", "reviewer"]);
123
+
124
+ /**
125
+ * The skip-scout section injected into lead overlays when --skip-scout is passed.
126
+ * Instructs the lead to bypass Phase 1 (exploration) and go straight to Phase 2 (build).
127
+ */
128
+ const SKIP_SCOUT_SECTION = `
129
+ ## Skip Scout Mode
130
+
131
+ **IMPORTANT**: You have been spawned with \`--skip-scout\`. Skip Phase 1 (Scout) entirely.
132
+ Go directly to Phase 2 (Build): write specs from your existing knowledge and the
133
+ pre-loaded expertise above, then spawn builders immediately.
134
+
135
+ Do NOT spawn scout agents. Do NOT explore the codebase extensively.
136
+ Your parent has already gathered the context you need.
137
+ `;
138
+
139
+ /**
140
+ * Build the dispatch overrides section for lead overlays.
141
+ * Only generates content when overrides are actually set.
142
+ * The overlay is the source of truth -- leads read these directives, not mail.
143
+ */
144
+ function formatDispatchOverrides(config: OverlayConfig): string {
145
+ if (config.capability !== "lead") return "";
146
+
147
+ const sections: string[] = [];
148
+
149
+ if (config.skipReview) {
150
+ sections.push(
151
+ "- **SKIP REVIEW**: You have been instructed to skip the review phase. " +
152
+ "Self-verify by reading the diff and running quality gates instead of spawning a reviewer.",
153
+ );
154
+ }
155
+
156
+ if (config.maxAgentsOverride !== undefined && config.maxAgentsOverride > 0) {
157
+ if (config.maxAgentsOverride === 1) {
158
+ sections.push(
159
+ "- **MAX AGENTS**: Your per-lead agent ceiling has been set to **1**. " +
160
+ "Spend that slot on a single builder for the whole task — skip scouts and reviewers and self-verify the builder's diff yourself. " +
161
+ "Leads cannot implement directly (Write/Edit/`git add`/`git commit` are blocked by the harness), so the one slot must be a builder.",
162
+ );
163
+ } else if (config.maxAgentsOverride === 2) {
164
+ sections.push(
165
+ "- **MAX AGENTS**: Your per-lead agent ceiling has been set to **2**. " +
166
+ "Operate in compressed mode: spend the slots on builders (one or two), skip scouts and reviewers, and self-verify each diff yourself. " +
167
+ "Leads do not implement; every change requires a builder spawn.",
168
+ );
169
+ } else {
170
+ sections.push(
171
+ `- **MAX AGENTS**: Your per-lead agent ceiling has been set to **${config.maxAgentsOverride}**. ` +
172
+ "Do not spawn more than this many sub-workers.",
173
+ );
174
+ }
175
+ }
176
+
177
+ if (sections.length === 0) return "";
178
+
179
+ return [
180
+ "## Dispatch Overrides",
181
+ "",
182
+ "Your coordinator has set the following overrides for this work stream:",
183
+ "",
184
+ ...sections,
185
+ "",
186
+ "Honor these directives. They override the default workflow described in your base definition.",
187
+ ].join("\n");
188
+ }
189
+
190
+ /**
191
+ * Format the quality gates section. Read-only agents (scout, reviewer) get
192
+ * a lightweight section that only tells them to close the issue and report.
193
+ * Writable agents get the full quality gates (tests, lint, build, commit).
194
+ */
195
+ /**
196
+ * Resolve quality gates: use provided gates if non-empty, otherwise fall back to defaults.
197
+ */
198
+ function resolveGates(gates: QualityGate[] | undefined): QualityGate[] {
199
+ return gates && gates.length > 0 ? gates : DEFAULT_QUALITY_GATES;
200
+ }
201
+
202
+ /**
203
+ * Format quality gates as inline backtick-delimited commands for prose sections.
204
+ * Example: `bun test`, `bun run lint`, `bun run typecheck`
205
+ */
206
+ export function formatQualityGatesInline(gates: QualityGate[] | undefined): string {
207
+ return resolveGates(gates)
208
+ .map((g) => `\`${g.command}\``)
209
+ .join(", ");
210
+ }
211
+
212
+ /**
213
+ * Format quality gates as a numbered step list for completion-protocol sections.
214
+ * Example:
215
+ * 1. Run `bun test` -- all tests must pass.
216
+ * 2. Run `bun run lint` -- lint and formatting must be clean.
217
+ */
218
+ export function formatQualityGatesSteps(gates: QualityGate[] | undefined): string {
219
+ return resolveGates(gates)
220
+ .map((g, i) => `${i + 1}. Run \`${g.command}\` -- ${g.description}.`)
221
+ .join("\n");
222
+ }
223
+
224
+ /**
225
+ * Format quality gates as a bash code block for workflow sections.
226
+ * Example:
227
+ * ```bash
228
+ * bun test # All tests must pass
229
+ * bun run lint # Lint and format must be clean
230
+ * ```
231
+ */
232
+ export function formatQualityGatesBash(gates: QualityGate[] | undefined): string {
233
+ const resolved = resolveGates(gates);
234
+ // Pad commands to align comments
235
+ const maxLen = Math.max(...resolved.map((g) => g.command.length));
236
+ const lines = resolved.map((g) => {
237
+ const padded = g.command.padEnd(maxLen + 2);
238
+ return `${padded}# ${g.description[0]?.toUpperCase() ?? ""}${g.description.slice(1)}`;
239
+ });
240
+ return ["```bash", ...lines, "```"].join("\n");
241
+ }
242
+
243
+ /**
244
+ * Format quality gates as a bullet list for capabilities sections.
245
+ * Example:
246
+ * - `bun test` (run tests)
247
+ * - `bun run lint` (lint and format check via biome)
248
+ */
249
+ export function formatQualityGatesCapabilities(gates: QualityGate[] | undefined): string {
250
+ return resolveGates(gates)
251
+ .map((g) => ` - \`${g.command}\` (${g.description})`)
252
+ .join("\n");
253
+ }
254
+
255
+ function formatQualityGates(config: OverlayConfig): string {
256
+ if (READ_ONLY_CAPABILITIES.has(config.capability)) {
257
+ const completionType = completionMailTypeFor(config.capability);
258
+ return [
259
+ "## Completion",
260
+ "",
261
+ "Before reporting completion:",
262
+ "",
263
+ `1. **Record loam learnings:** \`lm record <domain> --type <convention|pattern|reference> --description "..."\` — capture reusable knowledge from your work`,
264
+ `2. **Signal completion:** send \`${completionType}\` mail to ${config.parentAgent ?? "coordinator"}: \`ap mail send --to ${config.parentAgent ?? "coordinator"} --subject "Worker done: ${config.taskId}" --body "Summary of findings" --type ${completionType} --agent ${config.agentName}\``,
265
+ `3. **Close issue:** \`${config.trackerCli ?? "sr"} close ${config.taskId} --reason "summary of findings"\``,
266
+ "",
267
+ "You are a read-only agent. Do NOT commit, modify files, or run quality gates.",
268
+ ].join("\n");
269
+ }
270
+
271
+ const gates =
272
+ config.qualityGates && config.qualityGates.length > 0
273
+ ? config.qualityGates
274
+ : DEFAULT_QUALITY_GATES;
275
+
276
+ const gateLines = gates.map(
277
+ (gate, i) => `${i + 1}. **${gate.name}:** \`${gate.command}\` — ${gate.description}`,
278
+ );
279
+
280
+ return [
281
+ "## Quality Gates",
282
+ "",
283
+ "Before reporting completion, you MUST pass all quality gates:",
284
+ "",
285
+ ...gateLines,
286
+ `${gateLines.length + 1}. **Commit:** all changes committed to your branch (${config.branchName})`,
287
+ `${gateLines.length + 2}. **Record loam learnings:** \`lm record <domain> --type <convention|pattern|failure|decision> --description "..." --outcome-status success --outcome-agent ${config.agentName}\` — capture insights from your work`,
288
+ `${gateLines.length + 3}. **Signal completion:** send \`worker_done\` mail to ${config.parentAgent ?? "coordinator"}: \`ap mail send --to ${config.parentAgent ?? "coordinator"} --subject "Worker done: ${config.taskId}" --body "Quality gates passed." --type worker_done --agent ${config.agentName}\``,
289
+ `${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "sr"} close ${config.taskId} --reason "summary of changes"\``,
290
+ "",
291
+ "Do NOT push to the canonical branch. Your work will be merged by the",
292
+ "coordinator via `ap merge`.",
293
+ ].join("\n");
294
+ }
295
+
296
+ /**
297
+ * Format the constraints section. Read-only agents get read-only constraints.
298
+ * Writable agents get file-scope and branch constraints.
299
+ */
300
+ function formatConstraints(config: OverlayConfig): string {
301
+ const completionType = completionMailTypeFor(config.capability);
302
+ if (READ_ONLY_CAPABILITIES.has(config.capability)) {
303
+ return [
304
+ "## Constraints",
305
+ "",
306
+ "- You are **read-only**: do NOT modify, create, or delete any files",
307
+ "- Do NOT commit, push, or make any git state changes",
308
+ `- Report completion via \`${config.trackerCli ?? "sr"} close\` AND \`ap mail send --type ${completionType}\``,
309
+ "- If you encounter a blocking issue, send mail with `--priority urgent --type error`",
310
+ ].join("\n");
311
+ }
312
+
313
+ return [
314
+ "## Constraints",
315
+ "",
316
+ `- **WORKTREE ISOLATION**: All writes MUST target files within your worktree at \`${config.worktreePath}\``,
317
+ "- NEVER write to the canonical repo root — all writes go to your worktree copy",
318
+ "- Only modify files in your File Scope",
319
+ `- Commit only to your branch: ${config.branchName}`,
320
+ "- Never push to the canonical branch",
321
+ `- Report completion via \`${config.trackerCli ?? "sr"} close\` AND \`ap mail send --type ${completionType}\``,
322
+ "- If you encounter a blocking issue, send mail with `--priority urgent --type error`",
323
+ ].join("\n");
324
+ }
325
+
326
+ /**
327
+ * Format the can-spawn section. If the agent can spawn sub-workers,
328
+ * include an example sling command. Otherwise, state the restriction.
329
+ */
330
+ function formatCanSpawn(config: OverlayConfig): string {
331
+ if (!config.canSpawn) {
332
+ return "You may NOT spawn sub-workers.";
333
+ }
334
+ return [
335
+ "You may spawn sub-workers using `ap sling`. Example:",
336
+ "",
337
+ "```bash",
338
+ "ap sling <task-id> --capability builder --name <worker-name> \\",
339
+ ` --parent ${config.agentName} --depth ${config.depth + 1}`,
340
+ "```",
341
+ ].join("\n");
342
+ }
343
+
344
+ /**
345
+ * Generate a per-worker CLAUDE.md overlay from the template.
346
+ *
347
+ * Reads `templates/overlay.md.tmpl` and replaces all `{{VARIABLE}}`
348
+ * placeholders with values derived from the provided config.
349
+ *
350
+ * @param config - The overlay configuration for this agent/task
351
+ * @returns The rendered overlay content as a string
352
+ * @throws {AgentError} If the template file cannot be found or read
353
+ */
354
+ export async function generateOverlay(config: OverlayConfig): Promise<string> {
355
+ const templatePath = getTemplatePath();
356
+ const file = Bun.file(templatePath);
357
+ const exists = await file.exists();
358
+
359
+ if (!exists) {
360
+ throw new AgentError(`Overlay template not found: ${templatePath}`, {
361
+ agentName: config.agentName,
362
+ });
363
+ }
364
+
365
+ let template: string;
366
+ try {
367
+ template = await file.text();
368
+ } catch (err) {
369
+ throw new AgentError(`Failed to read overlay template: ${templatePath}`, {
370
+ agentName: config.agentName,
371
+ cause: err instanceof Error ? err : undefined,
372
+ });
373
+ }
374
+
375
+ const specInstruction = config.specPath
376
+ ? "Read your task spec at the path above. It contains the full description of\nwhat you need to build or review."
377
+ : "No task spec was provided. Check your mail or ask your parent agent for details.";
378
+
379
+ const replacements: Record<string, string> = {
380
+ "{{AGENT_NAME}}": config.agentName,
381
+ "{{TASK_ID}}": config.taskId,
382
+ "{{SPEC_PATH}}": config.specPath ?? "No spec file provided",
383
+ "{{BRANCH_NAME}}": config.branchName,
384
+ "{{WORKTREE_PATH}}": config.worktreePath,
385
+ "{{PARENT_AGENT}}": config.parentAgent ?? "coordinator",
386
+ "{{DEPTH}}": String(config.depth),
387
+ "{{FILE_SCOPE}}": formatFileScope(config.fileScope),
388
+ "{{LOAM_DOMAINS}}": formatLoamDomains(config.loamDomains),
389
+ "{{LOAM_EXPERTISE}}": formatLoamExpertise(config.loamExpertise),
390
+ "{{CAN_SPAWN}}": formatCanSpawn(config),
391
+ "{{QUALITY_GATES}}": formatQualityGates(config),
392
+ "{{CONSTRAINTS}}": formatConstraints(config),
393
+ "{{SPEC_INSTRUCTION}}": specInstruction,
394
+ "{{SKIP_SCOUT}}": config.skipScout ? SKIP_SCOUT_SECTION : "",
395
+ "{{DISPATCH_OVERRIDES}}": formatDispatchOverrides(config),
396
+ "{{SIBLINGS}}": formatSiblings(config),
397
+ "{{BASE_DEFINITION}}": config.baseDefinition,
398
+ "{{PROFILE_INSTRUCTIONS}}": formatProfile(config.profileContent),
399
+ "{{QUALITY_GATE_INLINE}}": formatQualityGatesInline(config.qualityGates),
400
+ "{{QUALITY_GATE_STEPS}}": formatQualityGatesSteps(config.qualityGates),
401
+ "{{QUALITY_GATE_BASH}}": formatQualityGatesBash(config.qualityGates),
402
+ "{{QUALITY_GATE_CAPABILITIES}}": formatQualityGatesCapabilities(config.qualityGates),
403
+ "{{TRACKER_CLI}}": config.trackerCli ?? "sr",
404
+ "{{TRACKER_NAME}}": config.trackerName ?? "sprout",
405
+ "{{INSTRUCTION_PATH}}": config.instructionPath ?? ".claude/CLAUDE.md",
406
+ };
407
+
408
+ let result = template;
409
+ for (const [placeholder, value] of Object.entries(replacements)) {
410
+ // Replace all occurrences — some placeholders appear multiple times
411
+ while (result.includes(placeholder)) {
412
+ result = result.replace(placeholder, value);
413
+ }
414
+ }
415
+
416
+ return result;
417
+ }
418
+
419
+ /**
420
+ * Check whether a directory is the canonical project root by comparing resolved paths.
421
+ *
422
+ * Agent overlays must NEVER be written to the canonical repo root -- they belong
423
+ * in worktrees. Writing an overlay to the project root overwrites the orchestrator's
424
+ * `.claude/CLAUDE.md`, breaking the user's own Claude Code session (agentplate-uwg4).
425
+ *
426
+ * Uses deterministic path comparison instead of checking for `.agentplate/config.yaml`
427
+ * because when dogfooding (running agentplate on its own repo), that file is tracked
428
+ * in git and appears in every worktree checkout (agentplate-p4st).
429
+ *
430
+ * @param dir - Absolute path to check
431
+ * @param canonicalRoot - Absolute path to the canonical project root
432
+ * @returns true if dir resolves to the same path as canonicalRoot
433
+ */
434
+ export function isCanonicalRoot(dir: string, canonicalRoot: string): boolean {
435
+ return resolve(dir) === resolve(canonicalRoot);
436
+ }
437
+
438
+ /**
439
+ * Generate the overlay and write it to `{worktreePath}/.claude/CLAUDE.md`.
440
+ * Creates the `.claude/` directory if it does not exist.
441
+ *
442
+ * Includes a safety guard that prevents writing to the canonical project root.
443
+ * Agent overlays belong in worktrees, never at the orchestrator's root.
444
+ *
445
+ * @param worktreePath - Absolute path to the agent's git worktree
446
+ * @param config - The overlay configuration for this agent/task
447
+ * @param canonicalRoot - Absolute path to the canonical project root (for guard check)
448
+ * @throws {AgentError} If worktreePath is the canonical project root, or if
449
+ * the directory cannot be created or the file cannot be written
450
+ */
451
+ export async function writeOverlay(
452
+ worktreePath: string,
453
+ config: OverlayConfig,
454
+ canonicalRoot: string,
455
+ instructionPath = ".claude/CLAUDE.md",
456
+ ): Promise<void> {
457
+ // Guard: never write agent overlays to the canonical project root.
458
+ // The project root's .claude/CLAUDE.md belongs to the orchestrator/user.
459
+ // Uses path comparison instead of file-existence heuristic to handle
460
+ // dogfooding scenarios where .agentplate/config.yaml is tracked in git
461
+ // and appears in every worktree checkout (agentplate-p4st).
462
+ if (isCanonicalRoot(worktreePath, canonicalRoot)) {
463
+ throw new AgentError(
464
+ `Refusing to write overlay to canonical project root: ${worktreePath}. Agent overlays must target a worktree, not the orchestrator's root directory. This prevents overwriting the user's .claude/CLAUDE.md.`,
465
+ { agentName: config.agentName },
466
+ );
467
+ }
468
+
469
+ const content = await generateOverlay(config);
470
+ const outputPath = join(worktreePath, instructionPath);
471
+ const outputDir = dirname(outputPath);
472
+
473
+ try {
474
+ await mkdir(outputDir, { recursive: true });
475
+ } catch (err) {
476
+ throw new AgentError(`Failed to create directory for instruction file at: ${outputDir}`, {
477
+ agentName: config.agentName,
478
+ cause: err instanceof Error ? err : undefined,
479
+ });
480
+ }
481
+
482
+ try {
483
+ await Bun.write(outputPath, content);
484
+ } catch (err) {
485
+ throw new AgentError(`Failed to write overlay to: ${outputPath}`, {
486
+ agentName: config.agentName,
487
+ cause: err instanceof Error ? err : undefined,
488
+ });
489
+ }
490
+ }
@@ -0,0 +1,190 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ detectScopeViolation,
4
+ findScopeViolations,
5
+ hasExpansionReason,
6
+ IMPLEMENTATION_CAPABILITIES,
7
+ parseExpansionReasonsFromGitLog,
8
+ } from "./scope-detect.ts";
9
+
10
+ describe("findScopeViolations", () => {
11
+ test("literal match: in-scope file is not a violation", () => {
12
+ const result = findScopeViolations(["src/foo.ts"], ["src/foo.ts"]);
13
+ expect(result).toEqual([]);
14
+ });
15
+
16
+ test("glob match: src/foo/**/*.ts allows nested file", () => {
17
+ const result = findScopeViolations(
18
+ ["src/foo/bar/baz.ts", "src/foo/qux.ts"],
19
+ ["src/foo/**/*.ts"],
20
+ );
21
+ expect(result).toEqual([]);
22
+ });
23
+
24
+ test("out-of-scope file is reported", () => {
25
+ const result = findScopeViolations(["src/other.ts"], ["src/foo.ts"]);
26
+ expect(result).toEqual(["src/other.ts"]);
27
+ });
28
+
29
+ test("empty fileScope is treated as unrestricted", () => {
30
+ const result = findScopeViolations(["any/file.ts", "another.ts"], []);
31
+ expect(result).toEqual([]);
32
+ });
33
+
34
+ test("partial violations: returns only the out-of-scope subset", () => {
35
+ const result = findScopeViolations(
36
+ ["src/foo.ts", "src/other.ts", "src/bar.ts"],
37
+ ["src/foo.ts", "src/bar.ts"],
38
+ );
39
+ expect(result).toEqual(["src/other.ts"]);
40
+ });
41
+
42
+ test("glob match: literal path equals scope entry without a glob char", () => {
43
+ const result = findScopeViolations(["a.ts"], ["a.ts"]);
44
+ expect(result).toEqual([]);
45
+ });
46
+ });
47
+
48
+ describe("hasExpansionReason", () => {
49
+ test("expansion_reason: with value → true", () => {
50
+ expect(hasExpansionReason("expansion_reason: foo")).toBe(true);
51
+ });
52
+
53
+ test("Expansion_Reason: case-insensitive → true", () => {
54
+ expect(hasExpansionReason("Expansion_Reason: bar")).toBe(true);
55
+ });
56
+
57
+ test("EXPANSION_REASON: multi-word value → true", () => {
58
+ expect(hasExpansionReason("EXPANSION_REASON: baz quux")).toBe(true);
59
+ });
60
+
61
+ test("expansion_reason: empty value → false", () => {
62
+ expect(hasExpansionReason("expansion_reason:")).toBe(false);
63
+ });
64
+
65
+ test("expansion_reason: only whitespace → false", () => {
66
+ expect(hasExpansionReason("expansion_reason: ")).toBe(false);
67
+ });
68
+
69
+ test("expansion-reason: hyphen separator → false", () => {
70
+ expect(hasExpansionReason("expansion-reason: foo")).toBe(false);
71
+ });
72
+
73
+ test("no marker → false", () => {
74
+ expect(hasExpansionReason("no reason here")).toBe(false);
75
+ });
76
+
77
+ test("marker embedded in commit body with prefix → true", () => {
78
+ const body = "Refactor the thing\n\nexpansion_reason: had to update shared types\n";
79
+ expect(hasExpansionReason(body)).toBe(true);
80
+ });
81
+ });
82
+
83
+ describe("parseExpansionReasonsFromGitLog", () => {
84
+ test("returns each value across multiple commit bodies", () => {
85
+ const log = [
86
+ "feat: a thing",
87
+ "",
88
+ "expansion_reason: needed shared type",
89
+ "",
90
+ "fix: another thing",
91
+ "",
92
+ "expansion_reason: had to update barrel export",
93
+ ].join("\n");
94
+ const result = parseExpansionReasonsFromGitLog(log);
95
+ expect(result).toEqual(["needed shared type", "had to update barrel export"]);
96
+ });
97
+
98
+ test("trims whitespace around values", () => {
99
+ const log = "expansion_reason: surrounded by spaces \n";
100
+ const result = parseExpansionReasonsFromGitLog(log);
101
+ expect(result).toEqual(["surrounded by spaces"]);
102
+ });
103
+
104
+ test("ignores commits without the marker", () => {
105
+ const log = "feat: regular commit\n\nfix: another\n";
106
+ expect(parseExpansionReasonsFromGitLog(log)).toEqual([]);
107
+ });
108
+
109
+ test("empty log returns empty array", () => {
110
+ expect(parseExpansionReasonsFromGitLog("")).toEqual([]);
111
+ });
112
+
113
+ test("case-insensitive match", () => {
114
+ const log = "Expansion_Reason: capitalized variant\n";
115
+ expect(parseExpansionReasonsFromGitLog(log)).toEqual(["capitalized variant"]);
116
+ });
117
+ });
118
+
119
+ describe("detectScopeViolation", () => {
120
+ test("returns expected violations and expansion reasons via stub", () => {
121
+ const stub = (args: string[]): string => {
122
+ if (args[0] === "diff") return "src/foo.ts\nsrc/other.ts\n";
123
+ if (args[0] === "log") return "feat: change\n\nexpansion_reason: cross-cutting\n";
124
+ return "";
125
+ };
126
+ const result = detectScopeViolation({
127
+ worktreePath: "/tmp/wt",
128
+ baseRef: "main",
129
+ fileScope: ["src/foo.ts"],
130
+ gitRunner: stub,
131
+ });
132
+ expect(result.violations).toEqual(["src/other.ts"]);
133
+ expect(result.expansionReasons).toEqual(["cross-cutting"]);
134
+ });
135
+
136
+ test("returns empty when stub throws", () => {
137
+ const stub = (): string => {
138
+ throw new Error("boom");
139
+ };
140
+ const result = detectScopeViolation({
141
+ worktreePath: "/tmp/wt",
142
+ baseRef: "main",
143
+ fileScope: ["src/foo.ts"],
144
+ gitRunner: stub,
145
+ });
146
+ expect(result.violations).toEqual([]);
147
+ expect(result.expansionReasons).toEqual([]);
148
+ });
149
+
150
+ test("empty fileScope yields no violations regardless of diff", () => {
151
+ const stub = (args: string[]): string => {
152
+ if (args[0] === "diff") return "src/anything.ts\n";
153
+ return "";
154
+ };
155
+ const result = detectScopeViolation({
156
+ worktreePath: "/tmp/wt",
157
+ baseRef: "main",
158
+ fileScope: [],
159
+ gitRunner: stub,
160
+ });
161
+ expect(result.violations).toEqual([]);
162
+ });
163
+
164
+ test("blank diff lines are filtered", () => {
165
+ const stub = (args: string[]): string => {
166
+ if (args[0] === "diff") return "\nsrc/foo.ts\n\n\nsrc/bar.ts\n";
167
+ return "";
168
+ };
169
+ const result = detectScopeViolation({
170
+ worktreePath: "/tmp/wt",
171
+ baseRef: "main",
172
+ fileScope: ["src/foo.ts"],
173
+ gitRunner: stub,
174
+ });
175
+ expect(result.violations).toEqual(["src/bar.ts"]);
176
+ });
177
+ });
178
+
179
+ describe("IMPLEMENTATION_CAPABILITIES", () => {
180
+ test("includes builder and merger", () => {
181
+ expect(IMPLEMENTATION_CAPABILITIES.has("builder")).toBe(true);
182
+ expect(IMPLEMENTATION_CAPABILITIES.has("merger")).toBe(true);
183
+ });
184
+
185
+ test("excludes read-only roles", () => {
186
+ expect(IMPLEMENTATION_CAPABILITIES.has("scout")).toBe(false);
187
+ expect(IMPLEMENTATION_CAPABILITIES.has("reviewer")).toBe(false);
188
+ expect(IMPLEMENTATION_CAPABILITIES.has("lead")).toBe(false);
189
+ });
190
+ });