akm-cli 0.10.261012-beta → 0.10.261014-beta

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 (518) hide show
  1. package/CHANGELOG.md +710 -3
  2. package/README.md +15 -0
  3. package/SECURITY.md +11 -10
  4. package/STABILITY.md +47 -51
  5. package/dist/assets/hints/cli-hints-full.md +21 -19
  6. package/dist/assets/hints/cli-hints-short.md +3 -4
  7. package/dist/assets/opencode/akm-plugin.js +177 -0
  8. package/dist/assets/stash-skeleton/README.md +1 -1
  9. package/dist/assets/tasks/core/improve.yml +1 -1
  10. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
  11. package/dist/assets/ui/config.js +27 -10
  12. package/dist/assets/workflows/workflow-template.md +2 -2
  13. package/dist/cli/clack.js +3 -1
  14. package/dist/cli/selection.js +33 -0
  15. package/dist/cli/shared.js +14 -1
  16. package/dist/cli/unknown-flags.js +6 -37
  17. package/dist/cli.js +69 -48
  18. package/dist/commands/agent/agent-dispatch.js +17 -25
  19. package/dist/commands/agent/contribute-cli.js +9 -8
  20. package/dist/commands/command/command-cli.js +4 -8
  21. package/dist/commands/command/command-execution.js +25 -39
  22. package/dist/commands/command/execution-source-loader.js +10 -14
  23. package/dist/commands/completions.js +1 -1
  24. package/dist/commands/config-cli.js +14 -5
  25. package/dist/commands/env/child-env.js +12 -14
  26. package/dist/commands/env/env-binding.js +47 -58
  27. package/dist/commands/env/env-cli.js +22 -24
  28. package/dist/commands/env/env.js +10 -58
  29. package/dist/commands/env/secret-cli.js +11 -29
  30. package/dist/commands/env/secret.js +3 -10
  31. package/dist/commands/health/checks.js +10 -849
  32. package/dist/commands/health/connections-and-harnesses-check.js +109 -0
  33. package/dist/commands/health/egress.js +3 -3
  34. package/dist/commands/health/html-report.js +9 -10
  35. package/dist/commands/health/plugin-staleness.js +18 -11
  36. package/dist/commands/health/report-view-model.js +16 -12
  37. package/dist/commands/health/scheduler-binary.js +1 -1
  38. package/dist/commands/health/semantic-search-check.js +85 -0
  39. package/dist/commands/health/type-directory-check.js +0 -3
  40. package/dist/commands/health.js +30 -42
  41. package/dist/commands/improve/consolidate/pair-pass.js +10 -25
  42. package/dist/commands/improve/consolidate.js +10 -15
  43. package/dist/commands/improve/distill.js +5 -17
  44. package/dist/commands/improve/eligibility.js +1 -3
  45. package/dist/commands/improve/execution.js +37 -40
  46. package/dist/commands/improve/extract-cli.js +6 -11
  47. package/dist/commands/improve/extract-prompt.js +2 -2
  48. package/dist/commands/improve/extract.js +67 -97
  49. package/dist/commands/improve/improve-cli.js +22 -128
  50. package/dist/commands/improve/improve-report.js +1 -1
  51. package/dist/commands/improve/improve-strategies.js +25 -108
  52. package/dist/commands/improve/improve.js +10 -14
  53. package/dist/commands/improve/loop-stages.js +3 -0
  54. package/dist/commands/improve/memory/memory-improve.js +1 -1
  55. package/dist/commands/improve/preparation.js +8 -13
  56. package/dist/commands/improve/reflect.js +203 -332
  57. package/dist/commands/improve/retrieval-scope.js +1 -1
  58. package/dist/commands/improve/session-asset.js +3 -2
  59. package/dist/commands/improve/stage.js +87 -115
  60. package/dist/commands/lint/base-linter.js +3 -3
  61. package/dist/commands/lint/env-key-rules.js +7 -6
  62. package/dist/commands/lint/index.js +3 -3
  63. package/dist/commands/models-cli.js +103 -63
  64. package/dist/commands/proposal/drain.js +26 -39
  65. package/dist/commands/proposal/proposal-cli.js +14 -18
  66. package/dist/commands/proposal/propose-cli.js +4 -2
  67. package/dist/commands/proposal/propose.js +37 -64
  68. package/dist/commands/proposal/repository.js +17 -17
  69. package/dist/commands/proposal/validators/proposal-validators.js +3 -6
  70. package/dist/commands/read/curate.js +10 -6
  71. package/dist/commands/read/registry-search.js +2 -48
  72. package/dist/commands/read/search-cli.js +1 -8
  73. package/dist/commands/read/search.js +5 -6
  74. package/dist/commands/read/show.js +6 -29
  75. package/dist/commands/remember.js +25 -40
  76. package/dist/commands/sources/add-cli.js +8 -63
  77. package/dist/commands/sources/bundle-cli.js +8 -9
  78. package/dist/commands/sources/bundle-config-ops.js +2 -2
  79. package/dist/commands/sources/info.js +12 -1
  80. package/dist/commands/sources/init.js +1 -1
  81. package/dist/commands/sources/installed-stashes.js +119 -63
  82. package/dist/commands/sources/migration-help.js +1 -1
  83. package/dist/commands/sources/plugin-upgrade.js +8 -15
  84. package/dist/commands/sources/schema-repair.js +4 -10
  85. package/dist/commands/sources/source-add.js +2 -2
  86. package/dist/commands/sources/source-clone.js +31 -7
  87. package/dist/commands/sources/source-manage.js +4 -4
  88. package/dist/commands/targets.js +175 -0
  89. package/dist/commands/tasks/explain.js +104 -69
  90. package/dist/commands/tasks/tasks-cli.js +9 -11
  91. package/dist/commands/tasks/tasks.js +75 -97
  92. package/dist/commands/ui/api.js +11 -3
  93. package/dist/commands/ui/redact.js +5 -31
  94. package/dist/commands/ui/workspace-actions.js +1 -1
  95. package/dist/commands/workflow/plan.js +48 -9
  96. package/dist/commands/workflow-cli.js +40 -45
  97. package/dist/core/adapter/adapters/agent-skills-adapter.js +10 -11
  98. package/dist/core/adapter/adapters/akm-adapter.js +36 -41
  99. package/dist/core/adapter/adapters/akm-lint.js +10 -12
  100. package/dist/core/adapter/adapters/akm-metadata.js +2 -2
  101. package/dist/core/adapter/adapters/akm-task-adapter.js +8 -9
  102. package/dist/core/adapter/adapters/akm-workflow-adapter.js +5 -5
  103. package/dist/core/adapter/adapters/claude-adapter.js +1 -1
  104. package/dist/core/adapter/adapters/dotenv-adapter.js +7 -6
  105. package/dist/core/adapter/adapters/generic-files-adapter.js +5 -5
  106. package/dist/core/adapter/adapters/index.js +3 -4
  107. package/dist/core/adapter/adapters/llm-wiki-adapter.js +12 -14
  108. package/dist/core/adapter/adapters/okf-adapter.js +8 -10
  109. package/dist/core/adapter/adapters/opencode-adapter.js +1 -1
  110. package/dist/core/adapter/adapters/shared.js +3 -3
  111. package/dist/core/adapter/adapters/tool-dir-shared.js +7 -25
  112. package/dist/core/adapter/adapters/website-snapshot-adapter.js +3 -3
  113. package/dist/core/adapter/detect-adapter.js +1 -1
  114. package/dist/core/adapter/execution-source.js +13 -35
  115. package/dist/core/adapter/registry.js +1 -1
  116. package/dist/core/adapter/validate-context.js +2 -4
  117. package/dist/core/asset/asset-placement.js +1 -1
  118. package/dist/core/asset/asset-ref.js +3 -3
  119. package/dist/core/asset/resolve-ref.js +5 -0
  120. package/dist/core/bundle-rename.js +12 -4
  121. package/dist/core/closest-match.js +37 -0
  122. package/dist/core/common.js +37 -57
  123. package/dist/core/config/config-io.js +26 -18
  124. package/dist/core/config/config-schema.js +19 -106
  125. package/dist/core/config/config-sources.js +3 -3
  126. package/dist/core/config/config-types.js +4 -4
  127. package/dist/core/config/config-walker.js +50 -77
  128. package/dist/core/config/config.js +438 -109
  129. package/dist/core/config/convert-engine-references.js +114 -0
  130. package/dist/core/config/convert-engines-shared.js +40 -0
  131. package/dist/core/config/convert-engines.js +300 -0
  132. package/dist/core/config/convert-models-overlay.js +137 -0
  133. package/dist/core/config/credentials.js +304 -0
  134. package/dist/core/config/engine-semantics.js +3 -3
  135. package/dist/core/config/migrate-keys.js +222 -0
  136. package/dist/core/config/model-aliases.js +37 -0
  137. package/dist/core/config/models-file.js +34 -0
  138. package/dist/core/config/retired-engines.js +53 -0
  139. package/dist/core/config/schema/connections.js +41 -0
  140. package/dist/core/config/schema/embedding.js +2 -2
  141. package/dist/core/config/schema/harnesses.js +41 -0
  142. package/dist/core/config/schema/improve-processes.js +9 -28
  143. package/dist/core/config/schema/improve.js +36 -1
  144. package/dist/core/config/schema/index-config.js +18 -20
  145. package/dist/core/config/schema/primitives.js +42 -17
  146. package/dist/core/config/schema/search.js +6 -11
  147. package/dist/core/config/schema/sources-bundles.js +9 -9
  148. package/dist/core/config/schema/workflow.js +2 -6
  149. package/dist/core/config/unusable-entries.js +66 -0
  150. package/dist/core/content-safety.js +3 -6
  151. package/dist/core/endpoint-url.js +47 -0
  152. package/dist/core/env-secret-ref.js +84 -73
  153. package/dist/core/errors.js +10 -16
  154. package/dist/core/extra-params.js +3 -3
  155. package/dist/core/improve-result.js +8 -9
  156. package/dist/core/json-schema.js +1 -1
  157. package/dist/core/lesson-lint.js +1 -1
  158. package/dist/core/llm-inference.js +80 -0
  159. package/dist/core/parse.js +2 -2
  160. package/dist/core/paths.js +33 -10
  161. package/dist/core/recognition-util.js +8 -10
  162. package/dist/core/redaction.js +94 -55
  163. package/dist/core/ref-token.js +82 -0
  164. package/dist/core/secret-value.js +14 -0
  165. package/dist/core/state/migrations.js +17 -22
  166. package/dist/core/structured.js +2 -68
  167. package/dist/core/subprocess.js +63 -22
  168. package/dist/core/type-presentation.js +10 -14
  169. package/dist/core/write-source.js +20 -4
  170. package/dist/execution/input-contract.js +3 -6
  171. package/dist/execution/notices.js +49 -0
  172. package/dist/execution/resolved-request.js +24 -40
  173. package/dist/execution/source.js +21 -45
  174. package/dist/execution/target-ref.js +2 -2
  175. package/dist/indexer/bundle-identity-guard.js +6 -5
  176. package/dist/indexer/ensure-index.js +1 -1
  177. package/dist/indexer/index-writer-lock.js +1 -1
  178. package/dist/indexer/indexer.js +46 -42
  179. package/dist/indexer/installations.js +12 -22
  180. package/dist/indexer/lookup/adapter-concept-owner.js +40 -14
  181. package/dist/indexer/materialize-embeddings.js +20 -16
  182. package/dist/indexer/passes/memory-inference.js +21 -50
  183. package/dist/indexer/passes/metadata.js +5 -5
  184. package/dist/indexer/scan/doc-to-entry.js +1 -1
  185. package/dist/indexer/scan/drain-dir.js +9 -11
  186. package/dist/indexer/search/db-search.js +9 -7
  187. package/dist/indexer/search/search-source.js +42 -22
  188. package/dist/indexer/usage/usage-events.js +2 -3
  189. package/dist/indexer/walk/index-context.js +2 -1
  190. package/dist/indexer/walk/matchers.js +3 -11
  191. package/dist/indexer/walk/path-resolver.js +13 -68
  192. package/dist/indexer/walk/walker.js +32 -26
  193. package/dist/integrations/agent/akm-read-tools.js +81 -0
  194. package/dist/integrations/agent/config.js +1 -1
  195. package/dist/integrations/agent/detect.js +32 -36
  196. package/dist/integrations/agent/execution.js +130 -428
  197. package/dist/integrations/agent/index.js +1 -1
  198. package/dist/integrations/agent/profiles.js +24 -28
  199. package/dist/integrations/agent/prompts.js +17 -30
  200. package/dist/integrations/agent/request-lowering.js +67 -101
  201. package/dist/integrations/agent/resolution.js +332 -0
  202. package/dist/integrations/agent/runner-dispatch.js +156 -181
  203. package/dist/integrations/agent/runner.js +20 -70
  204. package/dist/integrations/agent/runtime-port.js +4 -0
  205. package/dist/integrations/agent/runtime.js +10 -0
  206. package/dist/integrations/agent/spawn.js +65 -190
  207. package/dist/integrations/harnesses/aider/index.js +19 -39
  208. package/dist/integrations/harnesses/amazonq/index.js +17 -32
  209. package/dist/integrations/harnesses/claude/index.js +59 -28
  210. package/dist/integrations/harnesses/claude/session-log.js +7 -5
  211. package/dist/integrations/harnesses/codex/index.js +19 -31
  212. package/dist/integrations/harnesses/copilot/index.js +17 -35
  213. package/dist/integrations/harnesses/gemini/index.js +18 -34
  214. package/dist/integrations/harnesses/harness.js +36 -0
  215. package/dist/integrations/harnesses/index.js +20 -51
  216. package/dist/integrations/harnesses/opencode/agent-builder.js +132 -100
  217. package/dist/integrations/harnesses/opencode/index.js +11 -33
  218. package/dist/integrations/harnesses/opencode/roots.js +50 -0
  219. package/dist/integrations/harnesses/opencode/runtime/binary.js +206 -0
  220. package/dist/integrations/harnesses/opencode/runtime/command.js +152 -0
  221. package/dist/integrations/harnesses/opencode/runtime/index.js +177 -0
  222. package/dist/integrations/harnesses/opencode/runtime/tar.js +107 -0
  223. package/dist/integrations/harnesses/opencode/session-log-v1.js +15 -7
  224. package/dist/integrations/harnesses/opencode/session-log-v2.js +72 -48
  225. package/dist/integrations/harnesses/opencode/session-log.js +58 -137
  226. package/dist/integrations/harnesses/opencode/version.js +34 -53
  227. package/dist/integrations/harnesses/openhands/index.js +13 -37
  228. package/dist/integrations/harnesses/pi/index.js +21 -32
  229. package/dist/integrations/lockfile.js +1 -1
  230. package/dist/integrations/session-logs/index.js +11 -31
  231. package/dist/llm/client.js +93 -111
  232. package/dist/llm/embedder.js +19 -34
  233. package/dist/llm/embedders/connection.js +31 -0
  234. package/dist/llm/embedders/local-model-files.js +56 -0
  235. package/dist/llm/embedders/local.js +146 -115
  236. package/dist/llm/embedders/remote.js +26 -23
  237. package/dist/llm/embedders/types.js +1 -1
  238. package/dist/llm/index-passes.js +33 -39
  239. package/dist/llm/memory-infer.js +67 -77
  240. package/dist/llm/model-list.js +35 -0
  241. package/dist/llm/rerank-client.js +8 -14
  242. package/dist/llm/structured-call.js +45 -99
  243. package/dist/llm/structured.js +60 -0
  244. package/dist/output/renderers.js +2 -8
  245. package/dist/output/shapes/helpers.js +18 -44
  246. package/dist/output/shapes/passthrough.js +1 -0
  247. package/dist/output/shapes.js +1 -7
  248. package/dist/output/text/command-format.js +41 -39
  249. package/dist/output/text/health-format.js +8 -2
  250. package/dist/output/text/helpers.js +1 -1
  251. package/dist/output/text/migrate.js +12 -0
  252. package/dist/output/text/models.js +3 -2
  253. package/dist/output/text/registry-commands.js +1 -2
  254. package/dist/output/text/show-directives.js +16 -28
  255. package/dist/output/text/show-format.js +0 -2
  256. package/dist/output/text/targets-table.js +70 -0
  257. package/dist/output/text/workflow-format.js +44 -15
  258. package/dist/output/text.js +1 -1
  259. package/dist/registry/providers/skills-sh.js +1 -28
  260. package/dist/registry/providers/static-index.js +1 -71
  261. package/dist/runtime.js +6 -2
  262. package/dist/scripts/akm-migrate-node.js +47095 -44905
  263. package/dist/scripts/akm-migrate.js +46946 -44755
  264. package/dist/setup/detect.js +28 -273
  265. package/dist/setup/endpoint.js +36 -0
  266. package/dist/setup/semantic-assets.js +13 -19
  267. package/dist/setup/setup-flags.js +77 -0
  268. package/dist/setup/setup-plan.js +144 -0
  269. package/dist/setup/setup.js +134 -358
  270. package/dist/setup/steps/connection-shared.js +49 -101
  271. package/dist/setup/steps/connection.js +121 -524
  272. package/dist/setup/steps/embeddings.js +151 -0
  273. package/dist/setup/steps/harnesses.js +118 -0
  274. package/dist/setup/steps/platforms.js +2 -81
  275. package/dist/setup/steps/tasks.js +1 -2
  276. package/dist/setup/steps.js +10 -0
  277. package/dist/setup/summary.js +67 -0
  278. package/dist/sources/freshness.js +16 -21
  279. package/dist/sources/providers/git-provider.js +2 -9
  280. package/dist/sources/providers/provider-utils.js +13 -5
  281. package/dist/sources/resolve.js +12 -16
  282. package/dist/sources/snapshot-fetchers/robots.js +11 -12
  283. package/dist/sources/snapshot-fetchers/secret-seam.js +2 -4
  284. package/dist/sources/snapshot-fetchers/website-ingest.js +8 -15
  285. package/dist/sources/snapshot-fetchers/x.js +1 -1
  286. package/dist/standalone/onnxruntime-library.js +52 -0
  287. package/dist/storage/repositories/improve-ledger-repository.js +1 -1
  288. package/dist/storage/repositories/improve-runs-repository.js +0 -13
  289. package/dist/storage/repositories/index-entries-repository.js +2 -2
  290. package/dist/storage/repositories/proposals-repository.js +2 -2
  291. package/dist/storage/repositories/task-history-repository.js +2 -2
  292. package/dist/storage/repositories/workflow-runs-repository.js +44 -129
  293. package/dist/storage/sqlite-read-snapshot.js +33 -29
  294. package/dist/tasks/embedded.js +1 -1
  295. package/dist/tasks/log-redaction.js +7 -119
  296. package/dist/tasks/prepare/prepare-support.js +8 -7
  297. package/dist/tasks/prepare/prepare.js +8 -9
  298. package/dist/tasks/prepare/script-capture.js +9 -25
  299. package/dist/tasks/run/attempt-lifecycle.js +17 -35
  300. package/dist/tasks/run/load-task.js +54 -27
  301. package/dist/tasks/run/run-command-task.js +15 -10
  302. package/dist/tasks/run/run-native-task.js +8 -21
  303. package/dist/tasks/run/run-task.js +5 -8
  304. package/dist/tasks/run/run-workflow-task.js +14 -21
  305. package/dist/tasks/run/task-history.js +9 -17
  306. package/dist/tasks/run/task-log.js +1 -5
  307. package/dist/tasks/scheduler-invocation.js +13 -18
  308. package/dist/tasks/scheduler-sync.js +0 -1
  309. package/dist/tasks/source/bounded-document.js +31 -117
  310. package/dist/tasks/source/parse-task-source.js +0 -1
  311. package/dist/tasks/source/project-v4.js +5 -3
  312. package/dist/tasks/source/task-input-diagnostics.js +3 -4
  313. package/dist/tasks/source/task-source-v4.js +48 -72
  314. package/dist/tasks/source-v3.js +2 -5
  315. package/dist/tasks/task-run-reserved-flags.js +1 -2
  316. package/dist/workflows/authoring/authoring.js +2 -7
  317. package/dist/workflows/compile.js +1 -15
  318. package/dist/workflows/concurrency-policy.js +4 -4
  319. package/dist/workflows/exec/child-workflow.js +1 -4
  320. package/dist/workflows/exec/dispatch-redaction.js +35 -86
  321. package/dist/workflows/exec/environment.js +13 -77
  322. package/dist/workflows/exec/exec-unit.js +14 -55
  323. package/dist/workflows/exec/frozen-judge.js +26 -28
  324. package/dist/workflows/exec/native-executor.js +126 -71
  325. package/dist/workflows/exec/run-workflow.js +38 -42
  326. package/dist/workflows/exec/step-work.js +13 -17
  327. package/dist/workflows/exec/unit-dispatch.js +7 -21
  328. package/dist/workflows/freeze/environment.js +14 -119
  329. package/dist/workflows/freeze/freeze.js +14 -33
  330. package/dist/workflows/freeze/step-values.js +6 -13
  331. package/dist/workflows/freeze/targets/child-workflow.js +3 -5
  332. package/dist/workflows/freeze/targets/command.js +9 -15
  333. package/dist/workflows/freeze/targets/shell.js +0 -3
  334. package/dist/workflows/freeze/task-bindings.js +0 -1
  335. package/dist/workflows/parser.js +50 -59
  336. package/dist/workflows/plan.js +8 -2
  337. package/dist/workflows/renderer.js +83 -9
  338. package/dist/workflows/resource-limits.js +2 -2
  339. package/dist/workflows/runtime/run-plan.js +38 -23
  340. package/dist/workflows/runtime/runs.js +55 -93
  341. package/dist/workflows/runtime/workflow-asset-loader.js +55 -86
  342. package/dist/workflows/source-files.js +7 -19
  343. package/docs/README.md +10 -16
  344. package/docs/architecture/README.md +2 -37
  345. package/docs/architecture/adapters.md +2 -2
  346. package/docs/architecture/akm-core-principles.md +256 -118
  347. package/docs/architecture/architecture.md +283 -99
  348. package/docs/architecture/improvement.md +8 -2
  349. package/docs/architecture/internals/classification.md +1 -3
  350. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +7 -6
  351. package/docs/architecture/internals/health-advisories.md +35 -74
  352. package/docs/architecture/internals/improve-workflow.md +27 -26
  353. package/docs/architecture/internals/indexing.md +3 -3
  354. package/docs/architecture/internals/storage-locations.md +48 -15
  355. package/docs/architecture/persisted-data-compat.md +10 -6
  356. package/docs/architecture/specs/okf-support.md +9 -11
  357. package/docs/architecture/specs/ref.md +7 -9
  358. package/docs/architecture/testing/testing-workflow.md +172 -52
  359. package/docs/architecture/workflow-engine.md +51 -34
  360. package/docs/guides/README.md +0 -1
  361. package/docs/guides/author-bundles.md +15 -20
  362. package/docs/guides/author-workflows.md +93 -71
  363. package/docs/guides/bundles.md +20 -16
  364. package/docs/guides/discover-and-load.md +1 -1
  365. package/docs/guides/getting-started.md +49 -3
  366. package/docs/guides/improve-the-library.md +10 -4
  367. package/docs/guides/recipes/headless-install.md +11 -16
  368. package/docs/guides/run-workflows.md +29 -20
  369. package/docs/guides/scheduling.md +2 -2
  370. package/docs/guides/use-with-any-agent.md +20 -22
  371. package/docs/integration/bundling-akm.md +19 -9
  372. package/docs/maintainers/README.md +2 -6
  373. package/docs/maintainers/release-checklist.md +73 -126
  374. package/docs/migration/README.md +10 -3
  375. package/docs/migration/release-notes/0.10.0.md +81 -16
  376. package/docs/migration/release-notes/0.6.0.md +1 -1
  377. package/docs/migration/release-notes/0.7.0.md +1 -1
  378. package/docs/migration/release-notes/0.9.0.md +1 -1
  379. package/docs/migration/release-notes/README.md +1 -1
  380. package/docs/migration/v0.9-to-v0.10.md +300 -60
  381. package/docs/migration/v0.9.1-to-v0.9.2.md +6 -5
  382. package/docs/posts/config-v2-lessons-16.md +1 -1
  383. package/docs/posts/llm-mode-reflect-13.md +1 -1
  384. package/docs/posts/release-0.6.0.md +11 -11
  385. package/docs/posts/release-0.8.0.md +4 -4
  386. package/docs/posts/workflows-vaults-09.md +1 -1
  387. package/docs/reference/README.md +1 -1
  388. package/docs/reference/asset-types.md +2 -2
  389. package/docs/reference/cli.md +477 -388
  390. package/docs/reference/configuration.md +569 -550
  391. package/docs/reference/data-and-telemetry.md +9 -6
  392. package/docs/reference/env-and-secrets.md +72 -15
  393. package/docs/reference/refs.md +3 -20
  394. package/docs/reference/registry.md +12 -38
  395. package/docs/reference/supported-formats.md +1 -1
  396. package/docs/reference/tasks.md +22 -11
  397. package/docs/reference/ui-api.md +14 -9
  398. package/docs/reference/workflow-schema.md +199 -154
  399. package/docs/reference/workflows.md +4 -6
  400. package/package.json +9 -11
  401. package/schemas/akm-config.json +1696 -2922
  402. package/schemas/akm-task.json +4 -4
  403. package/schemas/akm-workflow.json +36 -9
  404. package/ROADMAP.md +0 -112
  405. package/dist/assets/models.json +0 -35
  406. package/dist/commands/health/engine-usage.js +0 -45
  407. package/dist/commands/sources/dangerous-env-audit.js +0 -134
  408. package/dist/core/activation-policy.js +0 -67
  409. package/dist/core/config/schema/engines.js +0 -160
  410. package/dist/core/config/schema/execution.js +0 -23
  411. package/dist/core/spawn-env.js +0 -268
  412. package/dist/execution/directory-identity.js +0 -78
  413. package/dist/integrations/agent/builder-shared.js +0 -33
  414. package/dist/integrations/agent/builders.js +0 -43
  415. package/dist/integrations/agent/engine-fallback.js +0 -59
  416. package/dist/integrations/agent/engine-resolution.js +0 -333
  417. package/dist/integrations/agent/model-map.js +0 -475
  418. package/dist/integrations/harnesses/aider/agent-builder.js +0 -107
  419. package/dist/integrations/harnesses/aider/result-extractor.js +0 -53
  420. package/dist/integrations/harnesses/amazonq/agent-builder.js +0 -138
  421. package/dist/integrations/harnesses/amazonq/result-extractor.js +0 -48
  422. package/dist/integrations/harnesses/claude/agent-builder.js +0 -106
  423. package/dist/integrations/harnesses/claude/config-import.js +0 -67
  424. package/dist/integrations/harnesses/claude/result-extractor.js +0 -67
  425. package/dist/integrations/harnesses/codex/agent-builder.js +0 -150
  426. package/dist/integrations/harnesses/codex/result-extractor.js +0 -110
  427. package/dist/integrations/harnesses/copilot/agent-builder.js +0 -117
  428. package/dist/integrations/harnesses/copilot/result-extractor.js +0 -178
  429. package/dist/integrations/harnesses/gemini/agent-builder.js +0 -114
  430. package/dist/integrations/harnesses/gemini/result-extractor.js +0 -149
  431. package/dist/integrations/harnesses/opencode/config-import.js +0 -149
  432. package/dist/integrations/harnesses/opencode/model-config.js +0 -33
  433. package/dist/integrations/harnesses/opencode/model-work-agent.js +0 -115
  434. package/dist/integrations/harnesses/opencode/session-log-types.js +0 -12
  435. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -44
  436. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -1052
  437. package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +0 -199
  438. package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +0 -254
  439. package/dist/integrations/harnesses/opencode-sdk/wire.js +0 -66
  440. package/dist/integrations/harnesses/openhands/agent-builder.js +0 -120
  441. package/dist/integrations/harnesses/openhands/result-extractor.js +0 -135
  442. package/dist/integrations/harnesses/pi/agent-builder.js +0 -89
  443. package/dist/integrations/harnesses/pi/result-extractor.js +0 -168
  444. package/dist/integrations/harnesses/shared.js +0 -18
  445. package/dist/integrations/harnesses/types.js +0 -55
  446. package/dist/llm/feature-gate.js +0 -115
  447. package/dist/output/shapes/models-list.js +0 -16
  448. package/dist/output/shapes/registry-search.js +0 -10
  449. package/dist/setup/detected-engines.js +0 -136
  450. package/dist/setup/engine-config.js +0 -97
  451. package/dist/setup/harness-config-import.js +0 -47
  452. package/dist/setup/providers.js +0 -14
  453. package/dist/setup/steps/semantic.js +0 -39
  454. package/dist/workflows/authoring/scope-key.js +0 -79
  455. package/dist/workflows/exec/lowering-notices.js +0 -23
  456. package/dist/workflows/source-semantics.js +0 -110
  457. package/docs/architecture/0.10-process-rules.md +0 -832
  458. package/docs/architecture/akm-architecture-decision-history.md +0 -2055
  459. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +0 -550
  460. package/docs/architecture/decisions/0001-no-interpolation-attached-structured-context.md +0 -72
  461. package/docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md +0 -142
  462. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +0 -189
  463. package/docs/architecture/decisions/0004-task-input-contract-and-flag-coercion.md +0 -97
  464. package/docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md +0 -108
  465. package/docs/architecture/decisions/0006-task-source-version-routing.md +0 -124
  466. package/docs/architecture/decisions/0007-workflow-composition-bounds.md +0 -83
  467. package/docs/architecture/decisions/0008-task-binding-normalization.md +0 -88
  468. package/docs/architecture/decisions/0009-child-run-publication-column-parity.md +0 -80
  469. package/docs/architecture/decisions/0010-driver-protocol-cut.md +0 -36
  470. package/docs/architecture/decisions/0011-engine-run-loop-invariants.md +0 -125
  471. package/docs/architecture/decisions/README.md +0 -62
  472. package/docs/architecture/internals/functional-contract-patterns.md +0 -340
  473. package/docs/architecture/internals/registry-network-boundary.md +0 -57
  474. package/docs/architecture/internals/search.md +0 -248
  475. package/docs/architecture/reviews/env-secret-access.md +0 -319
  476. package/docs/architecture/runtime-boundary-design.md +0 -153
  477. package/docs/architecture/specs/0.9.0-close-out-plan.md +0 -361
  478. package/docs/architecture/specs/0.9.0-decisions.md +0 -532
  479. package/docs/architecture/specs/0.9.0-docs-code-drift-register.md +0 -745
  480. package/docs/architecture/specs/0.9.0-open-items-register.md +0 -135
  481. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +0 -740
  482. package/docs/architecture/specs/0.9.0-release-surface-review.md +0 -636
  483. package/docs/architecture/specs/agent-command-engine-model-design.md +0 -513
  484. package/docs/architecture/specs/akm-0.9.0-bundle-adapter-spec.md +0 -501
  485. package/docs/architecture/specs/akm-0.9.0-ref-grammar-decision.md +0 -291
  486. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +0 -2059
  487. package/docs/architecture/specs/di-seams-plan.md +0 -539
  488. package/docs/architecture/specs/driver-protocol-keep-or-cut.md +0 -275
  489. package/docs/architecture/specs/fact-asset-type.md +0 -142
  490. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +0 -634
  491. package/docs/architecture/specs/reviews/task-workflow-unification/README.md +0 -72
  492. package/docs/architecture/specs/reviews/task-workflow-unification/findings-consolidated.md +0 -152
  493. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-alternatives.md +0 -352
  494. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-bunqueue.md +0 -300
  495. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-openworkflow.md +0 -402
  496. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-1.md +0 -758
  497. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-2.md +0 -645
  498. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-3.md +0 -602
  499. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-adversarial.md +0 -27
  500. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-architecture.md +0 -27
  501. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-coverage.md +0 -74
  502. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-security.md +0 -15
  503. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-adversarial.md +0 -132
  504. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-verification.md +0 -147
  505. package/docs/architecture/specs/stash-conventions-code-spec.md +0 -275
  506. package/docs/architecture/specs/stash-organization-conventions.md +0 -377
  507. package/docs/architecture/specs/task-workflow-format-unification.md +0 -721
  508. package/docs/architecture/specs/workflow-engine-buy-vs-build.md +0 -231
  509. package/docs/architecture/specs/workflow-format-unification.md +0 -432
  510. package/docs/architecture/testing/manual-testing-checklist.md +0 -3411
  511. package/docs/architecture/testing/okf-v0.2-conformance-runbook.md +0 -924
  512. package/docs/architecture/testing/task-workflow-manual-tests.md +0 -1720
  513. package/docs/guides/claude-code-vs-akm-workflows.md +0 -29
  514. package/docs/guides/knowledge-management.md +0 -18
  515. package/docs/maintainers/curate-workmap.md +0 -335
  516. package/docs/migration/v0.5-to-v0.6.md +0 -759
  517. package/docs/migration/v0.7-to-v0.8.md +0 -1378
  518. package/docs/plans/0.9.2-agent-command-workflow-plan.md +0 -423
