akm-cli 0.9.0-rc.0 → 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 (262) hide show
  1. package/CHANGELOG.md +339 -2
  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/prompts/workflow-unit-preamble.md +26 -0
  11. package/dist/assets/stash-skeleton/README.md +28 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
  24. package/dist/assets/tasks/core/backup.yml +1 -0
  25. package/dist/assets/tasks/core/extract.yml +1 -0
  26. package/dist/assets/tasks/core/improve.yml +1 -0
  27. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  28. package/dist/assets/tasks/core/sync.yml +1 -0
  29. package/dist/assets/tasks/core/version-check.yml +1 -0
  30. package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
  31. package/dist/assets/templates/html/health.html +5 -1
  32. package/dist/cli/config-migrate.js +31 -138
  33. package/dist/cli/config-validate.js +10 -8
  34. package/dist/cli.js +48 -14
  35. package/dist/commands/agent/agent-dispatch.js +17 -16
  36. package/dist/commands/agent/agent-support.js +0 -24
  37. package/dist/commands/agent/contribute-cli.js +5 -15
  38. package/dist/commands/backup-cli.js +54 -0
  39. package/dist/commands/config-cli.js +45 -159
  40. package/dist/commands/env/env-binding.js +95 -0
  41. package/dist/commands/env/env-cli.js +8 -65
  42. package/dist/commands/env/secret.js +8 -5
  43. package/dist/commands/health/checks.js +130 -83
  44. package/dist/commands/health/html-report.js +4 -0
  45. package/dist/commands/health/improve-metrics.js +30 -32
  46. package/dist/commands/health/llm-usage.js +19 -19
  47. package/dist/commands/health/md-report.js +4 -0
  48. package/dist/commands/health/metrics.js +2 -1
  49. package/dist/commands/health/surfaces.js +5 -4
  50. package/dist/commands/health.js +1 -1
  51. package/dist/commands/improve/consolidate/chunking.js +2 -2
  52. package/dist/commands/improve/consolidate.js +28 -25
  53. package/dist/commands/improve/distill/promote-memory.js +5 -12
  54. package/dist/commands/improve/distill/quality-gate.js +5 -7
  55. package/dist/commands/improve/distill.js +16 -5
  56. package/dist/commands/improve/eligibility.js +22 -12
  57. package/dist/commands/improve/extract-cli.js +47 -19
  58. package/dist/commands/improve/extract.js +110 -62
  59. package/dist/commands/improve/improve-cli.js +38 -16
  60. package/dist/commands/improve/improve-result-file.js +30 -24
  61. package/dist/commands/improve/improve-strategies.js +137 -0
  62. package/dist/commands/improve/improve.js +60 -30
  63. package/dist/commands/improve/locks.js +66 -45
  64. package/dist/commands/improve/loop-stages.js +75 -33
  65. package/dist/commands/improve/memory/memory-belief.js +79 -7
  66. package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
  67. package/dist/commands/improve/preparation.js +71 -73
  68. package/dist/commands/improve/procedural.js +3 -2
  69. package/dist/commands/improve/recombine.js +2 -1
  70. package/dist/commands/improve/reflect.js +119 -214
  71. package/dist/commands/improve/shared.js +11 -5
  72. package/dist/commands/lint/base-linter.js +152 -42
  73. package/dist/commands/mv-cli.js +809 -0
  74. package/dist/commands/proposal/proposal-cli.js +18 -8
  75. package/dist/commands/proposal/propose.js +64 -69
  76. package/dist/commands/read/knowledge.js +436 -4
  77. package/dist/commands/read/remember-cli.js +39 -2
  78. package/dist/commands/read/search-cli.js +6 -1
  79. package/dist/commands/registry-cli.js +29 -14
  80. package/dist/commands/remember.js +2 -0
  81. package/dist/commands/sources/init.js +13 -14
  82. package/dist/commands/sources/migration-help.js +7 -4
  83. package/dist/commands/sources/schema-repair.js +2 -4
  84. package/dist/commands/sources/source-add.js +62 -73
  85. package/dist/commands/sources/source-manage.js +50 -46
  86. package/dist/commands/sources/stash-cli.js +41 -4
  87. package/dist/commands/tasks/default-tasks.js +12 -12
  88. package/dist/commands/tasks/tasks-cli.js +7 -3
  89. package/dist/commands/tasks/tasks.js +113 -18
  90. package/dist/commands/wiki-cli.js +9 -10
  91. package/dist/commands/workflow-cli.js +276 -12
  92. package/dist/core/asset/asset-spec.js +58 -1
  93. package/dist/core/asset/frontmatter.js +12 -2
  94. package/dist/core/common.js +5 -3
  95. package/dist/core/config/config-io.js +28 -17
  96. package/dist/core/config/config-schema.js +379 -66
  97. package/dist/core/config/config-types.js +3 -3
  98. package/dist/core/config/config-version.js +29 -0
  99. package/dist/core/config/config-walker.js +98 -27
  100. package/dist/core/config/config.js +132 -266
  101. package/dist/core/config/deep-merge.js +41 -0
  102. package/dist/core/config/engine-semantics.js +32 -0
  103. package/dist/core/errors.js +2 -2
  104. package/dist/core/extra-params.js +61 -0
  105. package/dist/core/file-lock.js +201 -56
  106. package/dist/core/improve-result.js +178 -0
  107. package/dist/core/json-schema.js +142 -0
  108. package/dist/core/maintenance-barrier.js +119 -0
  109. package/dist/core/migration-backup.js +416 -0
  110. package/dist/core/paths.js +3 -0
  111. package/dist/core/redaction.js +358 -0
  112. package/dist/core/state/migrations.js +17 -2
  113. package/dist/core/state-db.js +44 -1
  114. package/dist/indexer/db/db.js +118 -2
  115. package/dist/indexer/graph/graph-extraction.js +28 -16
  116. package/dist/indexer/index-writer-lock.js +31 -24
  117. package/dist/indexer/index-written-assets.js +15 -6
  118. package/dist/indexer/indexer.js +47 -2
  119. package/dist/indexer/passes/memory-inference.js +10 -6
  120. package/dist/indexer/passes/metadata.js +250 -0
  121. package/dist/indexer/search/db-search.js +111 -44
  122. package/dist/indexer/search/fts-query.js +41 -0
  123. package/dist/indexer/search/ranking-contributors.js +48 -0
  124. package/dist/indexer/search/ranking.js +36 -23
  125. package/dist/indexer/search/search-fields.js +11 -1
  126. package/dist/indexer/walk/matchers.js +39 -0
  127. package/dist/integrations/agent/builder-shared.js +7 -0
  128. package/dist/integrations/agent/builders.js +5 -50
  129. package/dist/integrations/agent/config.js +3 -143
  130. package/dist/integrations/agent/detect.js +17 -2
  131. package/dist/integrations/agent/engine-resolution.js +202 -0
  132. package/dist/integrations/agent/index.js +1 -2
  133. package/dist/integrations/agent/model-aliases.js +16 -2
  134. package/dist/integrations/agent/profiles.js +36 -62
  135. package/dist/integrations/agent/runner-dispatch.js +91 -4
  136. package/dist/integrations/agent/runner.js +76 -207
  137. package/dist/integrations/agent/spawn.js +141 -20
  138. package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
  139. package/dist/integrations/harnesses/aider/index.js +57 -0
  140. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  141. package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
  142. package/dist/integrations/harnesses/amazonq/index.js +58 -0
  143. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  144. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  145. package/dist/integrations/harnesses/claude/index.js +25 -25
  146. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  147. package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
  148. package/dist/integrations/harnesses/codex/index.js +62 -0
  149. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  150. package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
  151. package/dist/integrations/harnesses/copilot/index.js +59 -0
  152. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  153. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  154. package/dist/integrations/harnesses/gemini/index.js +59 -0
  155. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  156. package/dist/integrations/harnesses/index.js +27 -28
  157. package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
  158. package/dist/integrations/harnesses/opencode/index.js +15 -22
  159. package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
  160. package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
  161. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  162. package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
  163. package/dist/integrations/harnesses/openhands/index.js +57 -0
  164. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  165. package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
  166. package/dist/integrations/harnesses/pi/index.js +57 -0
  167. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  168. package/dist/integrations/harnesses/types.js +8 -32
  169. package/dist/integrations/lockfile.js +32 -21
  170. package/dist/integrations/session-logs/index.js +24 -11
  171. package/dist/llm/client.js +48 -14
  172. package/dist/llm/feature-gate.js +15 -47
  173. package/dist/llm/graph-extract.js +1 -1
  174. package/dist/llm/index-passes.js +8 -42
  175. package/dist/llm/memory-infer-impl.js +1 -1
  176. package/dist/llm/usage-persist.js +4 -0
  177. package/dist/llm/usage-telemetry.js +35 -5
  178. package/dist/output/renderers.js +3 -2
  179. package/dist/output/shapes/helpers.js +2 -1
  180. package/dist/output/shapes/passthrough.js +6 -0
  181. package/dist/output/text/helpers.js +215 -2
  182. package/dist/output/text/workflow.js +3 -1
  183. package/dist/schemas/akm-config.json +16638 -0
  184. package/dist/schemas/akm-task.json +87 -0
  185. package/dist/schemas/akm-workflow.json +372 -0
  186. package/dist/scripts/migrate-storage.js +10944 -8801
  187. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
  188. package/dist/setup/detected-engines.js +142 -0
  189. package/dist/setup/engine-config.js +89 -0
  190. package/dist/setup/setup.js +236 -132
  191. package/dist/setup/steps/connection.js +61 -32
  192. package/dist/setup/steps/platforms.js +4 -4
  193. package/dist/setup/steps.js +3 -2
  194. package/dist/storage/database.js +13 -1
  195. package/dist/storage/engines/sqlite-migrations.js +1 -0
  196. package/dist/storage/repositories/improve-runs-repository.js +5 -5
  197. package/dist/storage/repositories/task-history-repository.js +78 -0
  198. package/dist/storage/repositories/workflow-runs-repository.js +190 -1
  199. package/dist/tasks/parser.js +138 -52
  200. package/dist/tasks/runner.js +71 -75
  201. package/dist/tasks/schema.js +1 -1
  202. package/dist/tasks/validator.js +11 -6
  203. package/dist/text-import-hook.mjs +1 -1
  204. package/dist/wiki/wiki.js +9 -8
  205. package/dist/workflows/authoring/authoring.js +123 -10
  206. package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
  207. package/dist/workflows/cli.js +4 -0
  208. package/dist/workflows/concurrency-policy.js +15 -0
  209. package/dist/workflows/db.js +200 -13
  210. package/dist/workflows/exec/brief.js +478 -0
  211. package/dist/workflows/exec/frozen-judge.js +47 -0
  212. package/dist/workflows/exec/native-executor.js +1034 -0
  213. package/dist/workflows/exec/param-secrets.js +115 -0
  214. package/dist/workflows/exec/report.js +1355 -0
  215. package/dist/workflows/exec/run-workflow.js +609 -0
  216. package/dist/workflows/exec/scheduler.js +71 -0
  217. package/dist/workflows/exec/step-work.js +1212 -0
  218. package/dist/workflows/exec/unit-writer.js +23 -0
  219. package/dist/workflows/exec/watch.js +116 -0
  220. package/dist/workflows/exec/worktree.js +171 -0
  221. package/dist/workflows/ir/compile.js +375 -0
  222. package/dist/workflows/ir/freeze.js +243 -0
  223. package/dist/workflows/ir/params.js +54 -0
  224. package/dist/workflows/ir/plan-hash.js +68 -0
  225. package/dist/workflows/ir/schema.js +545 -0
  226. package/dist/workflows/parser.js +10 -1
  227. package/dist/workflows/program/expressions.js +369 -0
  228. package/dist/workflows/program/parser.js +869 -0
  229. package/dist/workflows/program/project.js +104 -0
  230. package/dist/workflows/program/schema.js +54 -0
  231. package/dist/workflows/renderer.js +82 -5
  232. package/dist/workflows/resource-limits.js +20 -0
  233. package/dist/workflows/runtime/agent-identity.js +59 -14
  234. package/dist/workflows/runtime/plan-classifier.js +187 -0
  235. package/dist/workflows/runtime/runs.js +246 -69
  236. package/dist/workflows/runtime/unit-checkin.js +45 -0
  237. package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
  238. package/dist/workflows/validate-summary.js +24 -3
  239. package/dist/workflows/validator.js +26 -1
  240. package/docs/data-and-telemetry.md +4 -3
  241. package/docs/migration/release-notes/0.6.0.md +1 -1
  242. package/docs/migration/release-notes/0.7.0.md +5 -4
  243. package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
  244. package/docs/migration/v0.8-to-v0.9.md +401 -0
  245. package/package.json +4 -2
  246. package/schemas/akm-config.json +16638 -0
  247. package/schemas/akm-task.json +87 -0
  248. package/schemas/akm-workflow.json +372 -0
  249. package/dist/commands/improve/improve-profiles.js +0 -168
  250. package/dist/core/config/config-migration.js +0 -602
  251. package/dist/core/deep-merge.js +0 -38
  252. package/dist/llm/call-ai.js +0 -62
  253. package/dist/setup/legacy-config.js +0 -106
  254. package/docs/README.md +0 -104
  255. /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
  256. /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
  257. /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
  258. /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
  259. /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
  260. /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
  261. /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
  262. /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({
@@ -0,0 +1,95 @@
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
+ * Env-ref → resolved values, shared between `akm env run` and the workflow
6
+ * engine's per-unit env bindings (orchestration plan, *Dispatch context*).
7
+ *
8
+ * Extracted from `env-cli.ts`'s `runEnvInjected` so the native executor
9
+ * REUSES the exact same machinery instead of forking it. Every safety
10
+ * invariant carries over unchanged:
11
+ *
12
+ * - `${secret:NAME}` tokens resolve against the env's own stash; a missing
13
+ * secret is a hard error and NOTHING is injected (no partial injection).
14
+ * - Known process-hijacking keys (LD_PRELOAD, PATH, …) are blocked for
15
+ * third-party-sourced stashes and warned about for first-party ones.
16
+ * - An `env_access` audit event records key NAMES only, never values.
17
+ * - Resolved values must never be written to stdout or logs by callers.
18
+ */
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ import { resolveAssetPathFromName } from "../../core/asset/asset-spec.js";
22
+ import { isWithin } from "../../core/common.js";
23
+ import { makeEnvRef, resolveEnvPath } from "../../core/env-secret-ref.js";
24
+ import { NotFoundError, UsageError } from "../../core/errors.js";
25
+ import { appendEvent } from "../../core/events.js";
26
+ import { isDangerousEnvKey } from "../lint/env-key-rules.js";
27
+ import { loadEnv, resolveSecretTokens } from "./env.js";
28
+ import { readValue } from "./secret.js";
29
+ /**
30
+ * Resolve an env ref to its injectable values: load the file, apply key
31
+ * filtering, substitute `${secret:NAME}` tokens from the sibling secrets
32
+ * directory, enforce the dangerous-key policy, and append the keys-only
33
+ * `env_access` audit event.
34
+ */
35
+ export function resolveEnvBinding(target, options = {}) {
36
+ const warn = options.warn ?? ((message) => process.stderr.write(`warning: ${message}\n`));
37
+ const { name, absPath, source } = resolveEnvPath(target);
38
+ const envRef = makeEnvRef(name, source);
39
+ if (!fs.existsSync(absPath)) {
40
+ throw new NotFoundError(`Env not found: ${envRef}`);
41
+ }
42
+ const allValues = loadEnv(absPath);
43
+ // Value-safe key filtering (--only / --except operate on key NAMES only).
44
+ let envValues = allValues;
45
+ if (options.only && options.except) {
46
+ throw new UsageError("Pass only one of --only or --except.", "INVALID_FLAG_VALUE");
47
+ }
48
+ if (options.only) {
49
+ const wanted = new Set(options.only);
50
+ const missing = options.only.filter((k) => !(k in allValues));
51
+ if (missing.length > 0) {
52
+ warn(`--only key(s) not present in ${envRef}: ${missing.join(", ")}`);
53
+ }
54
+ envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => wanted.has(k)));
55
+ }
56
+ else if (options.except) {
57
+ const excluded = new Set(options.except);
58
+ envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => !excluded.has(k)));
59
+ }
60
+ // Substitute `${secret:NAME}` tokens with the sibling secret asset in the
61
+ // SAME stash. A missing secret is a hard error — inject NOTHING.
62
+ const secretsRoot = path.join(source.path, "secrets");
63
+ const resolveSecret = (secretName) => {
64
+ const secretPath = resolveAssetPathFromName("secret", secretsRoot, secretName);
65
+ // Defense-in-depth: ensure the resolved path stays inside the secrets dir.
66
+ if (!isWithin(secretPath, secretsRoot)) {
67
+ throw new UsageError(`Secret name "${secretName}" escapes the secrets directory.`);
68
+ }
69
+ if (!fs.existsSync(secretPath))
70
+ return undefined;
71
+ // Match `secret run`: read utf8, do not trim (stay consistent with that path).
72
+ return readValue(secretPath).toString("utf8");
73
+ };
74
+ const { values: substituted, missing } = resolveSecretTokens(envValues, resolveSecret);
75
+ if (missing.length > 0) {
76
+ throw new NotFoundError(`Env "${envRef}" references secret(s) not found in its stash: ${missing.map((n) => `secret:${n}`).join(", ")}. Nothing was injected.`, "FILE_NOT_FOUND", `Create the missing secret, e.g. \`akm secret set secret:${missing[0]}\`.`);
77
+ }
78
+ envValues = substituted;
79
+ const keys = Object.keys(envValues);
80
+ // Scan injected keys for known process-hijacking variables. Block for
81
+ // third-party-sourced stashes (origin has a registryId); warn for the
82
+ // operator's own first-party stash, where they own the file.
83
+ const dangerous = keys.filter(isDangerousEnvKey);
84
+ if (dangerous.length > 0) {
85
+ const detail = `Env "${envRef}" injects process-hijacking variable(s): ${dangerous.join(", ")}.`;
86
+ if (source.registryId) {
87
+ throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
88
+ ` Review the file, then copy the values into a first-party env if you trust them.`, "INVALID_FLAG_VALUE");
89
+ }
90
+ warn(`${detail} Injecting anyway (first-party stash).`);
91
+ }
92
+ // Audit trail: keys only, never values.
93
+ appendEvent({ eventType: "env_access", ref: envRef, metadata: { keys } });
94
+ return { ref: envRef, values: envValues, keys };
95
+ }
@@ -27,7 +27,6 @@ import { isWithin, writeFileAtomic } from "../../core/common.js";
27
27
  import { loadConfig } from "../../core/config/config.js";
