akm-cli 0.9.0-rc.0 → 0.9.0-rc.13

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 (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -4,11 +4,20 @@
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { ConfigError } from "../errors.js";
7
- import { backupExistingConfig, parseConfigText, withConfigLock, writeConfigAtomic } from "./config-io.js";
8
- import { CURRENT_CONFIG_VERSION, compareConfigVersion, migrateConfigShape } from "./config-migration.js";
9
- import { AkmConfigSchema } from "./config-schema.js";
7
+ import { assertNoPendingMigrationOperation } from "../migration-operation.js";
8
+ import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
9
+ import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
10
+ import { bundlesToSourceEntries } from "./config-sources.js";
11
+ import { deepMergeConfig } from "./deep-merge.js";
10
12
  export { stripJsonComments } from "./config-io.js";
11
- import { getCacheDir, getConfigPath } from "../paths.js";
13
+ // requireLlmConfig / getDefaultLlmConfig moved to
14
+ // integrations/agent/engine-resolution.ts (WI-9.8 KILL 3, D.3 edge A) — they
15
+ // called that module's materializeLlmConnection/resolveLlmEngineUse, and that
16
+ // module imported LlmConnectionConfig back from this file, closing a 2-file
17
+ // cycle that also fused config.ts into the harness/agent-runtime SCC. This
18
+ // file can no longer re-export them (a re-export is still a graph edge to
19
+ // engine-resolution.ts) — import them from there directly.
20
+ import { getConfigPath } from "../paths.js";
12
21
  import { warn } from "../warn.js";
13
22
  // Canonical harness-id source of truth (#565) — runtime value re-export.
14
23
  export { VALID_HARNESS_IDS } from "./config-types.js";
@@ -48,7 +57,8 @@ export function resolveBatchSize(configured, contextLength) {
48
57
  }
49
58
  // ── Defaults ────────────────────────────────────────────────────────────────
50
59
  export const DEFAULT_CONFIG = {
51
- semanticSearchMode: "auto",
60
+ configVersion: "0.9.0",
61
+ semanticSearchMode: "off",
52
62
  registries: [
53
63
  { url: "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json", name: "akm-registry" },
54
64
  { url: "https://skills.sh", name: "skills.sh", provider: "skills-sh", enabled: false },
@@ -59,20 +69,23 @@ export const DEFAULT_CONFIG = {
59
69
  },
60
70
  };
61
71
  // ── Load / Save / Update ────────────────────────────────────────────────────
62
- const PROJECT_CONFIG_RELATIVE_PATH = path.join(".akm", "config.json");
63
72
  let cachedConfig;
64
73
  export function resetConfigCache() {
65
74
  cachedConfig = undefined;
66
75
  }
67
76
  export function loadUserConfig() {
77
+ assertNoPendingMigrationOperation();
68
78
  const configPath = getConfigPath();
69
79
  let stat;
70
80
  try {
71
81
  stat = fs.statSync(configPath);
72
82
  }
73
- catch {
83
+ catch (err) {
84
+ if (err.code !== "ENOENT") {
85
+ throw new ConfigError(`Unable to read config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE");
86
+ }
74
87
  cachedConfig = undefined;
75
- return applyRuntimeEnvApiKeys({ ...DEFAULT_CONFIG });
88
+ return { ...DEFAULT_CONFIG };
76
89
  }
77
90
  // Cache key: mtimeMs + size. Tests that write rapidly back-to-back inside
78
91
  // the mtime resolution window MUST call resetConfigCache() between writes —
@@ -85,17 +98,16 @@ export function loadUserConfig() {
85
98
  }
86
99
  let text;
87
100
  try {
88
- text = fs.readFileSync(configPath, "utf8");
101
+ text = readConfigText(configPath);
89
102
  }
90
- catch {
103
+ catch (err) {
104
+ throw new ConfigError(`Unable to read config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE");
105
+ }
106
+ if (text === undefined) {
91
107
  cachedConfig = undefined;
92
- return applyRuntimeEnvApiKeys({ ...DEFAULT_CONFIG });
108
+ return { ...DEFAULT_CONFIG };
93
109
  }
94
- // Auto-migration: rewrite legacy shapes to disk on cache miss so the schema
95
- // sees canonical input. AKM_NO_AUTO_MIGRATE=1 skips the disk rewrite (still
96
- // applies in-memory).
97
- text = maybeAutoMigrateConfigFile(configPath, text);
98
- const finalConfig = applyRuntimeEnvApiKeys(parseAndValidate(text, configPath));
110
+ const finalConfig = parseAndValidateConfigText(text, configPath);
99
111
  // Re-stat after potential write-back so the cache key reflects the new mtime.
100
112
  let finalStat = stat;
101
113
  try {
@@ -113,222 +125,141 @@ export function loadUserConfig() {
113
125
  return finalConfig;
114
126
  }
115
127
  /**
116
- * Parse raw config text, run the legacy-shape migration
117
- * ({@link migrateConfigShape}), then validate via Zod
128
+ * Parse raw config text and validate via Zod.
118
129
  * ({@link AkmConfigSchema}). Returns the merged-with-defaults AkmConfig.
119
130
  *
120
- * The migration handles all one-time 0.7→0.8 transforms (legacy keys,
121
- * boolean→string coercions, openviking rename); the schema then validates
122
- * the canonical shape and throws on anything it doesn't recognise.
131
+ * The schema accepts only the current config version and validates the
132
+ * canonical shape before defaults are merged.
123
133
  */
124
- function parseAndValidate(text, sourcePath) {
125
- // Migration absorbs 0.7→0.8 input transforms (semanticSearchMode bool→string,
126
- // stashes[] → sources[], openviking removal); the schema then sees a
127
- // canonical shape. Migration is idempotent on already-migrated input.
128
- const migrated = migrateConfigShape(parseConfigText(text, sourcePath)).result;
129
- const parsed = AkmConfigSchema.safeParse(migrated);
134
+ export function parseAndValidateConfigText(text, sourcePath) {
135
+ const raw = parseConfigText(text, sourcePath);
136
+ if (raw.configVersion !== CURRENT_CONFIG_VERSION) {
137
+ throw new ConfigError(`Unsupported configVersion${sourcePath ? ` at ${sourcePath}` : ""}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
138
+ }
139
+ const parsed = AkmConfigSchema.safeParse(raw);
130
140
  if (!parsed.success) {
131
141
  const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
132
142
  const where = sourcePath ? ` at ${sourcePath}` : "";
133
143
  throw new ConfigError(`Invalid config${where}:\n${lines}`, "INVALID_CONFIG_FILE");
134
144
  }
135
- return mergeLoadedConfig(DEFAULT_CONFIG, parsed.data);
136
- }
137
- export function getSources(config) {
138
- return config.sources ?? [];
139
- }
140
- export function getEffectiveRegistries(config) {
141
- return config.registries ?? DEFAULT_CONFIG.registries ?? [];
142
- }
143
- /**
144
- * Resolve the name of the default LLM profile.
145
- *
146
- * Resolution order:
147
- * 1. `defaults.llm` — explicit pointer set by the user.
148
- * 2. A profile literally named `default` under `profiles.llm` — implicit
149
- * fallback. The convention "name your default profile `default`" is
150
- * what `akm setup` produces, so an unset `defaults.llm` next to a
151
- * `profiles.llm.default` block is overwhelmingly a config-rewrite
152
- * casualty (see [[project_akm_config_clobber_trap]]) rather than
153
- * intent. Treating that shape as configured avoids the silent
154
- * `getDefaultLlmConfig → undefined → pass-returns-zero` failure mode
155
- * that produced 18h of no-op memory-inference runs on 2026-05-23.
156
- *
157
- * Returns `undefined` when neither path resolves to a profile name.
158
- */
159
- function resolveDefaultLlmProfileName(config) {
160
- const explicit = config.defaults?.llm;
161
- if (explicit)
162
- return explicit;
163
- if (config.profiles?.llm?.default)
164
- return "default";
165
- return undefined;
166
- }
167
- /**
168
- * Resolve the default LLM connection from `profiles.llm[defaults.llm]`.
169
- *
170
- * Throws {@link ConfigError} when no default profile can be resolved (neither
171
- * `defaults.llm` nor an implicit `profiles.llm.default`) or when the named
172
- * profile does not exist under `profiles.llm`. Use this in code paths that
173
- * must have an LLM configured (per-pass index calls, distill, consolidate,
174
- * etc).
175
- */
176
- export function requireLlmConfig(config) {
177
- const defaultName = resolveDefaultLlmProfileName(config);
178
- if (!defaultName) {
179
- throw new ConfigError("LLM is not configured. Run `akm setup` or set `defaults.llm` to a profile defined in `profiles.llm`.", "LLM_NOT_CONFIGURED");
180
- }
181
- const profile = config.profiles?.llm?.[defaultName];
182
- if (!profile) {
183
- throw new ConfigError(`LLM default profile "${defaultName}" not found in profiles.llm.`, "LLM_NOT_CONFIGURED", `Available profiles: ${Object.keys(config.profiles?.llm ?? {}).join(", ") || "none"}. Run \`akm setup\` to configure.`);
145
+ const merged = deepMergeConfig(DEFAULT_CONFIG, parsed.data);
146
+ const finalResult = AkmConfigSchema.safeParse(merged);
147
+ if (!finalResult.success) {
148
+ const lines = finalResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
149
+ throw new ConfigError(`Invalid merged config${sourcePath ? ` at ${sourcePath}` : ""}:\n${lines}`, "INVALID_CONFIG_FILE");
184
150
  }
185
- return profile;
151
+ return finalResult.data;
186
152
  }
187
153
  /**
188
- * Like {@link requireLlmConfig} but returns `undefined` instead of throwing
189
- * when no LLM is configured. Use in code paths where the LLM is optional.
190
- */
191
- export function getDefaultLlmConfig(config) {
192
- const defaultName = resolveDefaultLlmProfileName(config);
193
- if (!defaultName)
194
- return undefined;
195
- return config.profiles?.llm?.[defaultName];
196
- }
197
- /**
198
- * Resolve the per-process config section for an improve process,
199
- * centralizing the deeply-nested lookup
200
- * `profile?.processes?.<name>` that was previously copy-pasted across the
201
- * improve command family (20+ call sites).
154
+ * The configured stash sources as an ordered {@link SourceConfigEntry} list.
202
155
  *
203
- * When an `activeProfile` is supplied (the profile resolved for the current
204
- * `akm improve --profile <name>` run), its per-process override wins; otherwise
205
- * — and as a fallback when the active profile does not define the section — the
206
- * lookup falls back to the `"default"` improve profile from the on-disk config.
207
- * Callers that have not yet threaded the active profile pass only `config` and
208
- * get the historical default-profile behavior unchanged.
156
+ * 0.9.0 (spec §10.1): the retired `stashDir`/`sources[]`/`installed[]` trio is
157
+ * gone — every source is a `bundles.<slug>` entry. This derives the unified
158
+ * source list from `bundles` (defaultBundle first, then map order), so callers
159
+ * that iterated `sources[]` keep working over the new shape. Returns `[]` when
160
+ * no bundles are configured.
209
161
  */
210
- export function getImproveProcessConfig(config, processName, activeProfile) {
211
- const fromActiveProfile = activeProfile?.processes?.[processName];
212
- if (fromActiveProfile !== undefined)
213
- return fromActiveProfile;
214
- return config.profiles?.improve?.default?.processes?.[processName];
162
+ export function getSources(config) {
163
+ return bundlesToSourceEntries(config) ?? [];
164
+ }
165
+ export function getEffectiveRegistries(config) {
166
+ return config.registries ?? DEFAULT_CONFIG.registries ?? [];
215
167
  }
216
168
  /**
217
- * Run `migrateConfigShape` on the raw text and — unless `AKM_NO_AUTO_MIGRATE=1`
218
- * is set — persist the migrated result. Returns the (possibly migrated) text
219
- * for the caller to feed into `parseAndValidate`.
220
- *
221
- * If the on-disk config is newer than this binary's known version, the bytes
222
- * are left untouched (we won't silently strip fields on downgrade).
169
+ * Transitional internal accessor. It deliberately never consults a configured
170
+ * default strategy; callers must pass their already selected strategy.
223
171
  */
224
- function maybeAutoMigrateConfigFile(configPath, text) {
225
- let obj;
226
- try {
227
- obj = parseConfigText(text);
228
- }
229
- catch {
230
- return text; // Malformed JSON — let parseAndValidate surface the error.
231
- }
232
- // Downgrade protection. Skip migration when the on-disk config is NEWER than
233
- // this binary (=== 1), OR when it carries a configVersion we cannot order
234
- // against ours (compareConfigVersion returns undefined for an unparseable
235
- // value — e.g. one written by a newer/foreign akm). Migrating such a config
236
- // could strip fields a newer binary added, a cross-version data-loss path.
237
- //
238
- // A MISSING configVersion (absent → undefined) is a legacy pre-versioning
239
- // config that MUST still migrate, so the unparseable-skip is gated on the
240
- // field being PRESENT (`onDiskVersion !== undefined`).
241
- const onDiskVersion = obj.configVersion;
242
- const versionOrder = compareConfigVersion(onDiskVersion, CURRENT_CONFIG_VERSION);
243
- if (versionOrder === 1 || (onDiskVersion !== undefined && versionOrder === undefined)) {
244
- return text;
245
- }
246
- const { changed, result } = migrateConfigShape(obj);
247
- if (!changed)
248
- return text;
249
- const migratedText = `${JSON.stringify(result, null, 2)}\n`;
250
- if (process.env.AKM_NO_AUTO_MIGRATE === "1")
251
- return migratedText;
252
- try {
253
- withConfigLock(() => {
254
- backupExistingConfig(configPath);
255
- writeConfigAtomic(configPath, result);
256
- });
257
- const newVersion = typeof result.configVersion === "string" ? result.configVersion : "0.8.0";
258
- const backupDir = `${getCacheDir()}/config-backups`;
259
- // WS-2: emit a loud banner to BOTH stderr and stdout so pipelines and
260
- // interactive terminals both see it. Include the backup path (resolved,
261
- // not ~/...), opt-out env var, and preview diff command.
262
- const banner = [
263
- "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
264
- ` akm: auto-migrated config → ${newVersion} format`,
265
- ` file: ${configPath}`,
266
- ` backup: ${backupDir}/config-<timestamp>.json`,
267
- " to opt out of future auto-migration: AKM_NO_AUTO_MIGRATE=1",
268
- " to preview a dry-run diff: akm config migrate --dry-run --print-diff",
269
- "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
270
- ].join("\n");
271
- process.stderr?.write?.(`${banner}\n`);
272
- process.stdout?.write?.(`${banner}\n`);
273
- }
274
- catch (err) {
275
- // #461: never return migrated bytes when disk write fails — that triggers
276
- // an infinite re-migrate loop on every load. Hard-error so the user
277
- // notices and either fixes the disk issue or sets AKM_NO_AUTO_MIGRATE=1.
278
- throw new ConfigError(`Failed to write migrated config to ${configPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE", "Check filesystem permissions, free space, and disk health. To skip auto-migration, set AKM_NO_AUTO_MIGRATE=1.");
279
- }
280
- return migratedText;
172
+ export function getImproveProcessConfig(processName, selected) {
173
+ return selected?.processes?.[processName];
281
174
  }
282
175
  export function loadConfig() {
283
- // Single-layer load: only the user-level config file is read. Project-level
284
- // .akm/config.json files discovered under cwd-ancestors emit a one-time
285
- // deprecation warning (#457) but are NOT merged.
286
- warnIfProjectConfigPresent(process.cwd());
287
176
  return loadUserConfig();
288
177
  }
289
- let saveConfigOverride;
290
- /** TEST-ONLY. Swap the implementation of `saveConfig`; pass undefined to restore. */
291
- export function _setSaveConfigForTests(fake) {
292
- saveConfigOverride = fake;
293
- }
294
178
  export function saveConfig(config) {
295
- if (saveConfigOverride) {
296
- saveConfigOverride(config);
297
- return;
298
- }
299
- saveConfigReal(config);
179
+ // Every lifecycle write produces the only config version this binary can load.
180
+ const currentConfig = { ...config, configVersion: CURRENT_CONFIG_VERSION };
181
+ saveConfigReal(currentConfig);
300
182
  }
301
183
  function saveConfigReal(config) {
184
+ assertNoPendingMigrationOperation();
302
185
  cachedConfig = undefined;
303
186
  const configPath = getConfigPath();
304
187
  const dir = path.dirname(configPath);
305
188
  fs.mkdirSync(dir, { recursive: true });
306
- const sanitized = sanitizeConfigForWrite(config);
307
- // Final validation gate before bytes hit disk. Runs the FULL schema —
308
- // including the cross-field superRefine guards (removed `feedbackDistillation`
309
- // process key, `defaultWriteTarget` resolution, writable npm/website sources)
310
- // and all type/enum/range checks — so an `akm config set` (leaf OR object
311
- // form) cannot persist a guard-violating or mistyped value. NOTE: unknown
312
- // keys are intentionally NOT rejected here — object schemas are `.passthrough()`
313
- // so cross-version skew round-trips (see config-schema.ts header); the lenient
314
- // tolerance is by design, not an oversight.
315
- const parseResult = AkmConfigSchema.safeParse(sanitized);
316
- if (!parseResult.success) {
317
- const lines = parseResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
318
- throw new ConfigError(`Refusing to save invalid config:\n${lines}`, "INVALID_CONFIG_FILE", "Fix the listed fields, or undo the offending `akm config set`. " +
319
- "If this looks like an akm bug, re-run with --debug to attach the traceback.");
320
- }
321
- // WS-3: acquire the config write lock so concurrent `akm config set`
322
- // invocations do not interleave their backup+atomic-write cycles.
323
189
  withConfigLock(() => {
190
+ assertNoPendingMigrationOperation();
191
+ const validated = validateCompleteConfig(config);
324
192
  backupExistingConfig(configPath);
325
- writeConfigAtomic(configPath, sanitized);
193
+ writeConfigAtomic(configPath, sanitizeConfigForWrite(validated));
326
194
  });
327
195
  }
196
+ export function validateCompleteConfig(config) {
197
+ const parseResult = AkmConfigSchema.safeParse(config);
198
+ if (parseResult.success)
199
+ return parseResult.data;
200
+ const lines = parseResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
201
+ throw new ConfigError(`Refusing to save invalid config:\n${lines}`, "INVALID_CONFIG_FILE", "Fix the listed fields, or undo the offending `akm config set`. " +
202
+ "If this looks like an akm bug, re-run with --debug to attach the traceback.");
203
+ }
204
+ /**
205
+ * Mutate config under one fail-closed lock spanning read, merge, validation,
206
+ * ordinary backup, and atomic write.
207
+ */
208
+ export function mutateConfig(mutate, options) {
209
+ assertNoPendingMigrationOperation();
210
+ cachedConfig = undefined;
211
+ const configPath = getConfigPath();
212
+ return withConfigLock(() => {
213
+ assertNoPendingMigrationOperation();
214
+ const text = readConfigText(configPath);
215
+ if (text === undefined && options?.absentNoop) {
216
+ return { config: { ...DEFAULT_CONFIG }, written: false };
217
+ }
218
+ const current = text === undefined ? { ...DEFAULT_CONFIG } : parseAndValidateConfigText(text, configPath);
219
+ const mutated = mutate(current);
220
+ if (mutated === current)
221
+ return { config: current, written: false };
222
+ const next = validateCompleteConfig({ ...mutated, configVersion: CURRENT_CONFIG_VERSION });
223
+ if (text !== undefined)
224
+ backupExistingConfig(configPath);
225
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
226
+ writeConfigAtomic(configPath, sanitizeConfigForWrite(next));
227
+ return { config: next, written: true };
228
+ });
229
+ }
230
+ /**
231
+ * Mutate config while holding the write lock across one validated pre-commit
232
+ * side effect. Setup uses this to reject a three-way conflict before creating
233
+ * its stash, while preventing another config writer from racing the final save.
234
+ */
235
+ export async function mutateConfigWithPrecommit(mutate, precommit) {
236
+ assertNoPendingMigrationOperation();
237
+ cachedConfig = undefined;
238
+ const configPath = getConfigPath();
239
+ const release = acquireConfigLock();
240
+ try {
241
+ assertNoPendingMigrationOperation();
242
+ const text = readConfigText(configPath);
243
+ const current = text === undefined ? { ...DEFAULT_CONFIG } : parseAndValidateConfigText(text, configPath);
244
+ const mutated = mutate(current);
245
+ const next = validateCompleteConfig({ ...mutated, configVersion: CURRENT_CONFIG_VERSION });
246
+ if (text !== undefined)
247
+ backupExistingConfig(configPath);
248
+ const precommitResult = await precommit(next);
249
+ if (mutated === current)
250
+ return { config: current, written: false, precommit: precommitResult };
251
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
252
+ writeConfigAtomic(configPath, sanitizeConfigForWrite(next));
253
+ return { config: next, written: true, precommit: precommitResult };
254
+ }
255
+ finally {
256
+ release();
257
+ }
258
+ }
328
259
  /**
329
260
  * Strip literal apiKey fields before writing config to disk.
330
261
  * API keys are expected to come from environment variables
331
- * (AKM_EMBED_API_KEY, AKM_LLM_API_KEY, AKM_PROFILE_<NAME>_API_KEY).
262
+ * (AKM_EMBED_API_KEY, AKM_LLM_API_KEY, AKM_ENGINE_<NAME>_API_KEY).
332
263
  *
333
264
  * `${VAR}` / `$VAR` references are preserved — they are not secrets, they
334
265
  * are deferred lookups resolved at consumption by `resolveSecret`. Dropping
@@ -340,7 +271,7 @@ function saveConfigReal(config) {
340
271
  * --yes` with an `apiKey` field expected persistence and got a wiped config
341
272
  * with no feedback.
342
273
  */
343
- function sanitizeConfigForWrite(config) {
274
+ export function sanitizeConfigForWrite(config) {
344
275
  const sanitized = { ...config };
345
276
  const stripped = [];
346
277
  if (config.embedding?.apiKey !== undefined) {
@@ -359,50 +290,36 @@ function sanitizeConfigForWrite(config) {
359
290
  else if (config.embedding) {
360
291
  sanitized.embedding = { ...config.embedding };
361
292
  }
362
- if (config.profiles?.llm) {
363
- const llmProfiles = {};
364
- for (const [name, profile] of Object.entries(config.profiles.llm)) {
365
- if (profile.apiKey !== undefined) {
366
- if (isEnvReference(profile.apiKey)) {
367
- llmProfiles[name] = { ...profile };
368
- }
369
- else {
370
- const { apiKey: _drop, ...rest } = profile;
371
- llmProfiles[name] = rest;
372
- if (profile.apiKey) {
373
- const envVar = `AKM_PROFILE_${name.toUpperCase().replace(/-/g, "_")}_API_KEY`;
374
- stripped.push(`profiles.llm.${name}.apiKey (set ${envVar} to provide at runtime)`);
375
- }
376
- }
377
- }
378
- else {
379
- llmProfiles[name] = { ...profile };
293
+ if (config.engines) {
294
+ const engines = {};
295
+ for (const [name, engine] of Object.entries(config.engines)) {
296
+ if (engine.kind !== "llm" || engine.apiKey === undefined || isEnvReference(engine.apiKey)) {
297
+ engines[name] = { ...engine };
298
+ continue;
380
299
  }
300
+ const { apiKey: _drop, ...rest } = engine;
301
+ engines[name] = rest;
302
+ if (engine.apiKey)
303
+ stripped.push(`engines.${name}.apiKey`);
381
304
  }
382
- sanitized.profiles = {
383
- ...(sanitized.profiles ?? {}),
384
- llm: llmProfiles,
385
- };
305
+ sanitized.engines = engines;
386
306
  }
387
307
  if (stripped.length > 0) {
388
- warn(`Config sanitizer dropped API key(s) before writing to disk:\n - ${stripped.join("\n - ")}\n\nakm does not persist API keys to config.json. Set the listed environment variables to provide them at runtime, or use \`\${VAR}\` references in your config to defer lookup. See docs/data-and-telemetry.md.`);
308
+ warn(`Config sanitizer dropped API key(s) before writing to disk:\n - ${stripped.join("\n - ")}\n\nakm does not persist API keys to config.json. Set the listed environment variables to provide them at runtime, or use \`\${VAR}\` references in your config to defer lookup. See docs/reference/data-and-telemetry.md.`);
389
309
  }
390
310
  return sanitized;
391
311
  }
392
- /** Matches `${VAR}`, `${VAR:-default}`, or `$VAR`. */
312
+ /** Matches the only 0.9 symbolic secret forms: `${VAR}` or `$VAR`. */
393
313
  function isEnvReference(value) {
394
- return /^\$\{[^}]+\}$|^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value);
314
+ return /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$|^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value);
395
315
  }
396
316
  export function updateConfig(partial) {
397
- const current = loadUserConfig();
398
- const merged = mergeLoadedConfig(current, partial);
399
- saveConfig(merged);
400
- return merged;
317
+ return mutateConfig((current) => deepMergeConfig(current, partial)).config;
401
318
  }
402
319
  // ── Helpers ─────────────────────────────────────────────────────────────────
403
320
  /**
404
- * Resolve a single secret value by expanding `${VAR}` / `$VAR` /
405
- * `${VAR:-default}` references against `process.env`. Use this at apiKey /
321
+ * Resolve a single secret value by expanding `${VAR}` / `$VAR` references
322
+ * against `process.env`. Use this at apiKey /
406
323
  * authorization-header consumption sites (LLM client, embedder, agent SDK
407
324
  * runner) — NOT on the load path. Non-string inputs pass through unchanged.
408
325
  *
@@ -420,26 +337,21 @@ export function resolveSecret(value) {
420
337
  return value;
421
338
  if (!value.includes("$"))
422
339
  return value;
423
- return value.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
424
- if (braced) {
425
- const [name, ...rest] = braced.split(":-");
426
- const fallback = rest.join(":-");
427
- return process.env[name] ?? fallback ?? "";
428
- }
429
- return process.env[bare] ?? "";
340
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
341
+ return process.env[(braced ?? bare)] ?? "";
430
342
  });
431
343
  }
432
344
  /**
433
345
  * Read a per-pass {@link IndexPassConfig} entry from {@link IndexConfig},
434
346
  * filtering out the reserved feature-section keys so callers don't mistake
435
- * `metadataEnhance` / `stalenessDetection` for a pass.
347
+ * `metadataEnhance` for a pass.
436
348
  */
437
349
  /**
438
350
  * Reserved well-known keys on IndexConfig that are NOT per-pass entries.
439
- * `stalenessDetection` is retired (10-Q3) but stays reserved so a leftover
440
- * config section is never misread as a pass entry.
351
+ * `indexBodyOpening` (stash-conventions SPEC-8) is a boolean feature flag, not
352
+ * a pass section.
441
353
  */
442
- const INDEX_RESERVED_KEYS = new Set(["metadataEnhance", "stalenessDetection"]);
354
+ const INDEX_RESERVED_KEYS = new Set(["metadataEnhance", "indexBodyOpening"]);
443
355
  export function getIndexPassConfig(config, passName) {
444
356
  if (!config)
445
357
  return undefined;
@@ -451,99 +363,10 @@ export function getIndexPassConfig(config, passName) {
451
363
  return entry;
452
364
  }
453
365
  // Re-export source runtime helpers — implementation lives in config-sources.ts.
454
- export { parseSourceSpec, resolveConfiguredSources } from "./config-sources.js";
366
+ export { bundleComponentConfig, bundleEntryToSourceEntry, bundlesToSourceEntries, installedSourceDescriptor, parseSourceSpec, primaryBundlePath, resolveConfiguredSources, } from "./config-sources.js";
455
367
  /**
456
368
  * Merge a partial user-config override onto a base config. Used by
457
369
  * {@link loadUserConfig} (DEFAULT_CONFIG + on-disk) and {@link updateConfig}
458
370
  * (current config + partial patch). Sub-objects with named records (profiles,
459
371
  * defaults, etc.) shallow-merge; arrays override wholesale.
460
372
  */
461
- function mergeLoadedConfig(base, override) {
462
- if (!override)
463
- return { ...base };
464
- const merged = { ...base, ...override };
465
- // Shallow-merge sub-objects so a partial update to e.g. `output.format`
466
- // doesn't drop the existing `output.detail`.
467
- for (const key of ["output", "embedding", "index", "defaults"]) {
468
- if (base[key] && override[key]) {
469
- // biome-ignore lint/suspicious/noExplicitAny: heterogeneous structural merge
470
- merged[key] = { ...base[key], ...override[key] };
471
- }
472
- }
473
- if (base.profiles && override.profiles) {
474
- const next = { ...base.profiles };
475
- for (const k of ["llm", "agent", "improve"]) {
476
- const ovr = override.profiles[k];
477
- if (ovr)
478
- next[k] = { ...(next[k] ?? {}), ...ovr };
479
- }
480
- merged.profiles = next;
481
- }
482
- return merged;
483
- }
484
- function applyRuntimeEnvApiKeys(config) {
485
- const next = { ...config };
486
- if (next.embedding && !next.embedding.apiKey) {
487
- const envKey = process.env.AKM_EMBED_API_KEY?.trim();
488
- if (envKey)
489
- next.embedding = { ...next.embedding, apiKey: envKey };
490
- }
491
- // LLM profile keys: AKM_LLM_API_KEY for the default profile, then
492
- // AKM_PROFILE_<UPPER>_API_KEY for any profile (per-profile wins).
493
- // Resolve the default profile the SAME way the rest of the config layer does
494
- // (resolveDefaultLlmProfileName), so the implicit `profiles.llm.default`
495
- // fallback is honored. Keying off the raw `defaults.llm` field alone silently
496
- // dropped AKM_LLM_API_KEY for configs that rely on the implicit default —
497
- // the same no-op-run class as the 2026-05-23 incident.
498
- const defaultProfile = resolveDefaultLlmProfileName(next);
499
- if (next.profiles?.llm) {
500
- const updated = { ...next.profiles.llm };
501
- let changed = false;
502
- for (const [name, profile] of Object.entries(updated)) {
503
- if (profile.apiKey)
504
- continue;
505
- const perProfile = process.env[`AKM_PROFILE_${name.toUpperCase().replace(/-/g, "_")}_API_KEY`]?.trim();
506
- const fallback = name === defaultProfile ? process.env.AKM_LLM_API_KEY?.trim() : undefined;
507
- const envKey = perProfile || fallback;
508
- if (envKey) {
509
- updated[name] = { ...profile, apiKey: envKey };
510
- changed = true;
511
- }
512
- }
513
- if (changed)
514
- next.profiles = { ...next.profiles, llm: updated };
515
- }
516
- return next;
517
- }
518
- /**
519
- * Walk cwd-ancestors looking for `.akm/config.json`. If one is found, emit a
520
- * one-time deprecation warning per path. The file's contents are NOT read —
521
- * multi-layer project config was removed in this release; the warning stays
522
- * for one cycle so users notice they have a now-dead file on disk and can
523
- * migrate its settings to the user-level config.
524
- */
525
- const PROJECT_CONFIG_DEPRECATION_WARNED = new Set();
526
- function warnIfProjectConfigPresent(startDir) {
527
- let currentDir = path.resolve(startDir);
528
- while (true) {
529
- const configPath = path.join(currentDir, PROJECT_CONFIG_RELATIVE_PATH);
530
- if (isFile(configPath) && !PROJECT_CONFIG_DEPRECATION_WARNED.has(configPath)) {
531
- PROJECT_CONFIG_DEPRECATION_WARNED.add(configPath);
532
- warn(`[akm] DEPRECATED: project-level config file found at ${configPath}. ` +
533
- "Project-level config files are no longer merged (removed after 0.8.x deprecation). " +
534
- "Move any needed settings to ~/.config/akm/config.json; this file is ignored.");
535
- }
536
- const parentDir = path.dirname(currentDir);
537
- if (parentDir === currentDir)
538
- break;
539
- currentDir = parentDir;
540
- }
541
- }
542
- function isFile(filePath) {
543
- try {
544
- return fs.statSync(filePath).isFile();
545
- }
546
- catch {
547
- return false;
548
- }
549
- }
@@ -0,0 +1,41 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
5
+ export function isPlainObject(value) {
6
+ if (value === null || typeof value !== "object" || Array.isArray(value))
7
+ return false;
8
+ const prototype = Object.getPrototypeOf(value);
9
+ return prototype === Object.prototype || prototype === null;
10
+ }
11
+ function copyConfigValue(value) {
12
+ if (Array.isArray(value))
13
+ return value.map(copyConfigValue);
14
+ if (!isPlainObject(value))
15
+ return value;
16
+ const copy = {};
17
+ for (const key of Object.keys(value)) {
18
+ if (UNSAFE_KEYS.has(key))
19
+ throw new TypeError(`Unsafe configuration key: ${key}`);
20
+ copy[key] = copyConfigValue(value[key]);
21
+ }
22
+ return copy;
23
+ }
24
+ /**
25
+ * Merge configuration values without mutating either input. Plain objects merge
26
+ * recursively; arrays and scalar values replace the less-specific value.
27
+ */
28
+ export function deepMergeConfig(base, override) {
29
+ const result = copyConfigValue(base);
30
+ for (const key of Object.keys(override)) {
31
+ if (UNSAFE_KEYS.has(key))
32
+ throw new TypeError(`Unsafe configuration key: ${key}`);
33
+ const next = override[key];
34
+ if (next === undefined)
35
+ continue;
36
+ const current = result[key];
37
+ result[key] =
38
+ isPlainObject(current) && isPlainObject(next) ? deepMergeConfig(current, next) : copyConfigValue(next);
39
+ }
40
+ return result;
41
+ }