@@ -8,14 +8,20 @@ import { isDeepStrictEqual } from "node:util";
8
8
  import { isBundleSlug } from "../asset/asset-ref.js";
9
9
  import { deriveBundleId } from "../bundle-id.js";
10
10
  import { isRecord, resolveStashDir } from "../common.js";
11
- import { ConfigError } from "../errors.js";
11
+ import { ConfigError, UsageError } from "../errors.js";
12
+ import { isRefToken, substituteRefTokens } from "../ref-token.js";
12
13
  import { formatRegistryLabel, hasRegistryUrlCredentials } from "../registry-url.js";
13
14
  import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
14
15
  import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
15
16
  import { bundleComponentConfig, bundleContentRoot, bundleContentRoots, bundleSourceId, bundlesToSourceEntries, filesystemBundleSourceId, isBundleEnabled, } from "./config-sources.js";
16
- import { resolveSchemaAt } from "./config-walker.js";
17
+ import { isOpaqueSchema, resolveSchemaAt } from "./config-walker.js";
18
+ import { convertEngineConfig } from "./convert-engines.js";
19
+ import { fileFlatCredentials, writeCredential } from "./credentials.js";
17
20
  import { deepMergeConfig, isPlainObject } from "./deep-merge.js";
18
- import { isApiKeyReference, SECRET_STORE_REFERENCE_PATTERN } from "./schema/primitives.js";
21
+ import { documentEndpoint, planKeyMoves } from "./migrate-keys.js";
22
+ import { modelsFileExists, readModelsFile, retireModelsFile } from "./models-file.js";
23
+ import { rememberEngineTargets, rememberedEngineTargets } from "./retired-engines.js";
24
+ import { dropUnusableEntries, findUnusableEntries } from "./unusable-entries.js";
19
25
  export { stripJsonComments } from "./config-io.js";
