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
@@ -1,150 +1,43 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
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
- import fs from "node:fs";
5
- import path from "node:path";
6
- import { stripJsonComments } from "../core/config/config.js";
7
- import { unifiedDiff, withConfigLock, writeConfigAtomic } from "../core/config/config-io.js";
8
- import { migrateConfigShape } from "../core/config/config-migration.js";
9
- import { getCacheDir, getConfigPath } from "../core/paths.js";
10
- import { warn } from "../core/warn.js";
11
- export { migrateConfigShape } from "../core/config/config-migration.js";
12
- const PROJECT_CONFIG_RELATIVE_PATH = path.join(".akm", "config.json");
13
- function backupConfigFile(configPath) {
14
- if (!fs.existsSync(configPath))
15
- return;
16
- const backupDir = path.join(getCacheDir(), "config-backups");
17
- // 08-F4: lock the backup dir owner-only (0700) — see config-io.ts.
18
- fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
19
- fs.chmodSync(backupDir, 0o700);
20
- const timestamp = new Date().toISOString().replace(/[.:]/g, "-");
21
- const backupPath = path.join(backupDir, `config-${timestamp}.json`);
22
- fs.copyFileSync(configPath, backupPath);
23
- const latestPath = path.join(backupDir, "config.latest.json");
24
- fs.copyFileSync(configPath, latestPath);
25
- // 08-F4: config backups can carry secrets (endpoints/tokens) — keep them
26
- // owner-only rather than inheriting the source file's (often 0644) mode.
27
- fs.chmodSync(backupPath, 0o600);
28
- fs.chmodSync(latestPath, 0o600);
29
- }
30
- function acquireMigrateLock(lockPath, noWait) {
31
- const lockDir = path.dirname(lockPath);
32
- fs.mkdirSync(lockDir, { recursive: true });
33
- const maxAttempts = noWait ? 1 : 20;
34
- const delayMs = 200;
35
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
36
- try {
37
- fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
38
- return () => {
39
- try {
40
- fs.unlinkSync(lockPath);
41
- }
42
- catch {
43
- // ignore
44
- }
45
- };
46
- }
47
- catch {
48
- if (noWait) {
49
- return null;
50
- }
51
- // Simple busy-wait — synchronous since this is a one-shot CLI action
52
- const deadline = Date.now() + delayMs;
53
- while (Date.now() < deadline) {
54
- // spin
55
- }
56
- }
57
- }
58
- return null;
59
- }
60
- export async function migrateConfigFile(filePath, opts) {
61
- if (!fs.existsSync(filePath)) {
62
- return { changed: false, result: {} };
63
- }
64
- const text = fs.readFileSync(filePath, "utf8");
65
- let raw;
4
+ import { parseConfigText, readConfigText } from "../core/config/config-io.js";
5
+ import { CURRENT_CONFIG_VERSION, validateConfigShape } from "../core/config/config-schema.js";
6
+ import { ConfigError } from "../core/errors.js";
7
+ import { getConfigPath } from "../core/paths.js";
8
+ const MANUAL_GUIDANCE = "AKM 0.9 does not translate profile-based configuration. Recreate named engines, defaults.engine/defaults.llmEngine, and improve.strategies manually before retrying.";
9
+ /**
10
+ * Diagnose the user config without loading runtime configuration or mutating
11
+ * disk. Profile-to-engine conversion is intentionally ambiguous and never
12
+ * happens automatically.
13
+ */
14
+ export async function runConfigMigrate() {
15
+ const configPath = getConfigPath();
16
+ let text;
66
17
  try {
67
- const stripped = stripJsonComments(text);
68
- const parsed = JSON.parse(stripped);
69
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
70
- warn(`[akm] config-migrate: ${filePath} is not a valid JSON object, skipping.`);
71
- return { changed: false, result: {} };
72
- }
73
- raw = parsed;
74
- }
75
- catch {
76
- warn(`[akm] config-migrate: failed to parse ${filePath}, skipping.`);
77
- return { changed: false, result: {} };
78
- }
79
- const { changed, result } = migrateConfigShape(raw);
80
- if (!changed) {
81
- return { changed: false, result };
18
+ text = readConfigText(configPath);
82
19
  }
83
- const migratedText = `${JSON.stringify(result, null, 2)}\n`;
84
- // WS-2: compute a diff when requested (always computed for --print-diff;
85
- // never requires a write so safe in --dry-run).
86
- const diff = opts.printDiff ? unifiedDiff(text, migratedText, filePath) : undefined;
87
- if (opts.dryRun) {
88
- return { changed: true, result, diff };
20
+ catch (error) {
21
+ throw new ConfigError(`Could not read config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
89
22
  }
90
- // WS-3: acquire config write lock + use atomic write (tmp rename).
91
- withConfigLock(() => {
92
- backupConfigFile(filePath);
93
- writeConfigAtomic(filePath, result);
94
- });
95
- return { changed: true, result, diff };
96
- }
97
- function discoverProjectConfigPaths(startDir) {
98
- const paths = [];
99
- let currentDir = path.resolve(startDir);
100
- while (true) {
101
- const configPath = path.join(currentDir, PROJECT_CONFIG_RELATIVE_PATH);
102
- if (fs.existsSync(configPath) && fs.statSync(configPath).isFile()) {
103
- paths.unshift(configPath);
104
- }
105
- const parentDir = path.dirname(currentDir);
106
- if (parentDir === currentDir)
107
- break;
108
- currentDir = parentDir;
109
- }
110
- return paths;
111
- }
112
- export async function runConfigMigrate(opts) {
113
- const userConfigPath = getConfigPath();
114
- const projectPaths = discoverProjectConfigPaths(process.cwd());
115
- const allPaths = [userConfigPath, ...projectPaths].filter((p, i, arr) => arr.indexOf(p) === i && fs.existsSync(p));
116
- if (allPaths.length === 0) {
117
- console.log("No config files found to migrate.");
118
- return;
119
- }
120
- const lockPath = path.join(getCacheDir(), "config-migrate.lock");
121
- const releaseLock = acquireMigrateLock(lockPath, opts.noWait ?? false);
122
- if (!releaseLock) {
123
- warn("[akm] config-migrate: another migration is already in progress, skipping.");
23
+ if (text === undefined) {
24
+ console.log(JSON.stringify({ status: "absent", path: configPath }));
124
25
  return;
125
26
  }
27
+ let raw;
126
28
  try {
127
- let anyChanged = false;
128
- for (const filePath of allPaths) {
129
- const { changed, diff } = await migrateConfigFile(filePath, { dryRun: opts.dryRun, printDiff: opts.printDiff });
130
- if (changed) {
131
- const action = opts.dryRun ? "would migrate" : "migrated";
132
- console.log(`[akm] ${action}: ${filePath}`);
133
- // WS-2: print the unified diff to stdout when --print-diff is set.
134
- if (diff) {
135
- console.log(diff);
136
- }
137
- anyChanged = true;
138
- }
139
- else {
140
- console.log(`[akm] already up to date: ${filePath}`);
141
- }
142
- }
143
- if (!anyChanged) {
144
- console.log("All config files are already at the current version.");
145
- }
29
+ raw = parseConfigText(text, configPath);
30
+ }
31
+ catch (error) {
32
+ throw new ConfigError(`Invalid config file at ${configPath}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
33
+ }
34
+ const version = raw.configVersion;
35
+ if (version !== CURRENT_CONFIG_VERSION) {
36
+ throw new ConfigError(`Unsupported configVersion at ${configPath}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", MANUAL_GUIDANCE);
146
37
  }
147
- finally {
148
- releaseLock();
38
+ const result = validateConfigShape(raw);
39
+ if (!result.ok) {
40
+ throw new ConfigError(`Invalid config at ${configPath}: ${result.errors.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`, "INVALID_CONFIG_FILE");
149
41
  }
42
+ console.log(JSON.stringify({ status: "current", path: configPath }));
150
43
  }
@@ -8,27 +8,29 @@
8
8
  * prints "All checks passed." or a list of structured errors (path + message).
9
9
  * Exits non-zero on errors so it composes well in CI hooks.
10
10
  */
11
- import fs from "node:fs";
12
- import { parseConfigText } from "../core/config/config-io.js";
13
- import { validateConfigShape } from "../core/config/config-schema.js";
11
+ import { parseConfigText, readConfigText } from "../core/config/config-io.js";
12
+ import { CURRENT_CONFIG_VERSION, validateConfigShape } from "../core/config/config-schema.js";
14
13
  import { ConfigError } from "../core/errors.js";
15
14
  import { getConfigPath } from "../core/paths.js";
16
15
  export async function runConfigValidate() {
17
16
  const configPath = getConfigPath();
18
- if (!fs.existsSync(configPath)) {
19
- console.log(`No config file at ${configPath} — nothing to validate.`);
20
- return;
21
- }
22
17
  let text;
23
18
  try {
24
- text = fs.readFileSync(configPath, "utf8");
19
+ text = readConfigText(configPath);
25
20
  }
26
21
  catch (err) {
27
22
  const detail = err instanceof Error ? err.message : String(err);
28
23
  throw new ConfigError(`Could not read config at ${configPath}: ${detail}`, "INVALID_CONFIG_FILE");
29
24
  }
25
+ if (text === undefined) {
26
+ console.log(`No config file at ${configPath} — nothing to validate.`);
27
+ return;
28
+ }
30
29
  // parseConfigText throws ConfigError on malformed JSON (#458). Surface as-is.
31
30
  const raw = parseConfigText(text, configPath);
31
+ if (raw.configVersion !== CURRENT_CONFIG_VERSION) {
32
+ throw new ConfigError(`Unsupported configVersion at ${configPath}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually; AKM 0.9 never translates profile-based configuration.");
33
+ }
32
34
  const result = validateConfigShape(raw);
33
35
  if (result.ok) {
34
36
  console.log(`All checks passed. (${configPath})`);
package/dist/cli.js CHANGED
@@ -63,6 +63,7 @@ import { defineCommand, runMain } from "citty";
63
63
  import { findCittyTopLevelCommand } from "./cli/parse-args.js";
64
64
  import { EXIT_CODES, emitJsonError, output, parseAllFlagValues, runWithJsonErrors } from "./cli/shared.js";
65
65
  import { agentCommand, lintCommand, proposeCommand } from "./commands/agent/contribute-cli.js";
66
+ import { backupCommand } from "./commands/backup-cli.js";
66
67
  import { generateBashCompletions, installBashCompletions } from "./commands/completions.js";
67
68
  import { configCommand } from "./commands/config-cli.js";
68
69
  import { envCommand } from "./commands/env/env-cli.js";
@@ -74,6 +75,7 @@ import { renderRunsDetailMd, renderWindowCompareMd } from "./commands/health/md-
74
75
  import { parseWindowSpec } from "./commands/health/windows.js";
75
76
  import { extractCommand } from "./commands/improve/extract-cli.js";
76
77
  import { improveCommand } from "./commands/improve/improve-cli.js";
78
+ import { mvCommand } from "./commands/mv-cli.js";
77
79
  import { hintsCommand, lessonsCommand, logCommand } from "./commands/observability-cli.js";
78
80
  import { proposalCommand } from "./commands/proposal/proposal-cli.js";
79
81
  import { rememberCommand } from "./commands/read/remember-cli.js";
@@ -88,11 +90,12 @@ import { tasksCommand } from "./commands/tasks/tasks-cli.js";
88
90
  import { wikiCommand } from "./commands/wiki-cli.js";
89
91
  import { workflowCommand } from "./commands/workflow-cli.js";
90
92
  import { bestEffort } from "./core/best-effort.js";
91
- import { loadConfig } from "./core/config/config.js";
93
+ import { DEFAULT_CONFIG, loadConfig } from "./core/config/config.js";
92
94
  import { UsageError } from "./core/errors.js";
93
95
  import { getCacheDir, getConfigPath, getDbPath } from "./core/paths.js";
94
96
  import { plainize } from "./core/tty.js";
95
97
  import { info, isQuiet, setQuiet, setVerbose, warn } from "./core/warn.js";
98
+ import { disposeDispatchResources } from "./integrations/agent/runner-dispatch.js";
96
99
  import { getHyphenatedBoolean, getOutputMode, initOutputMode, parseFlagValue } from "./output/context.js";
97
100
  import { deliverRendered, renderHtml, resolveTemplatePath } from "./output/html-render.js";
98
101
  import { pkgVersion } from "./version.js";
@@ -195,10 +198,15 @@ const setupCommand = defineCommand({
195
198
  type: "string",
196
199
  description: "Stash directory path (overrides stashDir in config or --config JSON)",
197
200
  },
201
+ "no-init": {
202
+ type: "boolean",
203
+ default: false,
204
+ description: "Write configuration without scaffolding the stash directory",
205
+ },
198
206
  probe: {
199
207
  type: "boolean",
200
208
  default: false,
201
- description: "Probe LLM/embedding endpoints after writing config to verify connectivity",
209
+ description: "Probe LLM/embedding endpoints before writing config to verify connectivity",
202
210
  },
203
211
  "detect-only": {
204
212
  type: "boolean",
@@ -213,7 +221,9 @@ const setupCommand = defineCommand({
213
221
  },
214
222
  async run({ args }) {
215
223
  await runWithJsonErrors(async () => {
216
- const noInit = getHyphenatedBoolean(args, "no-init");
224
+ // citty treats a leading `no-` as boolean negation on some parse paths,
225
+ // so retain the raw argv spelling as the authoritative compatibility form.
226
+ const noInit = getHyphenatedBoolean(args, "no-init") || process.argv.includes("--no-init");
217
227
  const detectOnly = args["detect-only"];
218
228
  const resetRecommended = args["reset-recommended"];
219
229
  if (detectOnly) {
@@ -505,7 +515,9 @@ export const main = defineCommand({
505
515
  import: importKnowledgeCommand,
506
516
  sync: syncCommand,
507
517
  clone: cloneCommand,
518
+ mv: mvCommand,
508
519
  registry: registryCommand,
520
+ backup: backupCommand,
509
521
  config: configCommand,
510
522
  feedback: feedbackCommand,
511
523
  history: historyCommand,
@@ -527,6 +539,20 @@ export const main = defineCommand({
527
539
  },
528
540
  });
529
541
  const MAIN_TOP_LEVEL_ARGS = main.args;
542
+ /** Recovery/setup surfaces must remain reachable when config.json is invalid. */
543
+ export function shouldBypassConfigStartup(argv) {
544
+ const args = argv.slice(2);
545
+ if (args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-v"))
546
+ return true;
547
+ const command = findCittyTopLevelCommand([...args], MAIN_TOP_LEVEL_ARGS);
548
+ if (command === "setup" || command === "backup")
549
+ return true;
550
+ if (command !== "config")
551
+ return false;
552
+ const configIndex = args.indexOf("config");
553
+ const subcommand = args.slice(configIndex + 1).find((arg) => !arg.startsWith("-"));
554
+ return subcommand === "path" || subcommand === "validate" || subcommand === "migrate";
555
+ }
530
556
  // ── Exit codes ──────────────────────────────────────────────────────────────
531
557
  // Canonical table lives in `src/cli/shared.ts` (EXIT_CODES). These aliases keep
532
558
  // the local call sites terse. EXIT_HEALTH_WARN (4) is the `akm health` "warn"
@@ -556,7 +582,8 @@ if (import.meta.main || process.env.AKM_NODE_ENTRY === "1") {
556
582
  // rather than letting the raw exception escape with a stack trace.
557
583
  try {
558
584
  applyEarlyStderrFlags(process.argv);
559
- initOutputMode(process.argv, loadConfig().output ?? {});
585
+ const bypassConfig = shouldBypassConfigStartup(process.argv);
586
+ initOutputMode(process.argv, bypassConfig ? (DEFAULT_CONFIG.output ?? {}) : (loadConfig().output ?? {}));
560
587
  }
561
588
  catch (error) {
562
589
  emitJsonError(error);
@@ -575,15 +602,17 @@ if (import.meta.main || process.env.AKM_NODE_ENTRY === "1") {
575
602
  // 0.8.0 moved the index to $XDG_DATA_HOME/akm/index.db (getDataDir()).
576
603
  // If the old file exists at $XDG_CACHE_HOME/akm/index.db, remove it so the
577
604
  // user isn't confused by a phantom DB. Best-effort; never fatal.
578
- bestEffort(() => {
579
- const oldIndexPath = path.join(getCacheDir(), "index.db");
580
- if (fs.existsSync(oldIndexPath)) {
581
- fs.rmSync(oldIndexPath, { force: true });
582
- fs.rmSync(`${oldIndexPath}-shm`, { force: true });
583
- fs.rmSync(`${oldIndexPath}-wal`, { force: true });
584
- warn(`Cleaned up stale 0.7.x index from ${oldIndexPath}. Canonical path is now ${getDbPath()}.`);
585
- }
586
- }, "stale 0.7.x index cleanup is non-fatal");
605
+ if (!shouldBypassConfigStartup(process.argv)) {
606
+ bestEffort(() => {
607
+ const oldIndexPath = path.join(getCacheDir(), "index.db");
608
+ if (fs.existsSync(oldIndexPath)) {
609
+ fs.rmSync(oldIndexPath, { force: true });
610
+ fs.rmSync(`${oldIndexPath}-shm`, { force: true });
611
+ fs.rmSync(`${oldIndexPath}-wal`, { force: true });
612
+ warn(`Cleaned up stale 0.7.x index from ${oldIndexPath}. Canonical path is now ${getDbPath()}.`);
613
+ }
614
+ }, "stale 0.7.x index cleanup is non-fatal");
615
+ }
587
616
  // First-time-user breadcrumb: when run with no subcommand AND no config
588
617
  // exists yet AND stderr is a TTY, print a friendly pointer to `akm setup`
589
618
  // above citty's auto-generated usage block. Triggers only when stdin/stderr
@@ -608,5 +637,10 @@ if (import.meta.main || process.env.AKM_NODE_ENTRY === "1") {
608
637
  }
609
638
  console.error(plainize("👋 First time with akm? Run `akm setup` to get started.\n Docs: https://github.com/itlackey/akm#readme\n"));
610
639
  })();
611
- runMain(main);
640
+ try {
641
+ await runMain(main);
642
+ }
643
+ finally {
644
+ await disposeDispatchResources();
645
+ }
612
646
  }
@@ -2,9 +2,9 @@
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
  /**
5
- * `akm agent <profile> [--prompt <text>] [--command <ref>] [--workflow <ref>] [args...]`
5
+ * `akm agent [--engine <name>] [--prompt <text>] [--command <ref>] [--workflow <ref>] [args...]`
6
6
  *
7
- * Dispatch an agent by named profile, optionally injecting a prompt from
7
+ * Dispatch an agent by named engine, optionally injecting a prompt from
8
8
  * inline text, a stash command: asset, or a stash workflow: asset.
9
9
  *
10
10
  * When none of --prompt, --command, or --workflow are given, the agent is
@@ -16,9 +16,8 @@
16
16
  import fs from "node:fs";
17
17
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
18
18
  import { NotFoundError, UsageError } from "../../core/errors.js";
19
- import { requireAgentProfile } from "../../integrations/agent/config.js";
20
- import { runAgent } from "../../integrations/agent/spawn.js";
21
- import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
19
+ import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
20
+ import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
22
21
  /**
23
22
  * Fill `{{0}}`, `{{1}}`, ... placeholders in `template` with the
24
23
  * corresponding entries in `args`. Any placeholder index that exceeds the
@@ -59,12 +58,16 @@ async function resolveAssetBody(ref) {
59
58
  }
60
59
  }
61
60
  export async function akmAgentDispatch(options) {
62
- if (!options.profileName?.trim()) {
63
- throw new UsageError("agent: <profile> is required.", "MISSING_REQUIRED_ARGUMENT");
61
+ if (!options.agentConfig)
62
+ throw new UsageError("agent requires a valid config with an agent engine.", "MISSING_REQUIRED_ARGUMENT");
63
+ const engineName = options.engine ?? options.agentConfig.defaults?.engine;
64
+ if (!engineName)
65
+ throw new UsageError("agent requires --engine or defaults.engine.", "MISSING_REQUIRED_ARGUMENT");
66
+ const runner = resolveEngine(engineName, options.agentConfig);
67
+ if (runner.kind === "llm") {
68
+ throw new UsageError(`Engine "${engineName}" is an LLM engine; akm agent requires an agent engine.`, "INVALID_FLAG_VALUE");
64
69
  }
65
- // Resolve the profile — throws ConfigError with an actionable hint when
66
- // agent config is absent or the profile is not found.
67
- const profile = requireAgentProfile(options.agentConfig, options.profileName.trim());
70
+ const profile = runner.profile;
68
71
  // Resolve the prompt text from whichever source was provided.
69
72
  let prompt;
70
73
  if (options.commandRef) {
@@ -79,7 +82,7 @@ export async function akmAgentDispatch(options) {
79
82
  prompt = options.prompt;
80
83
  }
81
84
  // When prompt is undefined, the agent is launched interactively.
82
- const stdio = prompt === undefined && profile.sdkMode !== true ? "interactive" : profile.stdio;
85
+ const stdio = prompt === undefined && runner.kind !== "sdk" ? "interactive" : profile.stdio;
83
86
  // Build the final dispatch request: merge the caller-supplied dispatch with
84
87
  // the resolved prompt so the builder has all context in one place.
85
88
  const dispatchRequest = options.dispatch
@@ -93,14 +96,12 @@ export async function akmAgentDispatch(options) {
93
96
  ...(options.cwd ? { cwd: options.cwd } : {}),
94
97
  ...(dispatchRequest !== undefined ? { dispatch: dispatchRequest } : {}),
95
98
  };
96
- const result = profile.sdkMode
97
- ? await runOpencodeSdk(profile, prompt ?? "", runOptions, options.llmConfig)
98
- : await runAgent(profile, prompt, runOptions);
99
+ const result = await executeRunner(runner, prompt ?? "", runOptions);
99
100
  return {
100
- schemaVersion: 1,
101
+ schemaVersion: 2,
101
102
  ok: result.ok,
102
103
  shape: "agent-result",
103
- profileName: profile.name,
104
+ engine: engineName,
104
105
  exitCode: result.exitCode,
105
106
  stdout: result.stdout ?? "",
106
107
  stderr: result.stderr ?? "",
@@ -1,30 +1,6 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
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
- import { loadConfig } from "../../core/config/config.js";
5
- import { requireAgentProfile, } from "../../integrations/agent/index.js";
6
- // ── Config helpers ───────────────────────────────────────────────────────────
7
- /**
8
- * Load the loaded AkmConfig from disk.
9
- *
10
- * After 0.8.0, the legacy `agent` top-level block was removed — the agent
11
- * profile data now lives on the unified `AkmConfig` (via `profiles.agent` and
12
- * `defaults.agent`). This helper remains for source-compat with callers that
13
- * still expect an "AgentConfig"; it now returns the loaded `AkmConfig`.
14
- */
15
- export function loadAgentConfigFromDisk() {
16
- return loadConfig();
17
- }
18
- /**
19
- * Resolve the agent profile for a command's options.
20
- * Prefers an injected `agentProfile` (test seam) over the on-disk config.
21
- */
22
- export function resolveAgentProfile(options) {
23
- if (options.agentProfile)
24
- return options.agentProfile;
25
- const agent = options.agentConfig ?? loadAgentConfigFromDisk();
26
- return requireAgentProfile(agent, options.profile);
27
- }
28
4
  // ── Failure helpers ──────────────────────────────────────────────────────────
29
5
  /**
30
6
  * Base failure envelope shared by all agent command failures.
@@ -38,17 +38,13 @@ export const agentCommand = defineCommand({
38
38
  description: "Dispatch an agent CLI (opencode, claude, …) with an optional agent asset that provides the system prompt, model, and tool policy. Use <agent-ref> to embody a stash agent, --model to override the model, and --prompt/--command/--workflow to provide the task.",
39
39
  },
40
40
  args: {
41
- profile: {
42
- type: "positional",
43
- description: "Agent profile / platform to use (opencode, claude, …)",
44
- required: false,
45
- },
46
41
  "agent-ref": {
47
42
  type: "positional",
48
43
  description: "Optional agent asset ref (e.g. agent:code-reviewer). Loads system prompt, model, and tool policy from the stash asset.",
49
44
  required: false,
50
45
  },
51
46
  prompt: { type: "string", description: "Task prompt to pass to the agent" },
47
+ engine: { type: "string", description: "Agent engine to use (default: defaults.engine)" },
52
48
  command: { type: "string", description: "Load prompt from a command: asset" },
53
49
  workflow: { type: "string", description: "Load prompt from a workflow: asset" },
54
50
  model: {
@@ -63,13 +59,8 @@ export const agentCommand = defineCommand({
63
59
  },
64
60
  async run({ args }) {
65
61
  await runWithJsonErrors(async () => {
66
- if (!args.profile) {
67
- throw new UsageError("Usage: akm agent <profile> [<agent-ref>] [--prompt <text>] [--model <model>]", "MISSING_REQUIRED_ARGUMENT", "Provide the agent profile name. Available profiles are listed in profiles.agent.");
68
- }
69
62
  const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
70
63
  const config = loadConfig();
71
- const { getDefaultLlmConfig } = await import("../../core/config/config.js");
72
- // After 0.8.0 the agent block IS the loaded AkmConfig.
73
64
  const agentConfig = config;
74
65
  // Resolve agent asset ref → extract system prompt, model, and tool policy.
75
66
  const agentRef = getStringArg(args, "agent-ref");
@@ -95,12 +86,11 @@ export const agentCommand = defineCommand({
95
86
  // platform-specific flags beyond the profile's base args).
96
87
  const hasDispatchContent = !!(promptText ?? commandRef ?? workflowRef ?? systemPrompt ?? model ?? assetTools);
97
88
  const result = await akmAgentDispatch({
98
- profileName: String(args.profile),
89
+ engine: getStringArg(args, "engine"),
99
90
  prompt: promptText,
100
91
  commandRef,
101
92
  workflowRef,
102
93
  agentConfig,
103
- llmConfig: getDefaultLlmConfig(config),
104
94
  ...(hasDispatchContent
105
95
  ? {
106
96
  dispatch: {
@@ -125,7 +115,7 @@ export const agentCommand = defineCommand({
125
115
  export const lintCommand = defineCommand({
126
116
  meta: {
127
117
  name: "lint",
128
- description: "Scan stash .md files for structural issues (unquoted colons, missing updated field, orphaned stubs, placeholder stubs, missing name/type, stale paths). Use --fix to auto-fix Tier 1 issues. Exits 0 on success regardless of findings; use --fail-on-flagged for CI fail-on-finding behavior.",
118
+ description: "Scan stash .md files for structural issues (unquoted colons, missing updated field, orphaned stubs, placeholder stubs, missing name/type, stale paths, broken refs in body text and in refs/xrefs/supersededBy/contradictedBy frontmatter). Use --fix to auto-fix Tier 1 issues. Exits 0 on success regardless of findings; use --fail-on-flagged for CI fail-on-finding behavior.",
129
119
  },
130
120
  args: {
131
121
  fix: {
@@ -182,7 +172,7 @@ export const proposeCommand = defineCommand({
182
172
  },
183
173
  task: { type: "string", description: "Task description for the agent (what should the asset do?)" },
184
174
  file: { type: "string", description: "Read the task or prompt text from a UTF-8 file" },
185
- profile: { type: "string", description: "Override the agent profile (defaults to agent.default)" },
175
+ engine: { type: "string", description: "Engine to use (defaults to defaults.engine)" },
186
176
  "timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
187
177
  },
188
178
  async run({ args }) {
@@ -207,7 +197,7 @@ export const proposeCommand = defineCommand({
207
197
  type: String(args.type),
208
198
  name: proposedName,
209
199
  task: taskText,
210
- profile: getStringArg(args, "profile"),
200
+ engine: getStringArg(args, "engine"),
211
201
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
212
202
  });
213
203
  output("propose", result);
@@ -0,0 +1,54 @@
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
+ import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
5
+ import { UsageError } from "../core/errors.js";
6
+ import { createMigrationBackup, MIGRATION_BACKUP_VERSION, restoreMigrationBackup } from "../core/migration-backup.js";
7
+ function requireVersion(value) {
8
+ if (value !== MIGRATION_BACKUP_VERSION) {
9
+ throw new UsageError(`Unsupported migration backup target ${JSON.stringify(value)}; expected ${MIGRATION_BACKUP_VERSION}.`, "INVALID_FLAG_VALUE");
10
+ }
11
+ }
12
+ export const backupCommand = defineGroupCommand({
13
+ meta: { name: "backup", description: "Create or restore a versioned migration recovery bundle" },
14
+ subCommands: {
15
+ create: defineJsonCommand({
16
+ meta: { name: "create", description: "Create the immutable pre-0.9 migration recovery bundle" },
17
+ args: {
18
+ for: { type: "string", required: true, description: "Migration target version (0.9.0)" },
19
+ },
20
+ run({ args }) {
21
+ requireVersion(args.for);
22
+ const result = createMigrationBackup();
23
+ output("backup", {
24
+ action: "create",
25
+ for: MIGRATION_BACKUP_VERSION,
26
+ path: result.path,
27
+ created: result.created,
28
+ manifest: result.manifest,
29
+ });
30
+ },
31
+ }),
32
+ restore: defineJsonCommand({
33
+ meta: { name: "restore", description: "Restore the pre-0.9 config and durable databases" },
34
+ args: {
35
+ for: { type: "string", required: true, description: "Migration target version (0.9.0)" },
36
+ confirm: { type: "boolean", default: false, description: "Confirm destructive restoration" },
37
+ },
38
+ run({ args }) {
39
+ requireVersion(args.for);
40
+ const result = restoreMigrationBackup(args.confirm);
41
+ output("backup", {
42
+ action: "restore",
43
+ for: MIGRATION_BACKUP_VERSION,
44
+ path: result.path,
45
+ restored: true,
46
+ manifest: result.manifest,
47
+ });
48
+ },
49
+ }),
50
+ },
51
+ defaultRun() {
52
+ throw new UsageError("Choose `backup create` or `backup restore`.", "MISSING_REQUIRED_ARGUMENT");
53
+ },
54
+ });