@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,323 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import chalk from "chalk";
5
+ import type { Command } from "commander";
6
+ import { getRegistry } from "../registry/type-registry.ts";
7
+ import type { ExpertiseRecord } from "../schemas/record.ts";
8
+ import { getExpertiseDir, getExpertisePath, readConfig } from "../utils/config.ts";
9
+ import {
10
+ findMissingDomainFields,
11
+ getAllowedTypes,
12
+ getRequiredFields,
13
+ } from "../utils/domain-rules.ts";
14
+ import {
15
+ appendRecord,
16
+ readExpertiseFile,
17
+ resolveRecordId,
18
+ writeExpertiseFile,
19
+ } from "../utils/expertise.ts";
20
+ import { getRecordSummary } from "../utils/format.ts";
21
+ import { runHooks } from "../utils/hooks.ts";
22
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
23
+ import { withFileLock } from "../utils/lock.ts";
24
+ import { accent, brand, isQuiet } from "../utils/palette.ts";
25
+
26
+ interface MoveOptions {
27
+ dryRun: boolean;
28
+ force: boolean;
29
+ }
30
+
31
+ interface ReferenceHit {
32
+ domain: string;
33
+ id: string | null;
34
+ field: "relates_to" | "supersedes";
35
+ }
36
+
37
+ // Scan every other expertise file for references (relates_to / supersedes) to
38
+ // the moved record's ID. We don't rewrite — the ID is preserved across the
39
+ // move, so existing links still resolve. The scan exists purely as an
40
+ // informational warning so users can audit the link graph if they care.
41
+ async function findIncomingReferences(
42
+ movedId: string,
43
+ cwd: string,
44
+ skipFiles: Set<string>,
45
+ ): Promise<ReferenceHit[]> {
46
+ const expertiseDir = getExpertiseDir(cwd);
47
+ if (!existsSync(expertiseDir)) return [];
48
+ const entries = await readdir(expertiseDir).catch(() => [] as string[]);
49
+ const hits: ReferenceHit[] = [];
50
+ for (const entry of entries) {
51
+ if (!entry.endsWith(".jsonl")) continue;
52
+ const filePath = join(expertiseDir, entry);
53
+ if (skipFiles.has(filePath)) continue;
54
+ const domain = entry.slice(0, -".jsonl".length);
55
+ const records = await readExpertiseFile(filePath, { allowUnknownTypes: true }).catch(
56
+ () => [] as ExpertiseRecord[],
57
+ );
58
+ for (const r of records) {
59
+ if (r.relates_to?.includes(movedId)) {
60
+ hits.push({ domain, id: r.id ?? null, field: "relates_to" });
61
+ }
62
+ if (r.supersedes?.includes(movedId)) {
63
+ hits.push({ domain, id: r.id ?? null, field: "supersedes" });
64
+ }
65
+ }
66
+ }
67
+ return hits;
68
+ }
69
+
70
+ export function registerMoveCommand(program: Command): void {
71
+ program
72
+ .command("move")
73
+ .argument("<source-domain>", "current domain")
74
+ .argument("<id>", "record ID (e.g. mx-abc123, abc123, or abc)")
75
+ .argument("<target-domain>", "destination domain")
76
+ .description("Move a record from one domain to another, preserving its ID and metadata")
77
+ .option("--dry-run", "preview the move without making changes", false)
78
+ .option(
79
+ "--force",
80
+ "bypass target domain's allowed_types gate (required_fields still enforced)",
81
+ false,
82
+ )
83
+ .action(
84
+ async (sourceDomain: string, id: string, targetDomain: string, options: MoveOptions) => {
85
+ const jsonMode = program.opts().json === true;
86
+ try {
87
+ const config = await readConfig();
88
+ const availableDomains = Object.keys(config.domains);
89
+
90
+ if (sourceDomain === targetDomain) {
91
+ const msg = "Source and target domain are the same — nothing to move.";
92
+ if (jsonMode) outputJsonError("move", msg);
93
+ else console.error(chalk.red(`Error: ${msg}`));
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+
98
+ for (const d of [sourceDomain, targetDomain]) {
99
+ if (!(d in config.domains)) {
100
+ const msg = `Domain "${d}" not found in config. Available domains: ${availableDomains.join(", ") || "(none)"}`;
101
+ if (jsonMode) outputJsonError("move", msg);
102
+ else console.error(chalk.red(`Error: ${msg}`));
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ }
107
+
108
+ const sourcePath = getExpertisePath(sourceDomain);
109
+ const targetPath = getExpertisePath(targetDomain);
110
+
111
+ // Read source under its lock to resolve the record. We release
112
+ // the lock before validating so we don't hold it across hooks /
113
+ // target lock acquisition — the record is re-fetched under lock
114
+ // at write time below.
115
+ const sourceRecords = await withFileLock(sourcePath, async () =>
116
+ readExpertiseFile(sourcePath),
117
+ );
118
+ const resolved = resolveRecordId(sourceRecords, id);
119
+ if (!resolved.ok) {
120
+ if (jsonMode) outputJsonError("move", resolved.error);
121
+ else console.error(chalk.red(`Error: ${resolved.error}`));
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ const record = resolved.record;
126
+
127
+ // Reject archived records. The .loam/archive/ store is walked by
128
+ // `lm restore` and `lm search --archived`; live expertise files
129
+ // shouldn't carry archived rows, but a stray status field would.
130
+ if (record.status === "archived") {
131
+ const msg = `Record ${record.id ?? id} is archived. Run \`lm restore ${record.id ?? id}\` first.`;
132
+ if (jsonMode) outputJsonError("move", msg);
133
+ else console.error(chalk.red(`Error: ${msg}`));
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+
138
+ // Validate type against target allowed_types
139
+ const allowedTypes = getAllowedTypes(config, targetDomain);
140
+ if (allowedTypes && !allowedTypes.includes(record.type) && !options.force) {
141
+ const msg = `Type "${record.type}" is not in target domain "${targetDomain}" allowed_types (${allowedTypes.join(", ")}). Pass --force to override, or adjust loam.config.yaml.`;
142
+ if (jsonMode) outputJsonError("move", msg);
143
+ else console.error(chalk.red(`Error: ${msg}`));
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+
148
+ // Validate required_fields for target
149
+ const requiredFields = getRequiredFields(config, targetDomain);
150
+ if (requiredFields) {
151
+ const missing = findMissingDomainFields(
152
+ record as unknown as Record<string, unknown>,
153
+ requiredFields,
154
+ );
155
+ if (missing.length > 0) {
156
+ const fieldList = missing.map((f) => `"${f}"`).join(", ");
157
+ const msg = `Record is missing field(s) required by target domain "${targetDomain}": ${fieldList}. Edit the record (\`lm edit ${record.id ?? id}\`) before moving.`;
158
+ if (jsonMode) outputJsonError("move", msg);
159
+ else console.error(chalk.red(`Error: ${msg}`));
160
+ process.exitCode = 1;
161
+ return;
162
+ }
163
+ }
164
+
165
+ // Re-validate against the JSON schema. The record is presumed
166
+ // valid (it was written before), but a registry change since
167
+ // then could have tightened the schema. This is a cheap
168
+ // safeguard against silently writing a stale row into the new
169
+ // domain.
170
+ const validate = getRegistry().validator;
171
+ if (!validate(record)) {
172
+ const errs = (validate.errors ?? [])
173
+ .map((e) => `${e.instancePath} ${e.message}`)
174
+ .join("; ");
175
+ const msg = `Record fails schema validation: ${errs}. Edit the record before moving.`;
176
+ if (jsonMode) outputJsonError("move", msg);
177
+ else console.error(chalk.red(`Error: ${msg}`));
178
+ process.exitCode = 1;
179
+ return;
180
+ }
181
+
182
+ // Informational scan for inbound references. Skips both source
183
+ // and target files (target hasn't been written yet; source
184
+ // references to the record itself are not interesting here).
185
+ const incomingRefs = await findIncomingReferences(
186
+ record.id ?? "",
187
+ process.cwd(),
188
+ new Set([sourcePath, targetPath]),
189
+ );
190
+
191
+ if (options.dryRun) {
192
+ if (jsonMode) {
193
+ outputJson({
194
+ success: true,
195
+ command: "move",
196
+ dryRun: true,
197
+ sourceDomain,
198
+ targetDomain,
199
+ record: {
200
+ id: record.id ?? null,
201
+ type: record.type,
202
+ summary: getRecordSummary(record),
203
+ },
204
+ incomingReferences: incomingRefs,
205
+ });
206
+ } else if (!isQuiet()) {
207
+ const rid = record.id ? ` ${accent(record.id)}` : "";
208
+ console.log(
209
+ `${chalk.yellow("[DRY RUN]")} ${brand(`Would move ${record.type}`)}${rid} ${brand(
210
+ `from ${sourceDomain} → ${targetDomain}`,
211
+ )}: ${getRecordSummary(record)}`,
212
+ );
213
+ if (incomingRefs.length > 0) {
214
+ console.log(
215
+ chalk.dim(
216
+ ` ${incomingRefs.length} inbound reference(s) detected; ID is preserved so links remain valid.`,
217
+ ),
218
+ );
219
+ }
220
+ }
221
+ return;
222
+ }
223
+
224
+ // Fire pre-record against the target domain. A blocked hook
225
+ // aborts the move (record stays in source intact).
226
+ const preResult = await runHooks<{ domain: string; record: ExpertiseRecord }>(
227
+ "pre-record",
228
+ { domain: targetDomain, record },
229
+ );
230
+ if (preResult.blocked) {
231
+ const reason = preResult.blockReason ?? "pre-record hook blocked the move";
232
+ if (jsonMode) outputJsonError("move", reason);
233
+ else console.error(chalk.red(`Error: ${reason}`));
234
+ process.exitCode = 1;
235
+ return;
236
+ }
237
+ let recordToWrite = record;
238
+ if (preResult.ranAny && preResult.payload?.record) {
239
+ const mutated = preResult.payload.record;
240
+ if (mutated !== record) {
241
+ if (!validate(mutated)) {
242
+ const errs = (validate.errors ?? [])
243
+ .map((e) => `${e.instancePath} ${e.message}`)
244
+ .join("; ");
245
+ const msg = `pre-record hook produced an invalid record: ${errs}`;
246
+ if (jsonMode) outputJsonError("move", msg);
247
+ else console.error(chalk.red(`Error: ${msg}`));
248
+ process.exitCode = 1;
249
+ return;
250
+ }
251
+ recordToWrite = mutated;
252
+ }
253
+ }
254
+
255
+ // Append to target, then remove from source. Locks are nested
256
+ // in a fixed order (target before source) to keep concurrent
257
+ // moves deadlock-free. If a crash lands between the append and
258
+ // the source rewrite, the record appears in both domains —
259
+ // recoverable by hand and strictly better than losing it.
260
+ await withFileLock(targetPath, async () => {
261
+ await appendRecord(targetPath, recordToWrite);
262
+ await withFileLock(sourcePath, async () => {
263
+ const currentSource = await readExpertiseFile(sourcePath);
264
+ const filtered = currentSource.filter((r) => r.id !== recordToWrite.id);
265
+ await writeExpertiseFile(sourcePath, filtered);
266
+ });
267
+ });
268
+
269
+ const postResult = await runHooks("post-record", {
270
+ domain: targetDomain,
271
+ record: recordToWrite,
272
+ action: "created",
273
+ });
274
+
275
+ const warnings = [...preResult.warnings, ...postResult.warnings];
276
+
277
+ if (jsonMode) {
278
+ outputJson({
279
+ success: true,
280
+ command: "move",
281
+ sourceDomain,
282
+ targetDomain,
283
+ record: {
284
+ id: recordToWrite.id ?? null,
285
+ type: recordToWrite.type,
286
+ summary: getRecordSummary(recordToWrite),
287
+ },
288
+ incomingReferences: incomingRefs,
289
+ ...(warnings.length > 0 ? { warnings } : {}),
290
+ });
291
+ } else if (!isQuiet()) {
292
+ const rid = recordToWrite.id ? ` ${accent(recordToWrite.id)}` : "";
293
+ console.log(
294
+ `${brand("✓")} ${brand(`Moved ${recordToWrite.type}`)}${rid} ${brand(
295
+ `from ${sourceDomain} → ${targetDomain}`,
296
+ )}: ${getRecordSummary(recordToWrite)}`,
297
+ );
298
+ if (incomingRefs.length > 0) {
299
+ console.log(
300
+ chalk.yellow(
301
+ ` ${incomingRefs.length} inbound reference(s) found; ID preserved so existing links still resolve:`,
302
+ ),
303
+ );
304
+ for (const ref of incomingRefs) {
305
+ console.log(chalk.dim(` ${ref.domain}/${ref.id ?? "(no id)"} via ${ref.field}`));
306
+ }
307
+ }
308
+ for (const w of warnings) console.log(chalk.yellow(` warning: ${w}`));
309
+ }
310
+ } catch (err) {
311
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
312
+ const msg = "No .loam/ directory found. Run `loam init` first.";
313
+ if (jsonMode) outputJsonError("move", msg);
314
+ else console.error(chalk.red(`Error: ${msg}`));
315
+ } else {
316
+ if (jsonMode) outputJsonError("move", (err as Error).message);
317
+ else console.error(chalk.red(`Error: ${(err as Error).message}`));
318
+ }
319
+ process.exitCode = 1;
320
+ }
321
+ },
322
+ );
323
+ }
@@ -0,0 +1,374 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import chalk from "chalk";
4
+ import type { Command } from "commander";
5
+ import type { SessionCloseConfig } from "../schemas/config.ts";
6
+ import { readConfig } from "../utils/config.ts";
7
+ import { getSessionEndReminder } from "../utils/format.ts";
8
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
9
+ import { hasMarkerSection, replaceMarkerSection, wrapInMarkers } from "../utils/markers.ts";
10
+ import { isQuiet } from "../utils/palette.ts";
11
+ import { getCurrentVersion } from "../utils/version.ts";
12
+
13
+ // Single marker carries both display and detection: snippets are considered
14
+ // current iff they include the marker for the running CLI's package version.
15
+ // Patch bumps therefore prompt re-run, which is the desired UX (the visible
16
+ // version in CLAUDE.md should track the installed Loam).
17
+ export function getVersionMarker(): string {
18
+ return `<!-- loam-onboard:v${getCurrentVersion()} -->`;
19
+ }
20
+
21
+ function buildSnippet(sessionClose?: SessionCloseConfig): string {
22
+ const pkgVersion = getCurrentVersion();
23
+ return `## Project Expertise (Loam)
24
+ <!-- loam-onboard:v${pkgVersion} -->
25
+
26
+ This project uses [Loam](https://github.com/hgoudat/loam) v${pkgVersion} for structured expertise management.
27
+
28
+ **At the start of every session**, run:
29
+ \`\`\`bash
30
+ lm prime
31
+ \`\`\`
32
+
33
+ Injects project-specific conventions, patterns, decisions, failures, references, and guides into
34
+ your context. Run \`lm prime --files src/foo.ts\` before editing a file to load only records
35
+ relevant to that path (per-file framing, classification age, and confirmation scores included).
36
+
37
+ For monolith projects where dumping every record wastes context, set
38
+ \`prime.default_mode: manifest\` in \`.loam/loam.config.yaml\` (or pass \`--manifest\`) to emit a
39
+ quick reference + domain index. Agents then scope-load with \`lm prime <domain>\` or
40
+ \`lm prime --files <path>\`.
41
+
42
+ **Before completing your task**, record insights worth preserving — conventions discovered,
43
+ patterns applied, failures encountered, or decisions made:
44
+ \`\`\`bash
45
+ lm record <domain> --type <convention|pattern|failure|decision|reference|guide> --description "..."
46
+ \`\`\`
47
+
48
+ Evidence auto-populates from git (current commit + changed files). Link explicitly with
49
+ \`--evidence-sprout <id>\` / \`--evidence-gh <id>\` / \`--evidence-linear <id>\` / \`--evidence-bead <id>\`,
50
+ \`--evidence-commit <sha>\`, or \`--relates-to <mx-id>\`. Upserts of named records merge outcomes
51
+ instead of replacing them; validation failures print a copy-paste retry hint with missing fields
52
+ pre-filled.
53
+
54
+ Run \`lm status\` for domain health, \`lm doctor\` to check record integrity (add \`--fix\` to strip
55
+ broken file anchors), \`lm --help\` for the full command list. Write commands use file locking and
56
+ atomic writes, so multiple agents can record concurrently. Expertise survives \`git worktree\`
57
+ cleanup — \`.loam/\` resolves to the main repo.
58
+
59
+ \`lm prune\` soft-archives stale records to \`.loam/archive/\` instead of deleting them; pass
60
+ \`--hard\` for true deletion. Restore an archived record with \`lm restore <id>\`. Do not read
61
+ \`.loam/archive/\` directly — those records are stale by definition. If you need historical
62
+ context, run \`lm search --archived <query>\`.
63
+
64
+ ${getSessionEndReminder("embedded", sessionClose)}
65
+ `;
66
+ }
67
+
68
+ const LEGACY_HEADER = "## Project Expertise (Loam)";
69
+ const LEGACY_TAIL = 'loam validate && git add .loam/ && git commit -m "loam: record learnings"';
70
+
71
+ function getSnippet(_provider: string | undefined, sessionClose?: SessionCloseConfig): string {
72
+ // All providers use the same standardized snippet.
73
+ return buildSnippet(sessionClose);
74
+ }
75
+
76
+ // `lm onboard` may run before `lm init`, so reading the config gracefully
77
+ // degrades to "no preset configured" rather than failing the command.
78
+ async function readSessionCloseConfig(cwd: string): Promise<SessionCloseConfig | undefined> {
79
+ try {
80
+ const config = await readConfig(cwd);
81
+ return config.prime?.session_close;
82
+ } catch {
83
+ return undefined;
84
+ }
85
+ }
86
+
87
+ async function fileExists(filePath: string): Promise<boolean> {
88
+ try {
89
+ await access(filePath);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ interface OnboardTarget {
97
+ path: string;
98
+ fileName: string;
99
+ exists: boolean;
100
+ }
101
+
102
+ function hasLegacySnippet(content: string): boolean {
103
+ return content.includes(LEGACY_HEADER);
104
+ }
105
+
106
+ function replaceLegacySnippet(content: string, newSection: string): string {
107
+ const headerIdx = content.indexOf(LEGACY_HEADER);
108
+ if (headerIdx === -1) return content;
109
+
110
+ const tailIdx = content.indexOf(LEGACY_TAIL, headerIdx);
111
+
112
+ let endIdx: number;
113
+ if (tailIdx !== -1) {
114
+ // Find the closing ``` after the tail line
115
+ const afterTail = content.indexOf("```", tailIdx + LEGACY_TAIL.length);
116
+ if (afterTail !== -1) {
117
+ endIdx = afterTail + 3;
118
+ // Consume trailing newlines
119
+ while (endIdx < content.length && content[endIdx] === "\n") {
120
+ endIdx++;
121
+ }
122
+ } else {
123
+ endIdx = content.length;
124
+ }
125
+ } else {
126
+ // Tail not found (user edited the snippet): take from header to EOF
127
+ endIdx = content.length;
128
+ }
129
+
130
+ const before = content.substring(0, headerIdx);
131
+ const after = content.substring(endIdx);
132
+
133
+ return before + newSection + after;
134
+ }
135
+
136
+ function isSnippetCurrent(content: string): boolean {
137
+ if (!hasMarkerSection(content)) return false;
138
+ return content.includes(getVersionMarker());
139
+ }
140
+
141
+ async function findSnippetLocations(cwd: string): Promise<OnboardTarget[]> {
142
+ const candidates = [
143
+ { fileName: "CLAUDE.md", path: join(cwd, "CLAUDE.md") },
144
+ { fileName: ".claude/CLAUDE.md", path: join(cwd, ".claude", "CLAUDE.md") },
145
+ { fileName: "AGENTS.md", path: join(cwd, "AGENTS.md") },
146
+ ];
147
+
148
+ const results: OnboardTarget[] = [];
149
+ for (const c of candidates) {
150
+ const exists = await fileExists(c.path);
151
+ if (exists) {
152
+ const content = await readFile(c.path, "utf-8");
153
+ if (hasMarkerSection(content) || hasLegacySnippet(content)) {
154
+ results.push({ ...c, exists: true });
155
+ }
156
+ }
157
+ }
158
+ return results;
159
+ }
160
+
161
+ async function resolveTargetFile(cwd: string): Promise<{
162
+ target: OnboardTarget;
163
+ duplicates: OnboardTarget[];
164
+ }> {
165
+ const withSnippet = await findSnippetLocations(cwd);
166
+
167
+ // If snippet found in one or more locations, use the first; others are duplicates
168
+ const [firstSnippet, ...restSnippets] = withSnippet;
169
+ if (firstSnippet !== undefined) {
170
+ return {
171
+ target: firstSnippet,
172
+ duplicates: restSnippets,
173
+ };
174
+ }
175
+
176
+ // No snippet found anywhere. Prefer existing CLAUDE.md, else AGENTS.md
177
+ if (await fileExists(join(cwd, "CLAUDE.md"))) {
178
+ return {
179
+ target: {
180
+ fileName: "CLAUDE.md",
181
+ path: join(cwd, "CLAUDE.md"),
182
+ exists: true,
183
+ },
184
+ duplicates: [],
185
+ };
186
+ }
187
+
188
+ // If AGENTS.md already exists (no snippet), append there to respect Codex-style projects.
189
+ // Only create a new file when neither exists — prefer CLAUDE.md over AGENTS.md in that case.
190
+ const agentsExists = await fileExists(join(cwd, "AGENTS.md"));
191
+ if (agentsExists) {
192
+ return {
193
+ target: {
194
+ fileName: "AGENTS.md",
195
+ path: join(cwd, "AGENTS.md"),
196
+ exists: true,
197
+ },
198
+ duplicates: [],
199
+ };
200
+ }
201
+
202
+ return {
203
+ target: {
204
+ fileName: "CLAUDE.md",
205
+ path: join(cwd, "CLAUDE.md"),
206
+ exists: false,
207
+ },
208
+ duplicates: [],
209
+ };
210
+ }
211
+
212
+ type OnboardAction =
213
+ | "created"
214
+ | "appended"
215
+ | "updated"
216
+ | "migrated"
217
+ | "up_to_date"
218
+ | "not_installed"
219
+ | "outdated"
220
+ | "legacy";
221
+
222
+ export async function runOnboard(options: {
223
+ stdout?: boolean;
224
+ provider?: string;
225
+ check?: boolean;
226
+ cwd?: string;
227
+ jsonMode?: boolean;
228
+ }): Promise<void> {
229
+ const cwd = options.cwd ?? process.cwd();
230
+ const sessionClose = await readSessionCloseConfig(cwd);
231
+ const snippet = getSnippet(options.provider, sessionClose);
232
+ const wrappedSnippet = wrapInMarkers(snippet);
233
+
234
+ if (options.stdout) {
235
+ process.stdout.write(wrappedSnippet);
236
+ return;
237
+ }
238
+
239
+ const { target, duplicates } = await resolveTargetFile(cwd);
240
+
241
+ // --check: read-only inspection
242
+ if (options.check) {
243
+ let action: OnboardAction;
244
+
245
+ if (!target.exists) {
246
+ action = "not_installed";
247
+ } else {
248
+ const content = await readFile(target.path, "utf-8");
249
+ if (hasMarkerSection(content)) {
250
+ action = isSnippetCurrent(content) ? "up_to_date" : "outdated";
251
+ } else if (hasLegacySnippet(content)) {
252
+ action = "legacy";
253
+ } else {
254
+ action = "not_installed";
255
+ }
256
+ }
257
+
258
+ if (options.jsonMode) {
259
+ outputJson({
260
+ success: true,
261
+ command: "onboard",
262
+ file: target.fileName,
263
+ action,
264
+ });
265
+ } else {
266
+ const messages: Record<string, string> = {
267
+ not_installed: `Loam snippet is not installed in ${target.fileName}.`,
268
+ up_to_date: `Loam snippet in ${target.fileName} is up to date.`,
269
+ outdated: `Loam snippet in ${target.fileName} is outdated. Run \`lm onboard\` to update.`,
270
+ legacy: `Loam snippet in ${target.fileName} uses legacy format (no markers). Run \`lm onboard\` to migrate.`,
271
+ };
272
+ const colors: Record<string, (s: string) => string> = {
273
+ not_installed: chalk.yellow,
274
+ up_to_date: chalk.green,
275
+ outdated: chalk.yellow,
276
+ legacy: chalk.yellow,
277
+ };
278
+ const colorFn = colors[action] ?? chalk.white;
279
+ const msg = messages[action] ?? action;
280
+ if (!isQuiet()) console.log(colorFn(msg));
281
+ }
282
+
283
+ if (duplicates.length > 0) {
284
+ const names = duplicates.map((d) => d.fileName).join(", ");
285
+ if (!options.jsonMode && !isQuiet()) {
286
+ console.log(chalk.yellow(`Warning: loam snippet also found in: ${names}`));
287
+ }
288
+ }
289
+ return;
290
+ }
291
+
292
+ // Write path
293
+ let action: OnboardAction;
294
+
295
+ if (!target.exists) {
296
+ // Create new file
297
+ await mkdir(dirname(target.path), { recursive: true });
298
+ await writeFile(target.path, `${wrappedSnippet}\n`, "utf-8");
299
+ action = "created";
300
+ } else {
301
+ const content = await readFile(target.path, "utf-8");
302
+
303
+ if (hasMarkerSection(content)) {
304
+ // Check if current
305
+ if (isSnippetCurrent(content)) {
306
+ action = "up_to_date";
307
+ } else {
308
+ // Replace marker section
309
+ const updated = replaceMarkerSection(content, wrappedSnippet);
310
+ if (updated !== null) {
311
+ await writeFile(target.path, updated, "utf-8");
312
+ }
313
+ action = "updated";
314
+ }
315
+ } else if (hasLegacySnippet(content)) {
316
+ // Migrate legacy snippet
317
+ const migrated = replaceLegacySnippet(content, `${wrappedSnippet}\n`);
318
+ await writeFile(target.path, migrated, "utf-8");
319
+ action = "migrated";
320
+ } else {
321
+ // Append to existing file
322
+ await writeFile(target.path, `${content.trimEnd()}\n\n${wrappedSnippet}\n`, "utf-8");
323
+ action = "appended";
324
+ }
325
+ }
326
+
327
+ if (options.jsonMode) {
328
+ outputJson({
329
+ success: true,
330
+ command: "onboard",
331
+ file: target.fileName,
332
+ action,
333
+ });
334
+ } else {
335
+ const messages: Record<string, string> = {
336
+ created: `Loam onboarding snippet written to ${target.fileName}.`,
337
+ appended: `Loam onboarding snippet appended to ${target.fileName}.`,
338
+ updated: `Loam onboarding snippet updated in ${target.fileName}.`,
339
+ migrated: `Loam onboarding snippet migrated to marker format in ${target.fileName}.`,
340
+ up_to_date: `Loam snippet in ${target.fileName} is already up to date. No changes made.`,
341
+ };
342
+ const color = action === "up_to_date" ? chalk.yellow : chalk.green;
343
+ if (!isQuiet()) console.log(color(messages[action]));
344
+ }
345
+
346
+ if (duplicates.length > 0) {
347
+ const names = duplicates.map((d) => d.fileName).join(", ");
348
+ if (!options.jsonMode && !isQuiet()) {
349
+ console.log(chalk.yellow(`Warning: loam snippet also found in: ${names}`));
350
+ }
351
+ }
352
+ }
353
+
354
+ export function registerOnboardCommand(program: Command): void {
355
+ program
356
+ .command("onboard")
357
+ .description("Generate or update a CLAUDE.md/AGENTS.md snippet pointing to lm prime")
358
+ .option("--stdout", "print snippet to stdout instead of writing to file")
359
+ .option("--provider <provider>", "customize snippet for a specific provider (e.g. claude)")
360
+ .option("--check", "check if onboarding snippet is installed and up to date")
361
+ .action(async (options: { stdout?: boolean; provider?: string; check?: boolean }) => {
362
+ const jsonMode = program.opts().json === true;
363
+ try {
364
+ await runOnboard({ ...options, jsonMode });
365
+ } catch (err) {
366
+ if (jsonMode) {
367
+ outputJsonError("onboard", (err as Error).message);
368
+ } else {
369
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
370
+ }
371
+ process.exitCode = 1;
372
+ }
373
+ });
374
+ }