@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,887 @@
1
+ import { existsSync } from "node:fs";
2
+ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join, relative } from "node:path";
5
+ import chalk from "chalk";
6
+ import type { Command } from "commander";
7
+ import { getLoamDir, readConfig } from "../utils/config.ts";
8
+ import { getSessionEndReminder } from "../utils/format.ts";
9
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
10
+ import {
11
+ hasMarkerSection,
12
+ MARKER_END,
13
+ MARKER_START,
14
+ removeMarkerSection,
15
+ } from "../utils/markers.ts";
16
+ import {
17
+ listFilesystemRecipes,
18
+ NPM_RECIPE_PREFIX,
19
+ type ProviderRecipe,
20
+ type RecipeResult,
21
+ type RecipeWithSource,
22
+ resolveRecipe,
23
+ } from "../utils/recipe-discovery.ts";
24
+
25
+ // Read prime.session_close from .loam/loam.config.yaml. Returns undefined
26
+ // when loam isn't initialized yet (setup may run before `lm init`) so the
27
+ // caller falls back to the default preset rather than failing.
28
+ async function readSessionCloseConfig(cwd: string) {
29
+ try {
30
+ const config = await readConfig(cwd);
31
+ return config.prime?.session_close;
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ // ────────────────────────────────────────────────────────────
38
+ // Git hook helpers
39
+ // ────────────────────────────────────────────────────────────
40
+
41
+ const HOOK_MARKER_START = "# loam:start";
42
+ const HOOK_MARKER_END = "# loam:end";
43
+
44
+ const LOAM_HOOK_SECTION = `${HOOK_MARKER_START}
45
+ # Run loam validate before committing
46
+ if command -v loam >/dev/null 2>&1; then
47
+ loam validate
48
+ if [ $? -ne 0 ]; then
49
+ echo "loam validate failed. Commit aborted."
50
+ exit 1
51
+ fi
52
+ fi
53
+ ${HOOK_MARKER_END}`;
54
+
55
+ async function installGitHook(cwd: string): Promise<RecipeResult> {
56
+ const gitDir = join(cwd, ".git");
57
+ if (!existsSync(gitDir)) {
58
+ return {
59
+ success: false,
60
+ message: "Not a git repository — .git directory not found.",
61
+ };
62
+ }
63
+
64
+ const hooksDir = join(gitDir, "hooks");
65
+ await mkdir(hooksDir, { recursive: true });
66
+
67
+ const hookPath = join(hooksDir, "pre-commit");
68
+ let content = "";
69
+
70
+ if (existsSync(hookPath)) {
71
+ content = await readFile(hookPath, "utf-8");
72
+ if (content.includes(HOOK_MARKER_START)) {
73
+ return {
74
+ success: true,
75
+ message: "Git pre-commit hook already installed.",
76
+ };
77
+ }
78
+ }
79
+
80
+ if (content) {
81
+ content = `${content.trimEnd()}\n\n${LOAM_HOOK_SECTION}\n`;
82
+ } else {
83
+ content = `#!/bin/sh\n\n${LOAM_HOOK_SECTION}\n`;
84
+ }
85
+
86
+ await writeFile(hookPath, content, "utf-8");
87
+ await chmod(hookPath, 0o755);
88
+
89
+ return { success: true, message: "Installed loam pre-commit git hook." };
90
+ }
91
+
92
+ async function checkGitHook(cwd: string): Promise<RecipeResult> {
93
+ const hookPath = join(cwd, ".git", "hooks", "pre-commit");
94
+ if (!existsSync(hookPath)) {
95
+ return { success: false, message: "Git pre-commit hook not found." };
96
+ }
97
+
98
+ const content = await readFile(hookPath, "utf-8");
99
+ if (!content.includes(HOOK_MARKER_START)) {
100
+ return {
101
+ success: false,
102
+ message: "Git pre-commit hook exists but has no loam section.",
103
+ };
104
+ }
105
+
106
+ return { success: true, message: "Git pre-commit hook is installed." };
107
+ }
108
+
109
+ async function removeGitHook(cwd: string): Promise<RecipeResult> {
110
+ const hookPath = join(cwd, ".git", "hooks", "pre-commit");
111
+ if (!existsSync(hookPath)) {
112
+ return {
113
+ success: true,
114
+ message: "Git pre-commit hook not found; nothing to remove.",
115
+ };
116
+ }
117
+
118
+ const content = await readFile(hookPath, "utf-8");
119
+ if (!content.includes(HOOK_MARKER_START)) {
120
+ return {
121
+ success: true,
122
+ message: "No loam section in pre-commit hook; nothing to remove.",
123
+ };
124
+ }
125
+
126
+ const startIdx = content.indexOf(HOOK_MARKER_START);
127
+ const endIdx = content.indexOf(HOOK_MARKER_END);
128
+ const before = content.substring(0, startIdx);
129
+ const after = content.substring(endIdx + HOOK_MARKER_END.length);
130
+ const cleaned = (before + after).replace(/\n{3,}/g, "\n\n").trim();
131
+
132
+ // If only the shebang (or nothing) remains, delete the file
133
+ if (!cleaned || cleaned === "#!/bin/sh") {
134
+ await unlink(hookPath);
135
+ return {
136
+ success: true,
137
+ message: "Removed loam pre-commit hook (file deleted).",
138
+ };
139
+ }
140
+
141
+ await writeFile(hookPath, `${cleaned}\n`, "utf-8");
142
+ return {
143
+ success: true,
144
+ message: "Removed loam section from pre-commit hook.",
145
+ };
146
+ }
147
+
148
+ // ────────────────────────────────────────────────────────────
149
+ // Built-in provider recipes
150
+ // ────────────────────────────────────────────────────────────
151
+
152
+ // ── Claude ──────────────────────────────────────────────────
153
+
154
+ interface ClaudeHookEntry {
155
+ type: string;
156
+ command: string;
157
+ }
158
+
159
+ interface ClaudeHookGroup {
160
+ matcher: string;
161
+ hooks: ClaudeHookEntry[];
162
+ }
163
+
164
+ interface ClaudeSettings {
165
+ hooks?: {
166
+ [event: string]: ClaudeHookGroup[];
167
+ };
168
+ [key: string]: unknown;
169
+ }
170
+
171
+ const CLAUDE_HOOK_COMMAND = "lm prime";
172
+
173
+ function claudeSettingsPath(cwd: string): string {
174
+ return join(cwd, ".claude", "settings.json");
175
+ }
176
+
177
+ function hasLoamHook(groups: ClaudeHookGroup[]): boolean {
178
+ return groups.some((g) => g.hooks.some((h) => h.command === CLAUDE_HOOK_COMMAND));
179
+ }
180
+
181
+ function removeLoamHookGroups(groups: ClaudeHookGroup[]): ClaudeHookGroup[] {
182
+ return groups.filter((g) => !g.hooks.some((h) => h.command === CLAUDE_HOOK_COMMAND));
183
+ }
184
+
185
+ function createLoamHookGroup(): ClaudeHookGroup {
186
+ return {
187
+ matcher: "",
188
+ hooks: [{ type: "command", command: CLAUDE_HOOK_COMMAND }],
189
+ };
190
+ }
191
+
192
+ function parseClaudeSettings(raw: string, settingsPath: string): ClaudeSettings {
193
+ try {
194
+ return JSON.parse(raw) as ClaudeSettings;
195
+ } catch (err) {
196
+ const msg = err instanceof Error ? err.message : String(err);
197
+ throw new Error(`Failed to parse Claude settings at ${settingsPath}: ${msg}`);
198
+ }
199
+ }
200
+
201
+ const claudeRecipe: ProviderRecipe = {
202
+ async install(cwd) {
203
+ const settingsPath = claudeSettingsPath(cwd);
204
+ let settings: ClaudeSettings = {};
205
+
206
+ if (existsSync(settingsPath)) {
207
+ const raw = await readFile(settingsPath, "utf-8");
208
+ settings = parseClaudeSettings(raw, settingsPath);
209
+ }
210
+
211
+ if (!settings.hooks) {
212
+ settings.hooks = {};
213
+ }
214
+
215
+ // SessionStart with empty matcher already covers startup, resume, clear,
216
+ // and post-compact reload. PreCompact's stdout never reaches the model
217
+ // after compaction so registering there is dead weight.
218
+ const event = "SessionStart";
219
+ if (!settings.hooks[event]) {
220
+ settings.hooks[event] = [];
221
+ }
222
+ if (hasLoamHook(settings.hooks[event])) {
223
+ return { success: true, message: "Claude hooks already installed." };
224
+ }
225
+ settings.hooks[event].push(createLoamHookGroup());
226
+
227
+ await mkdir(dirname(settingsPath), { recursive: true });
228
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
229
+
230
+ return {
231
+ success: true,
232
+ message: "Installed Claude SessionStart hook.",
233
+ };
234
+ },
235
+
236
+ async check(cwd) {
237
+ const settingsPath = claudeSettingsPath(cwd);
238
+ if (!existsSync(settingsPath)) {
239
+ return { success: false, message: "Claude settings.json not found." };
240
+ }
241
+
242
+ const raw = await readFile(settingsPath, "utf-8");
243
+ const settings = parseClaudeSettings(raw, settingsPath);
244
+
245
+ if (!settings.hooks) {
246
+ return {
247
+ success: false,
248
+ message: "No hooks configured in Claude settings.",
249
+ };
250
+ }
251
+
252
+ const event = "SessionStart";
253
+ if (!settings.hooks[event] || !hasLoamHook(settings.hooks[event])) {
254
+ return {
255
+ success: false,
256
+ message: `Missing hooks for: ${event}.`,
257
+ };
258
+ }
259
+ return {
260
+ success: true,
261
+ message: "Claude hooks are installed and correct.",
262
+ };
263
+ },
264
+
265
+ async remove(cwd) {
266
+ const settingsPath = claudeSettingsPath(cwd);
267
+ if (!existsSync(settingsPath)) {
268
+ return {
269
+ success: true,
270
+ message: "Claude settings.json not found; nothing to remove.",
271
+ };
272
+ }
273
+
274
+ const raw = await readFile(settingsPath, "utf-8");
275
+ const settings = parseClaudeSettings(raw, settingsPath);
276
+
277
+ if (!settings.hooks) {
278
+ return {
279
+ success: true,
280
+ message: "No hooks in Claude settings; nothing to remove.",
281
+ };
282
+ }
283
+
284
+ let removed = false;
285
+ for (const event of Object.keys(settings.hooks)) {
286
+ const hookGroup = settings.hooks[event];
287
+ if (!hookGroup) continue;
288
+ const before = hookGroup.length;
289
+ const updated = removeLoamHookGroups(hookGroup);
290
+ settings.hooks[event] = updated;
291
+ if (updated.length < before) {
292
+ removed = true;
293
+ }
294
+ if (updated.length === 0) {
295
+ delete settings.hooks[event];
296
+ }
297
+ }
298
+
299
+ if (Object.keys(settings.hooks).length === 0) {
300
+ settings.hooks = undefined;
301
+ }
302
+
303
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
304
+
305
+ return {
306
+ success: true,
307
+ message: removed
308
+ ? "Removed loam hooks from Claude settings."
309
+ : "No loam hooks found in Claude settings.",
310
+ };
311
+ },
312
+ };
313
+
314
+ // ── Cursor ──────────────────────────────────────────────────
315
+
316
+ function cursorRulePath(cwd: string): string {
317
+ return join(cwd, ".cursor", "rules", "loam.mdc");
318
+ }
319
+
320
+ // Built from config at install/check time so projects can swap the session-close
321
+ // preset (prime.session_close.style) without forking the recipe.
322
+ async function buildCursorRuleContent(cwd: string): Promise<string> {
323
+ const sessionClose = await readSessionCloseConfig(cwd);
324
+ const footer = getSessionEndReminder("embedded", sessionClose);
325
+ return `---
326
+ description: Loam expertise integration
327
+ globs: *
328
+ alwaysApply: true
329
+ ---
330
+
331
+ # Loam Expertise
332
+
333
+ At the start of every session, run the following command to load project expertise:
334
+
335
+ \`\`\`
336
+ lm prime
337
+ \`\`\`
338
+
339
+ This injects project-specific conventions, patterns, decisions, and other learnings into your context.
340
+ Use \`lm prime --files src/foo.ts\` to load only records relevant to specific files.
341
+
342
+ Evidence auto-populates from git (current commit + changed files). Link trackers explicitly with \`--evidence-sprout <id>\` / \`--evidence-gh <id>\` / \`--evidence-linear <id>\` / \`--evidence-bead <id>\`, or \`--relates-to <mx-id>\`.
343
+
344
+ ${footer}
345
+ `;
346
+ }
347
+
348
+ const cursorRecipe: ProviderRecipe = {
349
+ async install(cwd) {
350
+ const rulePath = cursorRulePath(cwd);
351
+ const content = await buildCursorRuleContent(cwd);
352
+
353
+ if (existsSync(rulePath)) {
354
+ const existing = await readFile(rulePath, "utf-8");
355
+ if (existing === content) {
356
+ return { success: true, message: "Cursor rule already installed." };
357
+ }
358
+ }
359
+
360
+ await mkdir(dirname(rulePath), { recursive: true });
361
+ await writeFile(rulePath, content, "utf-8");
362
+
363
+ return {
364
+ success: true,
365
+ message: "Installed Cursor rule at .cursor/rules/loam.mdc.",
366
+ };
367
+ },
368
+
369
+ async check(cwd) {
370
+ const rulePath = cursorRulePath(cwd);
371
+ if (!existsSync(rulePath)) {
372
+ return { success: false, message: "Cursor rule file not found." };
373
+ }
374
+ const content = await readFile(rulePath, "utf-8");
375
+ const expected = await buildCursorRuleContent(cwd);
376
+ if (content !== expected) {
377
+ return {
378
+ success: false,
379
+ message: "Cursor rule file exists but has been modified.",
380
+ };
381
+ }
382
+ return { success: true, message: "Cursor rule is installed and correct." };
383
+ },
384
+
385
+ async remove(cwd) {
386
+ const rulePath = cursorRulePath(cwd);
387
+ if (!existsSync(rulePath)) {
388
+ return {
389
+ success: true,
390
+ message: "Cursor rule not found; nothing to remove.",
391
+ };
392
+ }
393
+ await unlink(rulePath);
394
+ return { success: true, message: "Removed Cursor rule file." };
395
+ },
396
+ };
397
+
398
+ // ── Codex ───────────────────────────────────────────────────
399
+
400
+ function codexAgentsPath(cwd: string): string {
401
+ return join(cwd, "AGENTS.md");
402
+ }
403
+
404
+ async function buildCodexSection(cwd: string): Promise<string> {
405
+ const sessionClose = await readSessionCloseConfig(cwd);
406
+ const footer = getSessionEndReminder("embedded", sessionClose);
407
+ return `${MARKER_START}
408
+ ## Loam Expertise
409
+
410
+ At the start of every session, run \`lm prime\` to load project expertise.
411
+
412
+ This injects project-specific conventions, patterns, decisions, and other learnings into your context.
413
+ Use \`lm prime --files src/foo.ts\` to load only records relevant to specific files.
414
+
415
+ Evidence auto-populates from git (current commit + changed files). Link trackers explicitly with \`--evidence-sprout <id>\` / \`--evidence-gh <id>\` / \`--evidence-linear <id>\` / \`--evidence-bead <id>\`, or \`--relates-to <mx-id>\`.
416
+
417
+ ${footer}
418
+ ${MARKER_END}`;
419
+ }
420
+
421
+ // Codex hooks via .codex/config.toml — Codex 0.124.0+ (April 2026) supports
422
+ // SessionStart hooks whose stdout JSON `additionalContext` is injected as
423
+ // extra developer context. Schema reference:
424
+ // https://developers.openai.com/codex/hooks
425
+ // We fence the managed block in marker comments so we can re-find it for
426
+ // idempotent install and clean removal without disturbing user-added entries.
427
+
428
+ const CODEX_TOML_MARKER_START = "# loam:start — managed by `lm setup codex`";
429
+ const CODEX_TOML_MARKER_END = "# loam:end";
430
+
431
+ const CODEX_TOML_BLOCK = `${CODEX_TOML_MARKER_START}
432
+ [features]
433
+ codex_hooks = true
434
+
435
+ [[hooks.SessionStart]]
436
+
437
+ [[hooks.SessionStart.hooks]]
438
+ type = "command"
439
+ command = "lm prime"
440
+ statusMessage = "Loading loam expertise"
441
+ ${CODEX_TOML_MARKER_END}`;
442
+
443
+ function codexConfigPath(cwd: string): string {
444
+ return join(cwd, ".codex", "config.toml");
445
+ }
446
+
447
+ function hasCodexTomlBlock(content: string): boolean {
448
+ return content.includes(CODEX_TOML_MARKER_START);
449
+ }
450
+
451
+ function removeCodexTomlBlock(content: string): string {
452
+ const startIdx = content.indexOf(CODEX_TOML_MARKER_START);
453
+ if (startIdx === -1) return content;
454
+ const endMarkerIdx = content.indexOf(CODEX_TOML_MARKER_END, startIdx);
455
+ if (endMarkerIdx === -1) return content;
456
+ const endLineIdx = content.indexOf("\n", endMarkerIdx);
457
+ const cutEnd = endLineIdx === -1 ? content.length : endLineIdx + 1;
458
+
459
+ const before = content.substring(0, startIdx);
460
+ const after = content.substring(cutEnd);
461
+ const cleaned = (before + after).replace(/\n{3,}/g, "\n\n").trimEnd();
462
+ return cleaned ? `${cleaned}\n` : "";
463
+ }
464
+
465
+ const codexRecipe: ProviderRecipe = {
466
+ async install(cwd) {
467
+ const agentsPath = codexAgentsPath(cwd);
468
+ const tomlPath = codexConfigPath(cwd);
469
+
470
+ // 1. AGENTS.md — fallback prose for Codex versions without hook support
471
+ let agentsContent = "";
472
+ let agentsAlready = false;
473
+ if (existsSync(agentsPath)) {
474
+ agentsContent = await readFile(agentsPath, "utf-8");
475
+ agentsAlready = hasMarkerSection(agentsContent);
476
+ }
477
+
478
+ if (!agentsAlready) {
479
+ const codexSection = await buildCodexSection(cwd);
480
+ const newAgents = agentsContent
481
+ ? `${agentsContent.trimEnd()}\n\n${codexSection}\n`
482
+ : `${codexSection}\n`;
483
+ await writeFile(agentsPath, newAgents, "utf-8");
484
+ }
485
+
486
+ // 2. .codex/config.toml — SessionStart hook running `lm prime`
487
+ let tomlContent = "";
488
+ let tomlAlready = false;
489
+ if (existsSync(tomlPath)) {
490
+ tomlContent = await readFile(tomlPath, "utf-8");
491
+ tomlAlready = hasCodexTomlBlock(tomlContent);
492
+ }
493
+
494
+ if (!tomlAlready) {
495
+ await mkdir(dirname(tomlPath), { recursive: true });
496
+ const newToml = tomlContent.trim()
497
+ ? `${tomlContent.trimEnd()}\n\n${CODEX_TOML_BLOCK}\n`
498
+ : `${CODEX_TOML_BLOCK}\n`;
499
+ await writeFile(tomlPath, newToml, "utf-8");
500
+ }
501
+
502
+ if (agentsAlready && tomlAlready) {
503
+ return {
504
+ success: true,
505
+ message: "Codex integration already installed (AGENTS.md + .codex/config.toml).",
506
+ };
507
+ }
508
+
509
+ const installed: string[] = [];
510
+ if (!agentsAlready) installed.push("AGENTS.md loam section");
511
+ if (!tomlAlready) installed.push(".codex/config.toml SessionStart hook");
512
+ return { success: true, message: `Installed Codex integration: ${installed.join(", ")}.` };
513
+ },
514
+
515
+ async check(cwd) {
516
+ const agentsPath = codexAgentsPath(cwd);
517
+ const tomlPath = codexConfigPath(cwd);
518
+
519
+ if (!existsSync(agentsPath)) {
520
+ return { success: false, message: "AGENTS.md not found." };
521
+ }
522
+ const agentsContent = await readFile(agentsPath, "utf-8");
523
+ if (!hasMarkerSection(agentsContent)) {
524
+ return {
525
+ success: false,
526
+ message: "AGENTS.md exists but has no loam section.",
527
+ };
528
+ }
529
+
530
+ if (!existsSync(tomlPath)) {
531
+ return { success: false, message: ".codex/config.toml not found." };
532
+ }
533
+ const tomlContent = await readFile(tomlPath, "utf-8");
534
+ if (!hasCodexTomlBlock(tomlContent)) {
535
+ return {
536
+ success: false,
537
+ message: ".codex/config.toml exists but has no loam SessionStart hook.",
538
+ };
539
+ }
540
+
541
+ return {
542
+ success: true,
543
+ message: "Codex integration installed (AGENTS.md + .codex/config.toml).",
544
+ };
545
+ },
546
+
547
+ async remove(cwd) {
548
+ const agentsPath = codexAgentsPath(cwd);
549
+ const tomlPath = codexConfigPath(cwd);
550
+ const removed: string[] = [];
551
+
552
+ if (existsSync(agentsPath)) {
553
+ const content = await readFile(agentsPath, "utf-8");
554
+ if (hasMarkerSection(content)) {
555
+ const cleaned = removeMarkerSection(content);
556
+ await writeFile(agentsPath, cleaned, "utf-8");
557
+ removed.push("AGENTS.md loam section");
558
+ }
559
+ }
560
+
561
+ if (existsSync(tomlPath)) {
562
+ const content = await readFile(tomlPath, "utf-8");
563
+ if (hasCodexTomlBlock(content)) {
564
+ const cleaned = removeCodexTomlBlock(content);
565
+ if (cleaned) {
566
+ await writeFile(tomlPath, cleaned, "utf-8");
567
+ } else {
568
+ // Block was the whole file — leave an empty file rather than
569
+ // deleting, so user-managed `[features]` etc. coming back
570
+ // later doesn't fight a recreate race. Empty file is harmless.
571
+ await writeFile(tomlPath, "", "utf-8");
572
+ }
573
+ removed.push(".codex/config.toml SessionStart hook");
574
+ }
575
+ }
576
+
577
+ if (removed.length === 0) {
578
+ return {
579
+ success: true,
580
+ message: "No Codex integration found; nothing to remove.",
581
+ };
582
+ }
583
+ return { success: true, message: `Removed Codex integration: ${removed.join(", ")}.` };
584
+ },
585
+ };
586
+
587
+ // ── Recipe registry ─────────────────────────────────────────
588
+
589
+ /**
590
+ * Built-in recipes shipped with loam. Filesystem (`.loam/recipes/<name>.{ts,sh}`)
591
+ * and npm (`loam-recipe-<name>`) discovery is handled by `resolveRecipe` in
592
+ * `utils/recipe-discovery.ts` and takes precedence over these in that order.
593
+ */
594
+ const BUILTIN_RECIPES = {
595
+ claude: claudeRecipe,
596
+ cursor: cursorRecipe,
597
+ codex: codexRecipe,
598
+ } as const satisfies Record<string, ProviderRecipe>;
599
+
600
+ /** @deprecated kept as alias for `BUILTIN_RECIPES` — used by tests. */
601
+ const recipes = BUILTIN_RECIPES;
602
+
603
+ const BUILTIN_PROVIDER_NAMES = Object.keys(BUILTIN_RECIPES).sort();
604
+
605
+ // ── Exported helpers for testing ────────────────────────────
606
+
607
+ export {
608
+ BUILTIN_RECIPES,
609
+ recipes,
610
+ BUILTIN_PROVIDER_NAMES,
611
+ buildCursorRuleContent,
612
+ buildCodexSection,
613
+ CLAUDE_HOOK_COMMAND,
614
+ LOAM_HOOK_SECTION,
615
+ installGitHook,
616
+ checkGitHook,
617
+ removeGitHook,
618
+ };
619
+
620
+ export type { ProviderRecipe };
621
+
622
+ // ── Command registration ────────────────────────────────────
623
+
624
+ export function registerSetupCommand(program: Command): void {
625
+ program
626
+ .command("setup")
627
+ .argument(
628
+ "[provider]",
629
+ `agent provider (built-in: ${BUILTIN_PROVIDER_NAMES.join(", ")}; or any name resolvable from .loam/recipes/ or ${NPM_RECIPE_PREFIX}*)`,
630
+ )
631
+ .description("Set up loam integration for a specific agent provider")
632
+ .option("--check", "verify provider integration is installed")
633
+ .option("--remove", "remove provider integration")
634
+ .option("--hooks", "install a pre-commit git hook running loam validate")
635
+ .option("--list", "list discovered providers (built-in, .loam/recipes/, loam-recipe-* npm)")
636
+ .action(
637
+ async (
638
+ provider: string | undefined,
639
+ options: {
640
+ check?: boolean;
641
+ remove?: boolean;
642
+ hooks?: boolean;
643
+ list?: boolean;
644
+ },
645
+ ) => {
646
+ const jsonMode = program.opts().json === true;
647
+
648
+ // Verify .loam/ exists
649
+ const loamDir = getLoamDir();
650
+ if (!existsSync(loamDir)) {
651
+ if (jsonMode) {
652
+ outputJsonError("setup", "No .loam/ directory found. Run `loam init` first.");
653
+ } else {
654
+ console.error(chalk.red("Error: No .loam/ directory found. Run `loam init` first."));
655
+ }
656
+ process.exitCode = 1;
657
+ return;
658
+ }
659
+
660
+ // Handle --list (no provider needed)
661
+ if (options.list) {
662
+ await runList(process.cwd(), jsonMode);
663
+ return;
664
+ }
665
+
666
+ if (!provider && !options.hooks) {
667
+ if (jsonMode) {
668
+ outputJsonError("setup", "Specify a provider, use --hooks, or use --list.");
669
+ } else {
670
+ console.error(chalk.red("Error: specify a provider, use --hooks, or use --list."));
671
+ }
672
+ process.exitCode = 1;
673
+ return;
674
+ }
675
+
676
+ // Handle --hooks
677
+ if (options.hooks) {
678
+ const cwd = process.cwd();
679
+ let hookResult: RecipeResult;
680
+ const action = options.check ? "check" : options.remove ? "remove" : "install";
681
+ if (options.check) {
682
+ hookResult = await checkGitHook(cwd);
683
+ } else if (options.remove) {
684
+ hookResult = await removeGitHook(cwd);
685
+ } else {
686
+ hookResult = await installGitHook(cwd);
687
+ }
688
+
689
+ if (jsonMode) {
690
+ outputJson({
691
+ success: hookResult.success,
692
+ command: "setup",
693
+ target: "hooks",
694
+ action,
695
+ message: hookResult.message,
696
+ });
697
+ } else if (hookResult.success) {
698
+ console.log(chalk.green(`\u2714 ${hookResult.message}`));
699
+ } else {
700
+ console.error(chalk.red(`\u2716 ${hookResult.message}`));
701
+ }
702
+
703
+ if (!hookResult.success) {
704
+ process.exitCode = 1;
705
+ }
706
+
707
+ // If no provider, stop here
708
+ if (!provider) return;
709
+ }
710
+
711
+ // Handle provider
712
+ if (!provider) return;
713
+
714
+ const cwd = process.cwd();
715
+ let resolved: RecipeWithSource | null;
716
+ try {
717
+ resolved = await resolveRecipe(provider, cwd, BUILTIN_RECIPES);
718
+ } catch (err) {
719
+ const msg = err instanceof Error ? err.message : String(err);
720
+ if (jsonMode) {
721
+ outputJsonError("setup", msg);
722
+ } else {
723
+ console.error(chalk.red(`Error: ${msg}`));
724
+ }
725
+ process.exitCode = 1;
726
+ return;
727
+ }
728
+
729
+ if (!resolved) {
730
+ const hint = `Unknown provider "${provider}". Run \`lm setup --list\` to see discovered providers, or add a recipe at .loam/recipes/${provider}.{ts,sh} or install ${NPM_RECIPE_PREFIX}${provider}.`;
731
+ if (jsonMode) {
732
+ outputJsonError("setup", hint);
733
+ } else {
734
+ console.error(chalk.red(`Error: ${hint}`));
735
+ }
736
+ process.exitCode = 1;
737
+ return;
738
+ }
739
+
740
+ const action = options.check ? "check" : options.remove ? "remove" : "install";
741
+ let result: RecipeResult;
742
+ try {
743
+ if (options.check) {
744
+ result = await resolved.recipe.check(cwd);
745
+ } else if (options.remove) {
746
+ result = await resolved.recipe.remove(cwd);
747
+ } else {
748
+ result = await resolved.recipe.install(cwd);
749
+ }
750
+ } catch (err) {
751
+ // A recipe that throws (instead of returning a RecipeResult) would
752
+ // otherwise surface as a raw Bun stack trace from a top-level
753
+ // awaited action. Convert to the same shape as a returned failure
754
+ // so users see a one-line, formatted error.
755
+ const msg = err instanceof Error ? err.message : String(err);
756
+ const sourceLabel =
757
+ resolved.source === "builtin" ? "built-in" : (resolved.path ?? resolved.source);
758
+ result = {
759
+ success: false,
760
+ message: `recipe "${provider}" ${action} threw (${sourceLabel}): ${msg}`,
761
+ };
762
+ }
763
+
764
+ if (jsonMode) {
765
+ outputJson({
766
+ success: result.success,
767
+ command: "setup",
768
+ provider,
769
+ source: resolved.source,
770
+ ...(resolved.path ? { path: resolved.path } : {}),
771
+ action,
772
+ message: result.message,
773
+ });
774
+ } else if (result.success) {
775
+ console.log(chalk.green(`\u2714 ${result.message}`));
776
+ } else if (options.check) {
777
+ console.log(chalk.yellow(`\u2716 ${result.message}`));
778
+ } else {
779
+ console.error(chalk.red(`Error: ${result.message}`));
780
+ }
781
+
782
+ if (!result.success) {
783
+ process.exitCode = 1;
784
+ }
785
+ },
786
+ );
787
+ }
788
+
789
+ interface ProviderListing {
790
+ name: string;
791
+ source: "builtin" | "filesystem-ts" | "filesystem-sh" | "npm";
792
+ path?: string;
793
+ shadowedBy?: "filesystem-ts" | "filesystem-sh" | "npm";
794
+ }
795
+
796
+ function npmShadowExists(name: string, cwd: string): boolean {
797
+ // resolveRecipe prefers filesystem \u2192 npm \u2192 built-in, so an installed
798
+ // `loam-recipe-<name>` package shadows the built-in (and is itself shadowed
799
+ // by a filesystem recipe of the same name). Probe per-builtin rather than
800
+ // enumerating node_modules \u2014 fast, and avoids rummaging through unrelated
801
+ // packages.
802
+ try {
803
+ const requireFn = createRequire(import.meta.url);
804
+ requireFn.resolve(`${NPM_RECIPE_PREFIX}${name}`, { paths: [cwd] });
805
+ return true;
806
+ } catch {
807
+ return false;
808
+ }
809
+ }
810
+
811
+ async function gatherProviderListings(cwd: string): Promise<ProviderListing[]> {
812
+ const fsRecipes = await listFilesystemRecipes(cwd);
813
+
814
+ const listings: ProviderListing[] = [];
815
+
816
+ for (const name of BUILTIN_PROVIDER_NAMES) {
817
+ // Resolution order is filesystem \u2192 npm \u2192 built-in, so a filesystem
818
+ // shadow wins over npm. Report the actual winner so the marker isn't a
819
+ // lie about what `lm setup <name>` would run.
820
+ const fsShadow = fsRecipes.find((r) => r.name === name);
821
+ const shadowedBy: ProviderListing["shadowedBy"] | undefined = fsShadow
822
+ ? fsShadow.source
823
+ : npmShadowExists(name, cwd)
824
+ ? "npm"
825
+ : undefined;
826
+ listings.push({
827
+ name,
828
+ source: "builtin",
829
+ ...(shadowedBy ? { shadowedBy } : {}),
830
+ });
831
+ }
832
+
833
+ for (const fs of fsRecipes) {
834
+ listings.push({ name: fs.name, source: fs.source, path: fs.path });
835
+ }
836
+
837
+ listings.sort((a, b) => {
838
+ if (a.name !== b.name) return a.name.localeCompare(b.name);
839
+ // Filesystem before builtin so the active recipe sorts first.
840
+ const order = { "filesystem-ts": 0, "filesystem-sh": 1, npm: 2, builtin: 3 } as const;
841
+ return order[a.source] - order[b.source];
842
+ });
843
+
844
+ return listings;
845
+ }
846
+
847
+ async function runList(cwd: string, jsonMode: boolean): Promise<void> {
848
+ const listings = await gatherProviderListings(cwd);
849
+
850
+ if (jsonMode) {
851
+ outputJson({
852
+ success: true,
853
+ command: "setup",
854
+ action: "list",
855
+ providers: listings.map((l) => ({
856
+ name: l.name,
857
+ source: l.source,
858
+ ...(l.path ? { path: relative(cwd, l.path) } : {}),
859
+ ...(l.shadowedBy ? { shadowed_by: l.shadowedBy } : {}),
860
+ })),
861
+ });
862
+ return;
863
+ }
864
+
865
+ console.log(chalk.bold("Available providers:"));
866
+ const labelWidth = Math.max(...listings.map((l) => l.name.length), 6);
867
+ for (const l of listings) {
868
+ const sourceLabel =
869
+ l.source === "builtin"
870
+ ? l.shadowedBy
871
+ ? chalk.dim(`built-in (shadowed by ${l.shadowedBy})`)
872
+ : "built-in"
873
+ : l.source === "filesystem-ts"
874
+ ? `filesystem-ts: ${relative(cwd, l.path ?? "")}`
875
+ : l.source === "filesystem-sh"
876
+ ? `filesystem-sh: ${relative(cwd, l.path ?? "")}`
877
+ : `npm: ${NPM_RECIPE_PREFIX}${l.name}`;
878
+ const marker = l.shadowedBy ? chalk.dim("\u00b7") : chalk.green("\u2713");
879
+ console.log(` ${marker} ${l.name.padEnd(labelWidth)} ${sourceLabel}`);
880
+ }
881
+ console.log("");
882
+ console.log(
883
+ chalk.dim(
884
+ `Resolution order: filesystem (.loam/recipes/<name>.{ts,sh}) \u2192 npm (${NPM_RECIPE_PREFIX}<name>) \u2192 built-in.`,
885
+ ),
886
+ );
887
+ }