@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,926 @@
1
+ /**
2
+ * Tiered conflict resolution for merging agent branches.
3
+ *
4
+ * Implements a 4-tier escalation strategy:
5
+ * 1. Clean merge — git merge with no conflicts
6
+ * 2. Auto-resolve — parse conflict markers, keep incoming (agent) changes
7
+ * 3. AI-resolve — use Claude to resolve remaining conflicts
8
+ * 4. Re-imagine — abort merge and reimplement changes from scratch
9
+ *
10
+ * Each tier is attempted in order. If a tier fails, the next is tried.
11
+ * Disabled tiers are skipped. Uses Bun.spawn for all subprocess calls.
12
+ */
13
+
14
+ import { unlinkSync } from "node:fs";
15
+ import { MergeError } from "../errors.ts";
16
+ import type { LoamClient } from "../loam/client.ts";
17
+ import { getRuntime } from "../runtimes/registry.ts";
18
+ import type {
19
+ AgentplateConfig,
20
+ ConflictHistory,
21
+ MergeEntry,
22
+ MergeResult,
23
+ ParsedConflictPattern,
24
+ ResolutionTier,
25
+ } from "../types.ts";
26
+
27
+ export interface MergeResolver {
28
+ /** Attempt to merge the entry's branch into the canonical branch with tiered resolution. */
29
+ resolve(entry: MergeEntry, canonicalBranch: string, repoRoot: string): Promise<MergeResult>;
30
+ }
31
+
32
+ /**
33
+ * Run a git command in the given repo root. Returns stdout, stderr, and exit code.
34
+ */
35
+ async function runGit(
36
+ repoRoot: string,
37
+ args: string[],
38
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
39
+ const proc = Bun.spawn(["git", ...args], {
40
+ cwd: repoRoot,
41
+ stdout: "pipe",
42
+ stderr: "pipe",
43
+ });
44
+
45
+ const [stdout, stderr, exitCode] = await Promise.all([
46
+ new Response(proc.stdout).text(),
47
+ new Response(proc.stderr).text(),
48
+ proc.exited,
49
+ ]);
50
+
51
+ return { stdout, stderr, exitCode };
52
+ }
53
+
54
+ /**
55
+ * ag-eco runtime state path prefixes and exact filenames.
56
+ * Files matching these are bookkeeping artifacts that change during normal
57
+ * orchestration and should be auto-committed rather than blocking merges.
58
+ */
59
+ const OS_ECO_STATE_PREFIXES = [
60
+ ".sprout/",
61
+ ".agentplate/",
62
+ ".greenhouse/",
63
+ ".loam/",
64
+ ".trellis/",
65
+ ".claude/",
66
+ ];
67
+ const OS_ECO_STATE_FILES = ["CLAUDE.md"];
68
+
69
+ /**
70
+ * Returns true if a file path is an ag-eco runtime state file
71
+ * (issue tracker, groups, expertise, prompts, etc.).
72
+ */
73
+ function isOsEcoStateFile(filePath: string): boolean {
74
+ if (OS_ECO_STATE_FILES.includes(filePath)) return true;
75
+ return OS_ECO_STATE_PREFIXES.some((prefix) => filePath.startsWith(prefix));
76
+ }
77
+
78
+ /**
79
+ * Get the list of tracked files with uncommitted changes (unstaged or staged).
80
+ * Returns deduplicated list of file paths. An empty list means the working tree is clean.
81
+ */
82
+ async function checkDirtyWorkingTree(repoRoot: string): Promise<string[]> {
83
+ const { stdout: unstaged } = await runGit(repoRoot, ["diff", "--name-only"]);
84
+ const { stdout: staged } = await runGit(repoRoot, ["diff", "--name-only", "--cached"]);
85
+ const files = [
86
+ ...unstaged
87
+ .trim()
88
+ .split("\n")
89
+ .filter((l) => l.length > 0),
90
+ ...staged
91
+ .trim()
92
+ .split("\n")
93
+ .filter((l) => l.length > 0),
94
+ ];
95
+ return [...new Set(files)];
96
+ }
97
+
98
+ /**
99
+ * Auto-commit ag-eco runtime state files so they don't block merges.
100
+ * Returns true if a commit was made, false if there was nothing to commit.
101
+ */
102
+ async function autoCommitStateFiles(repoRoot: string, stateFiles: string[]): Promise<boolean> {
103
+ if (stateFiles.length === 0) return false;
104
+
105
+ const { exitCode: addCode } = await runGit(repoRoot, ["add", ...stateFiles]);
106
+ if (addCode !== 0) return false;
107
+
108
+ const { exitCode: commitCode } = await runGit(repoRoot, [
109
+ "commit",
110
+ "-m",
111
+ "chore: sync ag-eco runtime state",
112
+ ]);
113
+ return commitCode === 0;
114
+ }
115
+
116
+ /**
117
+ * Get the list of conflicted files from `git diff --name-only --diff-filter=U`.
118
+ */
119
+ async function getConflictedFiles(repoRoot: string): Promise<string[]> {
120
+ const { stdout } = await runGit(repoRoot, ["diff", "--name-only", "--diff-filter=U"]);
121
+ return stdout
122
+ .trim()
123
+ .split("\n")
124
+ .filter((line) => line.length > 0);
125
+ }
126
+
127
+ /**
128
+ * Parse conflict markers in file content and keep the incoming (agent) changes.
129
+ *
130
+ * A conflict block looks like:
131
+ * ```
132
+ * <<<<<<< HEAD
133
+ * canonical content
134
+ * =======
135
+ * incoming content
136
+ * >>>>>>> branch
137
+ * ```
138
+ *
139
+ * This function replaces each conflict block with only the incoming content.
140
+ * Returns the resolved content, or null if no conflict markers were found.
141
+ */
142
+ function resolveConflictsKeepIncoming(content: string): string | null {
143
+ const conflictPattern = /^<{7} .+\n([\s\S]*?)^={7}\n([\s\S]*?)^>{7} .+\n?/gm;
144
+
145
+ if (!conflictPattern.test(content)) {
146
+ return null;
147
+ }
148
+
149
+ // Reset regex lastIndex after test()
150
+ conflictPattern.lastIndex = 0;
151
+
152
+ return content.replace(conflictPattern, (_match, _canonical: string, incoming: string) => {
153
+ return incoming;
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Parse conflict markers in file content and keep ALL lines from both sides.
159
+ * Used when the file has `merge=union` gitattribute — dedup-on-read handles duplicates.
160
+ *
161
+ * A conflict block looks like:
162
+ * ```
163
+ * <<<<<<< HEAD
164
+ * canonical content
165
+ * =======
166
+ * incoming content
167
+ * >>>>>>> branch
168
+ * ```
169
+ *
170
+ * This function replaces each conflict block with canonical + incoming content concatenated.
171
+ * Returns the resolved content, or null if no conflict markers were found.
172
+ */
173
+ export function resolveConflictsUnion(content: string): string | null {
174
+ const conflictPattern = /^<{7} .+\n([\s\S]*?)^={7}\n([\s\S]*?)^>{7} .+\n?/gm;
175
+
176
+ if (!conflictPattern.test(content)) {
177
+ return null;
178
+ }
179
+
180
+ // Reset regex lastIndex after test()
181
+ conflictPattern.lastIndex = 0;
182
+
183
+ return content.replace(conflictPattern, (_match, canonical: string, incoming: string) => {
184
+ return canonical + incoming;
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Detect if any conflict block has non-whitespace content on the canonical (HEAD) side.
190
+ * Returns true if auto-resolving with keep-incoming would silently discard canonical content.
191
+ * Use this before calling resolveConflictsKeepIncoming to prevent data loss.
192
+ */
193
+ export function hasContentfulCanonical(content: string): boolean {
194
+ const conflictPattern = /^<{7} .+\n([\s\S]*?)^={7}\n([\s\S]*?)^>{7} .+\n?/gm;
195
+ let match = conflictPattern.exec(content);
196
+ while (match !== null) {
197
+ const canonical = match[1] ?? "";
198
+ if (canonical.trim().length > 0) {
199
+ return true;
200
+ }
201
+ match = conflictPattern.exec(content);
202
+ }
203
+ return false;
204
+ }
205
+
206
+ /**
207
+ * Check if a file has the `merge=union` gitattribute set.
208
+ * Returns true if `git check-attr merge -- <file>` ends with ": merge: union".
209
+ */
210
+ export async function checkMergeUnion(repoRoot: string, filePath: string): Promise<boolean> {
211
+ const { stdout, exitCode } = await runGit(repoRoot, ["check-attr", "merge", "--", filePath]);
212
+ if (exitCode !== 0) return false;
213
+ return stdout.trim().endsWith(": merge: union");
214
+ }
215
+
216
+ /**
217
+ * Read a file's content using Bun.file().
218
+ */
219
+ async function readFile(filePath: string): Promise<string> {
220
+ const file = Bun.file(filePath);
221
+ return file.text();
222
+ }
223
+
224
+ /**
225
+ * Write content to a file using Bun.write().
226
+ */
227
+ async function writeFile(filePath: string, content: string): Promise<void> {
228
+ await Bun.write(filePath, content);
229
+ }
230
+
231
+ /**
232
+ * Tier 1: Attempt a clean merge (git merge --no-edit).
233
+ * Returns true if the merge succeeds with no conflicts.
234
+ */
235
+ async function tryCleanMerge(
236
+ entry: MergeEntry,
237
+ repoRoot: string,
238
+ ): Promise<{ success: boolean; conflictFiles: string[] }> {
239
+ const { exitCode } = await runGit(repoRoot, ["merge", "--no-edit", entry.branchName]);
240
+
241
+ if (exitCode === 0) {
242
+ return { success: true, conflictFiles: [] };
243
+ }
244
+
245
+ // Merge failed — get the list of conflicted files
246
+ const conflictFiles = await getConflictedFiles(repoRoot);
247
+ return { success: false, conflictFiles };
248
+ }
249
+
250
+ /**
251
+ * Tier 2: Auto-resolve conflicts by keeping incoming (agent) changes.
252
+ * Parses conflict markers and keeps the content between ======= and >>>>>>>.
253
+ * Skips files where the canonical side has non-whitespace content to prevent
254
+ * silent data loss — those files are escalated to higher tiers.
255
+ */
256
+ async function tryAutoResolve(
257
+ conflictFiles: string[],
258
+ repoRoot: string,
259
+ ): Promise<{ success: boolean; remainingConflicts: string[]; contentDropWarnings: string[] }> {
260
+ const remainingConflicts: string[] = [];
261
+ const contentDropWarnings: string[] = [];
262
+
263
+ for (const file of conflictFiles) {
264
+ const filePath = `${repoRoot}/${file}`;
265
+
266
+ try {
267
+ const content = await readFile(filePath);
268
+ const isUnion = await checkMergeUnion(repoRoot, file);
269
+
270
+ // For non-union files, check if the canonical side has content.
271
+ // If it does, auto-resolving would silently discard that content.
272
+ // Escalate to a higher tier instead.
273
+ if (!isUnion && hasContentfulCanonical(content)) {
274
+ contentDropWarnings.push(
275
+ `auto-resolve skipped for ${file}: canonical side has content that would be discarded`,
276
+ );
277
+ remainingConflicts.push(file);
278
+ continue;
279
+ }
280
+
281
+ const resolved = isUnion
282
+ ? resolveConflictsUnion(content)
283
+ : resolveConflictsKeepIncoming(content);
284
+
285
+ if (resolved === null) {
286
+ // No conflict markers found (shouldn't happen but be defensive)
287
+ remainingConflicts.push(file);
288
+ continue;
289
+ }
290
+
291
+ await writeFile(filePath, resolved);
292
+ const { exitCode } = await runGit(repoRoot, ["add", file]);
293
+ if (exitCode !== 0) {
294
+ remainingConflicts.push(file);
295
+ }
296
+ } catch {
297
+ remainingConflicts.push(file);
298
+ }
299
+ }
300
+
301
+ if (remainingConflicts.length > 0) {
302
+ return { success: false, remainingConflicts, contentDropWarnings };
303
+ }
304
+
305
+ // All files resolved — commit
306
+ const { exitCode } = await runGit(repoRoot, ["commit", "--no-edit"]);
307
+ return { success: exitCode === 0, remainingConflicts, contentDropWarnings };
308
+ }
309
+
310
+ /**
311
+ * Check if text looks like conversational prose rather than code.
312
+ * Returns true if the output is likely prose from the LLM rather than resolved code.
313
+ */
314
+ export function looksLikeProse(text: string): boolean {
315
+ const trimmed = text.trim();
316
+ if (trimmed.length === 0) return true;
317
+
318
+ // Common conversational opening patterns from LLMs
319
+ const prosePatterns = [
320
+ /^(I |I'[a-z]+ |Here |Here's |The |This |Let me |Sure|Unfortunately|Apologies|Sorry)/i,
321
+ /^(To resolve|Looking at|Based on|After reviewing|The conflict)/i,
322
+ /^```/m, // Markdown fencing — the model wrapped the code
323
+ /I need permission/i,
324
+ /I cannot/i,
325
+ /I don't have/i,
326
+ ];
327
+
328
+ for (const pattern of prosePatterns) {
329
+ if (pattern.test(trimmed)) return true;
330
+ }
331
+
332
+ return false;
333
+ }
334
+
335
+ /**
336
+ * Tier 3: AI-assisted conflict resolution using Claude.
337
+ * Spawns `claude --print` for each conflicted file with the conflict content.
338
+ * Validates that output looks like code, not conversational prose.
339
+ */
340
+ async function tryAiResolve(
341
+ conflictFiles: string[],
342
+ repoRoot: string,
343
+ pastResolutions?: string[],
344
+ config?: AgentplateConfig,
345
+ ): Promise<{ success: boolean; remainingConflicts: string[] }> {
346
+ const remainingConflicts: string[] = [];
347
+
348
+ for (const file of conflictFiles) {
349
+ const filePath = `${repoRoot}/${file}`;
350
+
351
+ try {
352
+ const content = await readFile(filePath);
353
+ const historyContext =
354
+ pastResolutions && pastResolutions.length > 0
355
+ ? `\n\nHistorical context from past merges:\n${pastResolutions.join("\n")}\n`
356
+ : "";
357
+ const prompt = [
358
+ "You are a merge conflict resolver. Output ONLY the resolved file content.",
359
+ "Rules: NO explanation, NO markdown fencing, NO conversation, NO preamble.",
360
+ "Output the raw file content as it should appear on disk.",
361
+ "Choose the best combination of both sides of this conflict:",
362
+ historyContext,
363
+ "\n\n",
364
+ content,
365
+ ].join(" ");
366
+
367
+ const runtime = getRuntime(config?.runtime?.printCommand ?? config?.runtime?.default, config);
368
+ const argv = runtime.buildPrintCommand(prompt);
369
+ const proc = Bun.spawn(argv, {
370
+ cwd: repoRoot,
371
+ stdout: "pipe",
372
+ stderr: "pipe",
373
+ });
374
+
375
+ const [resolved, , exitCode] = await Promise.all([
376
+ new Response(proc.stdout).text(),
377
+ new Response(proc.stderr).text(),
378
+ proc.exited,
379
+ ]);
380
+
381
+ if (exitCode !== 0 || resolved.trim() === "") {
382
+ remainingConflicts.push(file);
383
+ continue;
384
+ }
385
+
386
+ // Validate output is code, not prose — fall back to next tier if not
387
+ if (looksLikeProse(resolved)) {
388
+ remainingConflicts.push(file);
389
+ continue;
390
+ }
391
+
392
+ await writeFile(filePath, resolved);
393
+ const { exitCode: addExitCode } = await runGit(repoRoot, ["add", file]);
394
+ if (addExitCode !== 0) {
395
+ remainingConflicts.push(file);
396
+ }
397
+ } catch {
398
+ remainingConflicts.push(file);
399
+ }
400
+ }
401
+
402
+ if (remainingConflicts.length > 0) {
403
+ return { success: false, remainingConflicts };
404
+ }
405
+
406
+ // All files resolved — commit
407
+ const { exitCode } = await runGit(repoRoot, ["commit", "--no-edit"]);
408
+ return { success: exitCode === 0, remainingConflicts };
409
+ }
410
+
411
+ /**
412
+ * Tier 4: Re-imagine — abort the merge and reimplement changes from scratch.
413
+ * Uses Claude to reimplement the agent's changes on top of the canonical version.
414
+ */
415
+ async function tryReimagine(
416
+ entry: MergeEntry,
417
+ canonicalBranch: string,
418
+ repoRoot: string,
419
+ config?: AgentplateConfig,
420
+ ): Promise<{ success: boolean }> {
421
+ // Abort the current merge
422
+ await runGit(repoRoot, ["merge", "--abort"]);
423
+
424
+ for (const file of entry.filesModified) {
425
+ try {
426
+ // Get the canonical version
427
+ const { stdout: canonicalContent, exitCode: catCanonicalCode } = await runGit(repoRoot, [
428
+ "show",
429
+ `${canonicalBranch}:${file}`,
430
+ ]);
431
+
432
+ // Get the branch version
433
+ const { stdout: branchContent, exitCode: catBranchCode } = await runGit(repoRoot, [
434
+ "show",
435
+ `${entry.branchName}:${file}`,
436
+ ]);
437
+
438
+ if (catCanonicalCode !== 0 || catBranchCode !== 0) {
439
+ return { success: false };
440
+ }
441
+
442
+ const prompt = [
443
+ "You are a merge conflict resolver. Output ONLY the final file content.",
444
+ "Rules: NO explanation, NO markdown fencing, NO conversation, NO preamble.",
445
+ "Output the raw file content as it should appear on disk.",
446
+ "Reimplement the changes from the branch version onto the canonical version.",
447
+ `\n\n=== CANONICAL VERSION (${canonicalBranch}) ===\n`,
448
+ canonicalContent,
449
+ `\n\n=== BRANCH VERSION (${entry.branchName}) ===\n`,
450
+ branchContent,
451
+ ].join("");
452
+
453
+ const runtime = getRuntime(config?.runtime?.printCommand ?? config?.runtime?.default, config);
454
+ const argv = runtime.buildPrintCommand(prompt);
455
+ const proc = Bun.spawn(argv, {
456
+ cwd: repoRoot,
457
+ stdout: "pipe",
458
+ stderr: "pipe",
459
+ });
460
+
461
+ const [reimagined, , exitCode] = await Promise.all([
462
+ new Response(proc.stdout).text(),
463
+ new Response(proc.stderr).text(),
464
+ proc.exited,
465
+ ]);
466
+
467
+ if (exitCode !== 0 || reimagined.trim() === "") {
468
+ return { success: false };
469
+ }
470
+
471
+ // Validate output is code, not prose
472
+ if (looksLikeProse(reimagined)) {
473
+ return { success: false };
474
+ }
475
+
476
+ const filePath = `${repoRoot}/${file}`;
477
+ await writeFile(filePath, reimagined);
478
+ const { exitCode: addExitCode } = await runGit(repoRoot, ["add", file]);
479
+ if (addExitCode !== 0) {
480
+ return { success: false };
481
+ }
482
+ } catch {
483
+ return { success: false };
484
+ }
485
+ }
486
+
487
+ // Commit the reimagined changes
488
+ const { exitCode } = await runGit(repoRoot, [
489
+ "commit",
490
+ "-m",
491
+ `Reimagine merge: ${entry.branchName} onto ${canonicalBranch}`,
492
+ ]);
493
+
494
+ return { success: exitCode === 0 };
495
+ }
496
+
497
+ /**
498
+ * Parse loam search output for conflict patterns.
499
+ * Extracts structured data from pattern descriptions recorded by recordConflictPattern().
500
+ */
501
+ export function parseConflictPatterns(searchOutput: string): ParsedConflictPattern[] {
502
+ const patterns: ParsedConflictPattern[] = [];
503
+ // Simple approach: match to end of line/sentence and manually strip trailing period
504
+ const regex =
505
+ /Merge conflict (resolved|failed) at tier (clean-merge|auto-resolve|ai-resolve|reimagine)\.\s*Branch:\s*(\S+)\.\s*Agent:\s*(\S+)\.\s*Conflicting files:\s*(.+?)(?=\.(?:\s|$))/g;
506
+
507
+ let match = regex.exec(searchOutput);
508
+ while (match !== null) {
509
+ const outcome = match[1];
510
+ const tier = match[2];
511
+ const branch = match[3];
512
+ const agent = match[4];
513
+ const filesStr = match[5];
514
+
515
+ if (!outcome || !tier || !branch || !agent || !filesStr) {
516
+ match = regex.exec(searchOutput);
517
+ continue;
518
+ }
519
+
520
+ patterns.push({
521
+ tier: tier as ResolutionTier,
522
+ success: outcome === "resolved",
523
+ files: filesStr
524
+ .split(",")
525
+ .map((f) => f.trim())
526
+ .filter((f) => f.length > 0),
527
+ agent: agent.trim(),
528
+ branch: branch.trim(),
529
+ });
530
+
531
+ match = regex.exec(searchOutput);
532
+ }
533
+
534
+ return patterns;
535
+ }
536
+
537
+ /**
538
+ * Build conflict history from parsed patterns, scoped to the files in the current merge entry.
539
+ *
540
+ * Skip-tier logic: if a tier has failed >= 2 times for any overlapping file
541
+ * and never succeeded for those files, add it to skipTiers.
542
+ *
543
+ * Past resolutions: collect descriptions of successful resolutions involving
544
+ * overlapping files to enrich AI prompts.
545
+ *
546
+ * Predicted conflicts: files from historical patterns that overlap with the
547
+ * current entry files.
548
+ */
549
+ export function buildConflictHistory(
550
+ patterns: ParsedConflictPattern[],
551
+ entryFiles: string[],
552
+ ): ConflictHistory {
553
+ const entryFileSet = new Set(entryFiles);
554
+
555
+ // Filter patterns to those that share files with the current entry
556
+ const relevantPatterns = patterns.filter((p) => p.files.some((f) => entryFileSet.has(f)));
557
+
558
+ if (relevantPatterns.length === 0) {
559
+ return { skipTiers: [], pastResolutions: [], predictedConflictFiles: [] };
560
+ }
561
+
562
+ // Build tier success/failure counts
563
+ const tierCounts = new Map<ResolutionTier, { successes: number; failures: number }>();
564
+ for (const p of relevantPatterns) {
565
+ const counts = tierCounts.get(p.tier) ?? { successes: 0, failures: 0 };
566
+ if (p.success) {
567
+ counts.successes++;
568
+ } else {
569
+ counts.failures++;
570
+ }
571
+ tierCounts.set(p.tier, counts);
572
+ }
573
+
574
+ // Skip tiers that have failed >= 2 times and never succeeded
575
+ const skipTiers: ResolutionTier[] = [];
576
+ for (const [tier, counts] of tierCounts) {
577
+ if (counts.failures >= 2 && counts.successes === 0) {
578
+ skipTiers.push(tier);
579
+ }
580
+ }
581
+
582
+ // Collect past successful resolutions
583
+ const pastResolutions: string[] = [];
584
+ for (const p of relevantPatterns) {
585
+ if (p.success) {
586
+ pastResolutions.push(
587
+ `Previously resolved at tier ${p.tier} for files: ${p.files.join(", ")}`,
588
+ );
589
+ }
590
+ }
591
+
592
+ // Predict conflict files: all files from relevant historical patterns
593
+ const predictedFileSet = new Set<string>();
594
+ for (const p of relevantPatterns) {
595
+ for (const f of p.files) {
596
+ predictedFileSet.add(f);
597
+ }
598
+ }
599
+ const predictedConflictFiles = [...predictedFileSet].sort();
600
+
601
+ return { skipTiers, pastResolutions, predictedConflictFiles };
602
+ }
603
+
604
+ /**
605
+ * Query loam for historical conflict patterns related to the merge entry.
606
+ * Returns empty history if loam is unavailable or search fails (fire-and-forget).
607
+ */
608
+ async function queryConflictHistory(
609
+ loamClient: LoamClient,
610
+ entry: MergeEntry,
611
+ ): Promise<ConflictHistory> {
612
+ try {
613
+ const searchOutput = await loamClient.search("merge-conflict", { sortByScore: true });
614
+ const patterns = parseConflictPatterns(searchOutput);
615
+ return buildConflictHistory(patterns, entry.filesModified);
616
+ } catch {
617
+ return { skipTiers: [], pastResolutions: [], predictedConflictFiles: [] };
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Record a merge conflict pattern to loam for future learning.
623
+ * Uses fire-and-forget (try/catch swallowing errors) so recording
624
+ * never blocks or fails the merge itself.
625
+ */
626
+ function recordConflictPattern(
627
+ loamClient: LoamClient,
628
+ entry: MergeEntry,
629
+ tier: ResolutionTier,
630
+ conflictFiles: string[],
631
+ success: boolean,
632
+ ): void {
633
+ const outcome = success ? "resolved" : "failed";
634
+ const description = [
635
+ `Merge conflict ${outcome} at tier ${tier}.`,
636
+ `Branch: ${entry.branchName}.`,
637
+ `Agent: ${entry.agentName}.`,
638
+ `Conflicting files: ${conflictFiles.join(", ")}.`,
639
+ ].join(" ");
640
+
641
+ // Fire-and-forget per convention mx-09e10f
642
+ loamClient
643
+ .record("architecture", {
644
+ type: "pattern",
645
+ description,
646
+ tags: ["merge-conflict"],
647
+ evidenceBead: entry.taskId,
648
+ })
649
+ .catch(() => {});
650
+ }
651
+
652
+ /**
653
+ * Create a MergeResolver with configurable tier enablement.
654
+ *
655
+ * @param options.aiResolveEnabled - Enable tier 3 (AI-assisted resolution)
656
+ * @param options.reimagineEnabled - Enable tier 4 (full reimagine)
657
+ * @param options.loamClient - Optional LoamClient for conflict pattern recording
658
+ */
659
+ export function createMergeResolver(options: {
660
+ aiResolveEnabled: boolean;
661
+ reimagineEnabled: boolean;
662
+ loamClient?: LoamClient;
663
+ config?: AgentplateConfig;
664
+ onMergeSuccess?: (entry: MergeEntry) => Promise<void>;
665
+ }): MergeResolver {
666
+ return {
667
+ async resolve(
668
+ entry: MergeEntry,
669
+ canonicalBranch: string,
670
+ repoRoot: string,
671
+ ): Promise<MergeResult> {
672
+ // Check current branch — skip checkout if already on canonical.
673
+ // Avoids "already checked out" error when worktrees exist.
674
+ const { stdout: currentRef, exitCode: refCode } = await runGit(repoRoot, [
675
+ "symbolic-ref",
676
+ "--short",
677
+ "HEAD",
678
+ ]);
679
+ const needsCheckout = refCode !== 0 || currentRef.trim() !== canonicalBranch;
680
+
681
+ if (needsCheckout) {
682
+ const { exitCode: checkoutCode, stderr: checkoutErr } = await runGit(repoRoot, [
683
+ "checkout",
684
+ canonicalBranch,
685
+ ]);
686
+ if (checkoutCode !== 0) {
687
+ throw new MergeError(`Failed to checkout ${canonicalBranch}: ${checkoutErr.trim()}`, {
688
+ branchName: canonicalBranch,
689
+ });
690
+ }
691
+ }
692
+
693
+ // Pre-check: auto-commit ag-eco state files, stash any remaining dirty tracked files.
694
+ // When dirty tracked files exist, git merge refuses to start (exit 1, no conflict markers),
695
+ // causing all tiers to cascade with empty conflict lists and a misleading final error.
696
+ const dirtyFiles = await checkDirtyWorkingTree(repoRoot);
697
+ if (dirtyFiles.length > 0) {
698
+ const stateFiles = dirtyFiles.filter(isOsEcoStateFile);
699
+
700
+ // Auto-commit ag-eco runtime state files so they don't block merges
701
+ if (stateFiles.length > 0) {
702
+ await autoCommitStateFiles(repoRoot, stateFiles);
703
+ }
704
+ }
705
+
706
+ // Re-check after auto-commit: any remaining dirty tracked files get stashed
707
+ // so clean-merge-eligible branches can proceed without manual intervention.
708
+ let didStash = false;
709
+ const remainingDirty = await checkDirtyWorkingTree(repoRoot);
710
+ if (remainingDirty.length > 0) {
711
+ const { exitCode: stashCode } = await runGit(repoRoot, [
712
+ "stash",
713
+ "push",
714
+ "-m",
715
+ "ap-merge: auto-stash dirty files",
716
+ ]);
717
+ if (stashCode !== 0) {
718
+ throw new MergeError(
719
+ `Working tree has uncommitted changes to tracked files: ${remainingDirty.join(", ")}. Commit or stash changes before running ap merge.`,
720
+ { branchName: entry.branchName },
721
+ );
722
+ }
723
+ didStash = true;
724
+ }
725
+
726
+ const warnings: string[] = [];
727
+ let lastTier: ResolutionTier = "clean-merge";
728
+ let conflictFiles: string[] = [];
729
+
730
+ try {
731
+ // Delete untracked files overlapping entry.filesModified before merging.
732
+ // git merge refuses to run if untracked files in the working tree would
733
+ // be overwritten by the incoming branch. Deleting them lets the merge
734
+ // proceed and bring in the branch version.
735
+ const { stdout: untrackedOut } = await runGit(repoRoot, [
736
+ "ls-files",
737
+ "--others",
738
+ "--exclude-standard",
739
+ ]);
740
+ const untrackedFiles = untrackedOut
741
+ .trim()
742
+ .split("\n")
743
+ .filter((f) => f.length > 0);
744
+ const entryFileSet = new Set(entry.filesModified);
745
+ const overlappingUntracked = untrackedFiles.filter((f) => entryFileSet.has(f));
746
+ for (const file of overlappingUntracked) {
747
+ const filePath = `${repoRoot}/${file}`;
748
+ try {
749
+ if (await Bun.file(filePath).exists()) {
750
+ unlinkSync(filePath);
751
+ }
752
+ warnings.push(
753
+ `untracked file deleted before merge: ${file} (branch version will be used)`,
754
+ );
755
+ } catch {
756
+ // Ignore errors removing untracked files
757
+ }
758
+ }
759
+
760
+ // Tier 1: Clean merge
761
+ const cleanResult = await tryCleanMerge(entry, repoRoot);
762
+ if (cleanResult.success) {
763
+ if (options.onMergeSuccess) {
764
+ try {
765
+ await options.onMergeSuccess({
766
+ ...entry,
767
+ status: "merged",
768
+ resolvedTier: "clean-merge",
769
+ });
770
+ } catch {
771
+ // callback failures must not fail the merge
772
+ }
773
+ }
774
+ return {
775
+ entry: { ...entry, status: "merged", resolvedTier: "clean-merge" },
776
+ success: true,
777
+ tier: "clean-merge",
778
+ conflictFiles: [],
779
+ errorMessage: null,
780
+ warnings,
781
+ };
782
+ }
783
+ conflictFiles = cleanResult.conflictFiles;
784
+
785
+ // Query conflict history (if loamClient available)
786
+ let history: ConflictHistory = {
787
+ skipTiers: [],
788
+ pastResolutions: [],
789
+ predictedConflictFiles: [],
790
+ };
791
+ if (options.loamClient) {
792
+ history = await queryConflictHistory(options.loamClient, entry);
793
+ }
794
+
795
+ // Tier 2: Auto-resolve (keep incoming)
796
+ if (!history.skipTiers.includes("auto-resolve")) {
797
+ lastTier = "auto-resolve";
798
+ const autoResult = await tryAutoResolve(conflictFiles, repoRoot);
799
+ if (autoResult.contentDropWarnings.length > 0) {
800
+ warnings.push(...autoResult.contentDropWarnings);
801
+ }
802
+ if (autoResult.success) {
803
+ if (options.loamClient) {
804
+ recordConflictPattern(options.loamClient, entry, "auto-resolve", conflictFiles, true);
805
+ }
806
+ if (options.onMergeSuccess) {
807
+ try {
808
+ await options.onMergeSuccess({
809
+ ...entry,
810
+ status: "merged",
811
+ resolvedTier: "auto-resolve",
812
+ });
813
+ } catch {
814
+ // callback failures must not fail the merge
815
+ }
816
+ }
817
+ return {
818
+ entry: { ...entry, status: "merged", resolvedTier: "auto-resolve" },
819
+ success: true,
820
+ tier: "auto-resolve",
821
+ conflictFiles,
822
+ errorMessage: null,
823
+ warnings,
824
+ };
825
+ }
826
+ conflictFiles = autoResult.remainingConflicts;
827
+ } // If skipped, fall through to next tier
828
+
829
+ // Tier 3: AI-resolve
830
+ if (options.aiResolveEnabled && !history.skipTiers.includes("ai-resolve")) {
831
+ lastTier = "ai-resolve";
832
+ const aiResult = await tryAiResolve(
833
+ conflictFiles,
834
+ repoRoot,
835
+ history.pastResolutions,
836
+ options.config,
837
+ );
838
+ if (aiResult.success) {
839
+ if (options.loamClient) {
840
+ recordConflictPattern(options.loamClient, entry, "ai-resolve", conflictFiles, true);
841
+ }
842
+ if (options.onMergeSuccess) {
843
+ try {
844
+ await options.onMergeSuccess({
845
+ ...entry,
846
+ status: "merged",
847
+ resolvedTier: "ai-resolve",
848
+ });
849
+ } catch {
850
+ // callback failures must not fail the merge
851
+ }
852
+ }
853
+ return {
854
+ entry: { ...entry, status: "merged", resolvedTier: "ai-resolve" },
855
+ success: true,
856
+ tier: "ai-resolve",
857
+ conflictFiles,
858
+ errorMessage: null,
859
+ warnings,
860
+ };
861
+ }
862
+ conflictFiles = aiResult.remainingConflicts;
863
+ }
864
+
865
+ // Tier 4: Re-imagine
866
+ if (options.reimagineEnabled && !history.skipTiers.includes("reimagine")) {
867
+ lastTier = "reimagine";
868
+ const reimagineResult = await tryReimagine(
869
+ entry,
870
+ canonicalBranch,
871
+ repoRoot,
872
+ options.config,
873
+ );
874
+ if (reimagineResult.success) {
875
+ if (options.loamClient) {
876
+ recordConflictPattern(options.loamClient, entry, "reimagine", conflictFiles, true);
877
+ }
878
+ if (options.onMergeSuccess) {
879
+ try {
880
+ await options.onMergeSuccess({
881
+ ...entry,
882
+ status: "merged",
883
+ resolvedTier: "reimagine",
884
+ });
885
+ } catch {
886
+ // callback failures must not fail the merge
887
+ }
888
+ }
889
+ return {
890
+ entry: { ...entry, status: "merged", resolvedTier: "reimagine" },
891
+ success: true,
892
+ tier: "reimagine",
893
+ conflictFiles: [],
894
+ errorMessage: null,
895
+ warnings,
896
+ };
897
+ }
898
+ }
899
+
900
+ // All enabled tiers failed — abort any in-progress merge
901
+ try {
902
+ await runGit(repoRoot, ["merge", "--abort"]);
903
+ } catch {
904
+ // merge --abort may fail if there's no merge in progress (e.g., after reimagine)
905
+ }
906
+
907
+ if (options.loamClient) {
908
+ recordConflictPattern(options.loamClient, entry, lastTier, conflictFiles, false);
909
+ }
910
+
911
+ return {
912
+ entry: { ...entry, status: "failed", resolvedTier: null },
913
+ success: false,
914
+ tier: lastTier,
915
+ conflictFiles,
916
+ errorMessage: `All enabled resolution tiers failed (last attempted: ${lastTier})`,
917
+ warnings,
918
+ };
919
+ } finally {
920
+ if (didStash) {
921
+ await runGit(repoRoot, ["stash", "pop"]);
922
+ }
923
+ }
924
+ },
925
+ };
926
+ }