akm-cli 0.7.5 → 0.8.0-rc.6

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 (236) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
  2. package/README.md +20 -4
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli.js +1995 -551
  9. package/dist/commands/agent-dispatch.js +110 -0
  10. package/dist/commands/agent-support.js +68 -0
  11. package/dist/commands/completions.js +3 -0
  12. package/dist/commands/config-cli.js +130 -534
  13. package/dist/commands/consolidate.js +1531 -0
  14. package/dist/commands/curate.js +44 -3
  15. package/dist/commands/db-cli.js +23 -0
  16. package/dist/commands/distill-promotion-policy.js +660 -0
  17. package/dist/commands/distill.js +990 -75
  18. package/dist/commands/eval-cases.js +43 -0
  19. package/dist/commands/events.js +5 -23
  20. package/dist/commands/graph.js +477 -0
  21. package/dist/commands/health.js +400 -0
  22. package/dist/commands/help/help-accept.md +9 -0
  23. package/dist/commands/help/help-improve.md +77 -0
  24. package/dist/commands/help/help-proposals.md +15 -0
  25. package/dist/commands/help/help-propose.md +17 -0
  26. package/dist/commands/help/help-reject.md +8 -0
  27. package/dist/commands/history.js +54 -46
  28. package/dist/commands/improve-profiles.js +146 -0
  29. package/dist/commands/improve-result-file.js +103 -0
  30. package/dist/commands/improve.js +2175 -0
  31. package/dist/commands/info.js +5 -2
  32. package/dist/commands/init.js +50 -2
  33. package/dist/commands/installed-stashes.js +102 -139
  34. package/dist/commands/knowledge.js +136 -0
  35. package/dist/commands/lint/agent-linter.js +49 -0
  36. package/dist/commands/lint/base-linter.js +479 -0
  37. package/dist/commands/lint/command-linter.js +49 -0
  38. package/dist/commands/lint/default-linter.js +16 -0
  39. package/dist/commands/lint/index.js +183 -0
  40. package/dist/commands/lint/knowledge-linter.js +16 -0
  41. package/dist/commands/lint/markdown-insertion.js +343 -0
  42. package/dist/commands/lint/memory-linter.js +61 -0
  43. package/dist/commands/lint/registry.js +36 -0
  44. package/dist/commands/lint/skill-linter.js +45 -0
  45. package/dist/commands/lint/task-linter.js +50 -0
  46. package/dist/commands/lint/types.js +4 -0
  47. package/dist/commands/lint/vault-key-rules.js +139 -0
  48. package/dist/commands/lint/workflow-linter.js +56 -0
  49. package/dist/commands/lint.js +4 -0
  50. package/dist/commands/migration-help.js +5 -2
  51. package/dist/commands/proposal.js +66 -12
  52. package/dist/commands/propose.js +86 -31
  53. package/dist/commands/reflect.js +1119 -73
  54. package/dist/commands/registry-search.js +5 -2
  55. package/dist/commands/remember.js +69 -6
  56. package/dist/commands/schema-repair.js +203 -0
  57. package/dist/commands/search.js +115 -14
  58. package/dist/commands/self-update.js +3 -0
  59. package/dist/commands/show.js +144 -25
  60. package/dist/commands/source-add.js +17 -45
  61. package/dist/commands/source-clone.js +3 -0
  62. package/dist/commands/source-manage.js +14 -19
  63. package/dist/commands/tasks.js +438 -0
  64. package/dist/commands/url-checker.js +42 -0
  65. package/dist/commands/vault.js +130 -77
  66. package/dist/core/action-contributors.js +28 -0
  67. package/dist/core/asset-ref.js +7 -0
  68. package/dist/core/asset-registry.js +7 -16
  69. package/dist/core/asset-serialize.js +88 -0
  70. package/dist/core/asset-spec.js +22 -0
  71. package/dist/core/common.js +157 -0
  72. package/dist/core/concurrent.js +25 -0
  73. package/dist/core/config-io.js +347 -0
  74. package/dist/core/config-migration.js +625 -0
  75. package/dist/core/config-schema.js +501 -0
  76. package/dist/core/config-sources.js +108 -0
  77. package/dist/core/config-types.js +4 -0
  78. package/dist/core/config-walker.js +337 -0
  79. package/dist/core/config.js +327 -987
  80. package/dist/core/errors.js +40 -19
  81. package/dist/core/events.js +91 -138
  82. package/dist/core/file-lock.js +104 -0
  83. package/dist/core/frontmatter.js +3 -6
  84. package/dist/core/lesson-lint.js +3 -0
  85. package/dist/core/markdown.js +20 -0
  86. package/dist/core/memory-belief.js +62 -0
  87. package/dist/core/memory-contradiction-detect.js +274 -0
  88. package/dist/core/memory-improve.js +806 -0
  89. package/dist/core/parse.js +158 -0
  90. package/dist/core/paths.js +326 -14
  91. package/dist/core/proposal-quality-validators.js +364 -0
  92. package/dist/core/proposal-validators.js +69 -0
  93. package/dist/core/proposals.js +498 -42
  94. package/dist/core/state-db.js +927 -0
  95. package/dist/core/text-truncation.js +107 -0
  96. package/dist/core/time.js +54 -0
  97. package/dist/core/warn.js +62 -1
  98. package/dist/core/write-source.js +3 -0
  99. package/dist/indexer/db-backup.js +391 -0
  100. package/dist/indexer/db-search.js +152 -253
  101. package/dist/indexer/db.js +933 -103
  102. package/dist/indexer/ensure-index.js +64 -0
  103. package/dist/indexer/file-context.js +3 -0
  104. package/dist/indexer/graph-boost.js +376 -101
  105. package/dist/indexer/graph-db.js +391 -0
  106. package/dist/indexer/graph-dedup.js +95 -0
  107. package/dist/indexer/graph-extraction.js +550 -124
  108. package/dist/indexer/index-context.js +4 -0
  109. package/dist/indexer/indexer.js +506 -291
  110. package/dist/indexer/llm-cache.js +47 -0
  111. package/dist/indexer/manifest.js +3 -0
  112. package/dist/indexer/matchers.js +148 -160
  113. package/dist/indexer/memory-inference.js +99 -74
  114. package/dist/indexer/metadata-contributors.js +29 -0
  115. package/dist/indexer/metadata.js +255 -196
  116. package/dist/indexer/path-resolver.js +92 -0
  117. package/dist/indexer/project-context.js +192 -0
  118. package/dist/indexer/ranking-contributors.js +331 -0
  119. package/dist/indexer/ranking.js +81 -0
  120. package/dist/indexer/search-fields.js +5 -9
  121. package/dist/indexer/search-hit-enrichers.js +111 -0
  122. package/dist/indexer/search-source.js +44 -10
  123. package/dist/indexer/semantic-status.js +5 -16
  124. package/dist/indexer/staleness-detect.js +447 -0
  125. package/dist/indexer/usage-events.js +12 -9
  126. package/dist/indexer/walker.js +28 -0
  127. package/dist/integrations/agent/builders.js +135 -0
  128. package/dist/integrations/agent/config.js +122 -230
  129. package/dist/integrations/agent/detect.js +3 -0
  130. package/dist/integrations/agent/index.js +7 -13
  131. package/dist/integrations/agent/model-aliases.js +55 -0
  132. package/dist/integrations/agent/profiles.js +70 -5
  133. package/dist/integrations/agent/prompts.js +150 -74
  134. package/dist/integrations/agent/runner.js +151 -0
  135. package/dist/integrations/agent/sdk-runner.js +126 -0
  136. package/dist/integrations/agent/spawn.js +118 -23
  137. package/dist/integrations/github.js +3 -0
  138. package/dist/integrations/lockfile.js +32 -69
  139. package/dist/integrations/session-logs/index.js +68 -0
  140. package/dist/integrations/session-logs/providers/claude-code.js +59 -0
  141. package/dist/integrations/session-logs/providers/opencode.js +55 -0
  142. package/dist/integrations/session-logs/types.js +4 -0
  143. package/dist/llm/call-ai.js +62 -0
  144. package/dist/llm/client.js +72 -124
  145. package/dist/llm/embedder.js +3 -19
  146. package/dist/llm/embedders/cache.js +3 -7
  147. package/dist/llm/embedders/local.js +3 -0
  148. package/dist/llm/embedders/remote.js +20 -8
  149. package/dist/llm/embedders/types.js +3 -7
  150. package/dist/llm/feature-gate.js +89 -48
  151. package/dist/llm/graph-extract.js +676 -70
  152. package/dist/llm/index-passes.js +9 -23
  153. package/dist/llm/memory-infer.js +52 -71
  154. package/dist/llm/metadata-enhance.js +42 -29
  155. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  156. package/dist/output/cli-hints-full.md +281 -0
  157. package/dist/output/cli-hints-short.md +65 -0
  158. package/dist/output/cli-hints.js +5 -318
  159. package/dist/output/context.js +3 -0
  160. package/dist/output/renderers.js +223 -256
  161. package/dist/output/shapes.js +150 -105
  162. package/dist/output/text.js +318 -30
  163. package/dist/registry/build-index.js +3 -0
  164. package/dist/registry/create-provider-registry.js +3 -0
  165. package/dist/registry/factory.js +3 -0
  166. package/dist/registry/origin-resolve.js +3 -0
  167. package/dist/registry/providers/index.js +3 -0
  168. package/dist/registry/providers/skills-sh.js +70 -49
  169. package/dist/registry/providers/static-index.js +53 -48
  170. package/dist/registry/providers/types.js +3 -24
  171. package/dist/registry/resolve.js +11 -16
  172. package/dist/registry/types.js +3 -0
  173. package/dist/scripts/migrate-storage.js +17307 -0
  174. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
  175. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  176. package/dist/setup/detect.js +3 -0
  177. package/dist/setup/ripgrep-install.js +3 -0
  178. package/dist/setup/ripgrep-resolve.js +3 -0
  179. package/dist/setup/setup.js +775 -37
  180. package/dist/setup/steps.js +3 -15
  181. package/dist/sources/include.js +3 -0
  182. package/dist/sources/provider-factory.js +5 -12
  183. package/dist/sources/provider.js +3 -20
  184. package/dist/sources/providers/filesystem.js +19 -23
  185. package/dist/sources/providers/git.js +7 -5
  186. package/dist/sources/providers/index.js +3 -0
  187. package/dist/sources/providers/install-types.js +3 -13
  188. package/dist/sources/providers/npm.js +3 -4
  189. package/dist/sources/providers/provider-utils.js +3 -0
  190. package/dist/sources/providers/sync-from-ref.js +3 -11
  191. package/dist/sources/providers/tar-utils.js +3 -0
  192. package/dist/sources/providers/website.js +18 -22
  193. package/dist/sources/resolve.js +3 -0
  194. package/dist/sources/types.js +3 -0
  195. package/dist/sources/website-ingest.js +7 -0
  196. package/dist/tasks/backends/cron.js +203 -0
  197. package/dist/tasks/backends/exec-utils.js +28 -0
  198. package/dist/tasks/backends/index.js +24 -0
  199. package/dist/tasks/backends/launchd-template.xml +19 -0
  200. package/dist/tasks/backends/launchd.js +187 -0
  201. package/dist/tasks/backends/schtasks-template.xml +29 -0
  202. package/dist/tasks/backends/schtasks.js +215 -0
  203. package/dist/tasks/parser.js +211 -0
  204. package/dist/tasks/resolveAkmBin.js +87 -0
  205. package/dist/tasks/runner.js +458 -0
  206. package/dist/tasks/schedule.js +211 -0
  207. package/dist/tasks/schema.js +15 -0
  208. package/dist/tasks/validator.js +62 -0
  209. package/dist/version.js +3 -0
  210. package/dist/wiki/index-template.md +12 -0
  211. package/dist/wiki/ingest-workflow-template.md +54 -0
  212. package/dist/wiki/log-template.md +8 -0
  213. package/dist/wiki/schema-template.md +61 -0
  214. package/dist/wiki/wiki-templates.js +15 -0
  215. package/dist/wiki/wiki.js +13 -61
  216. package/dist/workflows/authoring.js +8 -25
  217. package/dist/workflows/cli.js +3 -0
  218. package/dist/workflows/db.js +140 -10
  219. package/dist/workflows/document-cache.js +3 -10
  220. package/dist/workflows/parser.js +3 -0
  221. package/dist/workflows/renderer.js +11 -3
  222. package/dist/workflows/runs.js +62 -91
  223. package/dist/workflows/schema.js +3 -0
  224. package/dist/workflows/scope-key.js +3 -0
  225. package/dist/workflows/validator.js +4 -8
  226. package/dist/workflows/workflow-template.md +24 -0
  227. package/docs/README.md +9 -2
  228. package/docs/data-and-telemetry.md +225 -0
  229. package/docs/migration/release-notes/0.7.0.md +1 -1
  230. package/docs/migration/release-notes/0.7.5.md +2 -2
  231. package/docs/migration/release-notes/0.8.0.md +48 -0
  232. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  233. package/package.json +20 -8
  234. package/.github/LICENSE +0 -374
  235. package/dist/commands/install-audit.js +0 -381
  236. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,625 @@
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
+ /**
5
+ * Config shape migration logic for AKM v0.8.0.
6
+ *
7
+ * This module is intentionally kept free of imports from `config.ts` to avoid
8
+ * circular dependencies: `config.ts` imports `migrateConfigShape` from here,
9
+ * and `config-migrate.ts` (the CLI command) also imports from here.
10
+ *
11
+ * Migration policy (0.8.0): every legacy field is migrated to its NEW final
12
+ * location and stripped from the raw config. Production code reads ONLY the
13
+ * new shape. There are no backward-compat shims after this migration.
14
+ */
15
+ import { warn } from "./warn";
16
+ /**
17
+ * Current config schema version sentinel.
18
+ * Configs at this version are considered fully migrated and will not be rewritten.
19
+ */
20
+ export const CURRENT_CONFIG_VERSION = "0.8.0";
21
+ /**
22
+ * Compare two `configVersion` values and return -1/0/1 (a<b / a==b / a>b).
23
+ *
24
+ * Both the legacy numeric scheme (`1`, `2`, …) and the semver-string scheme
25
+ * (`"0.8.0"`, `"0.9.1"`) are accepted. Mixed comparisons promote the numeric
26
+ * value to a semver-like string of the form `0.N.0` (so legacy `2` ≈ `"0.2.0"`
27
+ * is compared element-wise against the string form). Returns `undefined`
28
+ * when either value cannot be parsed at all.
29
+ */
30
+ export function compareConfigVersion(a, b) {
31
+ const partsA = normalizeVersion(a);
32
+ const partsB = normalizeVersion(b);
33
+ if (!partsA || !partsB)
34
+ return undefined;
35
+ const len = Math.max(partsA.length, partsB.length);
36
+ for (let i = 0; i < len; i++) {
37
+ const ai = partsA[i] ?? 0;
38
+ const bi = partsB[i] ?? 0;
39
+ if (ai < bi)
40
+ return -1;
41
+ if (ai > bi)
42
+ return 1;
43
+ }
44
+ return 0;
45
+ }
46
+ function normalizeVersion(v) {
47
+ if (typeof v === "number" && Number.isFinite(v)) {
48
+ return [0, Math.trunc(v), 0];
49
+ }
50
+ if (typeof v === "string") {
51
+ const trimmed = v.trim();
52
+ if (!trimmed)
53
+ return undefined;
54
+ const cleaned = trimmed.replace(/^v/i, "").split(/[-+]/, 1)[0];
55
+ const segments = cleaned.split(".").map((part) => Number.parseInt(part, 10));
56
+ if (segments.length === 0 || segments.some((n) => !Number.isFinite(n)))
57
+ return undefined;
58
+ return segments;
59
+ }
60
+ return undefined;
61
+ }
62
+ // ── Helpers for deep-merging into nested locations ──────────────────────────
63
+ function getObj(parent, key) {
64
+ const existing = parent[key];
65
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
66
+ return existing;
67
+ }
68
+ const next = {};
69
+ parent[key] = next;
70
+ return next;
71
+ }
72
+ /** Get the `profiles.improve.default.processes.<name>` object (creating intermediate nodes). */
73
+ function getImproveProcess(result, processName) {
74
+ const profiles = getObj(result, "profiles");
75
+ const improve = getObj(profiles, "improve");
76
+ const defaultProfile = getObj(improve, "default");
77
+ const processes = getObj(defaultProfile, "processes");
78
+ return getObj(processes, processName);
79
+ }
80
+ function isObj(v) {
81
+ return !!v && typeof v === "object" && !Array.isArray(v);
82
+ }
83
+ function hasOpenvikingSource(raw) {
84
+ const sources = raw.sources;
85
+ if (!Array.isArray(sources))
86
+ return false;
87
+ for (const entry of sources) {
88
+ if (isObj(entry) && entry.type === "openviking")
89
+ return true;
90
+ }
91
+ return false;
92
+ }
93
+ /**
94
+ * Convert a snake_case or kebab-case identifier into camelCase. Leaves an
95
+ * already-camelCased value untouched. Used by the catch-all branches to
96
+ * normalize unknown legacy keys (e.g. `my_custom_process` → `myCustomProcess`).
97
+ */
98
+ function toCamelCase(key) {
99
+ return key.replace(/[-_]([a-zA-Z0-9])/g, (_, ch) => ch.toUpperCase());
100
+ }
101
+ /**
102
+ * Migrate a generic feature-gate-shaped value (boolean OR an object that may
103
+ * carry `{ enabled, options }`) into a target object at `result[section][key]`
104
+ * (where section is "index" or "search"). Preserves recognized `options.*`
105
+ * fields by spreading them onto the target shallowly.
106
+ *
107
+ * Returns true when the value was understood enough to set anything.
108
+ */
109
+ function migrateGenericGateToSection(result, section, camelKey, legacy) {
110
+ const parent = getObj(result, section);
111
+ const target = getObj(parent, camelKey);
112
+ if (typeof legacy === "boolean") {
113
+ target.enabled = legacy;
114
+ return true;
115
+ }
116
+ if (!isObj(legacy))
117
+ return false;
118
+ let touched = false;
119
+ if (typeof legacy.enabled === "boolean") {
120
+ target.enabled = legacy.enabled;
121
+ touched = true;
122
+ }
123
+ if (isObj(legacy.options)) {
124
+ // Best-effort: copy primitive option values onto the target so they aren't
125
+ // dropped. The exact final shape is unknown for unrecognized keys, so we
126
+ // place them under an `options` sub-object to keep the new shape clean.
127
+ target.options = { ...(isObj(target.options) ? target.options : {}), ...legacy.options };
128
+ touched = true;
129
+ }
130
+ return touched;
131
+ }
132
+ /**
133
+ * Migrate a single legacy ProcessEntry-like value (boolean or
134
+ * { enabled, mode, profile, timeoutMs, options }) into an ImproveProcessConfig
135
+ * at the target location. Merges shallowly with anything already present.
136
+ */
137
+ function migrateProcessEntryToImprove(result, processName, legacy) {
138
+ const target = getImproveProcess(result, processName);
139
+ if (typeof legacy === "boolean") {
140
+ target.enabled = legacy;
141
+ return;
142
+ }
143
+ if (!isObj(legacy))
144
+ return;
145
+ if (typeof legacy.enabled === "boolean")
146
+ target.enabled = legacy.enabled;
147
+ if (typeof legacy.mode === "string")
148
+ target.mode = legacy.mode;
149
+ if (typeof legacy.profile === "string")
150
+ target.profile = legacy.profile;
151
+ if (legacy.timeoutMs === null || typeof legacy.timeoutMs === "number")
152
+ target.timeoutMs = legacy.timeoutMs;
153
+ if (isObj(legacy.options)) {
154
+ const opts = legacy.options;
155
+ if (typeof opts.cooldown === "object" && opts.cooldown !== null) {
156
+ target.cooldownByType = opts.cooldown;
157
+ }
158
+ if (typeof opts.cooldownDays === "number") {
159
+ target.cooldownDays = opts.cooldownDays;
160
+ }
161
+ if (Array.isArray(opts.allowedTypes)) {
162
+ target.allowedTypes = opts.allowedTypes;
163
+ }
164
+ }
165
+ }
166
+ /**
167
+ * Determine whether a raw config object needs migration to the 0.8.0 shape and
168
+ * apply any necessary field renames or promotions.
169
+ *
170
+ * A config is considered already migrated when `configVersion === "0.8.0"`
171
+ * (canonical string sentinel for this release) or when `configVersion` is a
172
+ * number ≥ 2 AND the config carries no recognized legacy keys.
173
+ */
174
+ export function migrateConfigShape(raw) {
175
+ const hasLegacyKeys = Object.hasOwn(raw, "features") ||
176
+ (isObj(raw.llm) && (Object.hasOwn(raw.llm, "endpoint") || Object.hasOwn(raw.llm, "features"))) ||
177
+ isObj(raw.agent) ||
178
+ Object.hasOwn(raw, "stashes") ||
179
+ typeof raw.semanticSearchMode === "boolean" ||
180
+ hasOpenvikingSource(raw);
181
+ // Already migrated — string sentinel "0.8.0" with no legacy keys present.
182
+ if (raw.configVersion === CURRENT_CONFIG_VERSION && !hasLegacyKeys) {
183
+ return { changed: false, result: raw };
184
+ }
185
+ // Legacy numeric versioning sentinel (>= 2) with no legacy keys present.
186
+ if (typeof raw.configVersion === "number" && raw.configVersion >= 2 && !hasLegacyKeys) {
187
+ return { changed: false, result: raw };
188
+ }
189
+ const result = { ...raw };
190
+ let changed = false;
191
+ // ── 0) Pre-migrations: legacy keys and shape coercions ─────────────────
192
+ //
193
+ // These run before the deeper feature-block migrations because they
194
+ // affect keys the later passes assume are already canonical.
195
+ // 0a) Coerce semanticSearchMode boolean → string ("auto" | "off").
196
+ if (typeof result.semanticSearchMode === "boolean") {
197
+ result.semanticSearchMode = result.semanticSearchMode ? "auto" : "off";
198
+ changed = true;
199
+ }
200
+ // 0b) Rename legacy stashes[] → sources[].
201
+ if (Array.isArray(result.stashes)) {
202
+ if (!Array.isArray(result.sources)) {
203
+ result.sources = result.stashes;
204
+ console.warn("[akm config-migrate] Legacy `stashes[]` config key renamed to `sources[]`. " +
205
+ "Re-save your config to remove the deprecation notice.");
206
+ }
207
+ else {
208
+ console.warn("[akm config-migrate] Both `stashes[]` and `sources[]` present; `stashes[]` dropped (sources takes precedence).");
209
+ }
210
+ delete result.stashes;
211
+ changed = true;
212
+ }
213
+ // 0c) Rename openviking source type. The old type is gone in 0.8.0; the
214
+ // canonical replacement is a website source. Users with non-trivial
215
+ // openviking config likely need manual intervention — we warn loudly.
216
+ if (Array.isArray(result.sources)) {
217
+ const sources = result.sources;
218
+ const mapped = [];
219
+ let renamed = false;
220
+ for (const entry of sources) {
221
+ if (isObj(entry) && entry.type === "openviking") {
222
+ const name = typeof entry.name === "string" && entry.name ? entry.name : "unnamed";
223
+ console.warn(`[akm config-migrate] Source "${name}" (type: openviking) is no longer supported. ` +
224
+ "Remove it from your config, or replace with a `website`/`git` source. " +
225
+ "Entry dropped from sources[].");
226
+ renamed = true;
227
+ continue;
228
+ }
229
+ mapped.push(entry);
230
+ }
231
+ if (renamed) {
232
+ result.sources = mapped;
233
+ changed = true;
234
+ }
235
+ }
236
+ // ── 1) Migrate `llm.features.*` → new homes ────────────────────────────
237
+ if (isObj(result.llm)) {
238
+ const llm = { ...result.llm };
239
+ if (isObj(llm.features)) {
240
+ const llmFeatures = llm.features;
241
+ const setProcessEnabled = (processName, enabled) => {
242
+ if (typeof enabled !== "boolean")
243
+ return;
244
+ const target = getImproveProcess(result, processName);
245
+ target.enabled = enabled;
246
+ };
247
+ if ("memory_consolidation" in llmFeatures) {
248
+ setProcessEnabled("consolidate", llmFeatures.memory_consolidation);
249
+ }
250
+ if ("feedback_distillation" in llmFeatures) {
251
+ setProcessEnabled("feedbackDistillation", llmFeatures.feedback_distillation);
252
+ }
253
+ if ("memory_inference" in llmFeatures) {
254
+ setProcessEnabled("memoryInference", llmFeatures.memory_inference);
255
+ }
256
+ if ("graph_extraction" in llmFeatures) {
257
+ setProcessEnabled("graphExtraction", llmFeatures.graph_extraction);
258
+ }
259
+ if ("metadata_enhance" in llmFeatures) {
260
+ const index = getObj(result, "index");
261
+ const me = getObj(index, "metadataEnhance");
262
+ if (typeof llmFeatures.metadata_enhance === "boolean")
263
+ me.enabled = llmFeatures.metadata_enhance;
264
+ }
265
+ if ("curate_rerank" in llmFeatures) {
266
+ const search = getObj(result, "search");
267
+ const cr = getObj(search, "curateRerank");
268
+ if (typeof llmFeatures.curate_rerank === "boolean")
269
+ cr.enabled = llmFeatures.curate_rerank;
270
+ }
271
+ if ("lesson_quality_gate" in llmFeatures) {
272
+ const distill = getImproveProcess(result, "distill");
273
+ const qg = getObj(distill, "qualityGate");
274
+ if (typeof llmFeatures.lesson_quality_gate === "boolean")
275
+ qg.enabled = llmFeatures.lesson_quality_gate;
276
+ }
277
+ if ("proposal_quality_gate" in llmFeatures) {
278
+ const reflect = getImproveProcess(result, "reflect");
279
+ const qg = getObj(reflect, "qualityGate");
280
+ if (typeof llmFeatures.proposal_quality_gate === "boolean")
281
+ qg.enabled = llmFeatures.proposal_quality_gate;
282
+ }
283
+ if ("memory_contradiction_detection" in llmFeatures) {
284
+ const consolidate = getImproveProcess(result, "consolidate");
285
+ const cd = getObj(consolidate, "contradictionDetection");
286
+ if (typeof llmFeatures.memory_contradiction_detection === "boolean") {
287
+ cd.enabled = llmFeatures.memory_contradiction_detection;
288
+ }
289
+ }
290
+ delete llm.features;
291
+ changed = true;
292
+ }
293
+ // 2) Migrate top-level `llm` (LlmConnectionConfig) → `profiles.llm.default`
294
+ // + `defaults.llm = "default"` if it actually has connection fields.
295
+ if (typeof llm.endpoint === "string") {
296
+ const profiles = getObj(result, "profiles");
297
+ const llmProfiles = getObj(profiles, "llm");
298
+ // Don't overwrite if a "default" entry already exists.
299
+ if (!isObj(llmProfiles.default)) {
300
+ llmProfiles.default = { ...llm };
301
+ }
302
+ const defaults = getObj(result, "defaults");
303
+ if (typeof defaults.llm !== "string")
304
+ defaults.llm = "default";
305
+ changed = true;
306
+ }
307
+ // Always strip the legacy `llm` block — its only remaining job was holding
308
+ // connection fields and `features`, both migrated.
309
+ delete result.llm;
310
+ }
311
+ // ── 3) Migrate `features.*` (top-level) → new homes ────────────────────
312
+ if (isObj(result.features)) {
313
+ const features = result.features;
314
+ if (isObj(features.improve)) {
315
+ const fi = features.improve;
316
+ // memory_consolidation (bool or ProcessEntry) → processes.consolidate
317
+ if ("memory_consolidation" in fi) {
318
+ migrateProcessEntryToImprove(result, "consolidate", fi.memory_consolidation);
319
+ }
320
+ // feedback_distillation (bool) → processes.feedbackDistillation
321
+ if ("feedback_distillation" in fi) {
322
+ migrateProcessEntryToImprove(result, "feedbackDistillation", fi.feedback_distillation);
323
+ }
324
+ // validation (ProcessEntry) → processes.validation
325
+ if ("validation" in fi) {
326
+ migrateProcessEntryToImprove(result, "validation", fi.validation);
327
+ }
328
+ // reflect/distill/consolidate (ProcessEntry) — merged with .options.cooldown → cooldownByType
329
+ if ("reflect" in fi)
330
+ migrateProcessEntryToImprove(result, "reflect", fi.reflect);
331
+ if ("distill" in fi)
332
+ migrateProcessEntryToImprove(result, "distill", fi.distill);
333
+ if ("consolidate" in fi)
334
+ migrateProcessEntryToImprove(result, "consolidate", fi.consolidate);
335
+ // Catch-all: any remaining keys are treated as custom processes and
336
+ // migrated to profiles.improve.default.processes.<camelKey>. Without
337
+ // this branch, user-defined entries would be silently dropped when the
338
+ // legacy `features` block is deleted below.
339
+ const knownImproveKeys = new Set([
340
+ "memory_consolidation",
341
+ "feedback_distillation",
342
+ "validation",
343
+ "reflect",
344
+ "distill",
345
+ "consolidate",
346
+ ]);
347
+ for (const [legacyKey, legacyVal] of Object.entries(fi)) {
348
+ if (knownImproveKeys.has(legacyKey))
349
+ continue;
350
+ const camelKey = toCamelCase(legacyKey);
351
+ if (typeof legacyVal === "boolean" || isObj(legacyVal)) {
352
+ migrateProcessEntryToImprove(result, camelKey, legacyVal);
353
+ warn(`[akm config-migrate] Unknown features.improve.${legacyKey} migrated to ` +
354
+ `profiles.improve.default.processes.${camelKey}. Please verify the new location.`);
355
+ }
356
+ else {
357
+ warn(`[akm config-migrate] features.improve.${legacyKey} has an unrecognized value type ` +
358
+ `(${typeof legacyVal}); dropping. Please re-add it under profiles.improve.* manually.`);
359
+ }
360
+ }
361
+ changed = true;
362
+ }
363
+ if (isObj(features.index)) {
364
+ const findex = features.index;
365
+ if ("memory_inference" in findex) {
366
+ migrateProcessEntryToImprove(result, "memoryInference", findex.memory_inference);
367
+ }
368
+ if ("graph_extraction" in findex) {
369
+ migrateProcessEntryToImprove(result, "graphExtraction", findex.graph_extraction);
370
+ }
371
+ if ("metadata_enhance" in findex) {
372
+ const index = getObj(result, "index");
373
+ const me = getObj(index, "metadataEnhance");
374
+ const val = findex.metadata_enhance;
375
+ if (typeof val === "boolean")
376
+ me.enabled = val;
377
+ else if (isObj(val) && typeof val.enabled === "boolean")
378
+ me.enabled = val.enabled;
379
+ }
380
+ if ("staleness_detection" in findex) {
381
+ const index = getObj(result, "index");
382
+ const sd = getObj(index, "stalenessDetection");
383
+ const val = findex.staleness_detection;
384
+ if (typeof val === "boolean")
385
+ sd.enabled = val;
386
+ else if (isObj(val)) {
387
+ if (typeof val.enabled === "boolean")
388
+ sd.enabled = val.enabled;
389
+ if (isObj(val.options) && typeof val.options.thresholdDays === "number") {
390
+ sd.thresholdDays = val.options.thresholdDays;
391
+ }
392
+ }
393
+ }
394
+ // Catch-all: unknown features.index.<key> entries land at
395
+ // index.<keyAsCamelCase> (preserving { enabled, options } when present).
396
+ const knownIndexKeys = new Set([
397
+ "memory_inference",
398
+ "graph_extraction",
399
+ "metadata_enhance",
400
+ "staleness_detection",
401
+ ]);
402
+ for (const [legacyKey, legacyVal] of Object.entries(findex)) {
403
+ if (knownIndexKeys.has(legacyKey))
404
+ continue;
405
+ const camelKey = toCamelCase(legacyKey);
406
+ const ok = migrateGenericGateToSection(result, "index", camelKey, legacyVal);
407
+ if (ok) {
408
+ warn(`[akm config-migrate] Unknown features.index.${legacyKey} migrated to ` +
409
+ `index.${camelKey}. Please verify the new location is correct.`);
410
+ }
411
+ else {
412
+ warn(`[akm config-migrate] features.index.${legacyKey} has an unrecognized value shape; ` +
413
+ `dropping. Please re-add it under index.${camelKey} manually if needed.`);
414
+ }
415
+ }
416
+ changed = true;
417
+ }
418
+ if (isObj(features.search)) {
419
+ const fsearch = features.search;
420
+ if ("curate_rerank" in fsearch) {
421
+ const search = getObj(result, "search");
422
+ const cr = getObj(search, "curateRerank");
423
+ const val = fsearch.curate_rerank;
424
+ if (typeof val === "boolean")
425
+ cr.enabled = val;
426
+ else if (isObj(val) && typeof val.enabled === "boolean")
427
+ cr.enabled = val.enabled;
428
+ }
429
+ // Catch-all: unknown features.search.<key> entries land at
430
+ // search.<keyAsCamelCase> (preserving { enabled, options } when present).
431
+ const knownSearchKeys = new Set(["curate_rerank"]);
432
+ for (const [legacyKey, legacyVal] of Object.entries(fsearch)) {
433
+ if (knownSearchKeys.has(legacyKey))
434
+ continue;
435
+ const camelKey = toCamelCase(legacyKey);
436
+ const ok = migrateGenericGateToSection(result, "search", camelKey, legacyVal);
437
+ if (ok) {
438
+ warn(`[akm config-migrate] Unknown features.search.${legacyKey} migrated to ` +
439
+ `search.${camelKey}. Please verify the new location is correct.`);
440
+ }
441
+ else {
442
+ warn(`[akm config-migrate] features.search.${legacyKey} has an unrecognized value shape; ` +
443
+ `dropping. Please re-add it under search.${camelKey} manually if needed.`);
444
+ }
445
+ }
446
+ changed = true;
447
+ }
448
+ delete result.features;
449
+ }
450
+ // ── 4) Migrate `agent.*` (v1) → `profiles.agent` + `defaults.agent` ──────
451
+ if (isObj(result.agent)) {
452
+ const agent = result.agent;
453
+ // 4a) agent.default → defaults.agent
454
+ if (typeof agent.default === "string" && agent.default.trim()) {
455
+ const defaults = getObj(result, "defaults");
456
+ if (typeof defaults.agent !== "string")
457
+ defaults.agent = agent.default.trim();
458
+ changed = true;
459
+ }
460
+ // 4b) agent.profiles → profiles.agent (only entries with valid `platform`)
461
+ if (isObj(agent.profiles)) {
462
+ const v1Profiles = agent.profiles;
463
+ const profiles = getObj(result, "profiles");
464
+ const agentProfiles = getObj(profiles, "agent");
465
+ for (const [name, raw] of Object.entries(v1Profiles)) {
466
+ if (!isObj(raw))
467
+ continue;
468
+ if (isObj(agentProfiles[name]))
469
+ continue; // do not overwrite existing v2 entries
470
+ // v1 profiles do not carry a "platform" — synthesize one from name where possible.
471
+ const platform = typeof raw.platform === "string" ? raw.platform : guessAgentPlatform(name);
472
+ if (!platform)
473
+ continue;
474
+ const v2 = { platform };
475
+ if (typeof raw.bin === "string" && raw.bin.trim())
476
+ v2.bin = raw.bin.trim();
477
+ if (Array.isArray(raw.args) && raw.args.every((a) => typeof a === "string"))
478
+ v2.args = raw.args;
479
+ if (typeof raw.model === "string" && raw.model.trim())
480
+ v2.model = raw.model.trim();
481
+ if (typeof raw.workspace === "string" && raw.workspace.trim())
482
+ v2.workspace = raw.workspace.trim();
483
+ agentProfiles[name] = v2;
484
+ changed = true;
485
+ }
486
+ }
487
+ // 4c) agent.processes.<name> (v1 binding) → profiles.improve.default.processes.<name>.profile
488
+ if (isObj(agent.processes)) {
489
+ const v1Processes = agent.processes;
490
+ for (const [processName, raw] of Object.entries(v1Processes)) {
491
+ if (processName === "task")
492
+ continue; // legacy v1-only; drop
493
+ let profileName;
494
+ let timeoutMs;
495
+ if (typeof raw === "string" && raw.trim()) {
496
+ profileName = raw.trim();
497
+ }
498
+ else if (isObj(raw)) {
499
+ if (typeof raw.profile === "string" && raw.profile.trim())
500
+ profileName = raw.profile.trim();
501
+ if (raw.timeoutMs === null || typeof raw.timeoutMs === "number") {
502
+ timeoutMs = raw.timeoutMs;
503
+ }
504
+ }
505
+ if (!profileName)
506
+ continue;
507
+ // Map v1 process names to v2 improve process names.
508
+ const v2Name = mapV1ProcessName(processName);
509
+ if (!v2Name)
510
+ continue;
511
+ const target = getImproveProcess(result, v2Name);
512
+ target.profile = profileName;
513
+ target.mode = "agent";
514
+ if (timeoutMs !== undefined)
515
+ target.timeoutMs = timeoutMs;
516
+ changed = true;
517
+ }
518
+ }
519
+ delete result.agent;
520
+ }
521
+ // ── 5) Migrate `improve.*` (top-level pipeline options) ──────────────────
522
+ if (isObj(result.improve)) {
523
+ const improve = { ...result.improve };
524
+ if (isObj(improve.reflectCooldownByType)) {
525
+ const reflect = getImproveProcess(result, "reflect");
526
+ reflect.cooldownByType = improve.reflectCooldownByType;
527
+ changed = true;
528
+ }
529
+ if (typeof improve.limit === "number") {
530
+ const profiles = getObj(result, "profiles");
531
+ const improveProfiles = getObj(profiles, "improve");
532
+ const defaultProfile = getObj(improveProfiles, "default");
533
+ defaultProfile.limit = improve.limit;
534
+ changed = true;
535
+ }
536
+ delete improve.reflectCooldownByType;
537
+ delete improve.limit;
538
+ delete improve.schedule;
539
+ if (Object.keys(improve).length > 0) {
540
+ result.improve = improve;
541
+ }
542
+ else {
543
+ delete result.improve;
544
+ }
545
+ }
546
+ // ── 6) Legacy `defaults.improve` object form ({ limit, preset }) ─────────
547
+ if (isObj(result.defaults)) {
548
+ const defaultsRaw = { ...result.defaults };
549
+ const defaultsImprove = defaultsRaw.improve;
550
+ if (isObj(defaultsImprove)) {
551
+ const improveObj = defaultsImprove;
552
+ if (typeof improveObj.limit === "number") {
553
+ const profiles = getObj(result, "profiles");
554
+ const improveProfiles = getObj(profiles, "improve");
555
+ const defaultProfile = getObj(improveProfiles, "default");
556
+ if (typeof defaultProfile.limit !== "number")
557
+ defaultProfile.limit = improveObj.limit;
558
+ changed = true;
559
+ }
560
+ if (improveObj.preset !== undefined) {
561
+ console.warn("[akm config-migrate] defaults.improve.preset is no longer supported. " +
562
+ "Use `--profile <name>` (built-ins: default, quick, thorough, memory-focus) instead.");
563
+ }
564
+ delete defaultsRaw.improve;
565
+ if (Object.keys(defaultsRaw).length > 0) {
566
+ result.defaults = defaultsRaw;
567
+ }
568
+ else {
569
+ delete result.defaults;
570
+ }
571
+ changed = true;
572
+ }
573
+ }
574
+ // Stamp the new version sentinel on any migration that did substantive work.
575
+ if (changed) {
576
+ result.configVersion = CURRENT_CONFIG_VERSION;
577
+ }
578
+ return { changed, result };
579
+ }
580
+ /**
581
+ * Guess a v2 agent platform for a known v1 profile name. Returns `undefined`
582
+ * for unknown names — the caller drops those entries (they have no usable
583
+ * platform).
584
+ */
585
+ function guessAgentPlatform(name) {
586
+ const lower = name.toLowerCase();
587
+ if (lower === "claude" || lower === "claude-code")
588
+ return "claude";
589
+ if (lower.startsWith("opencode-sdk"))
590
+ return "opencode-sdk";
591
+ if (lower.startsWith("opencode"))
592
+ return "opencode";
593
+ return undefined;
594
+ }
595
+ /**
596
+ * Map a v1 process name (e.g. `"reflect"`, `"propose"`) to its v2 improve
597
+ * process name. Returns `undefined` for names that have no v2 home (e.g.
598
+ * `"task"`, which was removed).
599
+ */
600
+ function mapV1ProcessName(name) {
601
+ switch (name) {
602
+ case "reflect":
603
+ return "reflect";
604
+ case "distill":
605
+ return "distill";
606
+ case "consolidate":
607
+ return "consolidate";
608
+ case "propose":
609
+ // v1 "propose" mapped to reflect/distill at runtime; bind to reflect by default.
610
+ return "reflect";
611
+ case "validation":
612
+ return "validation";
613
+ case "memoryInference":
614
+ case "memory_inference":
615
+ return "memoryInference";
616
+ case "graphExtraction":
617
+ case "graph_extraction":
618
+ return "graphExtraction";
619
+ case "feedbackDistillation":
620
+ case "feedback_distillation":
621
+ return "feedbackDistillation";
622
+ default:
623
+ return undefined;
624
+ }
625
+ }