@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,103 @@
1
+ import { existsSync } from "node:fs";
2
+ import chalk from "chalk";
3
+ import type { Command } from "commander";
4
+ import { getExpertisePath, getLoamDir, readConfig } from "../utils/config.ts";
5
+ import {
6
+ calculateDomainHealth,
7
+ countRecords,
8
+ getFileModTime,
9
+ readExpertiseFile,
10
+ } from "../utils/expertise.ts";
11
+ import { formatStatusOutput } from "../utils/format.ts";
12
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
13
+
14
+ export function registerStatusCommand(program: Command): void {
15
+ program
16
+ .command("status")
17
+ .description("Show status of expertise records")
18
+ .action(async () => {
19
+ const jsonMode = program.opts().json === true;
20
+ const loamDir = getLoamDir();
21
+
22
+ if (!existsSync(loamDir)) {
23
+ if (jsonMode) {
24
+ outputJsonError("status", "No .loam/ directory found. Run `loam init` first.");
25
+ } else {
26
+ console.error(chalk.red("No .loam/ directory found. Run `loam init` first."));
27
+ }
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+
32
+ const config = await readConfig();
33
+ const observationalShelfLifeDays = config.classification_defaults.shelf_life.observational;
34
+ const now = Date.now();
35
+
36
+ const domainStats = await Promise.all(
37
+ Object.keys(config.domains).map(async (domain) => {
38
+ const filePath = getExpertisePath(domain);
39
+ const records = await readExpertiseFile(filePath);
40
+ const lastUpdated = await getFileModTime(filePath);
41
+ const health = calculateDomainHealth(
42
+ records,
43
+ config.governance.max_entries,
44
+ config.classification_defaults.shelf_life,
45
+ );
46
+ const oldestRecorded = health.oldest_timestamp ? new Date(health.oldest_timestamp) : null;
47
+ const newestRecorded = health.newest_timestamp ? new Date(health.newest_timestamp) : null;
48
+ let rotting = false;
49
+ let rottingDays: number | null = null;
50
+ if (newestRecorded) {
51
+ const ageDays = Math.floor((now - newestRecorded.getTime()) / 86400000);
52
+ if (ageDays > observationalShelfLifeDays) {
53
+ rotting = true;
54
+ rottingDays = ageDays;
55
+ }
56
+ }
57
+ return {
58
+ domain,
59
+ count: countRecords(records),
60
+ lastUpdated,
61
+ oldestRecorded,
62
+ newestRecorded,
63
+ rotting,
64
+ rottingDays,
65
+ health,
66
+ };
67
+ }),
68
+ );
69
+
70
+ if (jsonMode) {
71
+ outputJson({
72
+ success: true,
73
+ command: "status",
74
+ domains: domainStats.map((s) => ({
75
+ domain: s.domain,
76
+ count: s.count,
77
+ lastUpdated: s.lastUpdated?.toISOString() ?? null,
78
+ oldest_recorded: s.oldestRecorded?.toISOString() ?? null,
79
+ newest_recorded: s.newestRecorded?.toISOString() ?? null,
80
+ rotting: s.rotting,
81
+ rotting_days: s.rottingDays,
82
+ health: s.health,
83
+ })),
84
+ governance: config.governance,
85
+ shelf_life: config.classification_defaults.shelf_life,
86
+ });
87
+ } else {
88
+ const output = formatStatusOutput(
89
+ domainStats.map((s) => ({
90
+ domain: s.domain,
91
+ count: s.count,
92
+ lastUpdated: s.lastUpdated,
93
+ oldestRecorded: s.oldestRecorded,
94
+ newestRecorded: s.newestRecorded,
95
+ rotting: s.rotting,
96
+ rottingDays: s.rottingDays,
97
+ })),
98
+ config.governance,
99
+ );
100
+ console.log(output);
101
+ }
102
+ });
103
+ }
@@ -0,0 +1,298 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import chalk from "chalk";
4
+ import type { Command } from "commander";
5
+ import { initRegistryFromConfig } from "../registry/init.ts";
6
+ import { getRegistry } from "../registry/type-registry.ts";
7
+ import { getExpertisePath, isInsideWorktree, readConfig } from "../utils/config.ts";
8
+ import {
9
+ findMissingDomainFields,
10
+ getAllowedTypes,
11
+ getRequiredFields,
12
+ } from "../utils/domain-rules.ts";
13
+ import { applyAliases } from "../utils/expertise.ts";
14
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
15
+ import { brand, isQuiet } from "../utils/palette.ts";
16
+
17
+ function isGitRepo(cwd: string): boolean {
18
+ try {
19
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
20
+ cwd,
21
+ stdio: "pipe",
22
+ });
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function gitHasChanges(cwd: string): boolean {
30
+ try {
31
+ const status = execFileSync("git", ["status", "--porcelain", ".loam/"], {
32
+ cwd,
33
+ encoding: "utf-8",
34
+ stdio: ["pipe", "pipe", "pipe"],
35
+ });
36
+ return status.trim().length > 0;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ function gitAdd(cwd: string): void {
43
+ execFileSync("git", ["add", ".loam/"], { cwd, stdio: "pipe" });
44
+ }
45
+
46
+ function gitCommit(cwd: string, message: string): string {
47
+ return execFileSync("git", ["commit", "-m", message], {
48
+ cwd,
49
+ encoding: "utf-8",
50
+ stdio: ["pipe", "pipe", "pipe"],
51
+ });
52
+ }
53
+
54
+ interface ValidateResult {
55
+ valid: boolean;
56
+ totalRecords: number;
57
+ errors: Array<{ domain: string; line: number; message: string }>;
58
+ }
59
+
60
+ async function validateExpertise(cwd?: string): Promise<ValidateResult> {
61
+ // Re-init registry from on-disk config so sync validates against the user's
62
+ // current types — important for worktree/CI lag where JSONL (merge=union)
63
+ // can land before loam.config.yaml does. Once config catches up, sync
64
+ // reconciles correctly without needing a restart.
65
+ await initRegistryFromConfig(cwd);
66
+ const config = await readConfig(cwd);
67
+ const domains = Object.keys(config.domains);
68
+
69
+ const registry = getRegistry();
70
+ const validate = registry.validator;
71
+
72
+ let totalRecords = 0;
73
+ const errors: Array<{ domain: string; line: number; message: string }> = [];
74
+
75
+ for (const domain of domains) {
76
+ const filePath = getExpertisePath(domain, cwd);
77
+ // Resolve domain rules from the just-reloaded config so worktree/CI lag
78
+ // (records landing before config) is caught once config catches up.
79
+ // Sync intentionally ignores --allow-domain-mismatch — like
80
+ // --allow-unknown-types, escape hatches stop at the commit gate.
81
+ const allowedTypes = getAllowedTypes(config, domain);
82
+ const requiredFields = getRequiredFields(config, domain);
83
+ let content: string;
84
+ try {
85
+ content = await readFile(filePath, "utf-8");
86
+ } catch {
87
+ continue;
88
+ }
89
+
90
+ const lines = content.split("\n");
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const line = (lines[i] ?? "").trim();
93
+ if (line.length === 0) continue;
94
+ totalRecords++;
95
+ const lineNumber = i + 1;
96
+
97
+ let parsed: unknown;
98
+ try {
99
+ parsed = JSON.parse(line);
100
+ } catch {
101
+ errors.push({
102
+ domain,
103
+ line: lineNumber,
104
+ message: "Invalid JSON: failed to parse",
105
+ });
106
+ continue;
107
+ }
108
+
109
+ // Targeted unknown-type error + alias resolution before Ajv. Sync
110
+ // intentionally ignores --allow-unknown-types — its job is to
111
+ // gatekeep commits.
112
+ if (parsed && typeof parsed === "object" && "type" in parsed) {
113
+ const t = (parsed as { type: unknown }).type;
114
+ if (typeof t === "string") {
115
+ const def = registry.get(t);
116
+ if (!def) {
117
+ const id = (parsed as { id?: unknown }).id;
118
+ const idPart = typeof id === "string" ? ` (id=${id})` : "";
119
+ errors.push({
120
+ domain,
121
+ line: lineNumber,
122
+ message: `Unknown record type "${t}"${idPart}. Register it under custom_types in loam.config.yaml or remove the record.`,
123
+ });
124
+ continue;
125
+ }
126
+ if (def.aliases) applyAliases(parsed as Record<string, unknown>, def.aliases);
127
+ }
128
+ }
129
+
130
+ if (!validate(parsed)) {
131
+ const schemaErrors = (validate.errors ?? [])
132
+ .map((err) => `${err.instancePath} ${err.message}`)
133
+ .join("; ");
134
+ errors.push({
135
+ domain,
136
+ line: lineNumber,
137
+ message: `Schema validation failed: ${schemaErrors}`,
138
+ });
139
+ continue;
140
+ }
141
+
142
+ if (parsed && typeof parsed === "object") {
143
+ const record = parsed as Record<string, unknown>;
144
+ const recordType = typeof record.type === "string" ? record.type : "<unknown>";
145
+ const id = typeof record.id === "string" ? record.id : null;
146
+ const idPart = id ? ` (id=${id})` : "";
147
+
148
+ if (allowedTypes && !allowedTypes.includes(recordType)) {
149
+ errors.push({
150
+ domain,
151
+ line: lineNumber,
152
+ message: `type "${recordType}"${idPart} is not allowed in domain "${domain}". Allowed types: ${allowedTypes.join(", ")}. Fix the record or relax allowed_types in loam.config.yaml.`,
153
+ });
154
+ continue;
155
+ }
156
+
157
+ if (requiredFields) {
158
+ const missing = findMissingDomainFields(record, requiredFields);
159
+ if (missing.length > 0) {
160
+ errors.push({
161
+ domain,
162
+ line: lineNumber,
163
+ message: `record${idPart} missing required field(s) ${missing.map((f) => `"${f}"`).join(", ")} in domain "${domain}". Fix the record or relax required_fields in loam.config.yaml.`,
164
+ });
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ return { valid: errors.length === 0, totalRecords, errors };
172
+ }
173
+
174
+ export function registerSyncCommand(program: Command): void {
175
+ program
176
+ .command("sync")
177
+ .description("Validate, stage, and commit .loam/ changes")
178
+ .option("--message <message>", "custom commit message")
179
+ .option("--no-validate", "skip validation step")
180
+ .action(async (options: { message?: string; validate?: boolean }) => {
181
+ const jsonMode = program.opts().json === true;
182
+ const cwd = process.cwd();
183
+
184
+ // Check if we're in a git repo
185
+ if (!isGitRepo(cwd)) {
186
+ if (jsonMode) {
187
+ outputJsonError(
188
+ "sync",
189
+ "Not in a git repository. Run this command from within a git repository.",
190
+ );
191
+ } else {
192
+ console.error(chalk.red("Error: not in a git repository."));
193
+ }
194
+ process.exitCode = 1;
195
+ return;
196
+ }
197
+
198
+ // Validate (unless --no-validate)
199
+ if (options.validate !== false) {
200
+ try {
201
+ const result = await validateExpertise();
202
+ if (!result.valid) {
203
+ if (jsonMode) {
204
+ outputJson({
205
+ success: false,
206
+ command: "sync",
207
+ validated: false,
208
+ committed: false,
209
+ errors: result.errors,
210
+ });
211
+ } else {
212
+ console.error(chalk.red("Validation failed:"));
213
+ for (const err of result.errors) {
214
+ console.error(chalk.red(` ${err.domain}:${err.line} - ${err.message}`));
215
+ }
216
+ console.error(chalk.red("\nFix errors and retry, or use --no-validate to skip."));
217
+ }
218
+ process.exitCode = 1;
219
+ return;
220
+ }
221
+ if (!jsonMode && !isQuiet()) {
222
+ console.log(`${brand("✓")} ${brand(`Validated ${result.totalRecords} records`)}`);
223
+ }
224
+ } catch (err) {
225
+ if (jsonMode) {
226
+ outputJsonError("sync", `Validation error: ${(err as Error).message}`);
227
+ } else {
228
+ console.error(chalk.red(`Validation error: ${(err as Error).message}`));
229
+ }
230
+ process.exitCode = 1;
231
+ return;
232
+ }
233
+ }
234
+
235
+ // Skip commit if inside a worktree — .loam/ changes belong to the main repo
236
+ if (isInsideWorktree(cwd)) {
237
+ if (jsonMode) {
238
+ outputJson({
239
+ success: true,
240
+ command: "sync",
241
+ validated: options.validate !== false,
242
+ committed: false,
243
+ message:
244
+ "Skipped commit: running inside a git worktree. Changes are written to the main repo's .loam/.",
245
+ });
246
+ } else {
247
+ if (!isQuiet())
248
+ console.log(
249
+ `${brand("ℹ")} Skipped commit: running inside a git worktree. Changes are written to the main repo's .loam/.`,
250
+ );
251
+ }
252
+ return;
253
+ }
254
+
255
+ // Check for changes
256
+ if (!gitHasChanges(cwd)) {
257
+ if (jsonMode) {
258
+ outputJson({
259
+ success: true,
260
+ command: "sync",
261
+ validated: options.validate !== false,
262
+ committed: false,
263
+ message: "No changes to commit",
264
+ });
265
+ } else {
266
+ if (!isQuiet()) console.log("No .loam/ changes to commit.");
267
+ }
268
+ return;
269
+ }
270
+
271
+ // Git add + commit
272
+ const commitMessage = options.message ?? "loam: update expertise";
273
+ try {
274
+ gitAdd(cwd);
275
+ gitCommit(cwd, commitMessage);
276
+
277
+ if (jsonMode) {
278
+ outputJson({
279
+ success: true,
280
+ command: "sync",
281
+ validated: options.validate !== false,
282
+ committed: true,
283
+ message: commitMessage,
284
+ });
285
+ } else {
286
+ if (!isQuiet())
287
+ console.log(`${brand("✓")} ${brand(`Committed .loam/ changes: "${commitMessage}"`)}`);
288
+ }
289
+ } catch (err) {
290
+ if (jsonMode) {
291
+ outputJsonError("sync", `Git error: ${(err as Error).message}`);
292
+ } else {
293
+ console.error(chalk.red(`Git error: ${(err as Error).message}`));
294
+ }
295
+ process.exitCode = 1;
296
+ }
297
+ });
298
+ }
@@ -0,0 +1,19 @@
1
+ import type { Command } from "commander";
2
+ import { outputJsonError } from "../utils/json-output.ts";
3
+ import { printWarning } from "../utils/palette.ts";
4
+
5
+ export function registerUpdateCommand(program: Command): void {
6
+ program
7
+ .command("update", { hidden: true })
8
+ .description("Deprecated: use 'upgrade' instead")
9
+ .option("--check", "only check for updates, do not install")
10
+ .action(async () => {
11
+ const jsonMode = program.opts().json === true;
12
+ if (jsonMode) {
13
+ outputJsonError("update", "'loam update' is deprecated. Use 'loam upgrade' instead.");
14
+ } else {
15
+ printWarning("'loam update' is deprecated. Use 'loam upgrade' instead.");
16
+ }
17
+ process.exitCode = 1;
18
+ });
19
+ }
@@ -0,0 +1,93 @@
1
+ import type { Command } from "commander";
2
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
3
+ import { printError, printSuccess, printWarning } from "../utils/palette.ts";
4
+ import { compareSemver, getCurrentVersion, getLatestVersion } from "../utils/version.ts";
5
+
6
+ const PACKAGE_NAME = "@ag-eco/loam-cli";
7
+
8
+ export function registerUpgradeCommand(program: Command): void {
9
+ program
10
+ .command("upgrade")
11
+ .description("Upgrade loam to the latest version")
12
+ .option("--check", "Check for updates without installing")
13
+ .action(async (options: { check?: boolean }) => {
14
+ const jsonMode = program.opts().json === true;
15
+
16
+ const latest = getLatestVersion();
17
+ if (latest === null) {
18
+ if (jsonMode) {
19
+ outputJsonError(
20
+ "upgrade",
21
+ "Unable to reach npm registry. Check your internet connection.",
22
+ );
23
+ } else {
24
+ printError("Failed to check for updates: Unable to reach npm registry");
25
+ }
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+
30
+ const current = getCurrentVersion();
31
+ const upToDate = compareSemver(current, latest) >= 0;
32
+
33
+ if (upToDate) {
34
+ if (jsonMode) {
35
+ outputJson({
36
+ success: true,
37
+ command: "upgrade",
38
+ current,
39
+ latest,
40
+ upToDate: true,
41
+ updated: false,
42
+ });
43
+ } else {
44
+ printSuccess(`loam is up to date (v${current})`);
45
+ }
46
+ return;
47
+ }
48
+
49
+ if (options.check) {
50
+ if (jsonMode) {
51
+ outputJson({
52
+ success: true,
53
+ command: "upgrade",
54
+ current,
55
+ latest,
56
+ upToDate: false,
57
+ updated: false,
58
+ });
59
+ } else {
60
+ printWarning(`Update available: v${current} → v${latest}`);
61
+ }
62
+ return;
63
+ }
64
+
65
+ const result = Bun.spawnSync(["bun", "install", "-g", `${PACKAGE_NAME}@latest`], {
66
+ stdout: jsonMode ? "pipe" : "inherit",
67
+ stderr: jsonMode ? "pipe" : "inherit",
68
+ });
69
+
70
+ if (result.exitCode !== 0) {
71
+ if (jsonMode) {
72
+ outputJsonError("upgrade", `bun install failed with exit code ${result.exitCode}`);
73
+ } else {
74
+ printError(`Failed to upgrade: bun install failed with exit code ${result.exitCode}`);
75
+ }
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+
80
+ if (jsonMode) {
81
+ outputJson({
82
+ success: true,
83
+ command: "upgrade",
84
+ current,
85
+ latest,
86
+ upToDate: false,
87
+ updated: true,
88
+ });
89
+ } else {
90
+ printSuccess(`Updated loam v${current} → v${latest}`);
91
+ }
92
+ });
93
+ }
@@ -0,0 +1,190 @@
1
+ import { readFile } 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 { getExpertisePath, readConfig } from "../utils/config.ts";
6
+ import {
7
+ findMissingDomainFields,
8
+ getAllowedTypes,
9
+ getRequiredFields,
10
+ } from "../utils/domain-rules.ts";
11
+ import { applyAliases } from "../utils/expertise.ts";
12
+ import { outputJson } from "../utils/json-output.ts";
13
+ import { isAllowDomainMismatch, isAllowUnknownTypes } from "../utils/runtime-flags.ts";
14
+
15
+ export function registerValidateCommand(program: Command): void {
16
+ program
17
+ .command("validate")
18
+ .description("Validate expertise records against schemas")
19
+ .action(async () => {
20
+ const jsonMode = program.opts().json === true;
21
+ const config = await readConfig();
22
+ const domains = Object.keys(config.domains);
23
+
24
+ const registry = getRegistry();
25
+ const validate = registry.validator;
26
+ const allowUnknown = isAllowUnknownTypes();
27
+ const allowDomainMismatch = isAllowDomainMismatch();
28
+
29
+ let totalRecords = 0;
30
+ let totalErrors = 0;
31
+ let totalWarnings = 0;
32
+ const errors: Array<{ domain: string; line: number; message: string }> = [];
33
+ const warnings: Array<{ domain: string; line: number; message: string }> = [];
34
+
35
+ for (const domain of domains) {
36
+ const filePath = getExpertisePath(domain);
37
+ let content: string;
38
+ try {
39
+ content = await readFile(filePath, "utf-8");
40
+ } catch {
41
+ // File doesn't exist yet, skip
42
+ continue;
43
+ }
44
+
45
+ const allowedTypes = getAllowedTypes(config, domain);
46
+ const requiredFields = getRequiredFields(config, domain);
47
+
48
+ const lines = content.split("\n");
49
+ for (let i = 0; i < lines.length; i++) {
50
+ const line = (lines[i] ?? "").trim();
51
+ if (line.length === 0) continue;
52
+
53
+ totalRecords++;
54
+ const lineNumber = i + 1;
55
+
56
+ let parsed: unknown;
57
+ try {
58
+ parsed = JSON.parse(line);
59
+ } catch {
60
+ totalErrors++;
61
+ const msg = "Invalid JSON: failed to parse";
62
+ errors.push({ domain, line: lineNumber, message: msg });
63
+ if (!jsonMode) {
64
+ console.error(chalk.red(`${domain}:${lineNumber} - ${msg}`));
65
+ }
66
+ continue;
67
+ }
68
+
69
+ // Targeted unknown-type error + alias resolution. Ajv runs against
70
+ // the canonical (post-alias) shape, otherwise records with a
71
+ // legacy field name (e.g. before a schema rename) would fail
72
+ // validation. --allow-unknown-types skips both the targeted
73
+ // error AND the downstream Ajv check for unregistered types.
74
+ if (parsed !== null && typeof parsed === "object" && "type" in parsed) {
75
+ const t = (parsed as { type: unknown }).type;
76
+ if (typeof t === "string") {
77
+ const def = registry.get(t);
78
+ if (!def) {
79
+ if (allowUnknown) continue;
80
+ totalErrors++;
81
+ const id = (parsed as { id?: unknown }).id;
82
+ const idPart = typeof id === "string" ? ` (id=${id})` : "";
83
+ const msg = `Unknown record type "${t}"${idPart}. Register it under custom_types in loam.config.yaml or remove the record.`;
84
+ errors.push({ domain, line: lineNumber, message: msg });
85
+ if (!jsonMode) {
86
+ console.error(chalk.red(`${domain}:${lineNumber} - ${msg}`));
87
+ }
88
+ continue;
89
+ }
90
+ if (def.aliases) applyAliases(parsed as Record<string, unknown>, def.aliases);
91
+ }
92
+ }
93
+
94
+ // Detect legacy singular "outcome" field — warn, don't error
95
+ if (
96
+ parsed !== null &&
97
+ typeof parsed === "object" &&
98
+ "outcome" in parsed &&
99
+ !("outcomes" in parsed)
100
+ ) {
101
+ totalWarnings++;
102
+ const msg =
103
+ 'Legacy "outcome" field (singular); run `loam doctor --fix` to migrate to "outcomes[]"';
104
+ warnings.push({ domain, line: lineNumber, message: msg });
105
+ if (!jsonMode) {
106
+ console.error(chalk.yellow(`${domain}:${lineNumber} - ${msg}`));
107
+ }
108
+ } else if (!validate(parsed)) {
109
+ totalErrors++;
110
+ const schemaErrors = (validate.errors ?? [])
111
+ .map((err) => `${err.instancePath} ${err.message}`)
112
+ .join("; ");
113
+ const msg = `Schema validation failed: ${schemaErrors}`;
114
+ errors.push({ domain, line: lineNumber, message: msg });
115
+ if (!jsonMode) {
116
+ console.error(chalk.red(`${domain}:${lineNumber} - Schema validation failed:`));
117
+ for (const err of validate.errors ?? []) {
118
+ console.error(chalk.red(` ${err.instancePath} ${err.message}`));
119
+ }
120
+ }
121
+ continue;
122
+ }
123
+
124
+ // Per-domain rule checks. --allow-domain-mismatch skips these,
125
+ // matching the worktree/CI lag escape hatch pattern. Sync ignores
126
+ // the flag; validate honors it because validate is read-only and
127
+ // useful pre-config-catch-up.
128
+ if (
129
+ !allowDomainMismatch &&
130
+ parsed !== null &&
131
+ typeof parsed === "object" &&
132
+ (allowedTypes !== null || requiredFields !== null)
133
+ ) {
134
+ const record = parsed as Record<string, unknown>;
135
+ const recordType = typeof record.type === "string" ? record.type : "<unknown>";
136
+ const recId = typeof record.id === "string" ? record.id : null;
137
+ const idPart = recId ? ` (id=${recId})` : "";
138
+
139
+ if (allowedTypes && !allowedTypes.includes(recordType)) {
140
+ totalErrors++;
141
+ const msg = `type "${recordType}"${idPart} is not allowed in domain "${domain}". Allowed types: ${allowedTypes.join(", ")}.`;
142
+ errors.push({ domain, line: lineNumber, message: msg });
143
+ if (!jsonMode) {
144
+ console.error(chalk.red(`${domain}:${lineNumber} - ${msg}`));
145
+ }
146
+ }
147
+
148
+ if (requiredFields) {
149
+ const missing = findMissingDomainFields(record, requiredFields);
150
+ if (missing.length > 0) {
151
+ totalErrors++;
152
+ const msg = `record${idPart} missing required field(s) ${missing.map((f) => `"${f}"`).join(", ")} in domain "${domain}".`;
153
+ errors.push({ domain, line: lineNumber, message: msg });
154
+ if (!jsonMode) {
155
+ console.error(chalk.red(`${domain}:${lineNumber} - ${msg}`));
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ if (jsonMode) {
164
+ outputJson({
165
+ success: totalErrors === 0,
166
+ command: "validate",
167
+ valid: totalErrors === 0,
168
+ totalRecords,
169
+ totalErrors,
170
+ totalWarnings,
171
+ errors,
172
+ warnings,
173
+ });
174
+ } else if (totalErrors > 0) {
175
+ console.log(chalk.red(`${totalRecords} records validated, ${totalErrors} errors found`));
176
+ } else if (totalWarnings > 0) {
177
+ console.log(
178
+ chalk.yellow(
179
+ `${totalRecords} records validated, ${totalErrors} errors found, ${totalWarnings} warning(s)`,
180
+ ),
181
+ );
182
+ } else {
183
+ console.log(chalk.green(`${totalRecords} records validated, ${totalErrors} errors found`));
184
+ }
185
+
186
+ if (totalErrors > 0) {
187
+ process.exitCode = 1;
188
+ }
189
+ });
190
+ }