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
@@ -9,19 +9,20 @@
9
9
  * Collects all choices and writes config once at the end.
10
10
  *
11
11
  * This module holds the wizard orchestration; the individual wizard steps,
12
- * config-shape adapters, prompt shims, provider table, and semantic-asset
13
- * preparation live in sibling modules (`steps/*`, `legacy-config`, `prompt`,
12
+ * engine writers, prompt shims, provider table, and semantic-asset preparation
13
+ * live in sibling modules (`steps/*`, `engine-config`, `prompt`,
14
14
  * `providers`, `semantic-assets`).
15
15
  */
16
16
  import { promises as dnsPromises } from "node:dns";
17
17
  import fs from "node:fs";
18
18
  import os from "node:os";
19
19
  import path from "node:path";
20
+ import { isDeepStrictEqual } from "node:util";
20
21
  import * as p from "../cli/clack.js";
21
22
  import { akmInit } from "../commands/sources/init.js";
22
- import { DEFAULT_CONFIG, getDefaultLlmConfig, loadUserConfig, saveConfig } from "../core/config/config.js";
23
- import { backupExistingConfig } from "../core/config/config-io.js";
24
- import { deepMergeConfig } from "../core/deep-merge.js";
23
+ import { DEFAULT_CONFIG, loadUserConfig, mutateConfigWithPrecommit, parseAndValidateConfigText, validateCompleteConfig, } from "../core/config/config.js";
24
+ import { readConfigText } from "../core/config/config-io.js";
25
+ import { deepMergeConfig } from "../core/config/deep-merge.js";
25
26
  import { ConfigError, UsageError } from "../core/errors.js";
26
27
  import { getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths.js";
27
28
  import { warn } from "../core/warn.js";
@@ -30,10 +31,10 @@ import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticSt
30
31
  import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../integrations/agent/index.js";
31
32
  import { defaultProfileName } from "../integrations/harnesses/index.js";
32
33
  import { probeLlmCapabilities } from "../llm/client.js";
33
- import { getOutputMode } from "../output/context.js";
34
34
  import { detectEnvironment, detectLMStudio, renderDetectionSummary, } from "./detect.js";
35
+ import { upsertDetectedAgentEngine, upsertDetectedLlmEngine, verifyOpenAiCompatibleEndpoint } from "./detected-engines.js";
36
+ import { readCurrentLlmEngine, writeAgentEngines, writeLlmEngine } from "./engine-config.js";
35
37
  import { detectHarnessConfigs } from "./harness-config-import.js";
36
- import { applyLegacyAgent, applyLegacyLlm } from "./legacy-config.js";
37
38
  import { bail, prompt } from "./prompt.js";
38
39
  import { PROVIDER_DEFAULTS } from "./providers.js";
39
40
  import { prepareSemanticSearchAssets } from "./semantic-assets.js";
@@ -95,6 +96,110 @@ function applyStashIsolationToEnv(stashDir, dirExplicitlyProvided) {
95
96
  process.env.AKM_STASH_DIR = stashDir;
96
97
  }
97
98
  // ── Helpers ─────────────────────────────────────────────────────────────────
99
+ /** Raw preflight used before setup performs prompts, initialization, or writes. */
100
+ export function assertSetupConfigPreflight() {
101
+ const configPath = getConfigPath();
102
+ let text;
103
+ try {
104
+ text = readConfigText(configPath);
105
+ }
106
+ catch (error) {
107
+ throw new ConfigError(`Could not read config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
108
+ }
109
+ if (text !== undefined)
110
+ parseAndValidateConfigText(text, configPath);
111
+ }
112
+ function isPlainRecord(value) {
113
+ if (!value || typeof value !== "object" || Array.isArray(value))
114
+ return false;
115
+ const prototype = Object.getPrototypeOf(value);
116
+ return prototype === Object.prototype || prototype === null;
117
+ }
118
+ function sameConfigValue(left, right) {
119
+ return isDeepStrictEqual(left, right);
120
+ }
121
+ function configArrayItemKey(value) {
122
+ if (!isPlainRecord(value))
123
+ return `value:${JSON.stringify(value)}`;
124
+ if (typeof value.id === "string")
125
+ return `id:${value.id}`;
126
+ if (typeof value.type === "string" && typeof value.url === "string")
127
+ return `source:${value.type}:url:${value.url}`;
128
+ if (typeof value.type === "string" && typeof value.path === "string")
129
+ return `source:${value.type}:path:${path.resolve(value.path)}`;
130
+ if (typeof value.url === "string")
131
+ return `url:${value.url}`;
132
+ if (typeof value.name === "string")
133
+ return `name:${value.name}`;
134
+ return `value:${JSON.stringify(value)}`;
135
+ }
136
+ function setupConflict(pathParts) {
137
+ const field = pathParts.length > 0 ? pathParts.join(".") : "(root)";
138
+ throw new ConfigError(`Setup config conflict at ${field}: another process changed the same field after setup started. Rerun setup against the latest config.`, "INVALID_CONFIG_FILE");
139
+ }
140
+ /** Reapply setup's changes while rejecting concurrent edits to the same field. */
141
+ export function rebaseSetupChanges(original, desired, latest, pathParts = []) {
142
+ if (sameConfigValue(original, desired))
143
+ return latest;
144
+ if (Array.isArray(original) && Array.isArray(desired)) {
145
+ if (latest !== undefined && !Array.isArray(latest))
146
+ setupConflict(pathParts);
147
+ const latestItems = Array.isArray(latest) ? latest : [];
148
+ const originalByKey = new Map(original.map((item) => [configArrayItemKey(item), item]));
149
+ const desiredByKey = new Map(desired.map((item) => [configArrayItemKey(item), item]));
150
+ const latestByKey = new Map(latestItems.map((item) => [configArrayItemKey(item), item]));
151
+ const result = [];
152
+ for (const [key, desiredItem] of desiredByKey) {
153
+ const originalItem = originalByKey.get(key);
154
+ const latestItem = latestByKey.get(key);
155
+ if (originalByKey.has(key)) {
156
+ if (!latestByKey.has(key))
157
+ setupConflict([...pathParts, key]);
158
+ result.push(rebaseSetupChanges(originalItem, desiredItem, latestItem, [...pathParts, key]));
159
+ }
160
+ else if (!latestByKey.has(key) || sameConfigValue(latestItem, desiredItem)) {
161
+ result.push(desiredItem);
162
+ }
163
+ else {
164
+ setupConflict([...pathParts, key]);
165
+ }
166
+ }
167
+ for (const [key, originalItem] of originalByKey) {
168
+ if (desiredByKey.has(key) || !latestByKey.has(key))
169
+ continue;
170
+ if (!sameConfigValue(latestByKey.get(key), originalItem))
171
+ setupConflict([...pathParts, key]);
172
+ }
173
+ for (const item of latestItems) {
174
+ const key = configArrayItemKey(item);
175
+ if (!originalByKey.has(key) && !desiredByKey.has(key))
176
+ result.push(item);
177
+ }
178
+ return result;
179
+ }
180
+ if (!isPlainRecord(original) || !isPlainRecord(desired)) {
181
+ if (!sameConfigValue(latest, original) && !sameConfigValue(latest, desired))
182
+ setupConflict(pathParts);
183
+ return desired;
184
+ }
185
+ if (latest !== undefined && !isPlainRecord(latest))
186
+ setupConflict(pathParts);
187
+ const result = isPlainRecord(latest) ? { ...latest } : {};
188
+ for (const key of new Set([...Object.keys(original), ...Object.keys(desired)])) {
189
+ if (!Object.hasOwn(desired, key)) {
190
+ if (Object.hasOwn(result, key) && !sameConfigValue(result[key], original[key]))
191
+ setupConflict([...pathParts, key]);
192
+ delete result[key];
193
+ continue;
194
+ }
195
+ result[key] = rebaseSetupChanges(original[key], desired[key], result[key], [...pathParts, key]);
196
+ }
197
+ return result;
198
+ }
199
+ async function saveSetupConfig(original, desired, precommit) {
200
+ const result = await mutateConfigWithPrecommit((latest) => rebaseSetupChanges(original, desired, latest), precommit);
201
+ return { config: result.config, precommit: result.precommit };
202
+ }
98
203
  /**
99
204
  * Quick connectivity check. Returns true if we can resolve a hostname
100
205
  * the user has already implicitly trusted within 3 seconds, false
@@ -178,7 +283,7 @@ export function buildSetupSteps(options) {
178
283
  return;
179
284
  }
180
285
  const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels, lmStudioResult, harnessConfigs);
181
- ctx.apply(applyLegacyLlm(ctx.config, llm));
286
+ ctx.apply(writeLlmEngine(ctx.config, llm));
182
287
  },
183
288
  },
184
289
  {
@@ -226,11 +331,10 @@ export function buildSetupSteps(options) {
226
331
  else {
227
332
  p.log.info("No agent CLIs detected on PATH. Agent commands will be disabled until one is installed and `akm setup` is re-run.");
228
333
  }
229
- // Inject the detected agent block into a synthetic AkmConfig so
230
- // stepAgentSelection can read it via getCurrentAgentBlock().
231
- const synthConfig = { ...ctx.config, ...applyLegacyAgent(ctx.config, result.agent) };
334
+ // Apply detected engines to a synthetic config for the selection UI.
335
+ const synthConfig = deepMergeConfig(ctx.config, writeAgentEngines(ctx.config, result.agent));
232
336
  const agent = await stepAgentSelection(synthConfig, result.detections);
233
- ctx.apply(applyLegacyAgent(ctx.config, agent));
337
+ ctx.apply(writeAgentEngines(ctx.config, agent));
234
338
  },
235
339
  },
236
340
  {
@@ -255,6 +359,7 @@ export function buildSetupSteps(options) {
255
359
  return { steps, outcome };
256
360
  }
257
361
  export async function runSetupWizard(opts) {
362
+ assertSetupConfigPreflight();
258
363
  p.intro("akm setup");
259
364
  const current = loadUserConfig();
260
365
  const configPath = getConfigPath();
@@ -264,11 +369,6 @@ export async function runSetupWizard(opts) {
264
369
  // config from being clobbered with a stashDir that the OS may reap.
265
370
  assertSetupSandbox(resolvedStashDir, opts?.dir != null);
266
371
  applyStashIsolationToEnv(resolvedStashDir, opts?.dir != null);
267
- // Bootstrap directory structure before any prompts so the stash exists
268
- // even if the wizard is interrupted after this point.
269
- if (!opts?.noInit) {
270
- await akmInit({ dir: resolvedStashDir, setDefault: true });
271
- }
272
372
  // Quick connectivity check — skip network-dependent steps when offline
273
373
  const online = await isOnline();
274
374
  if (!online) {
@@ -313,11 +413,11 @@ export async function runSetupWizard(opts) {
313
413
  // Step 1/2: Small model connection (for enrichment features)
314
414
  const smallModelResult = await stepSmallModelConnection(ctx.config);
315
415
  if (!smallModelResult.skipped) {
316
- ctx.apply(applyLegacyLlm(ctx.config, smallModelResult.llm));
416
+ ctx.apply(writeLlmEngine(ctx.config, smallModelResult.llm));
317
417
  }
318
418
  // Step 2/2: Agent connection (for agentic features)
319
419
  const agentConfig = await stepAgentConnection(ctx.config, smallModelResult);
320
- ctx.apply(applyLegacyAgent(ctx.config, agentConfig));
420
+ ctx.apply(writeAgentEngines(ctx.config, agentConfig));
321
421
  const newConfig = {
322
422
  ...ctx.config,
323
423
  // Preserve fields the steps don't manage explicitly.
@@ -326,7 +426,7 @@ export async function runSetupWizard(opts) {
326
426
  const semanticSearchMode = outcome.semantic;
327
427
  const stashDir = newConfig.stashDir ?? current.stashDir ?? getDefaultStashDir();
328
428
  const embedding = newConfig.embedding;
329
- const llm = getDefaultLlmConfig(newConfig);
429
+ const llm = readCurrentLlmEngine(newConfig);
330
430
  const registries = newConfig.registries;
331
431
  const allStashes = newConfig.sources ?? [];
332
432
  // Feature capability summary
@@ -341,7 +441,7 @@ export async function runSetupWizard(opts) {
341
441
  `Semantic search: ${semanticSearchMode.mode}`,
342
442
  `Registries: ${effectiveRegistries.filter((r) => r.enabled !== false).length} enabled`,
343
443
  `Stash sources: ${allStashes.length}`,
344
- `Agent default: ${newConfig.defaults?.agent ?? "disabled"}`,
444
+ `Agent default: ${newConfig.defaults?.engine ?? "disabled"}`,
345
445
  `Output: ${newConfig.output?.format ?? "json"} / ${newConfig.output?.detail ?? "brief"}`,
346
446
  ].join("\n"), "Configuration Summary");
347
447
  const shouldSave = await prompt(() => p.confirm({
@@ -350,22 +450,23 @@ export async function runSetupWizard(opts) {
350
450
  }));
351
451
  if (!shouldSave)
352
452
  bail();
353
- // Save config
354
- const cfgPath1 = getConfigPath();
355
- backupAndAnnounce(cfgPath1);
356
- saveConfig(newConfig);
453
+ validateCompleteConfig(newConfig);
454
+ const { config: savedConfig } = await saveSetupConfig(current, newConfig, async () => {
455
+ if (!opts?.noInit)
456
+ await akmInit({ dir: resolvedStashDir, setDefault: true, persistConfig: false });
457
+ });
357
458
  if (semanticSearchMode.mode === "off") {
358
459
  clearSemanticStatus();
359
460
  }
360
461
  if (semanticSearchMode.mode === "auto") {
361
462
  if (semanticSearchMode.prepareAssets) {
362
- const ready = await prepareSemanticSearchAssets(newConfig);
463
+ const ready = await prepareSemanticSearchAssets(savedConfig);
363
464
  if (!ready.ok) {
364
465
  writeSemanticStatus({
365
466
  status: "blocked",
366
467
  reason: ready.reason,
367
468
  message: ready.message,
368
- providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
469
+ providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
369
470
  lastCheckedAt: new Date().toISOString(),
370
471
  });
371
472
  p.log.warn("Semantic search remains set to auto, but is currently blocked. Re-run `akm index --full --verbose` once the issue is resolved.");
@@ -374,7 +475,7 @@ export async function runSetupWizard(opts) {
374
475
  writeSemanticStatus({
375
476
  status: "pending",
376
477
  message: "Semantic prerequisites verified. Building the index to finish activation.",
377
- providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
478
+ providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
378
479
  lastCheckedAt: new Date().toISOString(),
379
480
  });
380
481
  }
@@ -383,7 +484,7 @@ export async function runSetupWizard(opts) {
383
484
  writeSemanticStatus({
384
485
  status: "pending",
385
486
  message: "Semantic search is enabled, but asset preparation was skipped.",
386
- providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
487
+ providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
387
488
  lastCheckedAt: new Date().toISOString(),
388
489
  });
389
490
  p.log.info("Semantic search is set to auto, but asset preparation was skipped. Run `akm index --full --verbose` later to verify it.");
@@ -396,7 +497,7 @@ export async function runSetupWizard(opts) {
396
497
  try {
397
498
  const indexResult = await akmIndex({ stashDir });
398
499
  spin.stop(`Indexed ${indexResult.totalEntries} assets.`);
399
- if (newConfig.semanticSearchMode === "auto") {
500
+ if (savedConfig.semanticSearchMode === "auto") {
400
501
  if (indexResult.verification.ok) {
401
502
  p.log.success(indexResult.verification.message);
402
503
  }
@@ -411,12 +512,12 @@ export async function runSetupWizard(opts) {
411
512
  catch (err) {
412
513
  spin.stop("Indexing failed — you can run `akm index` manually later.");
413
514
  p.log.warn(String(err));
414
- if (newConfig.semanticSearchMode === "auto") {
515
+ if (savedConfig.semanticSearchMode === "auto") {
415
516
  writeSemanticStatus({
416
517
  status: "blocked",
417
518
  reason: "index-failed",
418
519
  message: String(err),
419
- providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
520
+ providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
420
521
  lastCheckedAt: new Date().toISOString(),
421
522
  });
422
523
  }
@@ -436,54 +537,21 @@ export async function runSetupWizard(opts) {
436
537
  p.outro(`Configuration saved to ${configPath}`);
437
538
  }
438
539
  // ── Non-interactive / scripting entry points ─────────────────────────────────
439
- /**
440
- * Back up an existing config file and print the real, timestamped backup
441
- * location (not a generic display string). On a fresh install where there is
442
- * nothing to back up, print a "nothing to back up" notice instead.
443
- */
444
- function backupAndAnnounce(configPath) {
445
- const result = backupExistingConfig(configPath);
446
- const message = result ? `Config backed up to ${result.timestamped}` : "No existing config to back up.";
447
- // In JSON output mode the structured envelope (which already carries
448
- // `configPath`) MUST be the only thing on stdout — `setup --yes | jq` is a
449
- // supported scripting contract. @clack's `p.log.info` writes to stdout, which
450
- // would corrupt that envelope, so route this human-progress notice to stderr
451
- // when emitting JSON. Interactive/text runs keep the inline clack banner.
452
- if (isJsonOutputMode()) {
453
- process.stderr.write(`${message}\n`);
454
- }
455
- else {
456
- p.log.info(message);
457
- }
458
- }
459
- /**
460
- * True when the process-level output mode is JSON (the default machine format).
461
- * Defensive: setup is also invoked programmatically (tests) where the output
462
- * mode singleton may not be initialized — treat that as "not JSON" so the
463
- * human-readable clack banner is used.
464
- */
465
- function isJsonOutputMode() {
466
- try {
467
- return getOutputMode().format === "json";
468
- }
469
- catch {
470
- return false;
471
- }
472
- }
473
540
  /**
474
541
  * Run setup in non-interactive mode, applying all defaults.
475
542
  * Safe to call from CI or scripts. Idempotent — re-running produces the same result.
476
543
  */
477
544
  export async function runSetupWithDefaults(opts) {
478
- const current = loadUserConfig();
479
- const stashDir = opts.dir ? path.resolve(opts.dir) : (current.stashDir ?? getDefaultStashDir());
480
- assertSetupSandbox(stashDir, opts.dir != null);
481
- applyStashIsolationToEnv(stashDir, opts.dir != null);
482
- // Bootstrap directory structure first
483
- let initResult;
484
- if (!opts.noInit) {
485
- initResult = await akmInit({ dir: stashDir, setDefault: true });
545
+ assertSetupConfigPreflight();
546
+ const explicitStashDir = opts.dir != null ? path.resolve(opts.dir) : undefined;
547
+ if (explicitStashDir) {
548
+ assertSetupSandbox(explicitStashDir, true);
549
+ applyStashIsolationToEnv(explicitStashDir, true);
486
550
  }
551
+ const current = loadUserConfig();
552
+ const stashDir = explicitStashDir ?? current.stashDir ?? getDefaultStashDir();
553
+ assertSetupSandbox(stashDir, explicitStashDir != null);
554
+ applyStashIsolationToEnv(stashDir, explicitStashDir != null);
487
555
  // Run steps in non-interactive mode (applies defaults, skips prompts)
488
556
  const ctx = createSetupContext(current, { nonInteractive: true });
489
557
  const { steps } = buildSetupSteps({
@@ -498,12 +566,12 @@ export async function runSetupWithDefaults(opts) {
498
566
  // Aggregate environment detection — apply detected values directly.
499
567
  const env = await detectEnvironment({ existingStashDir: ctx.config.stashDir });
500
568
  // Apply a detected LLM (live local server) when the config has none yet.
501
- if (!getDefaultLlmConfig(ctx.config)) {
569
+ if (!readCurrentLlmEngine(ctx.config) && opts.probe) {
502
570
  const liveLocal = env.localServers.find((s) => s.available && s.defaultModel);
503
571
  if (liveLocal?.defaultModel) {
504
572
  const llm = {
505
573
  provider: "local",
506
- endpoint: `${liveLocal.baseUrl.replace(/\/$/, "")}/v1`,
574
+ endpoint: `${liveLocal.baseUrl.replace(/\/$/, "")}/v1/chat/completions`,
507
575
  model: liveLocal.defaultModel,
508
576
  };
509
577
  // A required field being unresolvable must fail loudly rather than write
@@ -511,11 +579,22 @@ export async function runSetupWithDefaults(opts) {
511
579
  if (!llm.endpoint?.trim() || !llm.model?.trim()) {
512
580
  throw new UsageError("Detected a local LLM server but could not resolve a required field (endpoint/model). Re-run `akm setup` interactively.", "MISSING_REQUIRED_ARGUMENT");
513
581
  }
514
- ctx.apply(applyLegacyLlm(ctx.config, llm));
582
+ const verified = await verifyOpenAiCompatibleEndpoint(llm);
583
+ if (verified.ok) {
584
+ const applied = upsertDetectedLlmEngine(ctx.config, {
585
+ provider: llm.provider ?? "local",
586
+ endpoint: verified.endpoint,
587
+ model: llm.model,
588
+ });
589
+ ctx.apply(applied.config);
590
+ }
591
+ else {
592
+ warn(`[akm setup] Skipping detected local LLM: ${verified.reason}. Verify it and rerun setup.`);
593
+ }
515
594
  }
516
595
  }
517
596
  // Auto-detect agent CLI if not already configured
518
- if (!ctx.config.defaults?.agent) {
597
+ if (!ctx.config.defaults?.engine) {
519
598
  let defaultProfile;
520
599
  if (env.harness !== "none") {
521
600
  defaultProfile = env.harness;
@@ -525,12 +604,15 @@ export async function runSetupWithDefaults(opts) {
525
604
  defaultProfile = pickDefaultAgentProfile(detected, undefined);
526
605
  }
527
606
  if (defaultProfile) {
528
- ctx.apply(applyLegacyAgent(ctx.config, { default: defaultProfile }));
607
+ ctx.apply(upsertDetectedAgentEngine(ctx.config, defaultProfile).config);
529
608
  }
530
609
  }
531
- const cfgPath2 = getConfigPath();
532
- backupAndAnnounce(cfgPath2);
533
- saveConfig(ctx.config);
610
+ validateCompleteConfig(ctx.config);
611
+ const { precommit: initResult } = await saveSetupConfig(current, ctx.config, async () => {
612
+ if (opts.noInit)
613
+ return undefined;
614
+ return akmInit({ dir: stashDir, setDefault: true, persistConfig: false });
615
+ });
534
616
  return {
535
617
  configPath: getConfigPath(),
536
618
  stashDir,
@@ -548,8 +630,7 @@ export async function runSetupWithDefaults(opts) {
548
630
  * value.
549
631
  */
550
632
  export async function runDetectOnly() {
551
- const current = loadUserConfig();
552
- return detectEnvironment({ existingStashDir: current.stashDir });
633
+ return detectEnvironment();
553
634
  }
554
635
  /**
555
636
  * Derive opinionated defaults from a detection result.
@@ -590,6 +671,7 @@ export function deriveRecommendedConfig(env) {
590
671
  const defaults = PROVIDER_DEFAULTS[cloud.provider];
591
672
  if (defaults) {
592
673
  result.llm = { provider: cloud.provider, endpoint: defaults.endpoint, model: defaults.model };
674
+ result.llmApiKeyEnvVar = cloud.envVar;
593
675
  }
594
676
  }
595
677
  }
@@ -603,16 +685,47 @@ export function deriveRecommendedConfig(env) {
603
685
  * (follows #511 semantics).
604
686
  */
605
687
  export async function runResetRecommended(opts) {
688
+ assertSetupConfigPreflight();
689
+ const explicitStashDir = opts.dir != null ? path.resolve(opts.dir) : undefined;
690
+ if (explicitStashDir) {
691
+ assertSetupSandbox(explicitStashDir, true);
692
+ applyStashIsolationToEnv(explicitStashDir, true);
693
+ }
606
694
  const current = loadUserConfig();
607
695
  const env = await detectEnvironment({ existingStashDir: current.stashDir });
608
696
  const recommended = deriveRecommendedConfig(env);
609
- const incoming = {};
610
- if (recommended.llm)
611
- incoming.llm = recommended.llm;
697
+ let incoming = {};
698
+ if (recommended.llm && recommended.llm.provider !== "anthropic") {
699
+ let endpoint = recommended.llm.endpoint;
700
+ let accepted = true;
701
+ if (opts.probe) {
702
+ const verified = await verifyOpenAiCompatibleEndpoint({
703
+ endpoint,
704
+ model: recommended.llm.model,
705
+ apiKeyEnvVar: recommended.llmApiKeyEnvVar,
706
+ });
707
+ if (verified.ok) {
708
+ endpoint = verified.endpoint;
709
+ }
710
+ else {
711
+ accepted = false;
712
+ warn(`[akm setup] Skipping detected LLM: ${verified.reason}. Verify it and rerun setup.`);
713
+ }
714
+ }
715
+ if (accepted) {
716
+ incoming = upsertDetectedLlmEngine(incoming, {
717
+ provider: recommended.llm.provider ?? "local",
718
+ endpoint,
719
+ model: recommended.llm.model,
720
+ apiKeyEnvVar: recommended.llmApiKeyEnvVar,
721
+ }).config;
722
+ }
723
+ }
612
724
  if (recommended.embedding)
613
725
  incoming.embedding = recommended.embedding;
614
- if (recommended.agentDefault)
615
- incoming.agent = { default: recommended.agentDefault };
726
+ if (recommended.agentDefault) {
727
+ incoming = deepMergeConfig(incoming, writeAgentEngines(incoming, { default: recommended.agentDefault }));
728
+ }
616
729
  if (recommended.taskSchedules) {
617
730
  incoming.setup = { taskSchedules: recommended.taskSchedules };
618
731
  }
@@ -628,6 +741,8 @@ export async function runResetRecommended(opts) {
628
741
  * Validates required sub-fields and strips unknown/restricted keys.
629
742
  */
630
743
  export async function runSetupFromConfig(opts) {
744
+ assertSetupConfigPreflight();
745
+ // Phase 1: Parse JSON
631
746
  let incoming;
632
747
  try {
633
748
  incoming = JSON.parse(opts.configJson);
@@ -637,13 +752,18 @@ export async function runSetupFromConfig(opts) {
637
752
  }
638
753
  // Phase 2: Validate — only allow safe top-level keys
639
754
  const ALLOWED_KEYS = new Set([
755
+ "configVersion",
756
+ "engines",
757
+ "defaults",
758
+ "improve",
759
+ "modelAliases",
640
760
  "stashDir",
641
- "llm",
642
761
  "embedding",
643
- "agent",
644
762
  "semanticSearchMode",
645
763
  "output",
646
- "profiles",
764
+ "sources",
765
+ "registries",
766
+ "defaultWriteTarget",
647
767
  "defaults",
648
768
  "setup",
649
769
  ]);
@@ -654,12 +774,6 @@ export async function runSetupFromConfig(opts) {
654
774
  }
655
775
  }
656
776
  // Validate required sub-fields
657
- if (incoming.llm) {
658
- if (!incoming.llm.endpoint?.trim())
659
- throw new Error("llm.endpoint is required when llm is provided");
660
- if (!incoming.llm.model?.trim())
661
- throw new Error("llm.model is required when llm is provided");
662
- }
663
777
  if (incoming.embedding) {
664
778
  if (!incoming.embedding.endpoint?.trim())
665
779
  throw new Error("embedding.endpoint is required when embedding is provided");
@@ -667,34 +781,24 @@ export async function runSetupFromConfig(opts) {
667
781
  throw new Error("embedding.model is required when embedding is provided");
668
782
  }
669
783
  // Phase 3: Merge with existing config
784
+ const explicitStashDir = opts.dir != null ? path.resolve(opts.dir) : incoming.stashDir != null ? path.resolve(incoming.stashDir) : undefined;
785
+ if (explicitStashDir) {
786
+ assertSetupSandbox(explicitStashDir, true);
787
+ applyStashIsolationToEnv(explicitStashDir, true);
788
+ }
670
789
  const current = loadUserConfig();
671
- const stashDir = opts.dir
672
- ? path.resolve(opts.dir)
673
- : incoming.stashDir
674
- ? path.resolve(incoming.stashDir)
675
- : (current.stashDir ?? getDefaultStashDir());
676
- const stashDirExplicit = opts.dir != null || incoming.stashDir != null;
790
+ const stashDir = explicitStashDir ?? current.stashDir ?? getDefaultStashDir();
791
+ const stashDirExplicit = explicitStashDir != null;
677
792
  assertSetupSandbox(stashDir, stashDirExplicit);
678
793
  applyStashIsolationToEnv(stashDir, stashDirExplicit);
679
- let merged = { ...current, stashDir };
680
- // Deep-merge non-llm/agent keys: nested objects merge key-by-key so a
794
+ let merged = deepMergeConfig(current, {
795
+ ...incoming,
796
+ stashDir,
797
+ });
798
+ // Deep-merge canonical keys: nested objects merge key-by-key so a
681
799
  // partial `--file` only updates the keys it carries and never drops sibling
682
800
  // subkeys (e.g. output.detail survives an output.format-only file). Arrays
683
801
  // and scalars replace wholesale.
684
- for (const key of Object.keys(incoming)) {
685
- if (key === "llm" || key === "agent")
686
- continue;
687
- const incomingVal = incoming[key];
688
- const mergedRec = merged;
689
- mergedRec[key] = deepMergeConfig(mergedRec[key], incomingVal);
690
- }
691
- // Translate legacy llm/agent inputs into the new shape.
692
- if (incoming.llm) {
693
- merged = { ...merged, ...applyLegacyLlm(merged, incoming.llm) };
694
- }
695
- if (incoming.agent) {
696
- merged = { ...merged, ...applyLegacyAgent(merged, incoming.agent) };
697
- }
698
802
  // With `--yes`, fill keys still missing after the merge with non-interactive
699
803
  // defaults. Steps start from `merged` and their nonInteractive path only
700
804
  // populates absent values, so nothing the file or existing config supplied
@@ -709,29 +813,26 @@ export async function runSetupFromConfig(opts) {
709
813
  await runSetupSteps(steps, ctx);
710
814
  if (!ctx.config.stashDir)
711
815
  ctx.apply({ stashDir });
712
- if (!ctx.config.defaults?.agent) {
816
+ if (!ctx.config.defaults?.engine) {
713
817
  const detected = detectAgentCliProfiles(undefined);
714
818
  const defaultProfile = pickDefaultAgentProfile(detected, undefined);
715
819
  if (defaultProfile) {
716
- ctx.apply(applyLegacyAgent(ctx.config, { default: defaultProfile }));
820
+ ctx.apply(upsertDetectedAgentEngine(ctx.config, defaultProfile).config);
717
821
  }
718
822
  }
719
823
  merged = ctx.config;
720
824
  }
721
- // Bootstrap directory structure
722
- let initResult;
723
- if (!opts.noInit) {
724
- initResult = await akmInit({ dir: stashDir, setDefault: true });
725
- }
825
+ // Reject an invalid merged engine graph before probing or touching the stash.
826
+ validateCompleteConfig(merged);
726
827
  // Optional probe
727
- const mergedLlm = getDefaultLlmConfig(merged);
828
+ const mergedLlm = readCurrentLlmEngine(merged);
728
829
  if (opts.probe && mergedLlm) {
729
830
  try {
730
831
  const caps = await probeLlmCapabilities(mergedLlm);
731
832
  if (caps.reachable) {
732
833
  merged = {
733
834
  ...merged,
734
- ...applyLegacyLlm(merged, {
835
+ ...writeLlmEngine(merged, {
735
836
  ...mergedLlm,
736
837
  capabilities: { structuredOutput: caps.structuredOutput ?? false },
737
838
  }),
@@ -742,9 +843,12 @@ export async function runSetupFromConfig(opts) {
742
843
  // Non-fatal: probe failure is informational only
743
844
  }
744
845
  }
745
- const cfgPath3 = getConfigPath();
746
- backupAndAnnounce(cfgPath3);
747
- saveConfig(merged);
846
+ validateCompleteConfig(merged);
847
+ const { precommit: initResult } = await saveSetupConfig(current, merged, async () => {
848
+ if (opts.noInit)
849
+ return undefined;
850
+ return akmInit({ dir: stashDir, setDefault: true, persistConfig: false });
851
+ });
748
852
  return {
749
853
  configPath: getConfigPath(),
750
854
  stashDir,