@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,614 @@
1
+ import chalk from "chalk";
2
+ import type { Command } from "commander";
3
+ import {
4
+ DEFAULT_ANCHOR_VALIDITY_GRACE_DAYS,
5
+ DEFAULT_ANCHOR_VALIDITY_THRESHOLD,
6
+ validateAnchorValidityConfig,
7
+ } from "../schemas/config.ts";
8
+ import type { Classification, ExpertiseRecord } from "../schemas/record.ts";
9
+ import {
10
+ type AnchorValidity,
11
+ computeAnchorValidity,
12
+ passedAnchorGrace,
13
+ } from "../utils/anchor-validity.ts";
14
+ import { archiveRecords } from "../utils/archive.ts";
15
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
16
+ import { readExpertiseFile, writeExpertiseFile } from "../utils/expertise.ts";
17
+ import { runHooks } from "../utils/hooks.ts";
18
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
19
+ import { withFileLock } from "../utils/lock.ts";
20
+ import { brand, isQuiet } from "../utils/palette.ts";
21
+
22
+ interface PruneResult {
23
+ domain: string;
24
+ before: number;
25
+ pruned: number;
26
+ demoted: number;
27
+ anchor_demoted: number;
28
+ supersession_demoted: number;
29
+ after: number;
30
+ }
31
+
32
+ type ActionReason = "stale" | "superseded" | "anchor_decay";
33
+
34
+ interface RecordAction {
35
+ domain: string;
36
+ id?: string;
37
+ type: string;
38
+ from: Classification;
39
+ // "archived" when the record bottomed out and moved to the archive (or was
40
+ // hard-deleted with --hard).
41
+ to: Classification | "archived";
42
+ reasons: ActionReason[];
43
+ anchors?: {
44
+ valid_fraction: number;
45
+ valid: number;
46
+ total: number;
47
+ broken: { kind: string; path: string }[];
48
+ };
49
+ }
50
+
51
+ export function isStale(
52
+ record: ExpertiseRecord,
53
+ now: Date,
54
+ shelfLife: { tactical: number; observational: number },
55
+ ): boolean {
56
+ const classification: Classification = record.classification;
57
+
58
+ if (classification === "foundational") {
59
+ return false;
60
+ }
61
+
62
+ const recordedAt = new Date(record.recorded_at);
63
+ const ageInDays = Math.floor((now.getTime() - recordedAt.getTime()) / (1000 * 60 * 60 * 24));
64
+
65
+ if (classification === "tactical") {
66
+ return ageInDays > shelfLife.tactical;
67
+ }
68
+
69
+ if (classification === "observational") {
70
+ return ageInDays > shelfLife.observational;
71
+ }
72
+
73
+ return false;
74
+ }
75
+
76
+ /**
77
+ * Next classification tier in the supersession-demotion ladder. Returns null
78
+ * when the record has bottomed out and should be archived (or hard-deleted
79
+ * with --hard).
80
+ */
81
+ function nextDemotionTier(c: Classification): Classification | null {
82
+ if (c === "foundational") return "tactical";
83
+ if (c === "tactical") return "observational";
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Identify ids that participate in any supersession cycle (SCC of size > 1).
89
+ * Iterative Tarjan, so long chains and self-loops can't blow the stack.
90
+ * Self-loops are filtered at edge-collection time so they don't register as
91
+ * trivial cycles.
92
+ */
93
+ function findSupersessionCycleIds(graph: ReadonlyMap<string, Set<string>>): Set<string> {
94
+ const cycleIds = new Set<string>();
95
+ const indices = new Map<string, number>();
96
+ const lowlinks = new Map<string, number>();
97
+ const onStack = new Set<string>();
98
+ const stack: string[] = [];
99
+ let nextIndex = 0;
100
+
101
+ type Frame = { node: string; iter: Iterator<string>; pendingChild: string | null };
102
+ for (const start of graph.keys()) {
103
+ if (indices.has(start)) continue;
104
+ const callStack: Frame[] = [];
105
+ const open = (node: string) => {
106
+ indices.set(node, nextIndex);
107
+ lowlinks.set(node, nextIndex);
108
+ nextIndex++;
109
+ stack.push(node);
110
+ onStack.add(node);
111
+ const edges = graph.get(node);
112
+ callStack.push({
113
+ node,
114
+ iter: (edges ?? new Set<string>()).values(),
115
+ pendingChild: null,
116
+ });
117
+ };
118
+ open(start);
119
+
120
+ while (callStack.length > 0) {
121
+ const frame = callStack[callStack.length - 1];
122
+ if (!frame) break;
123
+ if (frame.pendingChild !== null) {
124
+ const childLow = lowlinks.get(frame.pendingChild);
125
+ const nodeLow = lowlinks.get(frame.node);
126
+ if (childLow !== undefined && nodeLow !== undefined && childLow < nodeLow) {
127
+ lowlinks.set(frame.node, childLow);
128
+ }
129
+ frame.pendingChild = null;
130
+ }
131
+ const next = frame.iter.next();
132
+ if (next.done) {
133
+ const idx = indices.get(frame.node);
134
+ const low = lowlinks.get(frame.node);
135
+ if (idx !== undefined && low !== undefined && idx === low) {
136
+ const component: string[] = [];
137
+ while (stack.length > 0) {
138
+ const popped = stack.pop();
139
+ if (popped === undefined) break;
140
+ onStack.delete(popped);
141
+ component.push(popped);
142
+ if (popped === frame.node) break;
143
+ }
144
+ if (component.length > 1) {
145
+ for (const c of component) cycleIds.add(c);
146
+ }
147
+ }
148
+ callStack.pop();
149
+ continue;
150
+ }
151
+ const target = next.value;
152
+ if (!indices.has(target)) {
153
+ frame.pendingChild = target;
154
+ open(target);
155
+ } else if (onStack.has(target)) {
156
+ const targetIdx = indices.get(target);
157
+ const nodeLow = lowlinks.get(frame.node);
158
+ if (targetIdx !== undefined && nodeLow !== undefined && targetIdx < nodeLow) {
159
+ lowlinks.set(frame.node, targetIdx);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ return cycleIds;
165
+ }
166
+
167
+ /**
168
+ * Set of record IDs referenced by any live record's `supersedes` field,
169
+ * unioned across every domain. Cross-domain by design — supersession is
170
+ * content-relational, not domain-bound. Self-references are filtered out,
171
+ * and any record that participates in a multi-record cycle (e.g. A↔B) is
172
+ * excluded so cycle members aren't both demoted/archived together.
173
+ */
174
+ function collectSupersededIds(liveByDomain: ReadonlyArray<{ records: ExpertiseRecord[] }>): {
175
+ supersededIds: Set<string>;
176
+ cycleIds: Set<string>;
177
+ } {
178
+ const graph = new Map<string, Set<string>>();
179
+ const allEdges: Array<[string, string]> = [];
180
+ for (const { records } of liveByDomain) {
181
+ for (const r of records) {
182
+ if (!r.id || !r.supersedes || r.supersedes.length === 0) continue;
183
+ let edges = graph.get(r.id);
184
+ if (!edges) {
185
+ edges = new Set<string>();
186
+ graph.set(r.id, edges);
187
+ }
188
+ for (const targetId of r.supersedes) {
189
+ if (targetId === r.id) continue;
190
+ edges.add(targetId);
191
+ if (!graph.has(targetId)) graph.set(targetId, new Set<string>());
192
+ allEdges.push([r.id, targetId]);
193
+ }
194
+ }
195
+ }
196
+
197
+ const cycleIds = findSupersessionCycleIds(graph);
198
+ const supersededIds = new Set<string>();
199
+ for (const [, target] of allEdges) {
200
+ if (cycleIds.has(target)) continue;
201
+ supersededIds.add(target);
202
+ }
203
+ return { supersededIds, cycleIds };
204
+ }
205
+
206
+ export function registerPruneCommand(program: Command): void {
207
+ program
208
+ .command("prune")
209
+ .description(
210
+ "Soft-archive (default) or hard-delete stale records, plus tier-demote superseded ones",
211
+ )
212
+ .option("--dry-run", "Show what would be pruned without removing", false)
213
+ .option(
214
+ "--hard",
215
+ "Permanently delete stale records instead of moving them to .loam/archive/",
216
+ false,
217
+ )
218
+ .option(
219
+ "--aggressive",
220
+ "Collapse superseded records straight to archived in one pass instead of one tier at a time",
221
+ false,
222
+ )
223
+ .option(
224
+ "--check-anchors",
225
+ "Demote records whose file/dir anchors no longer resolve (R-05f)",
226
+ false,
227
+ )
228
+ .option(
229
+ "--explain",
230
+ "Print per-record reasons for each demotion (anchor list + decision)",
231
+ false,
232
+ )
233
+ .action(
234
+ async (options: {
235
+ dryRun: boolean;
236
+ hard: boolean;
237
+ aggressive: boolean;
238
+ checkAnchors: boolean;
239
+ explain: boolean;
240
+ }) => {
241
+ const jsonMode = program.opts().json === true;
242
+ const config = await readConfig();
243
+ const now = new Date();
244
+ const shelfLife = config.classification_defaults.shelf_life;
245
+ const projectRoot = process.cwd();
246
+ const anchorCfg = config.decay?.anchor_validity ?? {};
247
+ const anchorValidationErrors = validateAnchorValidityConfig(anchorCfg);
248
+ if (anchorValidationErrors.length > 0) {
249
+ const msg = `Invalid decay.anchor_validity config: ${anchorValidationErrors.join("; ")}. Edit .loam/loam.config.yaml.`;
250
+ if (jsonMode) {
251
+ outputJsonError("prune", msg);
252
+ } else {
253
+ console.error(chalk.red(`Error: ${msg}`));
254
+ }
255
+ process.exitCode = 1;
256
+ return;
257
+ }
258
+ const anchorThreshold = anchorCfg.threshold ?? DEFAULT_ANCHOR_VALIDITY_THRESHOLD;
259
+ const anchorGrace = anchorCfg.grace_days ?? DEFAULT_ANCHOR_VALIDITY_GRACE_DAYS;
260
+
261
+ const results: PruneResult[] = [];
262
+ const actions: RecordAction[] = [];
263
+ let totalPruned = 0;
264
+ let totalDemoted = 0;
265
+ let totalAnchorDemoted = 0;
266
+ let totalSupersessionDemoted = 0;
267
+
268
+ // Phase 1 — preview: load every live record across all domains so
269
+ // we can detect staleness candidates AND build the cross-domain
270
+ // supersession set in one pass. No locks here; phase 3 re-reads
271
+ // each candidate domain under its lock so concurrent writers don't
272
+ // lose data.
273
+ const liveByDomain: Array<{ domain: string; records: ExpertiseRecord[] }> = [];
274
+ for (const domain of Object.keys(config.domains)) {
275
+ const filePath = getExpertisePath(domain);
276
+ const records = await readExpertiseFile(filePath);
277
+ liveByDomain.push({ domain, records });
278
+ }
279
+
280
+ const { supersededIds, cycleIds } = collectSupersededIds(liveByDomain);
281
+ if (cycleIds.size > 0 && !jsonMode && !isQuiet()) {
282
+ console.error(
283
+ chalk.yellow(
284
+ `Warning: supersession cycle detected for ${cycleIds.size} record(s); cycle members will not be demoted. Run \`lm doctor\` for details.`,
285
+ ),
286
+ );
287
+ }
288
+
289
+ // Per-record anchor validity, keyed by record id (only when
290
+ // --check-anchors is set). Records with no id are evaluated inline
291
+ // inside phase 3 since they can't be looked up cross-pass.
292
+ const anchorValidityById = new Map<string, AnchorValidity>();
293
+ if (options.checkAnchors) {
294
+ for (const { records } of liveByDomain) {
295
+ for (const r of records) {
296
+ if (!r.id) continue;
297
+ anchorValidityById.set(r.id, computeAnchorValidity(r, projectRoot));
298
+ }
299
+ }
300
+ }
301
+
302
+ const isAnchorDecayed = (r: ExpertiseRecord): AnchorValidity | null => {
303
+ if (!options.checkAnchors) return null;
304
+ if (!passedAnchorGrace(r, now, anchorGrace)) return null;
305
+ const v = r.id ? anchorValidityById.get(r.id) : computeAnchorValidity(r, projectRoot);
306
+ if (!v) return null;
307
+ if (v.validFraction === null) return null; // exempt: zero anchors
308
+ if (v.validFraction >= anchorThreshold) return null;
309
+ return v;
310
+ };
311
+
312
+ const candidatesByDomain: Array<{
313
+ domain: string;
314
+ stale: ExpertiseRecord[];
315
+ demote: ExpertiseRecord[];
316
+ anchor_decay: ExpertiseRecord[];
317
+ }> = [];
318
+ for (const { domain, records } of liveByDomain) {
319
+ const stale = records.filter((r) => isStale(r, now, shelfLife));
320
+ const staleIds = new Set(stale.map((r) => r.id).filter((id): id is string => !!id));
321
+ const demote = records.filter(
322
+ (r) => r.id !== undefined && supersededIds.has(r.id) && !staleIds.has(r.id),
323
+ );
324
+ const anchor_decay = records.filter(
325
+ (r) => isAnchorDecayed(r) !== null && !stale.includes(r),
326
+ );
327
+ if (stale.length > 0 || demote.length > 0 || anchor_decay.length > 0) {
328
+ candidatesByDomain.push({ domain, stale, demote, anchor_decay });
329
+ }
330
+ }
331
+
332
+ // Phase 2 — pre-prune hook. Skipped in dry-run since hooks like
333
+ // digest-then-confirm imply user interaction that shouldn't fire on a
334
+ // preview. Block-on-non-zero, no payload mutation per spec.
335
+ if (!options.dryRun && candidatesByDomain.length > 0) {
336
+ const hookRes = await runHooks("pre-prune", { candidates: candidatesByDomain });
337
+ if (hookRes.blocked) {
338
+ const reason = hookRes.blockReason ?? "pre-prune hook blocked";
339
+ if (jsonMode) {
340
+ outputJsonError("prune", reason);
341
+ } else {
342
+ console.error(chalk.red(`Error: ${reason}`));
343
+ }
344
+ process.exitCode = 1;
345
+ return;
346
+ }
347
+ for (const w of hookRes.warnings) {
348
+ if (!jsonMode) console.error(chalk.yellow(`Warning: ${w}`));
349
+ }
350
+ }
351
+
352
+ // Phase 3 — perform writes. Re-read under the lock to absorb any
353
+ // records added since phase 1. Staleness wins over supersession on a
354
+ // record that hits both: no point demoting something we're already
355
+ // archiving. A record that's both superseded AND anchor-decayed
356
+ // still demotes only one tier per pass; both reasons get stamped
357
+ // onto the kept record.
358
+ const candidateDomains = new Set(candidatesByDomain.map((c) => c.domain));
359
+ for (const domain of Object.keys(config.domains)) {
360
+ if (!candidateDomains.has(domain)) continue;
361
+ const filePath = getExpertisePath(domain);
362
+
363
+ const archived: ExpertiseRecord[] = [];
364
+ const domainActions: RecordAction[] = [];
365
+ const domainResult = await withFileLock(filePath, async () => {
366
+ const records = await readExpertiseFile(filePath);
367
+ if (records.length === 0) return null;
368
+
369
+ const kept: ExpertiseRecord[] = [];
370
+ let pruned = 0;
371
+ let demoted = 0;
372
+ let anchorDemoted = 0;
373
+ let supersessionDemoted = 0;
374
+
375
+ for (const record of records) {
376
+ if (isStale(record, now, shelfLife)) {
377
+ pruned++;
378
+ archived.push(record);
379
+ domainActions.push({
380
+ domain,
381
+ id: record.id,
382
+ type: record.type,
383
+ from: record.classification,
384
+ to: "archived",
385
+ reasons: ["stale"],
386
+ });
387
+ continue;
388
+ }
389
+
390
+ const supersededHit = !!record.id && supersededIds.has(record.id);
391
+ const anchorHit = isAnchorDecayed(record);
392
+
393
+ if (!supersededHit && !anchorHit) {
394
+ kept.push(record);
395
+ continue;
396
+ }
397
+
398
+ const reasons: ActionReason[] = [];
399
+ if (supersededHit) reasons.push("superseded");
400
+ if (anchorHit) reasons.push("anchor_decay");
401
+
402
+ const target = options.aggressive ? null : nextDemotionTier(record.classification);
403
+ if (target === null) {
404
+ pruned++;
405
+ // Bottom-out via supersession/anchor_decay — stamp the
406
+ // archive_reason inline so the multi-record archive write
407
+ // in this domain preserves per-record reasons (vs the
408
+ // caller-wide reason param passed to archiveRecords).
409
+ const bottomReason =
410
+ supersededHit && anchorHit
411
+ ? "superseded+anchor_decay"
412
+ : supersededHit
413
+ ? "superseded"
414
+ : "anchor_decay";
415
+ archived.push({ ...record, archive_reason: bottomReason });
416
+ domainActions.push({
417
+ domain,
418
+ id: record.id,
419
+ type: record.type,
420
+ from: record.classification,
421
+ to: "archived",
422
+ reasons,
423
+ ...(anchorHit
424
+ ? {
425
+ anchors: {
426
+ valid_fraction: anchorHit.validFraction ?? 0,
427
+ valid: anchorHit.valid,
428
+ total: anchorHit.total,
429
+ broken: anchorHit.broken,
430
+ },
431
+ }
432
+ : {}),
433
+ });
434
+ continue;
435
+ }
436
+
437
+ const demotedRecord: ExpertiseRecord = {
438
+ ...record,
439
+ classification: target,
440
+ };
441
+ if (supersededHit) {
442
+ demotedRecord.supersession_demoted_at = now.toISOString();
443
+ supersessionDemoted++;
444
+ }
445
+ if (anchorHit) {
446
+ demotedRecord.anchor_decay_demoted_at = now.toISOString();
447
+ anchorDemoted++;
448
+ }
449
+ kept.push(demotedRecord);
450
+ demoted++;
451
+ domainActions.push({
452
+ domain,
453
+ id: record.id,
454
+ type: record.type,
455
+ from: record.classification,
456
+ to: target,
457
+ reasons,
458
+ ...(anchorHit
459
+ ? {
460
+ anchors: {
461
+ valid_fraction: anchorHit.validFraction ?? 0,
462
+ valid: anchorHit.valid,
463
+ total: anchorHit.total,
464
+ broken: anchorHit.broken,
465
+ },
466
+ }
467
+ : {}),
468
+ });
469
+ }
470
+
471
+ if (pruned > 0 || demoted > 0) {
472
+ if (!options.dryRun) {
473
+ await writeExpertiseFile(filePath, kept);
474
+ }
475
+ return {
476
+ domain,
477
+ before: records.length,
478
+ pruned,
479
+ demoted,
480
+ anchor_demoted: anchorDemoted,
481
+ supersession_demoted: supersessionDemoted,
482
+ after: kept.length,
483
+ };
484
+ }
485
+ return null;
486
+ });
487
+
488
+ if (domainResult) {
489
+ if (!options.dryRun && !options.hard && archived.length > 0) {
490
+ // Default reason "stale" covers records pushed via the
491
+ // shelf-life path; bottom-out records pre-stamp their own
492
+ // reason and archiveRecords preserves it.
493
+ await archiveRecords(domain, archived, now, "stale");
494
+ }
495
+ results.push(domainResult);
496
+ totalPruned += domainResult.pruned;
497
+ totalDemoted += domainResult.demoted;
498
+ totalAnchorDemoted += domainResult.anchor_demoted;
499
+ totalSupersessionDemoted += domainResult.supersession_demoted;
500
+ actions.push(...domainActions);
501
+ }
502
+ }
503
+
504
+ if (jsonMode) {
505
+ // `explanations` in JSON is the legacy shape: demotion-only,
506
+ // gated on --explain. Per-seed acceptance: JSON output is
507
+ // unchanged. Stale-archive entries live in `results` /
508
+ // `totalPruned`; they're never in `explanations`.
509
+ const explanations = options.explain
510
+ ? actions.filter((a) => a.reasons.some((r) => r !== "stale"))
511
+ : undefined;
512
+ outputJson({
513
+ success: true,
514
+ command: "prune",
515
+ dryRun: options.dryRun,
516
+ hard: options.hard,
517
+ aggressive: options.aggressive,
518
+ checkAnchors: options.checkAnchors,
519
+ totalPruned,
520
+ totalDemoted,
521
+ totalAnchorDemoted,
522
+ totalSupersessionDemoted,
523
+ results,
524
+ ...(explanations ? { explanations } : {}),
525
+ });
526
+ return;
527
+ }
528
+
529
+ if (totalPruned === 0 && totalDemoted === 0) {
530
+ if (!isQuiet())
531
+ console.log(brand("No stale or superseded records found. All records are current."));
532
+ return;
533
+ }
534
+
535
+ const action = options.hard ? "Deleted" : "Archived";
536
+ const wouldAction = options.hard ? "Would delete" : "Would archive";
537
+ const label = options.dryRun ? wouldAction : action;
538
+ const demoteLabel = options.dryRun ? "Would demote" : "Demoted";
539
+ const prefix = options.dryRun ? chalk.yellow("[DRY RUN] ") : "";
540
+
541
+ const quiet = isQuiet();
542
+ const actionsByDomain = new Map<string, RecordAction[]>();
543
+ for (const a of actions) {
544
+ const list = actionsByDomain.get(a.domain) ?? [];
545
+ list.push(a);
546
+ actionsByDomain.set(a.domain, list);
547
+ }
548
+
549
+ for (const result of results) {
550
+ let body: string;
551
+ if (result.pruned > 0 && result.demoted > 0) {
552
+ body = `${label} ${chalk.red(String(result.pruned))}, demoted ${chalk.yellow(String(result.demoted))}`;
553
+ } else if (result.pruned > 0) {
554
+ body = `${label} ${chalk.red(String(result.pruned))}`;
555
+ } else {
556
+ body = `${demoteLabel} ${chalk.yellow(String(result.demoted))}`;
557
+ }
558
+ if (!quiet) {
559
+ console.log(
560
+ `${prefix}${chalk.cyan(result.domain)}: ${body} of ${result.before} records (${result.after} remaining)`,
561
+ );
562
+ const domainActions = actionsByDomain.get(result.domain) ?? [];
563
+ for (const a of domainActions) {
564
+ const idPart = a.id ? chalk.dim(a.id) : chalk.dim("(no id)");
565
+ const reasonPart = a.reasons.join(" + ");
566
+ console.log(` ${idPart} [${a.type}]: ${a.from} → ${a.to} (${reasonPart})`);
567
+ if (options.explain && a.anchors) {
568
+ const frac = (a.anchors.valid_fraction * 100).toFixed(0);
569
+ console.log(
570
+ ` anchors: ${a.anchors.valid}/${a.anchors.total} valid (${frac}%)`,
571
+ );
572
+ for (const b of a.anchors.broken) {
573
+ console.log(` ${chalk.red("✗")} ${b.kind}: ${b.path}`);
574
+ }
575
+ }
576
+ }
577
+ }
578
+ }
579
+
580
+ const totals: string[] = [];
581
+ if (totalPruned > 0) {
582
+ totals.push(
583
+ `${label.toLowerCase()} ${totalPruned} stale ${totalPruned === 1 ? "record" : "records"}`,
584
+ );
585
+ }
586
+ if (totalDemoted > 0) {
587
+ const noun = totalDemoted === 1 ? "record" : "records";
588
+ const breakdown: string[] = [];
589
+ if (totalSupersessionDemoted > 0)
590
+ breakdown.push(`${totalSupersessionDemoted} superseded`);
591
+ if (totalAnchorDemoted > 0) breakdown.push(`${totalAnchorDemoted} anchor-decayed`);
592
+ const suffix = breakdown.length > 1 ? ` (${breakdown.join(", ")})` : "";
593
+ const tag =
594
+ breakdown.length === 1 && totalSupersessionDemoted > 0
595
+ ? "superseded "
596
+ : breakdown.length === 1 && totalAnchorDemoted > 0
597
+ ? "anchor-decayed "
598
+ : "";
599
+ totals.push(`${demoteLabel.toLowerCase()} ${totalDemoted} ${tag}${noun}${suffix}`);
600
+ }
601
+ // Totals print even under --quiet; the per-record list above is
602
+ // what --quiet suppresses (seed loam-5ce3).
603
+ const separator = quiet ? "" : "\n";
604
+ console.log(`${separator}${prefix}${chalk.bold(`Total: ${totals.join("; ")}.`)}`);
605
+ if (!quiet && !options.hard && !options.dryRun && totalPruned > 0) {
606
+ console.log(
607
+ chalk.dim(
608
+ "Records moved to .loam/archive/. Restore with `lm restore <id>` or use `--hard` next time to permanently delete.",
609
+ ),
610
+ );
611
+ }
612
+ },
613
+ );
614
+ }