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
@@ -8,105 +8,27 @@
8
8
  * new config field is one line of Zod schema in `core/config-schema.ts` and
9
9
  * zero lines here — the walker handles get/set/unset/coercion uniformly.
10
10
  *
11
- * Legacy behaviour preserved:
12
- * - `akm config set llm.<x>` writes to `profiles.llm.<defaults.llm>` (or
13
- * auto-creates a "default" profile), mirroring the pre-rewrite shim.
14
- * - `akm config set embedding.ollamaOptions.numCtx` is sugar for
15
- * `embedding.ollamaOptions.num_ctx` (camelCase ↔ snake_case bridge).
11
+ * `configVersion` is controlled by the config lifecycle. All execution
12
+ * settings use their canonical engine/strategy paths; retired aliases are not
13
+ * rewritten at this boundary.
16
14
  * - `parseConfigValue` returns a Partial<AkmConfig> so it can be merged with
17
15
  * the runtime config object via `mergeConfigValue`.
18
16
  */
19
17
  import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
20
18
  import { resolveStashDir } from "../core/common.js";
21
- import { DEFAULT_CONFIG, getSources, loadConfig, loadUserConfig, saveConfig, } from "../core/config/config.js";
19
+ import { DEFAULT_CONFIG, loadConfig, mutateConfig } from "../core/config/config.js";
22
20
  import { configGet, configSet, configUnset, unknownKeyHint } from "../core/config/config-walker.js";
23
21
  import { UsageError } from "../core/errors.js";
24
22
  import { getCacheDir, getConfigPath, getDbPath, getDefaultStashDir } from "../core/paths.js";
25
- // ── Legacy `llm.*` → `profiles.llm.<default>.*` aliasing ────────────────────
26
- /**
27
- * Map a legacy top-level `llm.<sub>` path onto the actual schema path. The
28
- * default profile name is "default" when `defaults.llm` is unset.
29
- */
30
- function rewriteLegacyLlmPath(config, key) {
31
- if (key !== "llm" && !key.startsWith("llm."))
32
- return key;
33
- const sub = key === "llm" ? "" : key.slice("llm.".length);
34
- const profileName = config.defaults?.llm ?? "default";
35
- return sub ? `profiles.llm.${profileName}.${sub}` : `profiles.llm.${profileName}`;
36
- }
37
- /**
38
- * Translate the legacy `embedding.ollamaOptions.numCtx` to the actual schema
39
- * key `embedding.ollamaOptions.num_ctx`.
40
- */
41
- function rewriteEmbeddingPath(key) {
42
- if (key === "embedding.ollamaOptions.numCtx")
43
- return "embedding.ollamaOptions.num_ctx";
44
- return key;
45
- }
46
- /**
47
- * Translate the deprecated `stashes` alias for `sources` (one-way: both read
48
- * and write go through `sources`).
49
- */
50
- function rewriteSourcesAlias(key) {
51
- if (key === "stashes")
52
- return "sources";
53
- if (key.startsWith("stashes."))
54
- return `sources.${key.slice("stashes.".length)}`;
55
- return key;
56
- }
57
- function rewriteKey(config, key) {
58
- let k = rewriteLegacyLlmPath(config, key);
59
- k = rewriteEmbeddingPath(k);
60
- k = rewriteSourcesAlias(k);
61
- return k;
62
- }
63
23
  // ── Public API ──────────────────────────────────────────────────────────────
64
24
  export function getConfigValue(config, key) {
65
- const k = rewriteKey(config, key);
66
- const value = configGet(config, k);
67
- if (k.split(".").at(-1) === "apiKey")
68
- return null;
69
- return omitApiKeysForOutput(value);
25
+ return redactConfigValue(configGet(config, key));
70
26
  }
