akm-cli 0.9.0-rc.1 → 0.9.0-rc.2

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 (228) hide show
  1. package/CHANGELOG.md +223 -9
  2. package/SECURITY.md +23 -24
  3. package/dist/assets/help/help-improve.md +10 -10
  4. package/dist/assets/hints/cli-hints-full.md +44 -10
  5. package/dist/assets/hints/cli-hints-short.md +6 -2
  6. package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
  7. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
  8. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
  9. package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
  10. package/dist/assets/stash-skeleton/README.md +28 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
  23. package/dist/assets/tasks/core/backup.yml +1 -0
  24. package/dist/assets/tasks/core/extract.yml +1 -0
  25. package/dist/assets/tasks/core/improve.yml +1 -0
  26. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  27. package/dist/assets/tasks/core/sync.yml +1 -0
  28. package/dist/assets/tasks/core/version-check.yml +1 -0
  29. package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
  30. package/dist/assets/templates/html/health.html +5 -1
  31. package/dist/cli/config-migrate.js +31 -138
  32. package/dist/cli/config-validate.js +10 -8
  33. package/dist/cli.js +48 -14
  34. package/dist/commands/agent/agent-dispatch.js +17 -16
  35. package/dist/commands/agent/agent-support.js +0 -24
  36. package/dist/commands/agent/contribute-cli.js +5 -15
  37. package/dist/commands/backup-cli.js +54 -0
  38. package/dist/commands/config-cli.js +45 -159
  39. package/dist/commands/env/secret.js +8 -5
  40. package/dist/commands/health/checks.js +130 -83
  41. package/dist/commands/health/html-report.js +4 -0
  42. package/dist/commands/health/improve-metrics.js +30 -32
  43. package/dist/commands/health/llm-usage.js +19 -19
  44. package/dist/commands/health/md-report.js +4 -0
  45. package/dist/commands/health/metrics.js +2 -1
  46. package/dist/commands/health/surfaces.js +5 -4
  47. package/dist/commands/health.js +1 -1
  48. package/dist/commands/improve/consolidate/chunking.js +2 -2
  49. package/dist/commands/improve/consolidate.js +28 -25
  50. package/dist/commands/improve/distill/promote-memory.js +5 -12
  51. package/dist/commands/improve/distill/quality-gate.js +5 -7
  52. package/dist/commands/improve/distill.js +16 -5
  53. package/dist/commands/improve/eligibility.js +22 -12
  54. package/dist/commands/improve/extract-cli.js +47 -19
  55. package/dist/commands/improve/extract.js +110 -62
  56. package/dist/commands/improve/improve-cli.js +38 -16
  57. package/dist/commands/improve/improve-result-file.js +30 -24
  58. package/dist/commands/improve/improve-strategies.js +137 -0
  59. package/dist/commands/improve/improve.js +60 -30
  60. package/dist/commands/improve/locks.js +66 -45
  61. package/dist/commands/improve/loop-stages.js +75 -33
  62. package/dist/commands/improve/memory/memory-belief.js +79 -7
  63. package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
  64. package/dist/commands/improve/preparation.js +71 -73
  65. package/dist/commands/improve/procedural.js +3 -2
  66. package/dist/commands/improve/recombine.js +2 -1
  67. package/dist/commands/improve/reflect.js +119 -214
  68. package/dist/commands/improve/shared.js +11 -5
  69. package/dist/commands/lint/base-linter.js +152 -42
  70. package/dist/commands/mv-cli.js +809 -0
  71. package/dist/commands/proposal/proposal-cli.js +18 -8
  72. package/dist/commands/proposal/propose.js +64 -69
  73. package/dist/commands/read/knowledge.js +436 -4
  74. package/dist/commands/read/remember-cli.js +39 -2
  75. package/dist/commands/read/search-cli.js +6 -1
  76. package/dist/commands/registry-cli.js +29 -14
  77. package/dist/commands/remember.js +2 -0
  78. package/dist/commands/sources/init.js +13 -14
  79. package/dist/commands/sources/schema-repair.js +2 -4
  80. package/dist/commands/sources/source-add.js +62 -73
  81. package/dist/commands/sources/source-manage.js +50 -46
  82. package/dist/commands/sources/stash-cli.js +41 -4
  83. package/dist/commands/tasks/default-tasks.js +12 -12
  84. package/dist/commands/tasks/tasks-cli.js +7 -3
  85. package/dist/commands/tasks/tasks.js +113 -18
  86. package/dist/commands/wiki-cli.js +9 -10
  87. package/dist/core/asset/frontmatter.js +12 -2
  88. package/dist/core/common.js +5 -3
  89. package/dist/core/config/config-io.js +28 -17
  90. package/dist/core/config/config-schema.js +358 -66
  91. package/dist/core/config/config-types.js +3 -3
  92. package/dist/core/config/config-version.js +29 -0
  93. package/dist/core/config/config-walker.js +98 -27
  94. package/dist/core/config/config.js +132 -266
  95. package/dist/core/config/deep-merge.js +41 -0
  96. package/dist/core/config/engine-semantics.js +32 -0
  97. package/dist/core/errors.js +2 -2
  98. package/dist/core/extra-params.js +61 -0
  99. package/dist/core/file-lock.js +201 -56
  100. package/dist/core/improve-result.js +178 -0
  101. package/dist/core/maintenance-barrier.js +119 -0
  102. package/dist/core/migration-backup.js +416 -0
  103. package/dist/core/paths.js +3 -0
  104. package/dist/core/redaction.js +358 -0
  105. package/dist/core/state/migrations.js +17 -2
  106. package/dist/core/state-db.js +44 -1
  107. package/dist/indexer/db/db.js +116 -1
  108. package/dist/indexer/graph/graph-extraction.js +28 -16
  109. package/dist/indexer/index-writer-lock.js +31 -24
  110. package/dist/indexer/index-written-assets.js +15 -6
  111. package/dist/indexer/indexer.js +47 -2
  112. package/dist/indexer/passes/memory-inference.js +10 -6
  113. package/dist/indexer/passes/metadata.js +250 -0
  114. package/dist/indexer/search/db-search.js +111 -44
  115. package/dist/indexer/search/fts-query.js +41 -0
  116. package/dist/indexer/search/ranking-contributors.js +48 -0
  117. package/dist/indexer/search/ranking.js +36 -23
  118. package/dist/indexer/search/search-fields.js +11 -1
  119. package/dist/integrations/agent/builder-shared.js +7 -0
  120. package/dist/integrations/agent/builders.js +5 -52
  121. package/dist/integrations/agent/config.js +3 -143
  122. package/dist/integrations/agent/detect.js +17 -2
  123. package/dist/integrations/agent/engine-resolution.js +202 -0
  124. package/dist/integrations/agent/index.js +1 -2
  125. package/dist/integrations/agent/model-aliases.js +7 -2
  126. package/dist/integrations/agent/profiles.js +6 -99
  127. package/dist/integrations/agent/runner-dispatch.js +76 -13
  128. package/dist/integrations/agent/runner.js +76 -207
  129. package/dist/integrations/agent/spawn.js +4 -6
  130. package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
  131. package/dist/integrations/harnesses/aider/index.js +0 -1
  132. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
  133. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  134. package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
  135. package/dist/integrations/harnesses/claude/index.js +0 -2
  136. package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
  137. package/dist/integrations/harnesses/codex/index.js +0 -1
  138. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
  139. package/dist/integrations/harnesses/copilot/index.js +0 -1
  140. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
  141. package/dist/integrations/harnesses/gemini/index.js +0 -1
  142. package/dist/integrations/harnesses/index.js +1 -24
  143. package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
  144. package/dist/integrations/harnesses/opencode/index.js +0 -6
  145. package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
  146. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
  147. package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
  148. package/dist/integrations/harnesses/openhands/index.js +0 -1
  149. package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
  150. package/dist/integrations/harnesses/pi/index.js +0 -1
  151. package/dist/integrations/harnesses/types.js +1 -32
  152. package/dist/integrations/lockfile.js +32 -21
  153. package/dist/llm/client.js +48 -14
  154. package/dist/llm/feature-gate.js +15 -47
  155. package/dist/llm/graph-extract.js +1 -1
  156. package/dist/llm/index-passes.js +8 -42
  157. package/dist/llm/memory-infer-impl.js +1 -1
  158. package/dist/llm/usage-persist.js +4 -0
  159. package/dist/llm/usage-telemetry.js +35 -5
  160. package/dist/output/shapes/helpers.js +2 -1
  161. package/dist/output/shapes/passthrough.js +2 -0
  162. package/dist/output/text/helpers.js +3 -1
  163. package/dist/schemas/akm-config.json +11013 -8600
  164. package/dist/schemas/akm-task.json +87 -0
  165. package/dist/schemas/akm-workflow.json +57 -13
  166. package/dist/scripts/migrate-storage.js +8591 -509
  167. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
  168. package/dist/setup/detected-engines.js +142 -0
  169. package/dist/setup/engine-config.js +89 -0
  170. package/dist/setup/setup.js +236 -132
  171. package/dist/setup/steps/connection.js +61 -32
  172. package/dist/setup/steps/platforms.js +4 -4
  173. package/dist/setup/steps.js +3 -2
  174. package/dist/storage/database.js +13 -1
  175. package/dist/storage/engines/sqlite-migrations.js +1 -0
  176. package/dist/storage/repositories/improve-runs-repository.js +5 -5
  177. package/dist/storage/repositories/task-history-repository.js +78 -0
  178. package/dist/storage/repositories/workflow-runs-repository.js +9 -8
  179. package/dist/tasks/parser.js +138 -52
  180. package/dist/tasks/runner.js +71 -75
  181. package/dist/tasks/schema.js +1 -1
  182. package/dist/tasks/validator.js +11 -6
  183. package/dist/wiki/wiki.js +9 -8
  184. package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
  185. package/dist/workflows/concurrency-policy.js +15 -0
  186. package/dist/workflows/db.js +65 -13
  187. package/dist/workflows/exec/brief.js +18 -24
  188. package/dist/workflows/exec/frozen-judge.js +47 -0
  189. package/dist/workflows/exec/native-executor.js +124 -65
  190. package/dist/workflows/exec/report.js +93 -33
  191. package/dist/workflows/exec/run-workflow.js +38 -25
  192. package/dist/workflows/exec/scheduler.js +12 -41
  193. package/dist/workflows/exec/step-work.js +91 -35
  194. package/dist/workflows/ir/compile.js +13 -26
  195. package/dist/workflows/ir/freeze.js +243 -0
  196. package/dist/workflows/ir/plan-hash.js +40 -5
  197. package/dist/workflows/ir/schema.js +542 -1
  198. package/dist/workflows/parser.js +7 -0
  199. package/dist/workflows/program/parser.js +132 -23
  200. package/dist/workflows/program/project.js +3 -4
  201. package/dist/workflows/program/schema.js +2 -2
  202. package/dist/workflows/resource-limits.js +20 -0
  203. package/dist/workflows/runtime/plan-classifier.js +187 -0
  204. package/dist/workflows/runtime/runs.js +54 -47
  205. package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
  206. package/dist/workflows/validator.js +25 -0
  207. package/docs/data-and-telemetry.md +2 -2
  208. package/docs/migration/release-notes/0.6.0.md +1 -1
  209. package/docs/migration/release-notes/0.7.0.md +5 -4
  210. package/docs/migration/v0.8-to-v0.9.md +401 -0
  211. package/package.json +4 -2
  212. package/schemas/akm-config.json +16638 -0
  213. package/schemas/akm-task.json +87 -0
  214. package/schemas/akm-workflow.json +372 -0
  215. package/dist/commands/improve/improve-profiles.js +0 -168
  216. package/dist/core/config/config-migration.js +0 -602
  217. package/dist/core/deep-merge.js +0 -38
  218. package/dist/llm/call-ai.js +0 -62
  219. package/dist/setup/legacy-config.js +0 -106
  220. package/docs/README.md +0 -104
  221. /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
  222. /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
  223. /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
  224. /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
  225. /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
  226. /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
  227. /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
  228. /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