28
28
  import { findEnvSource, makeEnvRef, parseEnvRef, resolveEnvPath } from "../../core/env-secret-ref.js";
29
29
  import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
30
- import { appendEvent } from "../../core/events.js";
31
30
  import { isQuiet } from "../../core/warn.js";
32
31
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
33
32
  import { parseFlagValue } from "../../output/context.js";
@@ -232,64 +231,14 @@ async function runEnvInjected(target, opts) {
232
231
  }
233
232
  throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
234
233
  }
235
- const { loadEnv } = await import("./env.js");
236
- const allValues = loadEnv(absPath);
237
- // Value-safe key filtering (--only / --except operate on key NAMES only).
238
- let envValues = allValues;
239
- if (opts.only && opts.except) {
240
- throw new UsageError("Pass only one of --only or --except.", "INVALID_FLAG_VALUE");
241
- }
242
- if (opts.only) {
243
- const wanted = new Set(opts.only);
244
- const missing = opts.only.filter((k) => !(k in allValues));
245
- if (missing.length > 0) {
246
- process.stderr.write(`warning: --only key(s) not present in ${makeEnvRef(name, source)}: ${missing.join(", ")}\n`);
247
- }
248
- envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => wanted.has(k)));
249
- }
250
- else if (opts.except) {
251
- const excluded = new Set(opts.except);
252
- envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => !excluded.has(k)));
253
- }
254
- // Substitute `${secret:NAME}` tokens in values with the value of the sibling
255
- // secret asset in the SAME stash. The lookup is injected so commands/env.ts
256
- // keeps its narrow dependency surface; we resolve each name against this env's
257
- // own `source`. A missing secret is a hard error — inject NOTHING (no partial
258
- // injection). Resolved values are never logged or printed.
259
- const { resolveSecretTokens } = await import("./env.js");
260
- const { readValue } = await import("./secret.js");
261
- const secretsRoot = path.join(source.path, "secrets");
262
- const resolveSecret = (secretName) => {
263
- const secretPath = resolveAssetPathFromName("secret", secretsRoot, secretName);
264
- // Defense-in-depth: ensure the resolved path stays inside the secrets dir.
265
- if (!isWithin(secretPath, secretsRoot)) {
266
- throw new UsageError(`Secret name "${secretName}" escapes the secrets directory.`);
267
- }
268
- if (!fs.existsSync(secretPath))
269
- return undefined;
270
- // Match `secret run`: read utf8, do not trim (stay consistent with that path).
271
- return readValue(secretPath).toString("utf8");
272
- };
273
- const { values: substituted, missing } = resolveSecretTokens(envValues, resolveSecret);
274
- if (missing.length > 0) {
275
- const envRef = makeEnvRef(name, source);
276
- throw new NotFoundError(`Env "${envRef}" references secret(s) not found in its stash: ${missing.map((n) => `secret:${n}`).join(", ")}. Nothing was injected.`, "FILE_NOT_FOUND", `Create the missing secret, e.g. \`akm secret set secret:${missing[0]}\`.`);
277
- }
278
- envValues = substituted;
279
- const keys = Object.keys(envValues);
280
- // Scan injected keys for known process-hijacking variables (LD_PRELOAD,
281
- // PATH, ...). Block for third-party-sourced stashes (origin has a registryId);
282
- // warn for the operator's own first-party stash, where they own the file.
283
- const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
284
- const dangerous = keys.filter(isDangerousEnvKey);
285
- if (dangerous.length > 0) {
286
- const detail = `Env "${makeEnvRef(name, source)}" injects process-hijacking variable(s): ${dangerous.join(", ")}.`;
287
- if (source.registryId) {
288
- throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
289
- ` Review the file, then copy the values into a first-party env if you trust them.`, "INVALID_FLAG_VALUE");
290
- }
291
- process.stderr.write(`warning: ${detail} Injecting anyway (first-party stash).\n`);
292
- }
234
+ // Load filter secret-substitute → dangerous-key policy → keys-only
235
+ // audit event. Shared with the workflow engine's per-unit env bindings —
236
+ // see env-binding.ts for the extracted core and its safety invariants.
237
+ const { resolveEnvBinding } = await import("./env-binding.js");
238
+ const { values: envValues } = resolveEnvBinding(target, {
239
+ only: opts.only,
240
+ except: opts.except,
241
+ });
293
242
  const mergedEnv = buildChildEnv(process.env, {
294
243
  clean: opts.clean === true,
295
244
  inherit: opts.inherit ?? [],
@@ -297,12 +246,6 @@ async function runEnvInjected(target, opts) {
297
246
  for (const [envKey, envValue] of Object.entries(envValues)) {
298
247
  mergedEnv[envKey] = envValue;
299
248
  }
300
- // Audit trail: keys only, never values.
301
- appendEvent({
302
- eventType: "env_access",
303
- ref: makeEnvRef(name, source),
304
- metadata: { keys },
305
- });
306
249
  const result = spawnSync(command[0], command.slice(1), {
307
250
  stdio: "inherit",
308
251
  env: mergedEnv,
@@ -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 ──────────────────────────────────────────────────────────