71
27
  export function setConfigValue(config, key, rawValue) {
72
- // #454: reject the legacy aliases up front so the error message names the
73
- // env var the user typed (AKM_LLM_API_KEY) rather than the rewritten profile
74
- // env var (AKM_PROFILE_DEFAULT_API_KEY) — both work at runtime, but the
75
- // shorter name matches the user's mental model.
76
- if (key === "llm.apiKey") {
77
- throw new UsageError("apiKey cannot be persisted in config; export AKM_LLM_API_KEY instead. (key: llm.apiKey)", "INVALID_FLAG_VALUE", "Storing API keys in config.json leaks them through backups, logs, and version control. " +
78
- "Use the corresponding environment variable. AKM reads it at request time.");
79
- }
80
- if (key === "embedding.apiKey") {
81
- throw new UsageError("apiKey cannot be persisted in config; export AKM_EMBED_API_KEY instead. (key: embedding.apiKey)", "INVALID_FLAG_VALUE", "Storing API keys in config.json leaks them through backups, logs, and version control. " +
82
- "Use the corresponding environment variable. AKM reads it at request time.");
83
- }
84
- const k = rewriteKey(config, key);
85
- // Legacy ergonomic: `akm config set semanticSearchMode true|false`
86
- let coerced = rawValue;
87
- if (k === "semanticSearchMode") {
88
- if (rawValue === "true")
89
- coerced = "auto";
90
- else if (rawValue === "false")
91
- coerced = "off";
92
- }
93
- let next = configSet(config, k, coerced);
94
- // Legacy ergonomic shim: when the user sets `llm.<field>` and no
95
- // `defaults.llm` is set, point it at the freshly-created profile so the
96
- // value actually takes effect at runtime.
97
- if (key === "llm" || key.startsWith("llm.")) {
98
- if (!next.defaults?.llm) {
99
- next = {
100
- ...next,
101
- defaults: { ...(next.defaults ?? {}), llm: "default" },
102
- };
103
- }
104
- }
105
- return next;
28
+ return configSet(config, key, rawValue);
106
29
  }
107
30
  export function unsetConfigValue(config, key) {
108
- const k = rewriteKey(config, key);
109
- return configUnset(config, k);
31
+ return configUnset(config, key);
110
32
  }