@@ -4,11 +4,13 @@
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 { ensureMigrationBackupWithConfigLockHeld } from "../migration-backup.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 { deepMergeConfig } from "./deep-merge.js";
10
11
  export { stripJsonComments } from "./config-io.js";
11
- import { getCacheDir, getConfigPath } from "../paths.js";
12
+ import { materializeLlmConnection, resolveLlmEngineUse } from "../../integrations/agent/engine-resolution.js";
13
+ import { getConfigPath } from "../paths.js";
12
14
  import { warn } from "../warn.js";
13
15
  // Canonical harness-id source of truth (#565) — runtime value re-export.
14
16
  export { VALID_HARNESS_IDS } from "./config-types.js";
@@ -48,6 +50,7 @@ export function resolveBatchSize(configured, contextLength) {
48
50
  }
49
51
  // ── Defaults ────────────────────────────────────────────────────────────────
50
52
  export const DEFAULT_CONFIG = {
53
+ configVersion: "0.9.0",
51
54
  semanticSearchMode: "auto",
52
55
  registries: [
53
56
  { url: "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json", name: "akm-registry" },
@@ -70,9 +73,12 @@ export function loadUserConfig() {
70
73
  try {
71
74
  stat = fs.statSync(configPath);
72
75
  }
73
- catch {
76
+ catch (err) {
77
+ if (err.code !== "ENOENT") {
78
+ throw new ConfigError(`Unable to read config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE");
79
+ }
74
80
  cachedConfig = undefined;
75
- return applyRuntimeEnvApiKeys({ ...DEFAULT_CONFIG });
81
+ return { ...DEFAULT_CONFIG };
76
82
  }
77
83
  // Cache key: mtimeMs + size. Tests that write rapidly back-to-back inside
78
84
  // the mtime resolution window MUST call resetConfigCache() between writes —
@@ -87,15 +93,14 @@ export function loadUserConfig() {
87
93
  try {
88
94
  text = fs.readFileSync(configPath, "utf8");
89
95
  }
90
- catch {
96
+ catch (err) {
97
+ if (err.code !== "ENOENT") {
98
+ throw new ConfigError(`Unable to read config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE");
99
+ }
91
100
  cachedConfig = undefined;
92
- return applyRuntimeEnvApiKeys({ ...DEFAULT_CONFIG });
101
+ return { ...DEFAULT_CONFIG };
93
102
  }
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));
103
+ const finalConfig = parseAndValidateConfigText(text, configPath);
99
104
  // Re-stat after potential write-back so the cache key reflects the new mtime.
100
105
  let finalStat = stat;
101
106
  try {
@@ -113,26 +118,31 @@ export function loadUserConfig() {
113
118
  return finalConfig;
114
119
  }
115
120
  /**
116
- * Parse raw config text, run the legacy-shape migration
117
- * ({@link migrateConfigShape}), then validate via Zod
121
+ * Parse raw config text and validate via Zod.
118
122
  * ({@link AkmConfigSchema}). Returns the merged-with-defaults AkmConfig.
119
123
  *
120
124
  * The migration handles all one-time 0.7→0.8 transforms (legacy keys,
121
125
  * boolean→string coercions, openviking rename); the schema then validates
122
126
  * the canonical shape and throws on anything it doesn't recognise.
123
127
  */
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);
128
+ export function parseAndValidateConfigText(text, sourcePath) {
129
+ const raw = parseConfigText(text, sourcePath);
130
+ if (raw.configVersion !== CURRENT_CONFIG_VERSION) {
131
+ 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.");
132
+ }
133
+ const parsed = AkmConfigSchema.safeParse(raw);
130
134
  if (!parsed.success) {
131
135
  const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
132
136
  const where = sourcePath ? ` at ${sourcePath}` : "";
133
137
  throw new ConfigError(`Invalid config${where}:\n${lines}`, "INVALID_CONFIG_FILE");
134
138
  }
135
- return mergeLoadedConfig(DEFAULT_CONFIG, parsed.data);
139
+ const merged = deepMergeConfig(DEFAULT_CONFIG, parsed.data);
140
+ const finalResult = AkmConfigSchema.safeParse(merged);
141
+ if (!finalResult.success) {
142
+ const lines = finalResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
143
+ throw new ConfigError(`Invalid merged config${sourcePath ? ` at ${sourcePath}` : ""}:\n${lines}`, "INVALID_CONFIG_FILE");
144
+ }
145
+ return finalResult.data;
136
146
  }
137
147
  export function getSources(config) {
138
148
  return config.sources ?? [];
@@ -140,144 +150,24 @@ export function getSources(config) {
140
150
  export function getEffectiveRegistries(config) {
141
151
  return config.registries ?? DEFAULT_CONFIG.registries ?? [];
142
152
  }
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
- */
153
+ /** Resolve and materialize the configured default LLM engine at dispatch time. */
176
154
  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.`);
184
- }
185
- return profile;
155
+ return materializeLlmConnection(resolveLlmEngineUse(config, []));
186
156
  }
187
157
  /**
188
158
  * Like {@link requireLlmConfig} but returns `undefined` instead of throwing
189
159
  * when no LLM is configured. Use in code paths where the LLM is optional.
190
160
  */
191
161
  export function getDefaultLlmConfig(config) {
192
- const defaultName = resolveDefaultLlmProfileName(config);
193
- if (!defaultName)
194
- return undefined;
195
- return config.profiles?.llm?.[defaultName];
162
+ const resolved = resolveLlmEngineUse(config, [], { optional: true });
163
+ return resolved ? materializeLlmConnection(resolved) : undefined;
196
164
  }
197
165
  /**
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).
202
- *
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.
166
+ * Transitional internal accessor. It deliberately never consults a configured
167
+ * default strategy; callers must pass their already selected strategy.
209
168
  */
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];
215
- }
216
- /**
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).
223
- */
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;
169
+ export function getImproveProcessConfig(_config, processName, selected) {
170
+ return selected?.processes?.[processName];
281
171
  }
282
172
  export function loadConfig() {
283
173
  // Single-layer load: only the user-level config file is read. Project-level
@@ -292,39 +182,90 @@ export function _setSaveConfigForTests(fake) {
292
182
  saveConfigOverride = fake;
293
183
  }
294
184
  export function saveConfig(config) {
185
+ // Every lifecycle write produces the only config version this binary can load.
186
+ const currentConfig = { ...config, configVersion: CURRENT_CONFIG_VERSION };
295
187
  if (saveConfigOverride) {
296
- saveConfigOverride(config);
188
+ saveConfigOverride(currentConfig);
297
189
  return;
298
190
  }
299
- saveConfigReal(config);
191
+ saveConfigReal(currentConfig);
300
192
  }
301
193
  function saveConfigReal(config) {
302
194
  cachedConfig = undefined;
303
195
  const configPath = getConfigPath();
304
196
  const dir = path.dirname(configPath);
305
197
  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
198
  withConfigLock(() => {
199
+ const validated = validateCompleteConfig(config);
200
+ ensureMigrationBackupWithConfigLockHeld();
324
201
  backupExistingConfig(configPath);
325
- writeConfigAtomic(configPath, sanitized);
202
+ writeConfigAtomic(configPath, sanitizeConfigForWrite(validated));
326
203
  });
327
204
  }
205
+ export function validateCompleteConfig(config) {
206
+ const parseResult = AkmConfigSchema.safeParse(config);
207
+ if (parseResult.success)
208
+ return parseResult.data;
209
+ const lines = parseResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
210
+ throw new ConfigError(`Refusing to save invalid config:\n${lines}`, "INVALID_CONFIG_FILE", "Fix the listed fields, or undo the offending `akm config set`. " +
211
+ "If this looks like an akm bug, re-run with --debug to attach the traceback.");
212
+ }
213
+ /**
214
+ * Mutate config under one fail-closed lock spanning read, merge, validation,
215
+ * migration backup, ordinary backup, and atomic write.
216
+ */
217
+ export function mutateConfig(mutate, options) {
218
+ cachedConfig = undefined;
219
+ const configPath = getConfigPath();
220
+ return withConfigLock(() => {
221
+ const text = readConfigText(configPath);
222
+ if (text === undefined && options?.absentNoop) {
223
+ return { config: { ...DEFAULT_CONFIG }, written: false };
224
+ }
225
+ const current = text === undefined ? { ...DEFAULT_CONFIG } : parseAndValidateConfigText(text, configPath);
226
+ const mutated = mutate(current);
227
+ if (mutated === current)
228
+ return { config: current, written: false };
229
+ const next = validateCompleteConfig({ ...mutated, configVersion: CURRENT_CONFIG_VERSION });
230
+ ensureMigrationBackupWithConfigLockHeld();
231
+ if (text !== undefined)
232
+ backupExistingConfig(configPath);
233
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
234
+ writeConfigAtomic(configPath, sanitizeConfigForWrite(next));
235
+ return { config: next, written: true };
236
+ });
237
+ }
238
+ /**
239
+ * Mutate config while holding the write lock across one validated pre-commit
240
+ * side effect. Setup uses this to reject a three-way conflict before creating
241
+ * its stash, while preventing another config writer from racing the final save.
242
+ */
243
+ export async function mutateConfigWithPrecommit(mutate, precommit) {
244
+ cachedConfig = undefined;
245
+ const configPath = getConfigPath();
246
+ const release = acquireConfigLock();
247
+ try {
248
+ const text = readConfigText(configPath);
249
+ const current = text === undefined ? { ...DEFAULT_CONFIG } : parseAndValidateConfigText(text, configPath);
250
+ const mutated = mutate(current);
251
+ const next = validateCompleteConfig({ ...mutated, configVersion: CURRENT_CONFIG_VERSION });
252
+ // Setup's pre-commit may initialize the stash. Prove the rebase and final
253
+ // config are valid, then require the recovery snapshots before that first
254
+ // external side effect.
255
+ ensureMigrationBackupWithConfigLockHeld();
256
+ if (text !== undefined)
257
+ backupExistingConfig(configPath);
258
+ const precommitResult = await precommit(next);
259
+ if (mutated === current)
260
+ return { config: current, written: false, precommit: precommitResult };
261
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
262
+ writeConfigAtomic(configPath, sanitizeConfigForWrite(next));
263
+ return { config: next, written: true, precommit: precommitResult };
264
+ }
265
+ finally {
266
+ release();
267
+ }
268
+ }
328
269
  /**
329
270
  * Strip literal apiKey fields before writing config to disk.
330
271
  * API keys are expected to come from environment variables
@@ -359,50 +300,36 @@ function sanitizeConfigForWrite(config) {
359
300
  else if (config.embedding) {
360
301
  sanitized.embedding = { ...config.embedding };
361
302
  }
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 };
303
+ if (config.engines) {
304
+ const engines = {};
305
+ for (const [name, engine] of Object.entries(config.engines)) {
306
+ if (engine.kind !== "llm" || engine.apiKey === undefined || isEnvReference(engine.apiKey)) {
307
+ engines[name] = { ...engine };
308
+ continue;
380
309
  }
310
+ const { apiKey: _drop, ...rest } = engine;
311
+ engines[name] = rest;
312
+ if (engine.apiKey)
313
+ stripped.push(`engines.${name}.apiKey`);
381
314
  }
382
- sanitized.profiles = {
383
- ...(sanitized.profiles ?? {}),
384
- llm: llmProfiles,
385
- };
315
+ sanitized.engines = engines;
386
316
  }
387
317
  if (stripped.length > 0) {
388
318
  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.`);
389
319
  }
390
320
  return sanitized;
391
321
  }
392
- /** Matches `${VAR}`, `${VAR:-default}`, or `$VAR`. */
322
+ /** Matches the only 0.9 symbolic secret forms: `${VAR}` or `$VAR`. */
393
323
  function isEnvReference(value) {
394
- return /^\$\{[^}]+\}$|^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value);
324
+ return /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$|^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value);
395
325
  }
