@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,667 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename, join, resolve } from "node:path";
4
+ import {
5
+ type AuditConfig,
6
+ type AuditThresholds,
7
+ type LoamConfig,
8
+ resolveAuditThresholds,
9
+ } from "../schemas/config.ts";
10
+ import type { ExpertiseRecord } from "../schemas/record.ts";
11
+ import { TRACKERS, type TrackerName } from "./active-work.ts";
12
+ import { getExpertisePath } from "./config.ts";
13
+ import { isRecordStale, readExpertiseFile } from "./expertise.ts";
14
+
15
+ // Words that indicate a convention encodes a rule or rationale rather than
16
+ // restating code structure. Mirrors the Python prototype's RULE_SIGNALS so
17
+ // existing audit baselines transfer 1:1.
18
+ //
19
+ // Known limitation (V1_PLAN §7): the regex over-counts conventions about
20
+ // Bun-isms ("avoid `process.exit`") and under-counts "we …" phrasings.
21
+ // Threshold defaults assume this skew.
22
+ export const RULE_SIGNAL_WORDS = [
23
+ "because",
24
+ "must not",
25
+ "do not",
26
+ "don't",
27
+ "avoid",
28
+ "always ",
29
+ "never ",
30
+ "prefer",
31
+ "required for",
32
+ "reason:",
33
+ "rationale",
34
+ "so that",
35
+ "otherwise",
36
+ ] as const;
37
+
38
+ export function hasRuleSignal(text: string | undefined): boolean {
39
+ if (!text) return false;
40
+ const t = text.toLowerCase();
41
+ return RULE_SIGNAL_WORDS.some((k) => t.includes(k));
42
+ }
43
+
44
+ export type Verdict = "PASS" | "WARN" | "FAIL";
45
+
46
+ export interface MetricVerdict {
47
+ verdict: Verdict;
48
+ value: number;
49
+ threshold: number;
50
+ warn_threshold?: number;
51
+ }
52
+
53
+ export interface DomainMix {
54
+ domain: string;
55
+ total: number;
56
+ type_counts: Record<string, number>;
57
+ high_value_pct: number; // (failure + decision) / total * 100
58
+ first_recorded_at: string | null;
59
+ last_recorded_at: string | null;
60
+ }
61
+
62
+ export interface TrackerCitation {
63
+ id: string;
64
+ tracker: TrackerName;
65
+ count: number;
66
+ status: string;
67
+ title: string;
68
+ }
69
+
70
+ export interface AuditReport {
71
+ repo: string;
72
+ total_records: number;
73
+ domains: string[];
74
+ ignored_domains: string[];
75
+ type_mix: Array<{ type: string; count: number; pct: number }>;
76
+ evidence: {
77
+ // Deprecated since v0.10.1 (loam-1334) — populated from `with_tracker.sprout`.
78
+ // Kept readable for one release so old JSON consumers don't break. Removed in v0.11.
79
+ with_sprout: number;
80
+ with_tracker: Record<TrackerName, number>;
81
+ with_any_tracker: number;
82
+ with_commit: number;
83
+ with_relates: number;
84
+ floaters: number;
85
+ };
86
+ convention_quality: {
87
+ total: number;
88
+ with_rule_signal: number;
89
+ likely_code_restatement: number;
90
+ } | null;
91
+ // Deprecated since v0.10.1 (loam-1334) — populated from sprout entries in
92
+ // `tracker_citations` for back-compat. Removed in v0.11. Read `tracker_citations`.
93
+ seed_citations: {
94
+ unique: number;
95
+ status_counts: Record<string, number>;
96
+ missing_in_sprout: number;
97
+ top_cited: Array<{ id: string; count: number; status: string; title: string }>;
98
+ };
99
+ tracker_citations: {
100
+ unique: number;
101
+ status_counts: Record<string, number>;
102
+ missing_in_index: number;
103
+ top_cited: TrackerCitation[];
104
+ };
105
+ by_domain: DomainMix[];
106
+ weak_domains: string[];
107
+ thresholds: Required<AuditThresholds>;
108
+ per_domain_thresholds: Record<string, Required<AuditThresholds>>;
109
+ signals: {
110
+ evidence_coverage: MetricVerdict;
111
+ rule_density: MetricVerdict | null;
112
+ floater_rate: MetricVerdict;
113
+ per_domain_max_records: Array<{ domain: string; count: number; verdict: Verdict }>;
114
+ per_domain_max_stale: Array<{ domain: string; count: number; verdict: Verdict }>;
115
+ };
116
+ failures: string[];
117
+ warnings: string[];
118
+ }
119
+
120
+ export interface AuditOptions {
121
+ cwd?: string;
122
+ domain?: string;
123
+ ignoreDomains?: string[];
124
+ }
125
+
126
+ interface RecordWithDomain {
127
+ record: ExpertiseRecord;
128
+ domain: string;
129
+ }
130
+
131
+ export interface TrackerRef {
132
+ tracker: TrackerName;
133
+ id: string;
134
+ }
135
+
136
+ export function extractTrackerRefs(record: ExpertiseRecord): TrackerRef[] {
137
+ const ev = record.evidence;
138
+ if (!ev) return [];
139
+ const refs: TrackerRef[] = [];
140
+ for (const tracker of TRACKERS) {
141
+ const value = ev[tracker];
142
+ if (typeof value === "string" && value.length > 0) {
143
+ refs.push({ tracker, id: value });
144
+ }
145
+ }
146
+ return refs;
147
+ }
148
+
149
+ function recordHasTracker(record: ExpertiseRecord): boolean {
150
+ const ev = record.evidence;
151
+ if (!ev) return false;
152
+ return Boolean(ev.sprout || ev.gh || ev.linear || ev.bead);
153
+ }
154
+
155
+ function recordIsFloater(record: ExpertiseRecord): boolean {
156
+ const ev = record.evidence;
157
+ if (!ev) {
158
+ return !(record.relates_to && record.relates_to.length > 0);
159
+ }
160
+ if (ev.sprout || ev.gh || ev.linear || ev.bead || ev.commit) return false;
161
+ if (record.relates_to && record.relates_to.length > 0) return false;
162
+ return true;
163
+ }
164
+
165
+ interface TrackerIndexEntry {
166
+ tracker: TrackerName;
167
+ id: string;
168
+ status?: string;
169
+ title?: string;
170
+ }
171
+
172
+ interface SeedRow {
173
+ id: string;
174
+ status?: string;
175
+ title?: string;
176
+ }
177
+
178
+ function trackerKey(tracker: TrackerName, id: string): string {
179
+ return `${tracker}:${id}`;
180
+ }
181
+
182
+ // Render a tracker ref for human-readable output. Sprout keep the bare id
183
+ // (back-compat with the pre-loam-1334 top_cited shape); gh uses the
184
+ // GitHub-native `gh#42` form (incoming `#42` is normalized to `gh#42`);
185
+ // linear and bead use a `<tracker>:<id>` prefix.
186
+ export function formatTrackerId(tracker: TrackerName, id: string): string {
187
+ if (tracker === "sprout") return id;
188
+ if (tracker === "gh") return `gh#${id.replace(/^#/, "")}`;
189
+ return `${tracker}:${id}`;
190
+ }
191
+
192
+ async function loadTrackerIndex(cwd: string): Promise<Map<string, TrackerIndexEntry>> {
193
+ const index = new Map<string, TrackerIndexEntry>();
194
+
195
+ // Sprout: read .sprout/issues.jsonl (only tracker with local resolution today).
196
+ const sproutPath = join(cwd, ".sprout", "issues.jsonl");
197
+ if (existsSync(sproutPath)) {
198
+ const content = await readFile(sproutPath, "utf-8");
199
+ for (const line of content.split("\n")) {
200
+ const trimmed = line.trim();
201
+ if (!trimmed || trimmed.startsWith("#")) continue;
202
+ try {
203
+ const row = JSON.parse(trimmed) as SeedRow;
204
+ if (row && typeof row.id === "string") {
205
+ index.set(trackerKey("sprout", row.id), {
206
+ tracker: "sprout",
207
+ id: row.id,
208
+ status: row.status,
209
+ title: row.title,
210
+ });
211
+ }
212
+ } catch {
213
+ // Skip malformed sprout rows — the audit is read-only and best-effort here.
214
+ }
215
+ }
216
+ }
217
+
218
+ // gh / linear / bead: no local resolution. Citations to these trackers fall
219
+ // through to `unresolved` in status_counts (per loam-1334 acceptance).
220
+ // A future seed can layer in networked status lookups (gh CLI, linear API).
221
+
222
+ return index;
223
+ }
224
+
225
+ function classifyEvidenceCoverage(
226
+ value: number,
227
+ thresholds: Required<AuditThresholds>,
228
+ ): MetricVerdict {
229
+ const verdict: Verdict =
230
+ value >= thresholds.evidence_coverage
231
+ ? "PASS"
232
+ : value >= thresholds.evidence_coverage_warn
233
+ ? "WARN"
234
+ : "FAIL";
235
+ return {
236
+ verdict,
237
+ value,
238
+ threshold: thresholds.evidence_coverage,
239
+ warn_threshold: thresholds.evidence_coverage_warn,
240
+ };
241
+ }
242
+
243
+ function classifyRuleDensity(value: number, thresholds: Required<AuditThresholds>): MetricVerdict {
244
+ const verdict: Verdict =
245
+ value >= thresholds.rule_density_min
246
+ ? "PASS"
247
+ : value >= thresholds.rule_density_warn
248
+ ? "WARN"
249
+ : "FAIL";
250
+ return {
251
+ verdict,
252
+ value,
253
+ threshold: thresholds.rule_density_min,
254
+ warn_threshold: thresholds.rule_density_warn,
255
+ };
256
+ }
257
+
258
+ function classifyFloaterRate(value: number, thresholds: Required<AuditThresholds>): MetricVerdict {
259
+ // Floater rate: PASS when ≤ floater_max. Above floater_max is WARN; above
260
+ // 2× floater_max is FAIL. The 2× FAIL band is asymmetric on purpose — V1_PLAN
261
+ // expects floaters to drop after attribution gates land in v0.11, so the
262
+ // FAIL band should fire only for corpora that are *flagrantly* unattributed.
263
+ const verdict: Verdict =
264
+ value <= thresholds.floater_max
265
+ ? "PASS"
266
+ : value <= thresholds.floater_max * 2
267
+ ? "WARN"
268
+ : "FAIL";
269
+ return {
270
+ verdict,
271
+ value,
272
+ threshold: thresholds.floater_max,
273
+ };
274
+ }
275
+
276
+ async function readDomainRecords(cwd: string, domains: string[]): Promise<RecordWithDomain[]> {
277
+ const out: RecordWithDomain[] = [];
278
+ for (const domain of domains) {
279
+ const filePath = getExpertisePath(domain, cwd);
280
+ if (!existsSync(filePath)) continue;
281
+ const records = await readExpertiseFile(filePath, { allowUnknownTypes: true });
282
+ for (const record of records) {
283
+ out.push({ record, domain });
284
+ }
285
+ }
286
+ return out;
287
+ }
288
+
289
+ export async function computeAudit(
290
+ config: LoamConfig,
291
+ options: AuditOptions = {},
292
+ ): Promise<AuditReport> {
293
+ const cwd = options.cwd ?? process.cwd();
294
+ const auditCfg: AuditConfig | undefined = config.audit;
295
+
296
+ const allDomains = Object.keys(config.domains);
297
+ const cfgIgnored = new Set(auditCfg?.ignore_domains ?? []);
298
+ const cliIgnored = new Set(options.ignoreDomains ?? []);
299
+ const ignored = [...new Set([...cfgIgnored, ...cliIgnored])];
300
+
301
+ let domains: string[];
302
+ if (options.domain) {
303
+ domains = allDomains.filter((d) => d === options.domain);
304
+ } else {
305
+ domains = allDomains.filter((d) => !cfgIgnored.has(d) && !cliIgnored.has(d));
306
+ }
307
+
308
+ const repo = basename(resolve(cwd)) || cwd;
309
+
310
+ const items = await readDomainRecords(cwd, domains);
311
+ const records = items.map((i) => i.record);
312
+ const total = records.length;
313
+
314
+ const thresholds = resolveAuditThresholds(auditCfg, options.domain);
315
+ const perDomainThresholds: Record<string, Required<AuditThresholds>> = {};
316
+ for (const d of domains) {
317
+ perDomainThresholds[d] = resolveAuditThresholds(auditCfg, d);
318
+ }
319
+
320
+ // Type mix
321
+ const typeCounts = new Map<string, number>();
322
+ for (const r of records) {
323
+ typeCounts.set(r.type, (typeCounts.get(r.type) ?? 0) + 1);
324
+ }
325
+ const type_mix = Array.from(typeCounts.entries())
326
+ .sort((a, b) => b[1] - a[1])
327
+ .map(([type, count]) => ({
328
+ type,
329
+ count,
330
+ pct: total > 0 ? Math.floor((100 * count) / total) : 0,
331
+ }));
332
+
333
+ // Evidence coverage (per-tracker breakdown + roll-ups)
334
+ const withTracker: Record<TrackerName, number> = { sprout: 0, gh: 0, linear: 0, bead: 0 };
335
+ let withCommit = 0;
336
+ let withRelates = 0;
337
+ let withAnyTracker = 0;
338
+ let floaters = 0;
339
+ for (const r of records) {
340
+ for (const ref of extractTrackerRefs(r)) {
341
+ withTracker[ref.tracker]++;
342
+ }
343
+ if (r.evidence?.commit) withCommit++;
344
+ if (r.relates_to && r.relates_to.length > 0) withRelates++;
345
+ if (recordHasTracker(r) || r.evidence?.commit) withAnyTracker++;
346
+ if (recordIsFloater(r)) floaters++;
347
+ }
348
+
349
+ // Tracker citations (generalized from seed_citations per loam-1334).
350
+ const trackerIndex = await loadTrackerIndex(cwd);
351
+ const refCounts = new Map<string, { tracker: TrackerName; id: string; count: number }>();
352
+ for (const r of records) {
353
+ for (const ref of extractTrackerRefs(r)) {
354
+ const key = trackerKey(ref.tracker, ref.id);
355
+ const existing = refCounts.get(key);
356
+ if (existing) {
357
+ existing.count++;
358
+ } else {
359
+ refCounts.set(key, { tracker: ref.tracker, id: ref.id, count: 1 });
360
+ }
361
+ }
362
+ }
363
+ const trackerStatusCounts: Record<string, number> = {};
364
+ let trackerMissing = 0;
365
+ for (const [key, entry] of refCounts.entries()) {
366
+ const hit = trackerIndex.get(key);
367
+ if (hit) {
368
+ const status = hit.status ?? "?";
369
+ trackerStatusCounts[status] = (trackerStatusCounts[status] ?? 0) + 1;
370
+ } else if (entry.tracker === "sprout") {
371
+ // Sprout is the one tracker with a local index — a missing entry means
372
+ // the citation points at a sprout id that isn't in .sprout/issues.jsonl.
373
+ trackerMissing++;
374
+ } else {
375
+ // gh/linear/bead have no local index yet — bucket into "unresolved"
376
+ // rather than "missing" so consumers don't confuse the two.
377
+ trackerStatusCounts.unresolved = (trackerStatusCounts.unresolved ?? 0) + 1;
378
+ }
379
+ }
380
+ const trackerTopCited: TrackerCitation[] = Array.from(refCounts.values())
381
+ .sort((a, b) => b.count - a.count)
382
+ .slice(0, 8)
383
+ .map(({ tracker, id, count }) => {
384
+ const hit = trackerIndex.get(trackerKey(tracker, id));
385
+ let status: string;
386
+ if (hit) {
387
+ status = hit.status ?? "?";
388
+ } else if (tracker === "sprout") {
389
+ status = "MISSING";
390
+ } else {
391
+ status = "unresolved";
392
+ }
393
+ return {
394
+ id: formatTrackerId(tracker, id),
395
+ tracker,
396
+ count,
397
+ status,
398
+ title: (hit?.title ?? "").slice(0, 55),
399
+ };
400
+ });
401
+
402
+ // Sprout-only back-compat shape (deprecated, populated from tracker_citations).
403
+ // Filter the tracker refs/index to sprout entries so old JSON consumers reading
404
+ // `report.seed_citations` still see exactly what they saw pre-loam-1334.
405
+ const sproutRefEntries = Array.from(refCounts.values()).filter((e) => e.tracker === "sprout");
406
+ const sproutStatusCounts: Record<string, number> = {};
407
+ let sproutMissing = 0;
408
+ for (const { tracker, id } of sproutRefEntries) {
409
+ const hit = trackerIndex.get(trackerKey(tracker, id));
410
+ if (hit) {
411
+ const status = hit.status ?? "?";
412
+ sproutStatusCounts[status] = (sproutStatusCounts[status] ?? 0) + 1;
413
+ } else {
414
+ sproutMissing++;
415
+ }
416
+ }
417
+ const sproutTopCited = sproutRefEntries
418
+ .sort((a, b) => b.count - a.count)
419
+ .slice(0, 8)
420
+ .map(({ id, count }) => {
421
+ const hit = trackerIndex.get(trackerKey("sprout", id));
422
+ return {
423
+ id,
424
+ count,
425
+ status: hit?.status ?? "MISSING",
426
+ title: (hit?.title ?? "MISSING").slice(0, 55),
427
+ };
428
+ });
429
+
430
+ // Convention quality
431
+ const conventions = records.filter((r) => r.type === "convention");
432
+ let conv_quality: AuditReport["convention_quality"] = null;
433
+ if (conventions.length > 0) {
434
+ const withSignal = conventions.filter((r) =>
435
+ hasRuleSignal((r as { content?: string }).content),
436
+ ).length;
437
+ conv_quality = {
438
+ total: conventions.length,
439
+ with_rule_signal: withSignal,
440
+ likely_code_restatement: conventions.length - withSignal,
441
+ };
442
+ }
443
+
444
+ // Per-domain mix
445
+ const byDomain = new Map<string, RecordWithDomain[]>();
446
+ for (const item of items) {
447
+ const list = byDomain.get(item.domain) ?? [];
448
+ list.push(item);
449
+ byDomain.set(item.domain, list);
450
+ }
451
+ const by_domain: DomainMix[] = [];
452
+ for (const [domain, group] of byDomain.entries()) {
453
+ const counts: Record<string, number> = {};
454
+ let first: string | null = null;
455
+ let last: string | null = null;
456
+ for (const { record } of group) {
457
+ counts[record.type] = (counts[record.type] ?? 0) + 1;
458
+ const ts = record.recorded_at;
459
+ if (ts) {
460
+ if (!first || ts < first) first = ts;
461
+ if (!last || ts > last) last = ts;
462
+ }
463
+ }
464
+ const tot = group.length;
465
+ const hv = (counts.failure ?? 0) + (counts.decision ?? 0);
466
+ by_domain.push({
467
+ domain,
468
+ total: tot,
469
+ type_counts: counts,
470
+ high_value_pct: tot > 0 ? Math.floor((100 * hv) / tot) : 0,
471
+ first_recorded_at: first,
472
+ last_recorded_at: last,
473
+ });
474
+ }
475
+ by_domain.sort((a, b) => b.total - a.total);
476
+
477
+ const weak_domains = by_domain
478
+ .filter((d) => d.total >= 5 && d.high_value_pct < 20)
479
+ .map((d) => d.domain);
480
+
481
+ // Verdicts
482
+ const evidence_coverage_rate = total > 0 ? withAnyTracker / total : 0;
483
+ const floater_rate = total > 0 ? floaters / total : 0;
484
+ const rule_density_rate =
485
+ conv_quality && conv_quality.total > 0 ? conv_quality.with_rule_signal / conv_quality.total : 0;
486
+
487
+ const evidence_coverage = classifyEvidenceCoverage(evidence_coverage_rate, thresholds);
488
+ const floater_rate_verdict = classifyFloaterRate(floater_rate, thresholds);
489
+ const rule_density: MetricVerdict | null = conv_quality
490
+ ? classifyRuleDensity(rule_density_rate, thresholds)
491
+ : null;
492
+
493
+ const now = new Date();
494
+ const shelfLife = config.classification_defaults.shelf_life;
495
+ const per_domain_max_records = by_domain.map((d) => {
496
+ const t = perDomainThresholds[d.domain] ?? thresholds;
497
+ return {
498
+ domain: d.domain,
499
+ count: d.total,
500
+ verdict: (d.total <= t.max_records_per_domain ? "PASS" : "WARN") as Verdict,
501
+ };
502
+ });
503
+ const per_domain_max_stale = by_domain.map((d) => {
504
+ const t = perDomainThresholds[d.domain] ?? thresholds;
505
+ const group = byDomain.get(d.domain) ?? [];
506
+ const stale = group.filter((i) => isRecordStale(i.record, now, shelfLife)).length;
507
+ return {
508
+ domain: d.domain,
509
+ count: stale,
510
+ verdict: (stale <= t.max_stale ? "PASS" : "WARN") as Verdict,
511
+ };
512
+ });
513
+
514
+ const failures: string[] = [];
515
+ const warnings: string[] = [];
516
+ const trackVerdict = (label: string, v: MetricVerdict | null) => {
517
+ if (!v) return;
518
+ if (v.verdict === "FAIL") failures.push(label);
519
+ else if (v.verdict === "WARN") warnings.push(label);
520
+ };
521
+ trackVerdict("evidence_coverage", evidence_coverage);
522
+ trackVerdict("rule_density", rule_density);
523
+ trackVerdict("floater_rate", floater_rate_verdict);
524
+ for (const row of per_domain_max_records) {
525
+ if (row.verdict === "WARN") warnings.push(`max_records_per_domain[${row.domain}]`);
526
+ }
527
+ for (const row of per_domain_max_stale) {
528
+ if (row.verdict === "WARN") warnings.push(`max_stale[${row.domain}]`);
529
+ }
530
+
531
+ return {
532
+ repo,
533
+ total_records: total,
534
+ domains,
535
+ ignored_domains: ignored,
536
+ type_mix,
537
+ evidence: {
538
+ with_sprout: withTracker.sprout,
539
+ with_tracker: withTracker,
540
+ with_any_tracker: withAnyTracker,
541
+ with_commit: withCommit,
542
+ with_relates: withRelates,
543
+ floaters,
544
+ },
545
+ convention_quality: conv_quality,
546
+ seed_citations: {
547
+ unique: sproutRefEntries.length,
548
+ status_counts: sproutStatusCounts,
549
+ missing_in_sprout: sproutMissing,
550
+ top_cited: sproutTopCited,
551
+ },
552
+ tracker_citations: {
553
+ unique: refCounts.size,
554
+ status_counts: trackerStatusCounts,
555
+ missing_in_index: trackerMissing,
556
+ top_cited: trackerTopCited,
557
+ },
558
+ by_domain,
559
+ weak_domains,
560
+ thresholds,
561
+ per_domain_thresholds: perDomainThresholds,
562
+ signals: {
563
+ evidence_coverage,
564
+ rule_density,
565
+ floater_rate: floater_rate_verdict,
566
+ per_domain_max_records,
567
+ per_domain_max_stale,
568
+ },
569
+ failures,
570
+ warnings,
571
+ };
572
+ }
573
+
574
+ // ---- Suggest helpers ----
575
+
576
+ export interface SuggestionGroup {
577
+ action: "archive" | "revise" | "attribute";
578
+ headline: string;
579
+ rationale: string;
580
+ record_ids: string[];
581
+ commands: string[];
582
+ }
583
+
584
+ export interface SuggestPayload {
585
+ groups: SuggestionGroup[];
586
+ }
587
+
588
+ export async function buildSuggestions(
589
+ config: LoamConfig,
590
+ options: AuditOptions = {},
591
+ ): Promise<SuggestPayload> {
592
+ const cwd = options.cwd ?? process.cwd();
593
+ const auditCfg = config.audit;
594
+ const cfgIgnored = new Set(auditCfg?.ignore_domains ?? []);
595
+ const cliIgnored = new Set(options.ignoreDomains ?? []);
596
+ let domains = Object.keys(config.domains);
597
+ if (options.domain) {
598
+ domains = domains.filter((d) => d === options.domain);
599
+ } else {
600
+ domains = domains.filter((d) => !cfgIgnored.has(d) && !cliIgnored.has(d));
601
+ }
602
+
603
+ const items = await readDomainRecords(cwd, domains);
604
+ const now = new Date();
605
+ const shelfLife = config.classification_defaults.shelf_life;
606
+
607
+ const archive: string[] = [];
608
+ const revise: string[] = [];
609
+ const attribute: string[] = [];
610
+
611
+ for (const { record } of items) {
612
+ const id = record.id ?? "";
613
+ if (!id) continue;
614
+
615
+ if (record.classification !== "foundational" && isRecordStale(record, now, shelfLife)) {
616
+ archive.push(id);
617
+ }
618
+ if (record.type === "convention") {
619
+ const content = (record as { content?: string }).content;
620
+ if (!hasRuleSignal(content)) revise.push(id);
621
+ }
622
+ if (recordIsFloater(record)) {
623
+ attribute.push(id);
624
+ }
625
+ }
626
+
627
+ const groups: SuggestionGroup[] = [];
628
+ if (archive.length > 0) {
629
+ groups.push({
630
+ action: "archive",
631
+ headline: `${archive.length} stale records past shelf life`,
632
+ rationale:
633
+ "These records are past their classification shelf life and are not foundational. Soft-archive them with `lm prune` (or `lm archive` once it lands in v0.10).",
634
+ record_ids: archive,
635
+ commands: [
636
+ `lm prune --ids ${archive.join(",")} --dry-run`,
637
+ `lm prune --ids ${archive.join(",")}`,
638
+ ],
639
+ });
640
+ }
641
+ if (revise.length > 0) {
642
+ groups.push({
643
+ action: "revise",
644
+ headline: `${revise.length} conventions lack rule-signal language`,
645
+ rationale:
646
+ "Conventions without rule-signal words (because, must not, avoid, always, never, …) often restate code rather than encode a rule. Revise the content or downgrade to a note.",
647
+ record_ids: revise,
648
+ commands: revise.slice(0, 10).map((id) => `lm edit ${id} --content "..."`),
649
+ });
650
+ }
651
+ if (attribute.length > 0) {
652
+ groups.push({
653
+ action: "attribute",
654
+ headline: `${attribute.length} records have no evidence or relates_to link`,
655
+ rationale:
656
+ "Floater records lack any tracker (sprout/gh/linear/bead), commit, or relates_to link. Attribute them so future audits and pruning can follow the trail.",
657
+ record_ids: attribute,
658
+ commands: attribute
659
+ .slice(0, 10)
660
+ .map(
661
+ (id) =>
662
+ `lm edit ${id} --evidence-sprout <id> # or --evidence-commit <sha> / --relates-to <mx-id>`,
663
+ ),
664
+ });
665
+ }
666
+ return { groups };
667
+ }