@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,688 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import chalk from "chalk";
3
+ import type { Command } from "commander";
4
+ import { getRegistry } from "../registry/type-registry.ts";
5
+ import type { ExpertiseRecord } from "../schemas/record.ts";
6
+ import { resolveActiveWork } from "../utils/active-work.ts";
7
+ import type { DomainRecords } from "../utils/budget.ts";
8
+ import {
9
+ applyBudget,
10
+ DEFAULT_BUDGET,
11
+ estimateTokens,
12
+ formatBudgetSummary,
13
+ } from "../utils/budget.ts";
14
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
15
+ import { getFileModTime, readExpertiseFile } from "../utils/expertise.ts";
16
+ import type { JsonDomain, ManifestDomain, PrimeFormat } from "../utils/format.ts";
17
+ import {
18
+ buildManifestPayload,
19
+ computeTypeCounts,
20
+ formatDomainExpertise,
21
+ formatDomainExpertiseCompact,
22
+ formatDomainExpertisePlain,
23
+ formatDomainExpertiseXml,
24
+ formatJsonOutput,
25
+ formatPrimeManifest,
26
+ formatPrimeOutput,
27
+ formatPrimeOutputCompact,
28
+ formatPrimeOutputPlain,
29
+ formatPrimeOutputXml,
30
+ formatProjectContract,
31
+ getSessionEndReminder,
32
+ shouldAutoFlipToManifest,
33
+ } from "../utils/format.ts";
34
+ import {
35
+ type ActiveContext,
36
+ activeContextHasSignal,
37
+ filterByActiveContext,
38
+ filterByContext,
39
+ getActiveFiles,
40
+ getChangedFiles,
41
+ isGitRepo,
42
+ } from "../utils/git.ts";
43
+ import { runHooks } from "../utils/hooks.ts";
44
+ import { outputJsonError } from "../utils/json-output.ts";
45
+ import { brand, isQuiet } from "../utils/palette.ts";
46
+ import {
47
+ buildSurfaceAnnotations,
48
+ resolveTierWeights,
49
+ sortByTrust,
50
+ } from "../utils/prime-ranking.ts";
51
+
52
+ interface PrimeOptions {
53
+ full?: boolean;
54
+ compact?: boolean;
55
+ manifest?: boolean;
56
+ export?: string;
57
+ domain?: string[];
58
+ excludeDomain?: string[];
59
+ context?: boolean;
60
+ files?: string[];
61
+ all?: boolean;
62
+ budget?: string;
63
+ // Commander parses `--no-limit` as `limit=false` (defaults true), not as
64
+ // `noLimit=true`. Field name follows Commander's parsed-attribute convention.
65
+ limit?: boolean;
66
+ dryRun?: boolean;
67
+ }
68
+
69
+ interface DryRunRecordSummary {
70
+ id: string;
71
+ type: string;
72
+ domain: string;
73
+ tokens: number;
74
+ }
75
+
76
+ interface DryRunPayload {
77
+ wouldPrime: DryRunRecordSummary[];
78
+ totalTokens: number;
79
+ budgetUsed: number | null;
80
+ budgetTotal: number | null;
81
+ }
82
+
83
+ function resolvePrimeFormat(
84
+ options: PrimeOptions,
85
+ globalFormat: PrimeFormat | undefined,
86
+ ): PrimeFormat {
87
+ if (globalFormat) return globalFormat;
88
+ if (options.full) return "markdown";
89
+ if (options.compact) return "compact";
90
+ return "compact";
91
+ }
92
+
93
+ /**
94
+ * Produce a rough text representation of a record for token estimation.
95
+ * Delegates to the type registry so custom types and unknown-but-tolerated
96
+ * types (via --allow-unknown-types) get a non-undefined estimate.
97
+ */
98
+ export function estimateRecordText(record: ExpertiseRecord): string {
99
+ const def = getRegistry().get(record.type);
100
+ if (!def) return `[${record.type}]`;
101
+ return def.formatCompactLine(record);
102
+ }
103
+
104
+ // Strict numeric flag parsing — see mx-5b9578 / src/commands/rank.ts.
105
+ // `Number.parseInt("10abc", 10)` silently returns 10; use regex + Number() so
106
+ // typos like `--budget 5000abc` or `--budget 3.7` are rejected.
107
+ const POSITIVE_INT_RE = /^\d+$/;
108
+
109
+ function parseStrictPositiveInt(raw: string): number | null {
110
+ if (!POSITIVE_INT_RE.test(raw)) return null;
111
+ const n = Number(raw);
112
+ return Number.isFinite(n) && n >= 1 ? n : null;
113
+ }
114
+
115
+ export function registerPrimeCommand(program: Command): void {
116
+ program
117
+ .command("prime")
118
+ .description("Generate a priming prompt from expertise records")
119
+ .argument("[domains...]", "optional domain(s) to scope output to")
120
+ .option("--compact", "alias for --format compact")
121
+ .option("--full", "alias for --format markdown (full record details)")
122
+ .option("--manifest", "emit a domain index instead of full records (for monolith projects)")
123
+ .option("--domain <domains...>", "domain(s) to include")
124
+ .option("--exclude-domain <domains...>", "domain(s) to exclude")
125
+ .option("--context", "filter records to only those relevant to changed files")
126
+ .option("--files <paths...>", "filter records to only those relevant to specified files")
127
+ .option(
128
+ "--all",
129
+ "opt out of auto-context-scope and emit the full corpus (full mode only; manifest mode is unaffected)",
130
+ )
131
+ .option("--export <path>", "export output to a file")
132
+ .option("--budget <tokens>", `token budget for output (default: ${DEFAULT_BUDGET})`)
133
+ .option("--no-limit", "disable token budget limit")
134
+ .option(
135
+ "--dry-run",
136
+ "emit JSON summary of records that would be primed (id, type, domain, tokens) without rendering content; respects --budget and skips pre-prime hooks",
137
+ )
138
+ .action(async (domainsArg: string[], options: PrimeOptions) => {
139
+ const globalOpts = program.opts();
140
+ const jsonMode = globalOpts.json === true;
141
+ const verbose = globalOpts.verbose === true;
142
+ try {
143
+ const config = await readConfig();
144
+ const format = resolvePrimeFormat(options, globalOpts.format as PrimeFormat | undefined);
145
+
146
+ if (options.manifest && options.full) {
147
+ const msg = "Cannot combine --manifest with --full.";
148
+ if (jsonMode) {
149
+ outputJsonError("prime", msg);
150
+ } else {
151
+ console.error(chalk.red(`Error: ${msg}`));
152
+ }
153
+ process.exitCode = 1;
154
+ return;
155
+ }
156
+
157
+ if (options.dryRun && options.manifest) {
158
+ const msg =
159
+ "Cannot combine --dry-run with --manifest. Manifest mode lists domains, not records; --dry-run previews which records would be primed.";
160
+ if (jsonMode) {
161
+ outputJsonError("prime", msg);
162
+ } else {
163
+ console.error(chalk.red(`Error: ${msg}`));
164
+ }
165
+ process.exitCode = 1;
166
+ return;
167
+ }
168
+
169
+ const requested = [...domainsArg, ...(options.domain ?? [])];
170
+ const unique = [...new Set(requested)];
171
+
172
+ const isScoped =
173
+ unique.length > 0 ||
174
+ (options.excludeDomain ?? []).length > 0 ||
175
+ options.context === true ||
176
+ (options.files !== undefined && options.files.length > 0);
177
+
178
+ if (options.manifest && isScoped) {
179
+ const msg =
180
+ "--manifest cannot be combined with scoping arguments. Manifest mode lists available domains; use `lm prime <domain>` or `lm prime --files <path>` to load records.";
181
+ if (jsonMode) {
182
+ outputJsonError("prime", msg);
183
+ } else {
184
+ console.error(chalk.red(`Error: ${msg}`));
185
+ }
186
+ process.exitCode = 1;
187
+ return;
188
+ }
189
+
190
+ // Mode resolution: explicit flags / config win. When neither is set,
191
+ // auto-flip to manifest above the size threshold (slice 1 of the
192
+ // v0.10 prime overhaul — the prior `consider --manifest` warning is
193
+ // gone because the default *is* the right thing). --dry-run targets
194
+ // record-level preview, so it opts out of auto-flip.
195
+ const configMode = config.prime?.default_mode;
196
+ const explicitMode: "manifest" | "full" | undefined = options.manifest
197
+ ? "manifest"
198
+ : options.full
199
+ ? "full"
200
+ : configMode;
201
+
202
+ for (const d of unique) {
203
+ if (!(d in config.domains)) {
204
+ if (jsonMode) {
205
+ outputJsonError(
206
+ "prime",
207
+ `Domain "${d}" not found in config. Available domains: ${Object.keys(config.domains).join(", ")}`,
208
+ );
209
+ } else {
210
+ console.error(
211
+ `Error: Domain "${d}" not found in config. Available domains: ${Object.keys(config.domains).join(", ")}`,
212
+ );
213
+ console.error(
214
+ `Hint: Run \`lm add ${d}\` to create this domain, or check .loam/loam.config.yaml`,
215
+ );
216
+ }
217
+ process.exitCode = 1;
218
+ return;
219
+ }
220
+ }
221
+
222
+ const excluded = options.excludeDomain ?? [];
223
+ for (const d of excluded) {
224
+ if (!(d in config.domains)) {
225
+ if (jsonMode) {
226
+ outputJsonError(
227
+ "prime",
228
+ `Excluded domain "${d}" not found in config. Available domains: ${Object.keys(config.domains).join(", ")}`,
229
+ );
230
+ } else {
231
+ console.error(
232
+ `Error: Excluded domain "${d}" not found in config. Available domains: ${Object.keys(config.domains).join(", ")}`,
233
+ );
234
+ console.error(
235
+ `Hint: Run \`lm add ${d}\` to create this domain, or check .loam/loam.config.yaml`,
236
+ );
237
+ }
238
+ process.exitCode = 1;
239
+ return;
240
+ }
241
+ }
242
+
243
+ let targetDomains = unique.length > 0 ? unique : Object.keys(config.domains);
244
+
245
+ targetDomains = targetDomains.filter((d) => !excluded.includes(d));
246
+
247
+ // Resolve changed files for --context or --files filtering
248
+ let filesToFilter: string[] | undefined;
249
+ if (options.context) {
250
+ const cwd = process.cwd();
251
+ if (!isGitRepo(cwd)) {
252
+ const msg = "Not in a git repository. --context requires git.";
253
+ if (jsonMode) {
254
+ outputJsonError("prime", msg);
255
+ } else {
256
+ console.error(chalk.red(`Error: ${msg}`));
257
+ }
258
+ process.exitCode = 1;
259
+ return;
260
+ }
261
+ filesToFilter = getChangedFiles(cwd, "HEAD~1");
262
+ if (filesToFilter.length === 0) {
263
+ if (jsonMode) {
264
+ outputJsonError("prime", "No changed files found. Nothing to filter by.");
265
+ } else {
266
+ console.log("No changed files found. Nothing to filter by.");
267
+ }
268
+ return;
269
+ }
270
+ } else if (options.files && options.files.length > 0) {
271
+ filesToFilter = options.files;
272
+ }
273
+
274
+ // Determine budget settings
275
+ const budgetEnabled = !jsonMode && options.limit !== false;
276
+ let budget: number;
277
+ if (options.budget) {
278
+ const parsed = parseStrictPositiveInt(options.budget);
279
+ if (parsed === null) {
280
+ const msg = `--budget must be a positive integer (got "${options.budget}").`;
281
+ if (jsonMode) {
282
+ outputJsonError("prime", msg);
283
+ } else {
284
+ console.error(chalk.red(`Error: ${msg}`));
285
+ }
286
+ process.exitCode = 1;
287
+ return;
288
+ }
289
+ budget = parsed;
290
+ } else {
291
+ budget = DEFAULT_BUDGET;
292
+ }
293
+
294
+ // Load records once, unfiltered. Both branches (manifest and full)
295
+ // need either counts or the records themselves; one read keeps the
296
+ // auto-flip decision and the format pipeline aligned on the same
297
+ // dataset. Auto-flip threshold uses unfiltered counts so a scoped
298
+ // session in a 200-record corpus still flips to manifest by default.
299
+ interface LoadedDomain {
300
+ domain: string;
301
+ records: ExpertiseRecord[];
302
+ lastUpdated: Date | null;
303
+ }
304
+ const loaded: LoadedDomain[] = [];
305
+ for (const domain of targetDomains) {
306
+ const filePath = getExpertisePath(domain);
307
+ const records = await readExpertiseFile(filePath);
308
+ const lastUpdated = await getFileModTime(filePath);
309
+ loaded.push({ domain, records, lastUpdated });
310
+ }
311
+
312
+ // Decide effective mode. Explicit flag / config always wins. With
313
+ // no explicit signal and no scoping, auto-flip to manifest above
314
+ // the size threshold (>100 records or >5 domains). --dry-run opts
315
+ // out — it previews records, which manifest mode wouldn't show.
316
+ let useManifest: boolean;
317
+ if (isScoped) {
318
+ useManifest = false;
319
+ } else if (explicitMode === "manifest") {
320
+ useManifest = true;
321
+ } else if (explicitMode === "full") {
322
+ useManifest = false;
323
+ } else if (options.dryRun) {
324
+ useManifest = false;
325
+ } else {
326
+ const totalRecords = loaded.reduce((s, l) => s + l.records.length, 0);
327
+ useManifest = shouldAutoFlipToManifest(totalRecords, loaded.length);
328
+ }
329
+
330
+ // Slice-2 auto-context-scope: in full mode, narrow records to the
331
+ // agent's current working set (git status + active-work resolver)
332
+ // unless the user opts out with `--all`. Explicit scoping (--files,
333
+ // --context, positional domain) takes precedence — those code paths
334
+ // already filter via `filesToFilter` below. JSON mode skips
335
+ // auto-scope so machine consumers see a deterministic corpus.
336
+ let scopedTotal = 0;
337
+ let scopedKept = 0;
338
+ let scopedFromGit = false;
339
+ let activeContext: ActiveContext | null = null;
340
+ if (
341
+ !useManifest &&
342
+ !isScoped &&
343
+ !jsonMode &&
344
+ options.all !== true &&
345
+ filesToFilter === undefined
346
+ ) {
347
+ const cwd = process.cwd();
348
+ if (isGitRepo(cwd)) {
349
+ const changedFiles = getActiveFiles(cwd);
350
+ const active = resolveActiveWork({ cwd });
351
+ const trackers = {
352
+ sprout: active.sprout,
353
+ gh: active.gh,
354
+ linear: active.linear,
355
+ bead: active.bead,
356
+ };
357
+ const ctx: ActiveContext = { changedFiles, trackers };
358
+ if (activeContextHasSignal(ctx)) {
359
+ activeContext = ctx;
360
+ scopedFromGit = true;
361
+ }
362
+ }
363
+ }
364
+
365
+ if (filesToFilter !== undefined) {
366
+ for (let i = 0; i < loaded.length; i++) {
367
+ const entry = loaded[i];
368
+ if (!entry) continue;
369
+ const filtered = filterByContext(entry.records, filesToFilter);
370
+ loaded[i] = { ...entry, records: filtered };
371
+ }
372
+ if (!jsonMode) {
373
+ for (let i = loaded.length - 1; i >= 0; i--) {
374
+ const entry = loaded[i];
375
+ if (entry && entry.records.length === 0) loaded.splice(i, 1);
376
+ }
377
+ }
378
+ } else if (activeContext && !useManifest) {
379
+ for (let i = 0; i < loaded.length; i++) {
380
+ const entry = loaded[i];
381
+ if (!entry) continue;
382
+ scopedTotal += entry.records.length;
383
+ const filtered = filterByActiveContext(entry.records, activeContext);
384
+ scopedKept += filtered.length;
385
+ loaded[i] = { ...entry, records: filtered };
386
+ }
387
+ if (scopedFromGit) {
388
+ const sources: string[] = [];
389
+ if (activeContext.changedFiles.length > 0) sources.push("git status");
390
+ const trackerBits: string[] = [];
391
+ for (const t of ["sprout", "gh", "linear", "bead"] as const) {
392
+ const v = activeContext.trackers[t];
393
+ if (v) trackerBits.push(`${t}:${v}`);
394
+ }
395
+ if (trackerBits.length > 0) sources.push(`active-work (${trackerBits.join(", ")})`);
396
+ const source = sources.length > 0 ? sources.join(" + ") : "git status";
397
+ console.error(
398
+ `prime: scoped to ${scopedKept} of ${scopedTotal} records based on ${source}; run with --all for the full corpus`,
399
+ );
400
+ }
401
+ }
402
+
403
+ // Contract block (write-side gates) leads non-JSON output in both
404
+ // manifest and full modes. Skipped for JSON (consumers parse config
405
+ // separately) and dry-run (output is a JSON record summary).
406
+ const contractBlock =
407
+ jsonMode || options.dryRun ? null : formatProjectContract(config, format);
408
+
409
+ let output: string;
410
+
411
+ if (useManifest) {
412
+ const manifestDomains: ManifestDomain[] = loaded.map((l) => ({
413
+ domain: l.domain,
414
+ count: l.records.length,
415
+ lastUpdated: l.lastUpdated,
416
+ typeCounts: computeTypeCounts(l.records),
417
+ }));
418
+
419
+ if (jsonMode) {
420
+ output = JSON.stringify(
421
+ buildManifestPayload(manifestDomains, config.governance),
422
+ null,
423
+ 2,
424
+ );
425
+ } else {
426
+ output = formatPrimeManifest(manifestDomains, config.governance, format);
427
+ // Plain format is the spawn-injection contract — warren / other
428
+ // embedders handle session framing in their own dispatch, so the
429
+ // reminder would be redundant noise inside a system prompt.
430
+ if (format !== "plain") {
431
+ const reminder = getSessionEndReminder(format, config.prime?.session_close);
432
+ if (reminder.length > 0) output += `\n\n${reminder}`;
433
+ }
434
+ }
435
+ } else {
436
+ // Trust-tier ranking (slice 3 of the v0.10 prime overhaul): sort
437
+ // each domain's records by (★ count * star_weight) + classification
438
+ // weight before budget so the most trustworthy records survive
439
+ // truncation. Manifest mode skips this — manifest emits domain
440
+ // counts, not records, so order is irrelevant.
441
+ const tierWeights = resolveTierWeights(config.prime?.tier_weights);
442
+ for (let i = 0; i < loaded.length; i++) {
443
+ const entry = loaded[i];
444
+ if (!entry) continue;
445
+ loaded[i] = { ...entry, records: sortByTrust(entry.records, tierWeights) };
446
+ }
447
+
448
+ // Build the "why surfaced now" context for downstream formatters.
449
+ // Priority: explicit --files/--context (treat the requested paths as
450
+ // the agent's working set) → auto-context-scope signal → null. The
451
+ // resulting context drives both the file-match and tracker-match
452
+ // branches inside `whySurfaced`; when no signal exists, suffixes
453
+ // fall back to stars / recency / "universal".
454
+ const annotationContext: ActiveContext | null =
455
+ filesToFilter !== undefined
456
+ ? { changedFiles: filesToFilter, trackers: {} }
457
+ : activeContext;
458
+
459
+ // --dry-run short-circuits: skip pre-prime hooks (they may have side
460
+ // effects like Slack posts) and emit a JSON summary of which records
461
+ // would be primed under the same budget rules as a real run. Format
462
+ // is irrelevant when dry-running — output is always JSON.
463
+ if (options.dryRun) {
464
+ const dryRunBudget = budgetEnabled ? budget : null;
465
+ const allDomainRecords: DomainRecords[] = loaded.map(({ domain, records }) => ({
466
+ domain,
467
+ records,
468
+ }));
469
+ const keptByDomain = budgetEnabled
470
+ ? applyBudget(allDomainRecords, budget, (record) => estimateRecordText(record)).kept
471
+ : allDomainRecords;
472
+
473
+ const wouldPrime: DryRunRecordSummary[] = [];
474
+ let totalTokens = 0;
475
+ for (const { domain, records } of keptByDomain) {
476
+ for (const record of records) {
477
+ const tokens = estimateTokens(estimateRecordText(record));
478
+ wouldPrime.push({
479
+ id: record.id ?? "",
480
+ type: record.type,
481
+ domain,
482
+ tokens,
483
+ });
484
+ totalTokens += tokens;
485
+ }
486
+ }
487
+ const payload: DryRunPayload = {
488
+ wouldPrime,
489
+ totalTokens,
490
+ budgetUsed: dryRunBudget !== null ? totalTokens / dryRunBudget : null,
491
+ budgetTotal: dryRunBudget,
492
+ };
493
+ output = JSON.stringify(payload, null, 2);
494
+
495
+ if (options.export) {
496
+ await writeFile(options.export, `${output}\n`, "utf-8");
497
+ if (!jsonMode && !isQuiet()) {
498
+ console.log(`${brand("✓")} ${brand(`Exported to ${options.export}`)}`);
499
+ }
500
+ } else {
501
+ console.log(output);
502
+ }
503
+ return;
504
+ }
505
+
506
+ const hookPayload = {
507
+ domains: loaded.map(({ domain, records }) => ({ domain, records })),
508
+ };
509
+ const hookRes = await runHooks<typeof hookPayload>("pre-prime", hookPayload);
510
+ if (hookRes.blocked) {
511
+ const reason = hookRes.blockReason ?? "pre-prime hook blocked output";
512
+ if (jsonMode) {
513
+ outputJsonError("prime", reason);
514
+ } else {
515
+ console.error(chalk.red(`Error: ${reason}`));
516
+ }
517
+ process.exitCode = 1;
518
+ return;
519
+ }
520
+ for (const w of hookRes.warnings) {
521
+ if (!jsonMode) console.error(`Warning: ${w}`);
522
+ }
523
+ // If a hook mutated the payload, replace records on a per-domain
524
+ // basis (matching original order); a hook-emitted domain not in
525
+ // `loaded` is ignored to prevent surfacing data the user didn't ask
526
+ // for in the budget/format paths.
527
+ const mutatedByDomain = new Map<string, ExpertiseRecord[]>();
528
+ if (hookRes.ranAny && hookRes.payload?.domains) {
529
+ for (const d of hookRes.payload.domains) {
530
+ if (d && typeof d.domain === "string" && Array.isArray(d.records)) {
531
+ mutatedByDomain.set(d.domain, d.records);
532
+ }
533
+ }
534
+ }
535
+ const finalLoaded: LoadedDomain[] = loaded.map((l) => {
536
+ const mut = mutatedByDomain.get(l.domain);
537
+ const base = mut ? { ...l, records: mut } : l;
538
+ // Re-sort post-hook so hook-mutated records also follow trust
539
+ // order. The pre-hook sort (on `loaded`) makes the dry-run path
540
+ // match what the real run would emit.
541
+ return { ...base, records: sortByTrust(base.records, tierWeights) };
542
+ });
543
+
544
+ if (jsonMode) {
545
+ const domains: JsonDomain[] = [];
546
+ for (const { domain, records } of finalLoaded) {
547
+ if (!filesToFilter || records.length > 0) {
548
+ domains.push({ domain, entry_count: records.length, records });
549
+ }
550
+ }
551
+ output = formatJsonOutput(domains);
552
+ } else {
553
+ // Reconstruct legacy structures for the existing budget+format pipeline.
554
+ const allDomainRecords: DomainRecords[] = finalLoaded.map(({ domain, records }) => ({
555
+ domain,
556
+ records,
557
+ }));
558
+ const modTimes = new Map<string, Date | null>();
559
+ for (const { domain, lastUpdated } of finalLoaded) {
560
+ modTimes.set(domain, lastUpdated);
561
+ }
562
+
563
+ // Apply budget filtering
564
+ let domainRecordsToFormat: DomainRecords[];
565
+ let droppedCount = 0;
566
+ let droppedDomainCount = 0;
567
+
568
+ if (budgetEnabled) {
569
+ const result = applyBudget(allDomainRecords, budget, (record) =>
570
+ estimateRecordText(record),
571
+ );
572
+ domainRecordsToFormat = result.kept;
573
+ droppedCount = result.droppedCount;
574
+ droppedDomainCount = result.droppedDomainCount;
575
+ } else {
576
+ domainRecordsToFormat = allDomainRecords;
577
+ }
578
+
579
+ // Build per-record "why surfaced" annotations using the same
580
+ // context (changedFiles + trackers) that drove the filter step.
581
+ // Computed after budget truncation so dropped records don't waste
582
+ // annotation work.
583
+ const annotationsByDomain = new Map<string, Map<string, string>>();
584
+ for (const { domain, records } of domainRecordsToFormat) {
585
+ annotationsByDomain.set(domain, buildSurfaceAnnotations(records, annotationContext));
586
+ }
587
+
588
+ // Format domain sections
589
+ const domainSections: string[] = [];
590
+ for (const { domain, records } of domainRecordsToFormat) {
591
+ const lastUpdated = modTimes.get(domain) ?? null;
592
+ const annotations = annotationsByDomain.get(domain);
593
+
594
+ switch (format) {
595
+ case "xml":
596
+ domainSections.push(
597
+ formatDomainExpertiseXml(domain, records, lastUpdated, annotations),
598
+ );
599
+ break;
600
+ case "plain":
601
+ domainSections.push(
602
+ formatDomainExpertisePlain(domain, records, lastUpdated, annotations),
603
+ );
604
+ break;
605
+ case "compact":
606
+ domainSections.push(
607
+ formatDomainExpertiseCompact(domain, records, lastUpdated, annotations),
608
+ );
609
+ break;
610
+ default:
611
+ domainSections.push(
612
+ formatDomainExpertise(
613
+ domain,
614
+ records,
615
+ lastUpdated,
616
+ {
617
+ full: options.full || verbose,
618
+ },
619
+ annotations,
620
+ ),
621
+ );
622
+ break;
623
+ }
624
+ }
625
+
626
+ switch (format) {
627
+ case "xml":
628
+ output = formatPrimeOutputXml(domainSections);
629
+ break;
630
+ case "plain":
631
+ output = formatPrimeOutputPlain(domainSections);
632
+ break;
633
+ case "compact":
634
+ output = formatPrimeOutputCompact(domainSections);
635
+ break;
636
+ default:
637
+ output = formatPrimeOutput(domainSections);
638
+ break;
639
+ }
640
+
641
+ // Append truncation summary before session reminder
642
+ if (droppedCount > 0) {
643
+ output += `\n\n${formatBudgetSummary(droppedCount, droppedDomainCount)}`;
644
+ }
645
+
646
+ // Plain format is the spawn-injection contract — warren / other
647
+ // embedders handle session framing in their own dispatch, so the
648
+ // reminder would be redundant noise inside a system prompt.
649
+ if (format !== "plain") {
650
+ const reminder = getSessionEndReminder(format, config.prime?.session_close);
651
+ if (reminder.length > 0) output += `\n\n${reminder}`;
652
+ }
653
+ }
654
+ }
655
+
656
+ // Lead with the project contract (write-side gates from config) on
657
+ // non-JSON / non-dry-run output. `contractBlock` is null when the
658
+ // project has no gates worth surfacing.
659
+ if (contractBlock) {
660
+ output = `${contractBlock}\n\n${output}`;
661
+ }
662
+
663
+ if (options.export) {
664
+ await writeFile(options.export, `${output}\n`, "utf-8");
665
+ if (!jsonMode && !isQuiet()) {
666
+ console.log(`${brand("✓")} ${brand(`Exported to ${options.export}`)}`);
667
+ }
668
+ } else {
669
+ console.log(output);
670
+ }
671
+ } catch (err) {
672
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
673
+ if (jsonMode) {
674
+ outputJsonError("prime", "No .loam/ directory found. Run `loam init` first.");
675
+ } else {
676
+ console.error(chalk.red("Error: No .loam/ directory found. Run `loam init` first."));
677
+ }
678
+ } else {
679
+ if (jsonMode) {
680
+ outputJsonError("prime", (err as Error).message);
681
+ } else {
682
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
683
+ }
684
+ }
685
+ process.exitCode = 1;
686
+ }
687
+ });
688
+ }