111
33
  /**
112
34
  * Compatibility shim: returns a `Partial<AkmConfig>` containing just the
@@ -128,48 +50,21 @@ export function parseConfigValue(key, value) {
128
50
  return patch;
129
51
  }
130
52
  export function listConfig(config) {
131
- const result = {
132
- semanticSearchMode: config.semanticSearchMode,
133
- registries: config.registries ?? DEFAULT_CONFIG.registries ?? [],
134
- output: { ...(DEFAULT_CONFIG.output ?? {}), ...(config.output ?? {}) },
135
- stashDir: config.stashDir ?? null,
136
- installed: config.installed ?? [],
137
- sources: getSources(config),
138
- };
139
- if (config.defaultWriteTarget)
140
- result.defaultWriteTarget = config.defaultWriteTarget;
141
- if (config.embedding)
142
- result.embedding = config.embedding;
143
- if (config.profiles)
144
- result.profiles = config.profiles;
145
- if (config.defaults)
146
- result.defaults = config.defaults;
147
- if (config.search)
148
- result.search = config.search;
149
- if (config.index)
150
- result.index = config.index;
151
- if (config.feedback)
152
- result.feedback = config.feedback;
153
- if (config.improve)
154
- result.improve = config.improve;
155
- if (config.archiveRetentionDays !== undefined)
156
- result.archiveRetentionDays = config.archiveRetentionDays;
157
- if (config.configVersion !== undefined)
158
- result.configVersion = config.configVersion;
159
- return omitApiKeysForOutput(result);
53
+ return redactConfigValue({ ...DEFAULT_CONFIG, ...config, sources: config.sources ?? [] });
160
54
  }
161
- function omitApiKeysForOutput(value) {
55
+ function redactConfigValue(value) {
162
56
  if (Array.isArray(value))
163
- return value.map(omitApiKeysForOutput);
57
+ return value.map(redactConfigValue);
164
58
  if (!value || typeof value !== "object")
165
59
  return value;
166
- const out = {};
60
+ const result = {};
167
61
  for (const [key, child] of Object.entries(value)) {
168
- if (key === "apiKey")
169
- continue;
170
- out[key] = omitApiKeysForOutput(child);
62
+ if (key !== "apiKey" ||
63
+ (typeof child === "string" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*|\{[A-Za-z_][A-Za-z0-9_]*\})$/.test(child))) {
64
+ result[key] = redactConfigValue(child);
65
+ }
171
66
  }
172
- return out;
67
+ return result;
173
68
  }
174
69
  export { unknownKeyHint };
175
70
  // ── `akm config` command surface ────────────────────────────────────────────
@@ -190,25 +85,21 @@ function normalizeToggleTarget(target) {
190
85
  throw new UsageError(`Unsupported target "${target}". Supported targets: skills.sh`);
191
86
  }
192
87
  function toggleSkillsShRegistry(enabled) {
193
- const config = loadUserConfig();
194
- const registries = (config.registries ?? DEFAULT_CONFIG.registries ?? []).map((registry) => ({ ...registry }));
195
- const idx = registries.findIndex((registry) => registry.provider === SKILLS_SH_PROVIDER || registry.name === SKILLS_SH_NAME || registry.url === SKILLS_SH_URL);
196
- if (idx >= 0) {
197
- const existing = registries[idx];
198
- const wasEnabled = existing.enabled !== false;
199
- existing.enabled = enabled;
200
- saveConfig({ ...config, registries });
201
- return { changed: wasEnabled !== enabled, component: SKILLS_SH_NAME, enabled };
202
- }
203
- if (!enabled) {
204
- // Materialize the skills.sh registry explicitly if absent.
205
- registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled: false });
206
- saveConfig({ ...config, registries });
207
- return { changed: true, component: SKILLS_SH_NAME, enabled: false };
208
- }
209
- registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled: true });
210
- saveConfig({ ...config, registries });
211
- return { changed: true, component: SKILLS_SH_NAME, enabled: true };
88
+ let changed = false;
89
+ mutateConfig((config) => {
90
+ const registries = (config.registries ?? DEFAULT_CONFIG.registries ?? []).map((registry) => ({ ...registry }));
91
+ const idx = registries.findIndex((registry) => registry.provider === SKILLS_SH_PROVIDER || registry.name === SKILLS_SH_NAME || registry.url === SKILLS_SH_URL);
92
+ if (idx >= 0) {
93
+ const wasEnabled = registries[idx].enabled !== false;
94
+ registries[idx].enabled = enabled;
95
+ changed = wasEnabled !== enabled;
96
+ return { ...config, registries };
97
+ }
98
+ registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled });
99
+ changed = true;
100
+ return { ...config, registries };
101
+ });
102
+ return { changed, component: SKILLS_SH_NAME, enabled };
212
103
  }
213
104
  function toggleComponent(targetRaw, enabled) {
214
105
  const target = normalizeToggleTarget(targetRaw);
@@ -276,7 +167,11 @@ export const configCommand = defineGroupCommand({
276
167
  set: defineJsonCommand({
277
168
  meta: { name: "set", description: "Set a configuration value by key" },
278
169
  args: {
279
- key: { type: "positional", required: true, description: "Config key (for example: embedding, llm)" },
170
+ key: {
171
+ type: "positional",
172
+ required: true,
173
+ description: "Config key (for example: embedding, engines.default)",
174
+ },
280
175
  value: { type: "positional", required: true, description: "Config value" },
281
176
  // #463: stable machine-friendly entry point for plugins / hooks.
282
177
  // `--silent` suppresses the config dump on stdout so hook-driven
@@ -297,20 +192,19 @@ export const configCommand = defineGroupCommand({
297
192
  },
298
193
  run({ args }) {
299
194
  if (args.layer && args.layer !== "user") {
300
- throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.8.0.`, "INVALID_FLAG_VALUE");
195
+ throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.9.0.`, "INVALID_FLAG_VALUE");
301
196
  }
302
197
  // Use loadConfig (not loadUserConfig) so the project-config
303
198
  // deprecation warning fires consistently with `akm config get`
304
199
  // (#457). Effective merged shape is identical post-0.8.0.
305
- const updated = setConfigValue(loadConfig(), args.key, args.value);
306
- saveConfig(updated);
200
+ const updated = mutateConfig((current) => setConfigValue(current, args.key, args.value)).config;
307
201
  if (!args.silent) {
308
202
  output("config", listConfig(updated));
309
203
  }
310
204
  },
311
205
  }),
312
206
  unset: defineJsonCommand({
313
- meta: { name: "unset", description: "Unset an optional configuration key or whole embedding/llm section" },
207
+ meta: { name: "unset", description: "Unset an optional configuration key or whole embedding/engine section" },
314
208
  args: {
315
209
  key: { type: "positional", required: true, description: "Config key to unset" },
316
210
  silent: {
@@ -326,10 +220,10 @@ export const configCommand = defineGroupCommand({
326
220
  },
327
221
  run({ args }) {
328
222
  if (args.layer && args.layer !== "user") {
329
- throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.8.0.`, "INVALID_FLAG_VALUE");
223
+ throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.9.0.`, "INVALID_FLAG_VALUE");
330
224
  }
331
- const updated = unsetConfigValue(loadConfig(), args.key);
332
- saveConfig(updated);
225
+ const result = mutateConfig((current) => unsetConfigValue(current, args.key), { absentNoop: true });
226
+ const updated = result.config;
333
227
  if (!args.silent) {
334
228
  output("config", listConfig(updated));
335
229
  }
@@ -348,19 +242,11 @@ export const configCommand = defineGroupCommand({
348
242
  migrate: defineJsonCommand({
349
243
  meta: {
350
244
  name: "migrate",
351
- description: "Migrate the config file to the current schema version. Use --dry-run to preview without writing.",
245
+ description: "Diagnose whether the user config is already in the current schema. Never writes or translates config.",
352
246
  },
353
- args: {
354
- "dry-run": { type: "boolean", description: "Preview the migration result without writing.", default: false },
355
- "print-diff": {
356
- type: "boolean",
357
- description: "Print a unified diff of old vs new config alongside the migration output.",
358
- default: false,
359
- },
360
- },
361
- async run({ args }) {
247
+ async run() {
362
248
  const { runConfigMigrate } = await import("../cli/config-migrate.js");
363
- await runConfigMigrate({ dryRun: Boolean(args["dry-run"]), printDiff: Boolean(args["print-diff"]) });
249
+ await runConfigMigrate();
364
250
  },
365
251
  }),
366
252
  enable: defineJsonCommand({
@@ -27,7 +27,7 @@
27
27
  import fs from "node:fs";
28
28
  import path from "node:path";
29
29
  import { writeFileAtomic } from "../../core/common.js";
30
- import { probeLock, releaseLock, tryAcquireLockSync } from "../../core/file-lock.js";
30
+ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
31
31
  import { sleepSync } from "../../runtime.js";
32
32
  // ── Write-lock helper ─────────────────────────────────────────────────────────
33
33
  /**
@@ -39,10 +39,13 @@ import { sleepSync } from "../../runtime.js";
39
39
  export function withSecretLock(secretPath, fn) {
40
40
  const lockPath = `${secretPath}.lock`;
41
41
  const deadline = Date.now() + 5000;
42
- while (!tryAcquireLockSync(lockPath, String(process.pid))) {
42
+ let ownership;
43
+ while (!ownership) {
44
+ ownership = tryAcquireLockSync(lockPath, createLockPayload());
45
+ if (ownership)
46
+ break;
43
47
  const probe = probeLock(lockPath);
44
- if (probe.state === "stale") {
45
- releaseLock(lockPath);
48
+ if (probe.state === "stale" && reclaimStaleLock(lockPath, probe)) {
46
49
  continue;
47
50
  }
48
51
  if (Date.now() > deadline) {
@@ -57,7 +60,7 @@ export function withSecretLock(secretPath, fn) {
57
60
  return fn();
58
61
  }
59
62
  finally {
60
- releaseLock(lockPath);
63
+ releaseLock(ownership);
61
64
  }
62
65
  }
63
66
  // ── Atomic byte write ──────────────────────────────────────────────────────────
@@ -2,111 +2,158 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { spawnSync } from "node:child_process";
5
+ import { createRequire } from "node:module";
5
6
  import { loadConfig } from "../../core/config/config.js";
6
- import { detectAgentCliProfiles, requireAgentProfile } from "../../integrations/agent/index.js";
7
+ import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
8
+ import { resolveModel } from "../../integrations/agent/model-aliases.js";
7
9
  import { TASK_FAIL_RATE_WARN, } from "./types.js";
8
10
  const ACTIVE_RUN_WARN_MS = 15 * 60 * 1000;
9
- /**
10
- * Probe the configured agent profile. Self-contained (reads config + PATH); the
11
- * only check that performs IO at dispatch time, preserving the original inline
12
- * `runAgentProbe()` call site behaviour exactly.
13
- */
14
- export function runAgentProbe() {
15
- const config = loadConfig();
16
- // v2: check profiles.agent first
17
- if (config.profiles?.agent) {
18
- const defaultName = config.defaults?.agent;
19
- const profileCount = Object.keys(config.profiles.agent).length;
20
- if (profileCount === 0) {
21
- return {
22
- name: "agent-profile",
23
- kind: "deterministic",
24
- status: "unknown",
25
- confidence: "high",
26
- message: "No agent profiles configured in profiles.agent.",
27
- };
28
- }
29
- const profileName = defaultName ?? Object.keys(config.profiles.agent)[0];
30
- const profile = config.profiles.agent[profileName];
11
+ export function runDefaultEngineProbe(deps = {}) {
12
+ const config = deps.loadConfig?.() ?? loadConfig();
13
+ const engineName = config.defaults?.engine ?? config.defaults?.llmEngine;
14
+ if (!engineName) {
31
15
  return {
32
- name: "agent-profile",
33
- kind: "deterministic",
34
- status: "pass",
35
- confidence: "high",
36
- message: `v2 agent profile "${profileName}" configured (platform: ${profile?.platform ?? "unknown"}).`,
37
- evidence: { profile: profileName, platform: profile?.platform, profileCount },
38
- };
39
- }
40
- if (!config.profiles?.agent && !config.defaults?.agent) {
41
- return {
42
- name: "agent-profile",
16
+ name: "default-engine",
43
17
  kind: "deterministic",
44
18
  status: "unknown",
45
19
  confidence: "high",
46
- message: "No agent config present.",
47
- };
48
- }
49
- let profile;
50
- try {
51
- profile = requireAgentProfile(config);
52
- }
53
- catch (error) {
54
- return {
55
- name: "agent-profile",
56
- kind: "deterministic",
57
- status: "warn",
58
- confidence: "high",
59
- message: error instanceof Error ? error.message : String(error),
20
+ message: "No default engine is configured.",
60
21
  };
61
22
  }
62
- if (profile.sdkMode === true) {
23
+ const configuredEngine = config.engines?.[engineName];
24
+ if (configuredEngine?.kind === "agent" && configuredEngine.platform === "opencode-sdk") {
25
+ let packageAvailable = false;
26
+ try {
27
+ const resolvePackage = deps.resolvePackage ?? ((name) => createRequire(import.meta.url).resolve(name));
28
+ resolvePackage("@opencode-ai/sdk");
29
+ packageAvailable = true;
30
+ }
31
+ catch {
32
+ packageAvailable = false;
33
+ }
34
+ const binary = configuredEngine.bin ?? "opencode";
35
+ const version = (deps.spawnSync ?? spawnSync)(binary, ["--version"], { encoding: "utf8", timeout: 5_000 });
36
+ const binaryAvailable = (version.status ?? 1) === 0;
37
+ const fallbackEngine = configuredEngine.llmEngine ?? config.defaults?.llmEngine;
38
+ let fallback;
39
+ let sdkRunner;
40
+ const resolve = deps.resolveEngine ?? resolveEngine;
41
+ try {
42
+ const resolved = resolve(engineName, config);
43
+ if (resolved.kind === "sdk")
44
+ sdkRunner = resolved;
45
+ }
46
+ catch {
47
+ sdkRunner = undefined;
48
+ }
49
+ if (sdkRunner?.fallbackConnection) {
50
+ fallback = { kind: "llm", connection: sdkRunner.fallbackConnection };
51
+ }
52
+ else if (fallbackEngine) {
53
+ try {
54
+ const resolved = resolve(fallbackEngine, config);
55
+ if (resolved.kind === "llm")
56
+ fallback = resolved;
57
+ }
58
+ catch {
59
+ fallback = undefined;
60
+ }
61
+ }
62
+ const configuredModel = configuredEngine.model
63
+ ? resolveModel(configuredEngine.model, "opencode-sdk", configuredEngine.modelAliases, config.modelAliases)
64
+ : undefined;
65
+ const effectiveModel = sdkRunner?.profile.model ?? configuredModel ?? fallback?.connection.model;
66
+ const missing = [
67
+ !packageAvailable ? "@opencode-ai/sdk package" : undefined,
68
+ !binaryAvailable ? `${binary} binary` : undefined,
69
+ !fallback ? "fallback LLM connection" : undefined,
70
+ !effectiveModel ? "effective SDK model" : undefined,
71
+ ].filter((value) => value !== undefined);
63
72
  return {
64
- name: "agent-profile",
73
+ name: "default-engine",
65
74
  kind: "deterministic",
66
- status: profile.model ? "pass" : "warn",
75
+ status: missing.length === 0 ? "pass" : "warn",
67
76
  confidence: "high",
68
- message: profile.model
69
- ? `SDK mode profile "${profile.name}" is configured.`
70
- : `SDK mode profile "${profile.name}" has no explicit model.`,
71
- evidence: { profile: profile.name, sdkMode: true, model: profile.model ?? null },
77
+ message: missing.length === 0
78
+ ? `SDK engine "${engineName}" is available.`
79
+ : `SDK engine "${engineName}" is incomplete: missing ${missing.join(", ")}.`,
80
+ evidence: {
81
+ engine: engineName,
82
+ platform: configuredEngine.platform,
83
+ runtimeKind: "sdk",
84
+ binary,
85
+ binaryAvailable,
86
+ package: "@opencode-ai/sdk",
87
+ packageAvailable,
88
+ model: effectiveModel ?? null,
89
+ configuredModel: configuredModel ?? null,
90
+ modelSource: configuredModel ? "sdk" : effectiveModel ? "fallback" : null,
91
+ fallbackEngine: fallbackEngine ?? null,
92
+ fallbackEndpoint: fallback?.connection.endpoint ?? null,
93
+ fallbackModel: fallback?.connection.model ?? null,
94
+ },
72
95
  };
73
96
  }
74
- const detections = detectAgentCliProfiles(config);
75
- const detection = detections.find((entry) => entry.name === profile.name);
76
- if (!detection?.available) {
97
+ try {
98
+ const runner = (deps.resolveEngine ?? resolveEngine)(engineName, config);
99
+ if (runner.kind === "llm") {
100
+ return {
101
+ name: "default-engine",
102
+ kind: "deterministic",
103
+ status: "pass",
104
+ confidence: "high",
105
+ message: `LLM engine "${engineName}" is configured.`,
106
+ evidence: {
107
+ engine: engineName,
108
+ platform: null,
109
+ runtimeKind: "llm",
110
+ model: runner.connection.model,
111
+ endpoint: runner.connection.endpoint,
112
+ },
113
+ };
114
+ }
115
+ const profile = runner.profile;
116
+ if (runner.kind === "sdk")
117
+ throw new Error(`SDK engine "${engineName}" has no matching SDK config.`);
118
+ const version = (deps.spawnSync ?? spawnSync)(profile.bin, ["--version"], { encoding: "utf8", timeout: 5_000 });
119
+ if ((version.status ?? 1) !== 0) {
120
+ return {
121
+ name: "default-engine",
122
+ kind: "deterministic",
123
+ status: "warn",
124
+ confidence: "medium",
125
+ message: `Agent engine "${engineName}" was found but \`--version\` failed.`,
126
+ evidence: {
127
+ engine: engineName,
128
+ platform: profile.platform ?? null,
129
+ runtimeKind: "agent",
130
+ model: profile.model ?? null,
131
+ },
132
+ };
133
+ }
77
134
  return {
78
- name: "agent-profile",
135
+ name: "default-engine",
79
136
  kind: "deterministic",
80
- status: "fail",
137
+ status: "pass",
81
138
  confidence: "high",
82
- message: `Default agent profile "${profile.name}" is not available on PATH.`,
83
- evidence: { profile: profile.name, bin: profile.bin },
139
+ message: `Agent engine "${engineName}" is available.`,
140
+ evidence: {
141
+ engine: engineName,
142
+ platform: profile.platform ?? null,
143
+ runtimeKind: "agent",
144
+ model: profile.model ?? null,
145
+ },
84
146
  };
85
147
  }
86
- const version = spawnSync(profile.bin, ["--version"], { encoding: "utf8", timeout: 5_000 });
87
- if ((version.status ?? 1) !== 0) {
148
+ catch (error) {
88
149
  return {
89
- name: "agent-profile",
150
+ name: "default-engine",
90
151
  kind: "deterministic",
91
152
  status: "warn",
92
- confidence: "medium",
93
- message: `Agent binary "${profile.bin}" was found but \`--version\` failed.`,
94
- evidence: {
95
- profile: profile.name,
96
- bin: profile.bin,
97
- exitCode: version.status ?? null,
98
- stderr: (version.stderr ?? "").trim(),
99
- },
153
+ confidence: "high",
154
+ message: error instanceof Error ? error.message : String(error),
100
155
  };
101
156
  }
102
- return {
103
- name: "agent-profile",
104
- kind: "deterministic",
105
- status: "pass",
106
- confidence: "high",
107
- message: `Agent profile "${profile.name}" is available.`,
108
- evidence: { profile: profile.name, bin: profile.bin, version: (version.stdout ?? "").trim() },
109
- };
110
157
  }
111
158
  /**
112
159
  * The ordered health-check registry. ORDER IS LOAD-BEARING: `akmHealth`
@@ -185,9 +232,9 @@ export const HEALTH_CHECKS = [
185
232
  }),
186
233
  },
187
234
  {
188
- name: "agent-profile",
235
+ name: "default-engine",
189
236
  channel: "hard",
190
- run: () => runAgentProbe(),
237
+ run: () => runDefaultEngineProbe(),
191
238
  },
192
239
  {
193
240
  // C2 (13-bus-factor): the cron task-failure rate was computed and rendered
@@ -93,6 +93,8 @@ function reshapeRun(r) {
93
93
  return {
94
94
  id: r.id,
95
95
  taskId: r.taskId ?? "manual",
96
+ strategy: r.strategy,
97
+ legacyProfile: r.legacyProfile,
96
98
  startedAt: r.startedAt,
97
99
  completedAt: r.completedAt,
98
100
  wallTimeMs: wall,
@@ -295,6 +297,8 @@ export function buildHealthHtmlReplacements(result, opts) {
295
297
  totalTokens: 0,
296
298
  reasoningTokens: 0,
297
299
  byStage: {},
300
+ byProcess: {},
301
+ byEngine: {},
298
302
  };
299
303
  const totalRuns = runs.length;
300
304
  const failedRuns = runs.filter((r) => !r.ok).length;