20
26
  import { getConfigPath } from "../paths.js";
21
27
  import { warn, warnOnce } from "../warn.js";
@@ -39,6 +45,24 @@ let cachedConfig;
39
45
  export function resetConfigCache() {
40
46
  cachedConfig = undefined;
41
47
  }
48
+ const INHERITED_CONNECTIONS = Symbol("inheritedConnections");
49
+ /**
50
+ * The connections only an `extends` base defines in `config`: this host's file names none of them. A base
51
+ * chooses such a connection's endpoint, so it never uses a key stored on this host: keys are for the
52
+ * connections this host's own config file defines. The loader attaches the set to the config it returns
53
+ * (not enumerable, so a copy made with spread has none), and a config that did not come from the loader
54
+ * has none inherited.
55
+ */
56
+ export function inheritedConnectionsOf(config) {
57
+ return config[INHERITED_CONNECTIONS] ?? new Set();
58
+ }
59
+ /**
60
+ * {@link inheritedConnectionsOf} the config as it is now on this host, loaded here when the cache holds
61
+ * nothing: for a caller with no config in hand.
62
+ */
63
+ export function isInheritedConnection(name) {
64
+ return inheritedConnectionsOf(loadUserConfig()).has(name);
65
+ }
42
66
  export function loadUserConfig() {
43
67
  const configPath = getConfigPath();
44
68
  let stat;
@@ -72,7 +96,10 @@ export function loadUserConfig() {
72
96
  cachedConfig = undefined;
73
97
  return { ...DEFAULT_CONFIG };
74
98
  }
75
- const finalConfig = parseAndValidateConfigText(text, configPath);
99
+ const lifted = runConfigFilePipeline(text, configPath);
100
+ const finalConfig = buildEffectiveConfig(lifted, configPath);
101
+ const written = isPlainConfigObject(lifted.connections) ? lifted.connections : {};
102
+ const inheritedConnections = new Set(Object.keys(finalConfig.connections ?? {}).filter((name) => !Object.hasOwn(written, name)));
76
103
  // Re-stat after potential write-back so the cache key reflects the new mtime.
77
104
  let finalStat = stat;
78
105
  try {
@@ -81,6 +108,7 @@ export function loadUserConfig() {
81
108
  catch {
82
109
  // Stat failed — use original stat for cache; no harm done.
83
110
  }
111
+ Object.defineProperty(finalConfig, INHERITED_CONNECTIONS, { value: inheritedConnections });
84
112
  cachedConfig = {
85
113
  config: finalConfig,
86
114
  path: configPath,
@@ -92,8 +120,9 @@ export function loadUserConfig() {
92
120
  /**
93
121
  * Run the per-file config pipeline every raw config object goes through
94
122
  * before it is either validated (the local/top-level file) or merged in as
95
- * an `extends` base: JSONC parse already done by the caller, then the
96
- * `configVersion` read ({@link readConfigVersion}). Unknown keys are not an
123
+ * an `extends` base: JSONC parse already done by the caller, then the retired
124
+ * `engines` form read as `connections`/`harnesses` ({@link readRetiredEngines})
125
+ * and the `configVersion` read ({@link readConfigVersion}). Unknown keys are not an
97
126
  * error: the schema drops them in memory. A shape older than 0.9.15 is not
98
127
  * converted here; it fails validation with a message naming `akm migrate
99
128
  * apply` under akm 0.9.x. Shared by {@link parseAndValidateConfigText} (the
@@ -102,7 +131,113 @@ export function loadUserConfig() {
102
131
  * the file that extends it.
103
132
  */
104
133
  function runConfigFilePipeline(text, sourcePath) {
105
- return readConfigVersion(parseConfigText(text, sourcePath), sourcePath);
134
+ return readRetiredEngines(readConfigFile(text, sourcePath), sourcePath);
135
+ }
136
+ /**
137
+ * A config file as it says it: parsed, with the retired `secret://` forms and
138
+ * `configVersion` read, and nothing else converted. An ordinary write edits this
139
+ * document, so what a read converts in memory (the retired `engines` form) is
140
+ * not rewritten until `akm migrate apply`.
141
+ */
142
+ function readConfigFile(text, sourcePath) {
143
+ const raw = parseConfigText(text, sourcePath);
144
+ const lifted = readRetiredSecretRefs(raw, [], sourcePath);
145
+ return readConfigVersion(lifted, sourcePath);
146
+ }
147
+ /** The overlay every file read folds in, unless the migrator is reading an `extends` base without it. */
148
+ let modelsOverlayOverride;
149
+ function modelsOverlay() {
150
+ return modelsOverlayOverride ? modelsOverlayOverride.overlay : readModelsFile();
151
+ }
152
+ /**
153
+ * The connections and harnesses an `extends` base defines on its own, without the
154
+ * models.json overlay the loader folds into them: what the migrator folds into
155
+ * entries only the base defines, as partial local entries, once the file is gone.
156
+ */
157
+ function inheritedEntries(document, configPath) {
158
+ if (document.extends === undefined)
159
+ return undefined;
160
+ modelsOverlayOverride = { overlay: undefined };
161
+ try {
162
+ const base = buildEffectiveConfig({ configVersion: document.configVersion, extends: document.extends }, configPath);
163
+ return { connections: { ...base.connections }, harnesses: { ...base.harnesses } };
164
+ }
165
+ finally {
166
+ modelsOverlayOverride = undefined;
167
+ }
168
+ }
169
+ /**
170
+ * What each old engine name now is, so an `engine:` selector (a flag, or a key in an asset or a task source)
171
+ * keeps meaning the service it always named: the conversion renames the engine `defaults` named to `default`
172
+ * and moves an old `default` aside. While the file is still in the `engines` form the answer is the
173
+ * conversion's own; after `akm migrate apply` has rewritten it, the names the migrator remembered, until
174
+ * every reference it can rewrite is rewritten. Resolution and the migrator both ask here.
175
+ */
176
+ export function retiredEngineTargets() {
177
+ const configPath = getConfigPath();
178
+ const text = readConfigText(configPath);
179
+ if (text === undefined)
180
+ return {};
181
+ const live = convertEngineConfig(readConfigFile(text, configPath), {
182
+ modelsOverlay: modelsOverlay(),
183
+ env: process.env,
184
+ }).targets;
185
+ return Object.keys(live).length > 0 ? live : rememberedEngineTargets();
186
+ }
187
+ /**
188
+ * `engines` / `defaults` are retired in favour of `connections` / `harnesses`.
189
+ * A config in the old form is read as the new one, in memory, with one warning;
190
+ * `akm migrate apply` writes it back (it runs {@link convertEngineConfig} too,
191
+ * through this pipeline, before it deletes the keys the schema does not know).
192
+ */
193
+ function readRetiredEngines(raw, sourcePath) {
194
+ const { config, notes, changed, keys } = convertEngineConfig(raw, {
195
+ modelsOverlay: modelsOverlay(),
196
+ env: process.env,
197
+ });
198
+ if (!changed)
199
+ return raw;
200
+ const keyNotes = keys.map(({ engine, field }) => `engines.${engine}: its ${field} is not read from config any more; \`akm migrate apply\` moves it to credentials.json.`);
201
+ const retiredForm = raw.engines !== undefined || raw.defaults !== undefined;
202
+ warnOnce(`config:retired-engines:${sourcePath ?? "inline"}`, `${sourcePath ?? "config.json"} ${retiredForm
203
+ ? "uses the retired engines/defaults form; reading it as connections/harnesses"
204
+ : "is read with the retired models.json folded into its models"}. \`akm migrate apply\` rewrites config.json${retiredForm ? "" : " and moves models.json aside"}.${[...notes, ...keyNotes].map((note) => `\n - ${note}`).join("")}`);
205
+ return config;
206
+ }
207
+ /** The retired `secret://<name>` credential form; it survives only in {@link readRetiredSecretRefs}. */
208
+ const SECRET_STORE_REFERENCE_PATTERN = /^secret:\/\/(.+)$/;
209
+ /**
210
+ * The `${ref}` a retired `secret://<x>` value reads as: `${<x>}` when `<x>` is
211
+ * already a full ref, else `${secrets/<x>}`. `undefined` for any other string.
212
+ * Shared by the config reader and the frozen workflow runner decoder.
213
+ */
214
+ export function liftRetiredSecretRef(value) {
215
+ const name = SECRET_STORE_REFERENCE_PATTERN.exec(value)?.[1];
216
+ if (name === undefined)
217
+ return undefined;
218
+ return isRefToken(`\${${name}}`) ? `\${${name}}` : `\${secrets/${name}}`;
219
+ }
220
+ /**
221
+ * `secret://<x>` is retired in favour of `${ref}`. A value in that form is read
222
+ * as its {@link liftRetiredSecretRef} with one warning naming the key; every
223
+ * ordinary config write and `akm migrate apply`'s `configFile` step then
224
+ * persist the new form.
225
+ */
226
+ function readRetiredSecretRefs(node, keyPath, sourcePath) {
227
+ if (typeof node === "string") {
228
+ const lifted = liftRetiredSecretRef(node);
229
+ if (lifted === undefined)
230
+ return node;
231
+ const dotted = keyPath.join(".");
232
+ warnOnce(`config:secret-scheme:${sourcePath ?? "inline"}:${dotted}`, `${dotted}${sourcePath ? ` at ${sourcePath}` : ""} uses the retired ${JSON.stringify(node)} form; reading it as ${JSON.stringify(lifted)}. \`akm migrate apply\` rewrites config.json.`);
233
+ return lifted;
234
+ }
235
+ if (Array.isArray(node))
236
+ return node.map((child, i) => readRetiredSecretRefs(child, [...keyPath, String(i)], sourcePath));
237
+ if (isPlainConfigObject(node)) {
238
+ return Object.fromEntries(Object.entries(node).map(([key, child]) => [key, readRetiredSecretRefs(child, [...keyPath, key], sourcePath)]));
239
+ }
240
+ return node;
106
241
  }
107
242
  /**
108
243
  * `configVersion` is read, never gated on. `"0.9.0"` is the only value akm
@@ -131,7 +266,11 @@ function readConfigVersion(raw, sourcePath) {
131
266
  * keeps around, instead of only getting the final merged `AkmConfig` back.
132
267
  */
133
268
  function buildEffectiveConfig(liftedLocalRaw, sourcePath) {
134
- const withExtends = resolveExtendsChain(liftedLocalRaw, sourcePath);
269
+ const chained = dropUnusableEntries(resolveExtendsChain(liftedLocalRaw, sourcePath), sourcePath);
270
+ // Writing an `embedding` block says semantic search is wanted: a config that names no mode reads as "auto".
271
+ const withExtends = chained.embedding !== undefined && chained.semanticSearchMode === undefined
272
+ ? { ...chained, semanticSearchMode: "auto" }
273
+ : chained;
135
274
  const where = sourcePath ? ` at ${sourcePath}` : "";
136
275
  warnUnknownConfigKeys(liftedLocalRaw, sourcePath);
137
276
  const parsed = AkmConfigSchema.safeParse(withExtends);
@@ -153,8 +292,8 @@ function buildEffectiveConfig(liftedLocalRaw, sourcePath) {
153
292
  * per-pass catchall schema (`IndexPassConfigSchema`), so `resolveSchemaAt`
154
293
  * below never returns `undefined` for them and the walk would otherwise
155
294
  * treat them as a live, ordinary pass. Named here so the same three
156
- * guarantees apply as any other retired config key (AGENTS.md "Reading
157
- * persisted data"): a config setting one keeps loading, is named once by
295
+ * guarantees apply as any other retired config key: a config setting one keeps
296
+ * loading, is named once by
158
297
  * the unknown-key warning, and is dropped only by `akm migrate apply`.
159
298
  * `index.graph` (the LLM entity-graph extraction pass) and
160
299
  * `index.metadataEnhance` (LLM metadata enrichment) were retired in
@@ -172,10 +311,14 @@ export function unknownConfigKeyPaths(root, node = root, prefix = []) {
172
311
  const found = [];
173
312
  for (const key of Object.keys(node).sort()) {
174
313
  const keyPath = [...prefix, key];
175
- if (RETIRED_CATCHALL_KEY_PATHS.has(keyPath.join(".")) || resolveSchemaAt(keyPath, root) === undefined) {
314
+ const schema = resolveSchemaAt(keyPath, root);
315
+ if (RETIRED_CATCHALL_KEY_PATHS.has(keyPath.join(".")) || schema === undefined) {
176
316
  found.push(keyPath);
177
317
  continue;
178
318
  }
319
+ // A request field on a connection (`chat_template_kwargs`, ...) is the endpoint's, not akm's: its contents are not keys akm knows.
320
+ if (isOpaqueSchema(schema))
321
+ continue;
179
322
  const value = node[key];
180
323
  if (isPlainConfigObject(value))
181
324
  found.push(...unknownConfigKeyPaths(root, value, keyPath));
@@ -291,7 +434,6 @@ const HOST_LOCAL_CONFIG_KEYS = new Set([
291
434
  "defaultBundle",
292
435
  "defaultWriteTarget",
293
436
  "embedding",
294
- "execution",
295
437
  "experimental",
296
438
  "registries",
297
439
  "scheduler",
@@ -312,27 +454,30 @@ function sanitizeInheritedConfig(raw, label) {
312
454
  delete inherited[key];
313
455
  warnOnce(`config:inherited-host-local:${label}:${key}`, `Ignoring inherited config key ${JSON.stringify(key)} from ${label}; it is host-local and must be declared in the top-level config file.`);
314
456
  }
315
- if (isPlainObject(inherited.engines)) {
316
- const engines = {};
317
- let strippedAuthority = false;
318
- for (const [name, engine] of Object.entries(inherited.engines)) {
319
- if (!isPlainObject(engine)) {
320
- engines[name] = engine;
457
+ let strippedAuthority = false;
458
+ for (const section of ["connections", "harnesses"]) {
459
+ const entries = inherited[section];
460
+ if (!isPlainObject(entries))
461
+ continue;
462
+ const portableEntries = {};
463
+ for (const [name, entry] of Object.entries(entries)) {
464
+ if (!isPlainObject(entry)) {
465
+ portableEntries[name] = entry;
321
466
  continue;
322
467
  }
323
- const portable = { ...engine };
324
- for (const key of ["apiKey", "apiKeyFile", "bin", "args", "workspace"]) {
468
+ const portable = { ...entry };
469
+ for (const key of ["apiKey", "bin", "args", "workspace"]) {
325
470
  if (!Object.hasOwn(portable, key))
326
471
  continue;
327
472
  delete portable[key];
328
473
  strippedAuthority = true;
329
474
  }
330
- engines[name] = portable;
331
- }
332
- inherited.engines = engines;
333
- if (strippedAuthority) {
334
- warnOnce(`config:inherited-host-local:${label}:engines-authority`, `Ignoring inherited engine credentials, executable arguments, or workspace from ${label}; those fields are host-local.`);
475
+ portableEntries[name] = portable;
335
476
  }
477
+ inherited[section] = portableEntries;
478
+ }
479
+ if (strippedAuthority) {
480
+ warnOnce(`config:inherited-host-local:${label}:connections-authority`, `Ignoring inherited connection credentials, harness executable arguments, or workspace from ${label}; those fields are host-local.`);
336
481
  }
337
482
  if (isPlainObject(inherited.search) && Object.hasOwn(inherited.search, "curateRerank")) {
338
483
  const { curateRerank: _curateRerank, ...portableSearch } = inherited.search;
@@ -359,7 +504,7 @@ function sanitizeInheritedConfig(raw, label) {
359
504
  }
360
505
  return inherited;
361
506
  }
362
- /** `~` expands to the home directory, mirroring `apiKeyFile`'s resolution (engine-resolution.ts). */
507
+ /** `~` expands to the home directory. */
363
508
  function expandExtendsHomePath(p) {
364
509
  return p.startsWith("~") ? path.join(os.homedir(), p.slice(1)) : p;
365
510
  }
@@ -524,6 +669,39 @@ export function getEffectiveRegistries(config) {
524
669
  export function getImproveProcessConfig(processName, selected) {
525
670
  return selected?.processes?.[processName];
526
671
  }
672
+ /** The entry named `default`, else the lone entry, else none. */
673
+ function defaultEntry(entries) {
674
+ if (!entries)
675
+ return undefined;
676
+ const names = Object.keys(entries);
677
+ const name = Object.hasOwn(entries, "default") ? "default" : names.length === 1 ? names[0] : undefined;
678
+ return name === undefined ? undefined : { name, entry: entries[name] };
679
+ }
680
+ /**
681
+ * What is wrong when connections or harnesses are configured but none of a kind is the default: several, and
682
+ * none called `default`. The phrase (no final period) names the fix. Undefined when every kind that is configured has a default.
683
+ */
684
+ export function ambiguousDefaultAdvice(config) {
685
+ const advice = ["connection", "harness"].flatMap((kind) => {
686
+ const names = Object.keys((kind === "connection" ? config.connections : config.harnesses) ?? {});
687
+ if (names.length < 2 || names.includes("default"))
688
+ return [];
689
+ return [
690
+ `Several ${kind}s are configured (${names.join(", ")}) and none is named "default": name one default in config.json, or pick one with --${kind}`,
691
+ ];
692
+ });
693
+ return advice.length > 0 ? advice.join("; ") : undefined;
694
+ }
695
+ /** The default connection: the one named `default`, else the only one. */
696
+ export function defaultConnection(config) {
697
+ const found = defaultEntry(config.connections);
698
+ return found ? { name: found.name, connection: found.entry } : undefined;
699
+ }
700
+ /** The default harness: the one named `default`, else the only one. */
701
+ export function defaultHarness(config) {
702
+ const found = defaultEntry(config.harnesses);
703
+ return found ? { name: found.name, harness: found.entry } : undefined;
704
+ }
527
705
  export function loadConfig() {
528
706
  return loadUserConfig();
529
707
  }
@@ -552,9 +730,9 @@ export function validateCompleteConfig(config) {
552
730
  "If this looks like an akm bug, re-run with --debug to attach the traceback.");
553
731
  }
554
732
  /**
555
- * #945 review finding: `mutateConfig` used to write the entire extends-merged
556
- * *effective* config (`next`) back to the local file on every `config
557
- * set`/`unset`, duplicating every inherited `engines`/`improve.strategies`
733
+ * `mutateConfig` must not write the entire extends-merged *effective* config
734
+ * (`next`) back to the local file on every `config set`/`unset`: that would
735
+ * duplicate every inherited `connections`/`improve.strategies`
558
736
  * field into the local file on the very first ordinary write after adopting
559
737
  * `extends` — defeating the feature (shared config, ≤20-line local files)
560
738
  * and silently freezing the local copy against future upstream changes.
@@ -590,10 +768,102 @@ function pruneUnchangedInheritedFields(before, after, localRaw) {
590
768
  return result;
591
769
  }
592
770
  /**
593
- * What to persist for a `mutateConfig`/`mutateConfigWithPrecommit` write:
594
- * only the changed-or-already-local fields. This applies both to inherited
595
- * configs and ordinary configs: lifecycle mutations must not serialize every
596
- * schema default merely because validation materialized it in memory (#972).
771
+ * Apply the edit `before` → `after` to `target` (the file's own document) and
772
+ * nothing else: a key that is the same on both sides is left as the file has it,
773
+ * whatever shape that is, so an unrelated write neither converts nor drops what
774
+ * the reader only reads (the retired `engines` form, an inherited field, a key
775
+ * the schema does not know). Only the paths that changed are written, at the same
776
+ * path in `target`; an object the file lacks gets only the changed part. The
777
+ * keys of a nested map follow `after`, which is how a mutation says where it
778
+ * inserted one (`bundles`); the top level keeps the file's authored order. A
779
+ * removal of a key the document does not hold is left undone and recorded in
780
+ * `unremoved`, with the path of the key.
781
+ */
782
+ function applyEdit(target, before, after, unremoved = [], at = []) {
783
+ for (const key of Object.keys(before)) {
784
+ if (Object.hasOwn(after, key))
785
+ continue;
786
+ if (Object.hasOwn(target, key))
787
+ delete target[key];
788
+ else
789
+ unremoved.push([...at, key]);
790
+ }
791
+ for (const [key, value] of Object.entries(after)) {
792
+ const was = before[key];
793
+ if (isDeepStrictEqual(value, was))
794
+ continue;
795
+ const existing = target[key];
796
+ // An entry the reader dropped (a connection with no endpoint) is absent from
797
+ // `before` but present in the file: an edit to it adds to what the file has.
798
+ const merges = was === undefined ? isPlainObject(existing) : isPlainObject(was);
799
+ if (!isPlainObject(value) || !merges || (existing !== undefined && !isPlainObject(existing))) {
800
+ target[key] = value;
801
+ continue;
802
+ }
803
+ const part = existing ?? {};
804
+ applyEdit(part, isPlainObject(was) ? was : {}, value, unremoved, [...at, key]);
805
+ followKeyOrder(part, value);
806
+ if (existing !== undefined || Object.keys(part).length > 0)
807
+ target[key] = part;
808
+ }
809
+ }
810
+ /** Put the keys of `target` in the order of `order`, those it does not name after. */
811
+ function followKeyOrder(target, order) {
812
+ const keys = [
813
+ ...Object.keys(order).filter((key) => Object.hasOwn(target, key)),
814
+ ...Object.keys(target).filter((key) => !Object.hasOwn(order, key)),
815
+ ];
816
+ const entries = keys.map((key) => [key, target[key]]);
817
+ for (const key of Object.keys(target))
818
+ delete target[key];
819
+ for (const [key, value] of entries)
820
+ target[key] = value;
821
+ }
822
+ /**
823
+ * What an ordinary `mutateConfig`/`mutateConfigWithPrecommit` write persists:
824
+ * the file's own document with the edit `current` → `next` applied
825
+ * ({@link applyEdit}). Lifecycle mutations must not serialize every schema
826
+ * default merely because validation materialized it in memory (#972), nor the
827
+ * fields that only came along from `extends` (#945).
828
+ */
829
+ function editedConfigFile(document, current, next, persistTopLevelKeys = [], unremoved = []) {
830
+ // A first write keeps the established full-default scaffold. Besides being
831
+ // useful to a new user, the add -> rejected-install -> remove lifecycle
832
+ // relies on that symmetry to return a pristine install to DEFAULT_CONFIG.
833
+ if (document === undefined)
834
+ return next;
835
+ // Keys stay in their authored order and new ones follow in `next` order: the
836
+ // add -> remove lifecycle writes the same logical object twice and must come
837
+ // back byte for byte.
838
+ const body = structuredClone(document);
839
+ applyEdit(body, current, next, unremoved);
840
+ // A caller may need to persist an explicit user choice even when it equals the
841
+ // schema default. Interactive setup uses this for semanticSearchMode: "off"
842
+ // means the user declined the opt-in, not an incidental default.
843
+ for (const key of persistTopLevelKeys) {
844
+ if (Object.hasOwn(next, key))
845
+ body[key] = next[key];
846
+ }
847
+ body.configVersion = CURRENT_CONFIG_VERSION;
848
+ return body;
849
+ }
850
+ /**
851
+ * A file still in the retired `engines` form holds `engines` and `defaults`, not
852
+ * the `connections`, `harnesses` and `improve` keys the reader makes of them, so
853
+ * removing one of those is a change the file cannot take: the next load would
854
+ * bring it back. Refuse it, once, until `akm migrate apply` has converted the file.
855
+ */
856
+ function refuseRemovalFromRetiredForm(unremoved, document, converted) {
857
+ const fromRetiredForm = unremoved.filter((keyPath) => hasRawPath(converted, [...keyPath]) && !hasRawPath(document, [...keyPath]));
858
+ if (fromRetiredForm.length === 0)
859
+ return;
860
+ throw new UsageError(`Cannot remove ${fromRetiredForm.map((keyPath) => keyPath.join(".")).join(", ")}: it comes from the retired engines form or models.json, which this config.json still reads. ` +
861
+ "Run `akm migrate apply` to convert the file, then repeat the change.");
862
+ }
863
+ /**
864
+ * What `akm migrate apply` persists: the file read as the current shape
865
+ * (the retired `engines` form converted, the schema's own normalizations), only
866
+ * the changed-or-already-local fields.
597
867
  */
598
868
  function configWriteBody(localRaw, current, next, persistTopLevelKeys = []) {
599
869
  // A first write keeps the established full-default scaffold. Besides being
@@ -627,12 +897,52 @@ function configWriteBody(localRaw, current, next, persistTopLevelKeys = []) {
627
897
  }
628
898
  return ordered;
629
899
  }
900
+ /**
901
+ * The connection or harness that `key` (`connections.<name>[.…]`,
902
+ * `harnesses.<name>[.…]`) is inside, when the reader drops it: this file holds it
903
+ * and nothing, an `extends` base included, completes it. `akm config` cannot edit
904
+ * an entry it cannot see. `inFile` says whether the file itself holds the entry
905
+ * under that name, or it comes from the retired `engines` form.
906
+ */
907
+ export function unusableEntryAt(key) {
908
+ const [group, name] = key.split(".");
909
+ if (group !== "connections" && group !== "harnesses")
910
+ return undefined;
911
+ const configPath = getConfigPath();
912
+ const text = readConfigText(configPath);
913
+ if (text === undefined)
914
+ return undefined;
915
+ const document = readConfigFile(text, configPath);
916
+ const localRaw = readRetiredEngines(document, configPath);
917
+ const entry = findUnusableEntries(localRaw).find((found) => found.name === name && found.kind === (group === "connections" ? "connection" : "harness"));
918
+ if (!entry || Object.hasOwn(buildEffectiveConfig(localRaw, configPath)[group] ?? {}, entry.name))
919
+ return undefined;
920
+ return { ...entry, group, inFile: isRecord(document[group]) && Object.hasOwn(document[group], entry.name) };
921
+ }
922
+ /**
923
+ * A connection or harness the reader drops stays in the file exactly as written,
924
+ * with a note on what to fix. One this file only half says, which an `extends`
925
+ * base completes, is not dropped (it is in `current`) and gets no note.
926
+ */
927
+ function keepUnusableEntries(body, localRaw, current, notes) {
928
+ for (const { kind, name, why } of findUnusableEntries(localRaw)) {
929
+ const holder = kind === "connection" ? "connections" : "harnesses";
930
+ if (Object.hasOwn(current[holder] ?? {}, name))
931
+ continue;
932
+ const entries = isRecord(body[holder]) ? body[holder] : {};
933
+ body[holder] = { ...entries, [name]: localRaw[holder][name] };
934
+ notes.push(`${holder}.${name}: kept as written, and ignored by akm because ${why}. Fix it, or remove it.`);
935
+ }
936
+ }
630
937
  /**
631
938
  * The migrator's one config step. The reader already tolerates every shape
632
939
  * 0.9.15 and later wrote (`configVersion` read as current, 0.9.16
633
940
  * `{kind, ref, sourceId}` scheduler entries, unknown keys dropped); this
634
- * writes that current shape back to `config.json` — the same body `mutateConfig` writes — so the tolerance
635
- * becomes durable. Reports without writing unless `apply` is set.
941
+ * writes the current shape back to `config.json` so the tolerance becomes
942
+ * durable: the retired `engines` form converted to `connections` / `harnesses`,
943
+ * every key akm no longer reads from config filed in `credentials.json`
944
+ * ({@link planKeyMoves}), and a connection or harness the reader drops kept as
945
+ * written. Reports without writing unless `apply` is set.
636
946
  */
637
947
  export function normalizeConfigFile(configPath, options) {
638
948
  return withConfigLock(() => {
@@ -640,7 +950,20 @@ export function normalizeConfigFile(configPath, options) {
640
950
  if (text === undefined)
641
951
  return { keys: [], changed: false, applied: false };
642
952
  const raw = parseConfigText(text, configPath);
643
- const localRaw = runConfigFilePipeline(text, configPath);
953
+ const document = readConfigFile(text, configPath);
954
+ // The conversion runs before the unknown-key sweep below deletes what the
955
+ // schema does not know (inventory surprise 8).
956
+ const overlay = readModelsFile();
957
+ const inherited = overlay === undefined ? undefined : inheritedEntries(document, configPath);
958
+ const converted = convertEngineConfig(document, {
959
+ modelsOverlay: overlay,
960
+ env: process.env,
961
+ ...(inherited ? { inherited } : {}),
962
+ });
963
+ const plan = planKeyMoves(document, converted, {
964
+ resolveSecretRef: (ref) => resolveSecret(ref, options.resolveFromStore),
965
+ });
966
+ const localRaw = plan.config;
644
967
  const current = buildEffectiveConfig(localRaw, configPath);
645
968
  const next = validateCompleteConfig({ ...current, configVersion: CURRENT_CONFIG_VERSION });
646
969
  const body = configWriteBody(localRaw, current, next);
@@ -656,15 +979,53 @@ export function normalizeConfigFile(configPath, options) {
656
979
  const strategies = body.improve?.strategies;
657
980
  if (strategies && Object.hasOwn(strategies, "graph-refresh"))
658
981
  delete strategies["graph-refresh"];
982
+ // A key in the older flat form is filed only under an endpoint this file itself gives the block.
983
+ const flat = fileFlatCredentials((slot) => documentEndpoint(plan.config, slot), false, plan.moves);
984
+ const notes = [...converted.notes, ...plan.notes, ...flat.notes];
985
+ keepUnusableEntries(body, localRaw, current, notes);
986
+ const hasModelsFile = modelsFileExists();
987
+ if (hasModelsFile) {
988
+ notes.push("models.json: its aliases are folded in where they name a configured connection or harness (the ones that do not are listed above), then the file moves to the config-backups directory.");
989
+ }
659
990
  const keys = [...new Set([...Object.keys(raw), ...Object.keys(body)])]
660
991
  .filter((key) => JSON.stringify(raw[key]) !== JSON.stringify(body[key]))
661
992
  .sort();
662
- if (keys.length === 0 || !options.apply)
663
- return { keys, changed: keys.length > 0, applied: false };
664
- const backup = backupExistingConfig(configPath);
665
- writeConfigAtomic(configPath, body);
666
- cachedConfig = undefined;
667
- return { keys, changed: true, applied: true, ...(backup ? { backupPath: backup.timestamped } : {}) };
993
+ const reported = {
994
+ keys,
995
+ ...(plan.moves.length > 0 || flat.filed.length > 0
996
+ ? { credentials: [...plan.moves.map((move) => move.name), ...flat.filed] }
997
+ : {}),
998
+ ...(notes.length > 0 ? { notes } : {}),
999
+ ...(Object.keys(converted.targets).length > 0 ? { engineTargets: converted.targets } : {}),
1000
+ };
1001
+ const changed = keys.length > 0 || hasModelsFile || flat.filed.length > 0;
1002
+ if (!changed || !options.apply)
1003
+ return { ...reported, changed, applied: false };
1004
+ let backup;
1005
+ const credentialsBackupPath = flat.filed.length > 0
1006
+ ? fileFlatCredentials((slot) => documentEndpoint(plan.config, slot), true, plan.moves).backupPath
1007
+ : undefined;
1008
+ if (keys.length > 0) {
1009
+ // The keys are filed before the file that holds them is rewritten; a store
1010
+ // that cannot take them (malformed, unwritable) throws here and loses none.
1011
+ for (const move of plan.moves)
1012
+ writeCredential(move.slot, move.value, move.endpoint);
1013
+ // The old engine names outlive the file that had them, until the references to them are migrated.
1014
+ if (Object.keys(converted.targets).length > 0)
1015
+ rememberEngineTargets(converted.targets);
1016
+ backup = backupExistingConfig(configPath);
1017
+ writeConfigAtomic(configPath, body);
1018
+ cachedConfig = undefined;
1019
+ }
1020
+ const modelsFileBackupPath = hasModelsFile ? retireModelsFile() : undefined;
1021
+ return {
1022
+ ...reported,
1023
+ changed: true,
1024
+ applied: true,
1025
+ ...(backup ? { backupPath: backup.timestamped } : {}),
1026
+ ...(credentialsBackupPath ? { credentialsBackupPath } : {}),
1027
+ ...(modelsFileBackupPath ? { modelsFileBackupPath } : {}),
1028
+ };
668
1029
  });
669
1030
  }
670
1031
  /**
@@ -679,16 +1040,20 @@ export function mutateConfig(mutate, options) {
679
1040
  if (text === undefined && options?.absentNoop) {
680
1041
  return { config: { ...DEFAULT_CONFIG }, written: false };
681
1042
  }
682
- const localRaw = text === undefined ? undefined : runConfigFilePipeline(text, configPath);
1043
+ const document = text === undefined ? undefined : readConfigFile(text, configPath);
1044
+ const localRaw = document === undefined ? undefined : readRetiredEngines(document, configPath);
683
1045
  const current = localRaw === undefined ? { ...DEFAULT_CONFIG } : buildEffectiveConfig(localRaw, configPath);
684
1046
  const mutated = mutate(current);
685
1047
  if (mutated === current)
686
1048
  return { config: current, written: false };
687
1049
  const next = validateCompleteConfig({ ...mutated, configVersion: CURRENT_CONFIG_VERSION });
1050
+ const unremoved = [];
1051
+ const body = editedConfigFile(document, current, next, [], unremoved);
1052
+ refuseRemovalFromRetiredForm(unremoved, document, localRaw);
688
1053
  if (text !== undefined)
689
1054
  backupExistingConfig(configPath);
690
1055
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
691
- writeConfigAtomic(configPath, sanitizeConfigForWrite(configWriteBody(localRaw, current, next)));
1056
+ writeConfigAtomic(configPath, sanitizeConfigForWrite(body));
692
1057
  return { config: next, written: true };
693
1058
  });
694
1059
  }
@@ -703,17 +1068,22 @@ export async function mutateConfigWithPrecommit(mutate, precommit, options) {
703
1068
  const release = acquireConfigLock();
704
1069
  try {
705
1070
  const text = readConfigText(configPath);
706
- const localRaw = text === undefined ? undefined : runConfigFilePipeline(text, configPath);
1071
+ const document = text === undefined ? undefined : readConfigFile(text, configPath);
1072
+ const localRaw = document === undefined ? undefined : readRetiredEngines(document, configPath);
707
1073
  const current = localRaw === undefined ? { ...DEFAULT_CONFIG } : buildEffectiveConfig(localRaw, configPath);
708
1074
  const mutated = mutate(current);
709
1075
  const next = validateCompleteConfig({ ...mutated, configVersion: CURRENT_CONFIG_VERSION });
1076
+ const unremoved = [];
1077
+ const body = editedConfigFile(document, current, next, options?.persistTopLevelKeys, unremoved);
1078
+ if (mutated !== current)
1079
+ refuseRemovalFromRetiredForm(unremoved, document, localRaw);
710
1080
  if (text !== undefined)
711
1081
  backupExistingConfig(configPath);
712
1082
  const precommitResult = await precommit(next);
713
1083
  if (mutated === current)
714
1084
  return { config: current, written: false, precommit: precommitResult };
715
1085
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
716
- writeConfigAtomic(configPath, sanitizeConfigForWrite(configWriteBody(localRaw, current, next, options?.persistTopLevelKeys)));
1086
+ writeConfigAtomic(configPath, sanitizeConfigForWrite(body));
717
1087
  return { config: next, written: true, precommit: precommitResult };
718
1088
  }
719
1089
  finally {
@@ -740,57 +1110,16 @@ export function schedulerSourceIdFor(config, bundleId) {
740
1110
  }
741
1111
  }
742
1112
  /**
743
- * Strip literal apiKey fields before writing config to disk.
744
- * API keys are expected to come from environment variables
745
- * (AKM_EMBED_API_KEY, AKM_LLM_API_KEY, AKM_ENGINE_<NAME>_API_KEY).
746
- *
747
- * `${VAR}` / `$VAR` references are preserved — they are not secrets, they
748
- * are deferred lookups resolved at consumption by `resolveSecret`. Dropping
749
- * them would break the documented config-on-disk pattern.
1113
+ * Drop what must not reach `config.json` before it is written.
750
1114
  *
751
- * When a non-reference literal value is stripped, emit a `warn()` so the
752
- * user knows their key was dropped and how to provide it at runtime (#474).
753
- * Previously the strip was silent — a user invoking `akm setup --from <file>
754
- * --yes` with an `apiKey` field expected persistence and got a wiped config
755
- * with no feedback. `quiet` skips the warnings for a caller that only asks
756
- * whether a write would drop anything and writes nothing itself.
1115
+ * A literal `apiKey` is not dropped: akm never reads or sends it (see
1116
+ * `envReferenceApiKey`), but it is the user's, and `akm migrate apply` is what
1117
+ * moves it. Only registry URLs carrying credentials are removed here.
1118
+ * `quiet` skips the warnings for a caller that only asks whether a write would
1119
+ * drop anything and writes nothing itself.
757
1120
  */
758
1121
  export function sanitizeConfigForWrite(config, options) {
759
1122
  const sanitized = { ...config };
760
- const stripped = [];
761
- if (config.embedding?.apiKey !== undefined) {
762
- const apiKey = config.embedding.apiKey;
763
- if (isApiKeyReference(apiKey)) {
764
- // Preserve reference verbatim — not a secret.
765
- sanitized.embedding = { ...config.embedding };
766
- }
767
- else {
768
- const { apiKey: _drop, ...rest } = config.embedding;
769
- sanitized.embedding = rest;
770
- if (apiKey)
771
- stripped.push("embedding.apiKey (set AKM_EMBED_API_KEY to provide at runtime)");
772
- }
773
- }
774
- else if (config.embedding) {
775
- sanitized.embedding = { ...config.embedding };
776
- }
777
- if (config.engines) {
778
- const engines = {};
779
- for (const [name, engine] of Object.entries(config.engines)) {
780
- if (engine.kind !== "llm" || engine.apiKey === undefined || isApiKeyReference(engine.apiKey)) {
781
- engines[name] = { ...engine };
782
- continue;
783
- }
784
- const { apiKey: _drop, ...rest } = engine;
785
- engines[name] = rest;
786
- if (engine.apiKey)
787
- stripped.push(`engines.${name}.apiKey`);
788
- }
789
- sanitized.engines = engines;
790
- }
791
- if (stripped.length > 0 && !options?.quiet) {
792
- 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.`);
793
- }
794
1123
  if (config.registries) {
795
1124
  const droppedRegistries = [];
796
1125
  const registries = config.registries.filter((entry) => {
@@ -812,10 +1141,10 @@ export function updateConfig(partial) {
812
1141
  }
813
1142
  /**
814
1143
  * Resolve a single secret value: expand `${VAR}` / `$VAR` against
815
- * `process.env`, or look up `secret://<name>` via `resolveFromStore`. Use this
816
- * at apiKey / authorization-header consumption sites (LLM client, embedder,
817
- * agent SDK runner) — NOT on the load path. Non-string inputs pass through
818
- * unchanged.
1144
+ * `process.env`, then substitute `${ref}` tokens (`${secrets/name}`) through
1145
+ * `resolveFromStore`. Use this at apiKey / authorization-header consumption
1146
+ * sites (LLM client, embedder, agent SDK runner) — NOT on the load path.
1147
+ * Non-string inputs pass through unchanged.
819
1148
  *
820
1149
  * Returns the input unchanged when no substitution markers are present, so
821
1150
  * literal API key strings (already-resolved secrets) are zero-cost.
@@ -824,26 +1153,18 @@ export function updateConfig(partial) {
824
1153
  * preserved verbatim on read — only fields explicitly routed through this
825
1154
  * helper are expanded.
826
1155
  *
827
- * A `secret://<name>` value that fails to resolve throws `ConfigError`
828
- * (naming the ref, never the secret) rather than silently sending an unusable
829
- * credential.
1156
+ * A `${ref}` that fails to resolve throws `ConfigError` (naming the ref, never
1157
+ * the secret) rather than silently sending an unusable credential.
830
1158
  */
831
1159
  export function resolveSecret(value, resolveFromStore) {
832
1160
  if (value === undefined)
833
1161
  return undefined;
834
1162
  if (typeof value !== "string")
835
1163
  return value;
836
- const storeRef = SECRET_STORE_REFERENCE_PATTERN.exec(value)?.[1];
837
- if (storeRef !== undefined) {
838
- const resolved = resolveFromStore?.(storeRef) ?? null;
839
- if (resolved === null) {
840
- throw new ConfigError(`Secret store reference "${value}" did not resolve to a stored value.`, "SECRET_REFERENCE_UNRESOLVED");
841
- }
842
- return resolved;
843
- }
844
1164
  if (!value.includes("$"))
845
1165
  return value;
846
- return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
1166
+ // Env expansion runs first so a secret value containing `$` is never re-expanded.
1167
+ const expanded = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
847
1168
  const name = (braced ?? bare);
848
1169
  const resolved = process.env[name];
849
1170
  if (!resolved) {
@@ -854,6 +1175,14 @@ export function resolveSecret(value, resolveFromStore) {
854
1175
  }
855
1176
  return resolved ?? "";
856
1177
  });
1178
+ const { values, missing } = substituteRefTokens({ value: expanded }, (ref) => {
1179
+ const stored = resolveFromStore?.(ref) ?? null;
1180
+ return stored === null ? undefined : { kind: "secret", value: stored };
1181
+ });
1182
+ if (missing.length > 0) {
1183
+ throw new ConfigError(`Secret store reference "\${${missing[0]}}" did not resolve to a stored value.`, "SECRET_REFERENCE_UNRESOLVED");
1184
+ }
1185
+ return values.value;
857
1186
  }
858
1187
  // Re-export source runtime helpers — implementation lives in config-sources.ts.
859
1188
  export { bundleComponentConfig, bundleContentRoot, bundleContentRoots, bundleEntryToSourceEntry, bundleKeyForContentRoot, bundlePhysicalContentRoot, bundleSourceId, bundlesToSourceEntries, installedSourceDescriptor, isBundleEnabled, parseSourceSpec, primaryBundlePath, resolveActiveConfiguredSources, resolveConfiguredSources, } from "./config-sources.js";