396
326
  export function updateConfig(partial) {
397
- const current = loadUserConfig();
398
- const merged = mergeLoadedConfig(current, partial);
399
- saveConfig(merged);
400
- return merged;
327
+ return mutateConfig((current) => deepMergeConfig(current, partial)).config;
401
328
  }
402
329
  // ── Helpers ─────────────────────────────────────────────────────────────────
403
330
  /**
404
- * Resolve a single secret value by expanding `${VAR}` / `$VAR` /
405
- * `${VAR:-default}` references against `process.env`. Use this at apiKey /
331
+ * Resolve a single secret value by expanding `${VAR}` / `$VAR` references
332
+ * against `process.env`. Use this at apiKey /
406
333
  * authorization-header consumption sites (LLM client, embedder, agent SDK
407
334
  * runner) — NOT on the load path. Non-string inputs pass through unchanged.
408
335
  *
@@ -420,13 +347,8 @@ export function resolveSecret(value) {
420
347
  return value;
421
348
  if (!value.includes("$"))
422
349
  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] ?? "";
350
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
351
+ return process.env[(braced ?? bare)] ?? "";
430
352
  });
431
353
  }
432
354
  /**
@@ -437,9 +359,10 @@ export function resolveSecret(value) {
437
359
  /**
438
360
  * Reserved well-known keys on IndexConfig that are NOT per-pass entries.
439
361
  * `stalenessDetection` is retired (10-Q3) but stays reserved so a leftover
440
- * config section is never misread as a pass entry.
362
+ * config section is never misread as a pass entry. `indexBodyOpening`
363
+ * (stash-conventions SPEC-8) is a boolean feature flag, not a pass section.
441
364
  */
442
- const INDEX_RESERVED_KEYS = new Set(["metadataEnhance", "stalenessDetection"]);
365
+ const INDEX_RESERVED_KEYS = new Set(["metadataEnhance", "stalenessDetection", "indexBodyOpening"]);
443
366
  export function getIndexPassConfig(config, passName) {
444
367
  if (!config)
445
368
  return undefined;
@@ -458,63 +381,6 @@ export { parseSourceSpec, resolveConfiguredSources } from "./config-sources.js";
458
381
  * (current config + partial patch). Sub-objects with named records (profiles,
459
382
  * defaults, etc.) shallow-merge; arrays override wholesale.
460
383
  */
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
384
  /**
519
385
  * Walk cwd-ancestors looking for `.akm/config.json`. If one is found, emit a
520
386
  * one-time deprecation warning per path. The file's contents are NOT read —
@@ -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
+ }
@@ -0,0 +1,32 @@
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
+ export const ENGINE_NAME_PATTERN_SOURCE = "^(?!akm-)[a-z][a-z0-9]*(?:-[a-z0-9]+)*$";
5
+ export const BUILTIN_IMPROVE_STRATEGY_NAMES = [
6
+ "default",
7
+ "quick",
8
+ "thorough",
9
+ "memory-focus",
10
+ "graph-refresh",
11
+ "frequent",
12
+ "consolidate",
13
+ "catchup",
14
+ "synthesize",
15
+ "reflect-distill",
16
+ "proactive-maintenance",
17
+ "recombine-only",
18
+ ];
19
+ /** Engine capability required by each configured improve process. `null` means engine-free. */
20
+ export const IMPROVE_PROCESS_ENGINE_CAPABILITIES = {
21
+ reflect: "llm",
22
+ distill: "llm",
23
+ consolidate: "llm",
24
+ memoryInference: "llm",
25
+ graphExtraction: "llm",
26
+ extract: "llm",
27
+ validation: "llm",
28
+ triage: "runner",
29
+ proactiveMaintenance: null,
30
+ recombine: "llm",
31
+ procedural: "llm",
32
+ };
@@ -10,11 +10,11 @@ const CONFIG_HINTS = {
10
10
  STASH_DIR_NOT_A_DIRECTORY: "The configured stashDir exists but isn't a directory. Update stashDir to point at a folder.",
11
11
  STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update stashDir.",
12
12
  EMBEDDING_NOT_CONFIGURED: 'Run `akm config set embedding \'{"endpoint":"...","model":"..."}\'` to enable embeddings.',
13
- LLM_NOT_CONFIGURED: 'Run `akm setup` or `akm config set profiles.llm.default \'{"endpoint":"...","model":"..."}\' to configure an LLM profile.',
13
+ LLM_NOT_CONFIGURED: 'Run `akm setup` or configure an `engines` entry with `kind: "llm"`, then select it with `defaults.llmEngine`.',
14
14
  TEST_ISOLATION_MISSING: "Under bun test, when AKM_STASH_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
15
15
  SETUP_TMP_STASH_REFUSED: "Use a persistent directory, or set AKM_FORCE_SETUP_TMP_STASH=1 to opt in to a sandboxed setup (setup also pre-sets AKM_STASH_DIR so config and cache writes auto-isolate into $stashDir/.akm/ — host config is preserved).",
16
16
  UNSAFE_STASH_DIR: "Choose a path inside your home directory (e.g. ~/akm) or another empty workspace. The stash directory cannot be the filesystem root, your home directory itself, or a sensitive system path like /etc, /var, ~/.config, or ~/.ssh.",
17
- UNKNOWN_IMPROVE_PROFILE: "Pass one of the listed profile names to `--profile`, or define it under `profiles.improve` in your config. Names are case-sensitive.",
17
+ UNKNOWN_IMPROVE_STRATEGY: "Pass one of the listed strategy names to `--strategy`, or define it under `improve.strategies`. Names are case-sensitive.",
18
18
  };
19
19
  /** Default hint for each UsageError code. */
20
20
  const USAGE_HINTS = {