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
@@ -50,6 +50,8 @@ export function buildMemoryFrontmatter(fields) {
50
50
  obj.tags = fields.tags;
51
51
  if (fields.source?.trim())
52
52
  obj.source = fields.source;
53
+ if (fields.xrefs && fields.xrefs.length > 0)
54
+ obj.xrefs = fields.xrefs;
53
55
  if (fields.observed_at?.trim())
54
56
  obj.observed_at = fields.observed_at;
55
57
  if (fields.expires?.trim())
@@ -11,7 +11,7 @@ import { spawnSync } from "node:child_process";
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
13
  import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
14
- import { loadUserConfig, saveConfig } from "../../core/config/config.js";
14
+ import { mutateConfig } from "../../core/config/config.js";
15
15
  import { ConfigError } from "../../core/errors.js";
16
16
  import { assertSafeStashDir, getBinDir, getConfigPath, getDefaultStashDir } from "../../core/paths.js";
17
17
  import { ensureRg } from "../../core/ripgrep/install.js";
@@ -115,21 +115,20 @@ async function akmInitReal(options) {
115
115
  // Otherwise (--dir + existing default + no --set-default) leave the default
116
116
  // pointer alone; the target dir is still scaffolded above.
117
117
  const configPath = getConfigPath();
118
- const existing = loadUserConfig();
119
- const existingStashDir = existing.stashDir;
120
- const shouldPersist = !dirExplicitlyProvided || !existingStashDir || setDefault;
121
118
  let defaultStashUpdated = false;
122
119
  let previousStashDir;
123
- if (shouldPersist) {
124
- if (!existingStashDir || existingStashDir !== stashDir) {
125
- saveConfig({ ...existing, stashDir });
126
- defaultStashUpdated = true;
127
- }
128
- // else: already pointed here — no-op, no spurious rewrite.
129
- }
130
- else {
131
- // Default left untouched; surface it so the CLI can inform the user.
132
- previousStashDir = existingStashDir;
120
+ if (options?.persistConfig !== false) {
121
+ const result = mutateConfig((latest) => {
122
+ const shouldPersist = !dirExplicitlyProvided || !latest.stashDir || setDefault;
123
+ if (!shouldPersist) {
124
+ previousStashDir = latest.stashDir;
125
+ return latest;
126
+ }
127
+ if (latest.stashDir === stashDir)
128
+ return latest;
129
+ return { ...latest, stashDir };
130
+ });
131
+ defaultStashUpdated = result.written;
133
132
  }
134
133
  // Ensure ripgrep is available (install to cache/bin if needed)
135
134
  let ripgrep;
@@ -116,12 +116,15 @@ export function renderMigrationHelp(versionInput, changelogText = loadChangelog(
116
116
  if (changelogText) {
117
117
  for (const candidate of candidates) {
118
118
  const section = extractChangelogSection(changelogText, candidate);
119
- if (section) {
120
- const bundled = loadReleaseNote(candidate);
121
- if (!bundled)
122
- return `${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
119
+ const bundled = loadReleaseNote(candidate);
120
+ if (bundled) {
121
+ if (!section)
122
+ return `${bundled.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
123
123
  return `${bundled.trim()}\n\nRelease notes\n-------------\n${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
124
124
  }
125
+ if (section) {
126
+ return `${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
127
+ }
125
128
  }
126
129
  }
127
130
  const fallbackVersion = candidates.find((candidate) => candidate !== "latest") ?? requested;
@@ -39,8 +39,8 @@ const SCHEMA_REPAIR_WINDOW_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
39
39
  // ── Main ─────────────────────────────────────────────────────────────────────
40
40
  /**
41
41
  * Run the schema-repair loop for a batch of validation failures.
42
- * Returns a list of per-asset outcome records and the set of refs that were
43
- * successfully repaired (so the caller can exclude them from skip logic).
42
+ * Returns a list of per-asset outcome records and the set of refs whose live
43
+ * files were repaired. Queued proposals never count as live repairs.
44
44
  */
45
45
  export async function runSchemaRepairPass(failures, options) {
46
46
  const repairs = [];
@@ -167,8 +167,6 @@ export async function runSchemaRepairPass(failures, options) {
167
167
  outcome: "queued",
168
168
  proposalId: proposalResult.id,
169
169
  });
170
- // Mark as repaired so the caller removes it from the validation-failure set.
171
- repairedRefs.add(failure.ref);
172
170
  }
173
171
  catch (e) {
174
172
  appendEvent({
@@ -4,7 +4,7 @@
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { isHttpUrl, resolveStashDir } from "../../core/common.js";
7
- import { getSources, loadConfig, loadUserConfig, saveConfig } from "../../core/config/config.js";
7
+ import { getSources, loadConfig, mutateConfig } from "../../core/config/config.js";
8
8
  import { ConfigError, UsageError } from "../../core/errors.js";
9
9
  import { akmIndex } from "../../indexer/indexer.js";
10
10
  import { upsertLockEntry } from "../../integrations/lockfile.js";
@@ -67,38 +67,33 @@ export async function registerWikiSource(input) {
67
67
  async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName) {
68
68
  const stashRoot = detectStashRoot(sourcePath);
69
69
  const resolvedPath = path.resolve(stashRoot);
70
- const config = loadUserConfig();
71
70
  // Derive the canonical name: explicit --name wins, then wiki name, then readable path.
72
71
  const derivedName = explicitName ?? wikiName ?? toReadableId(resolvedPath);
73
- // Check for duplicates in sources[]
74
- const sources = [...getSources(config)];
75
- const existing = sources.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
76
72
  let persistedEntry;
77
- if (!existing) {
78
- persistedEntry = {
79
- type: "filesystem",
80
- path: resolvedPath,
81
- name: derivedName,
82
- ...(wikiName ? { wikiName } : {}),
83
- };
84
- sources.push(persistedEntry);
85
- saveConfig({ ...config, sources });
86
- }
87
- else {
88
- let changed = false;
89
- // If --name was explicitly supplied, update the persisted name.
90
- if (explicitName && existing.name !== explicitName) {
91
- existing.name = explicitName;
92
- changed = true;
73
+ mutateConfig((config) => {
74
+ const sources = [...getSources(config)];
75
+ const index = sources.findIndex((source) => source.type === "filesystem" && source.path && path.resolve(source.path) === resolvedPath);
76
+ if (index < 0) {
77
+ persistedEntry = {
78
+ type: "filesystem",
79
+ path: resolvedPath,
80
+ name: derivedName,
81
+ ...(wikiName ? { wikiName } : {}),
82
+ };
83
+ sources.push(persistedEntry);
84
+ return { ...config, sources };
93
85
  }
94
- if (wikiName && existing.wikiName !== wikiName) {
86
+ const existing = { ...sources[index] };
87
+ if (explicitName)
88
+ existing.name = explicitName;
89
+ if (wikiName)
95
90
  existing.wikiName = wikiName;
96
- changed = true;
97
- }
98
- if (changed)
99
- saveConfig({ ...config, sources });
100
91
  persistedEntry = existing;
101
- }
92
+ if (JSON.stringify(existing) === JSON.stringify(sources[index]))
93
+ return config;
94
+ sources[index] = existing;
95
+ return { ...config, sources };
96
+ });
102
97
  const index = await akmIndex({ stashDir });
103
98
  const updatedConfig = loadConfig();
104
99
  return {
@@ -108,9 +103,9 @@ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName)
108
103
  sourceAdded: {
109
104
  type: "filesystem",
110
105
  path: resolvedPath,
111
- name: persistedEntry.name ?? toReadableId(resolvedPath),
106
+ name: persistedEntry?.name ?? toReadableId(resolvedPath),
112
107
  stashRoot: resolvedPath,
113
- ...(persistedEntry.wikiName ? { wiki: persistedEntry.wikiName } : {}),
108
+ ...(persistedEntry?.wikiName ? { wiki: persistedEntry.wikiName } : {}),
114
109
  },
115
110
  config: {
116
111
  sourceCount: getSources(updatedConfig).length,
@@ -128,33 +123,32 @@ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName)
128
123
  async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
129
124
  const allowPrivateHosts = shouldAllowPrivateWebsiteUrlForTests(ref);
130
125
  const normalizedUrl = validateWebsiteInputUrl(ref, { allowPrivateHosts });
131
- const config = loadUserConfig();
132
- const sources = [...getSources(config)];
133
- let entry = sources.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
134
- if (!entry) {
135
- entry = {
136
- type: "website",
137
- url: normalizedUrl,
138
- name: name ?? toWebsiteName(normalizedUrl),
139
- ...(options && Object.keys(options).length > 0 ? { options } : {}),
140
- ...(wikiName ? { wikiName } : {}),
141
- };
142
- sources.push(entry);
143
- saveConfig({ ...config, sources });
144
- }
145
- else {
146
- let changed = false;
147
- if (options && Object.keys(options).length > 0) {
148
- entry.options = { ...entry.options, ...options };
149
- changed = true;
150
- }
151
- if (wikiName && entry.wikiName !== wikiName) {
152
- entry.wikiName = wikiName;
153
- changed = true;
126
+ let entry;
127
+ mutateConfig((config) => {
128
+ const sources = [...getSources(config)];
129
+ const index = sources.findIndex((source) => source.type === "website" && source.url === normalizedUrl);
130
+ if (index < 0) {
131
+ entry = {
132
+ type: "website",
133
+ url: normalizedUrl,
134
+ name: name ?? toWebsiteName(normalizedUrl),
135
+ ...(options && Object.keys(options).length > 0 ? { options } : {}),
136
+ ...(wikiName ? { wikiName } : {}),
137
+ };
138
+ sources.push(entry);
139
+ return { ...config, sources };
154
140
  }
155
- if (changed)
156
- saveConfig({ ...config, sources });
157
- }
141
+ const existing = { ...sources[index] };
142
+ if (options && Object.keys(options).length > 0)
143
+ existing.options = { ...existing.options, ...options };
144
+ if (wikiName)
145
+ existing.wikiName = wikiName;
146
+ entry = existing;
147
+ if (JSON.stringify(existing) === JSON.stringify(sources[index]))
148
+ return config;
149
+ sources[index] = existing;
150
+ return { ...config, sources };
151
+ });
158
152
  const cachePaths = await ensureWebsiteMirror(entry, {
159
153
  requireStashDir: true,
160
154
  ...(allowPrivateHosts ? { allowPrivateHosts: true } : {}),
@@ -168,9 +162,9 @@ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
168
162
  sourceAdded: {
169
163
  type: "website",
170
164
  url: normalizedUrl,
171
- name: entry.name,
165
+ name: entry?.name,
172
166
  stashRoot: cachePaths.stashDir,
173
- ...(entry.wikiName ? { wiki: entry.wikiName } : {}),
167
+ ...(entry?.wikiName ? { wiki: entry.wikiName } : {}),
174
168
  },
175
169
  config: {
176
170
  sourceCount: getSources(updatedConfig).length,
@@ -258,25 +252,20 @@ async function addRegistryStash(ref, stashDir, writable, wikiName) {
258
252
  }
259
253
  /** Persist or replace an installed stash entry in the user config. */
260
254
  export function upsertInstalledRegistryEntry(entry) {
261
- const current = loadUserConfig();
262
- const currentInstalled = current.installed ?? [];
263
- const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id);
264
- const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)];
265
- const nextConfig = { ...current, installed: nextInstalled };
266
- saveConfig(nextConfig);
267
- return nextConfig;
255
+ return mutateConfig((current) => {
256
+ const withoutExisting = (current.installed ?? []).filter((item) => item.id !== entry.id);
257
+ return { ...current, installed: [...withoutExisting, normalizeInstalledEntry(entry)] };
258
+ }).config;
268
259
  }
269
260
  /** Remove an installed stash entry from the user config. */
270
261
  export function removeInstalledRegistryEntry(id) {
271
- const current = loadUserConfig();
272
- const currentInstalled = current.installed ?? [];
273
- const nextInstalled = currentInstalled.filter((item) => item.id !== id);
274
- const nextConfig = {
275
- ...current,
276
- installed: nextInstalled.length > 0 ? nextInstalled : undefined,
277
- };
278
- saveConfig(nextConfig);
279
- return nextConfig;
262
+ return mutateConfig((current) => {
263
+ const currentInstalled = current.installed ?? [];
264
+ const nextInstalled = currentInstalled.filter((item) => item.id !== id);
265
+ if (nextInstalled.length === currentInstalled.length)
266
+ return current;
267
+ return { ...current, installed: nextInstalled.length > 0 ? nextInstalled : undefined };
268
+ }).config;
280
269
  }
281
270
  function normalizeInstalledEntry(entry) {
282
271
  return {
@@ -3,7 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import path from "node:path";
5
5
  import { isRemoteUrl } from "../../core/common.js";
6
- import { getSources, loadConfig, loadUserConfig, saveConfig } from "../../core/config/config.js";
6
+ import { getSources, loadConfig, mutateConfig } from "../../core/config/config.js";
7
7
  import { ConfigError, UsageError } from "../../core/errors.js";
8
8
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
9
9
  // ── Operations ──────────────────────────────────────────────────────────────
@@ -22,65 +22,69 @@ export function addStash(opts) {
22
22
  if (writable === true && providerType && providerType !== "filesystem" && providerType !== "git") {
23
23
  throw new ConfigError("writable: true is only supported on filesystem and git sources", "INVALID_CONFIG_FILE");
24
24
  }
25
- const config = loadUserConfig();
26
- const sources = [...getSources(config)];
27
- let entry;
25
+ let result;
28
26
  if (isRemoteUrl(target)) {
29
27
  if (!providerType) {
30
28
  throw new UsageError("--provider is required for URL sources (e.g. --provider git --provider website)");
31
29
  }
32
- // Deduplicate by URL
33
- if (sources.some((s) => s.url === target)) {
34
- return { sources, added: false, message: "Source URL already configured" };
35
- }
36
- entry = { type: providerType, url: target };
37
- if (name)
38
- entry.name = name;
39
- if (writable)
40
- entry.writable = true;
41
- if (providerOptions)
42
- entry.options = providerOptions;
43
30
  }
44
- else {
45
- // Filesystem path
46
- const resolvedPath = path.resolve(target);
47
- if (sources.some((s) => s.path && path.resolve(s.path) === resolvedPath)) {
48
- return { sources, added: false, message: "Source path already configured" };
31
+ mutateConfig((config) => {
32
+ const sources = [...getSources(config)];
33
+ let entry;
34
+ if (isRemoteUrl(target)) {
35
+ if (sources.some((source) => source.url === target)) {
36
+ result = { sources, added: false, message: "Source URL already configured" };
37
+ return config;
38
+ }
39
+ entry = { type: providerType, url: target };
40
+ if (name)
41
+ entry.name = name;
42
+ if (writable)
43
+ entry.writable = true;
44
+ if (providerOptions)
45
+ entry.options = providerOptions;
49
46
  }
50
- entry = { type: "filesystem", path: resolvedPath };
51
- if (name)
52
- entry.name = name;
53
- }
54
- sources.push(entry);
55
- saveConfig({ ...config, sources });
56
- return { sources, added: true, entry };
47
+ else {
48
+ const resolvedPath = path.resolve(target);
49
+ if (sources.some((source) => source.path && path.resolve(source.path) === resolvedPath)) {
50
+ result = { sources, added: false, message: "Source path already configured" };
51
+ return config;
52
+ }
53
+ entry = { type: "filesystem", path: resolvedPath };
54
+ if (name)
55
+ entry.name = name;
56
+ }
57
+ sources.push(entry);
58
+ result = { sources, added: true, entry };
59
+ return { ...config, sources };
60
+ });
61
+ return result;
57
62
  }
58
63
  /**
59
64
  * Remove a stash source by URL, path, or name.
60
65
  * Match priority: URL > path > name (most specific first).
61
66
  */
62
67
  export function removeStash(target) {
63
- const config = loadUserConfig();
64
- const sources = [...getSources(config)];
65
68
  const isUrlTarget = isRemoteUrl(target);
66
69
  const resolvedPath = !isUrlTarget ? path.resolve(target) : undefined;
67
- // Try URL match first, then path, then name (most specific → least specific)
68
- let idx = -1;
69
- if (isUrlTarget) {
70
- idx = sources.findIndex((s) => s.url === target);
71
- }
72
- if (idx === -1 && resolvedPath) {
73
- idx = sources.findIndex((s) => s.path && path.resolve(s.path) === resolvedPath);
74
- }
75
- if (idx === -1) {
76
- idx = sources.findIndex((s) => s.name === target);
77
- }
78
- if (idx === -1) {
79
- return { sources, removed: false, message: "No matching source found" };
80
- }
81
- const removed = sources.splice(idx, 1)[0];
82
- saveConfig({ ...config, sources });
83
- return { sources, removed: true, entry: removed };
70
+ let result;
71
+ mutateConfig((config) => {
72
+ const sources = [...getSources(config)];
73
+ let idx = isUrlTarget ? sources.findIndex((source) => source.url === target) : -1;
74
+ if (idx === -1 && resolvedPath) {
75
+ idx = sources.findIndex((source) => source.path && path.resolve(source.path) === resolvedPath);
76
+ }
77
+ if (idx === -1)
78
+ idx = sources.findIndex((source) => source.name === target);
79
+ if (idx === -1) {
80
+ result = { sources, removed: false, message: "No matching source found" };
81
+ return config;
82
+ }
83
+ const removed = sources.splice(idx, 1)[0];
84
+ result = { sources, removed: true, entry: removed };
85
+ return { ...config, sources };
86
+ });
87
+ return result;
84
88
  }
85
89
  /**
86
90
  * List all stash sources (local filesystem + configured stashes).
@@ -27,8 +27,9 @@
27
27
  import path from "node:path";
28
28
  import { defineCommand } from "citty";
29
29
  import * as p from "../../cli/clack.js";
30
- import { defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
30
+ import { defineJsonCommand, output, parseAllFlagValues, runWithJsonErrors } from "../../cli/shared.js";
31
31
  import { assertFlatAssetName } from "../../core/asset/asset-create.js";
32
+ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
32
33
  import { isHttpUrl } from "../../core/common.js";
33
34
  import { loadConfig } from "../../core/config/config.js";
34
35
  import { UsageError } from "../../core/errors.js";
@@ -38,7 +39,7 @@ import { clearLogFile, info, isVerbose, setLogFile } from "../../core/warn.js";
38
39
  import { resolveWriteTarget } from "../../core/write-source.js";
39
40
  import { akmIndex } from "../../indexer/indexer.js";
40
41
  import { getHyphenatedBoolean, getOutputMode, parseFlagValue } from "../../output/context.js";
41
- import { readKnowledgeInput, writeMarkdownAsset } from "../read/knowledge.js";
42
+ import { inferAssetName, mergeXrefsIntoContent, readKnowledgeInput, resolveSupersedesForWrite, resolveXrefsForWrite, writeMarkdownAsset, } from "../read/knowledge.js";
42
43
  import { assembleInfo } from "./info.js";
43
44
  import { akmInit } from "./init.js";
44
45
  export const initCommand = defineJsonCommand({
@@ -181,21 +182,57 @@ export const importKnowledgeCommand = defineJsonCommand({
181
182
  type: "string",
182
183
  description: "Override the write destination. Accepts a source name from your config; falls back to defaultWriteTarget then the working stash.",
183
184
  },
185
+ xref: {
186
+ type: "string",
187
+ description: "Cross-reference ref merged into the document's `xrefs:` frontmatter (repeatable: --xref knowledge:auth-flow). Existing frontmatter is preserved (dedupe-append, never a nested block); a document whose frontmatter is not parseable YAML aborts the import rather than being rewritten lossily. Each ref must resolve in the write target or a configured source; an unresolvable ref aborts the import.",
188
+ },
189
+ supersedes: {
190
+ type: "string",
191
+ description: "Ref of an existing asset this document corrects (repeatable: --supersedes knowledge:legacy-guide). Imports the correction with an xref to the old asset AND demotes the old asset (`beliefState: superseded` + `supersededBy`, a metadata-only edit) so ranking prefers the correction and `--belief current` hides the stale version. An unresolvable or self-referencing ref aborts the import; a ref outside the write target and working stash still imports the correction but skips the demotion (reported as applied: false).",
192
+ },
184
193
  },
185
194
  async run({ args }) {
186
195
  // `--name` is a flat name; subdirectory placement is `--path`'s job.
187
196
  assertFlatAssetName(args.name);
197
+ // Collect and validate --xref occurrences (repeatable; citty only exposes
198
+ // the last value, so read argv directly). Validation happens BEFORE any
199
+ // read/write so an unresolvable ref (UsageError → exit 2) leaves the
200
+ // stash untouched.
201
+ const xrefs = resolveXrefsForWrite(parseAllFlagValues("--xref"), args.target);
202
+ // Collect and validate --supersedes occurrences (repeatable). Same
203
+ // before-any-read/write contract: an unresolvable ref exits 2 with nothing
204
+ // imported AND nothing demoted. The superseded refs fold into the imported
205
+ // doc's xrefs automatically (correction provenance); the demotion runs
206
+ // inside writeMarkdownAsset, ordered before the git boundary commit.
207
+ const supersedes = resolveSupersedesForWrite(parseAllFlagValues("--supersedes"), args.target);
208
+ for (const s of supersedes) {
209
+ if (!xrefs.includes(s.ref))
210
+ xrefs.push(s.ref);
211
+ }
188
212
  const stashDir = resolveWriteTarget(loadConfig(), args.target).source.path;
189
213
  const { content, preferredName } = await readKnowledgeInput(args.source, { stashDir });
214
+ // Imported docs may carry their own frontmatter: merge (dedupe-append)
215
+ // BEFORE the write so write-path indexing sees the final content and no
216
+ // second frontmatter block is ever nested.
217
+ // The slug must come from the document BODY: a merged (or self-carried)
218
+ // frontmatter block puts the `---` fence on the first line, which
219
+ // inferAssetName would slugify to "" and fall back to a random
220
+ // knowledge-<epoch>-<rand> name. A stdin import (no filename-derived
221
+ // preferredName) therefore pre-infers the name from the pre-merge
222
+ // content's PARSED body — not the raw text, whose first line is the fence
223
+ // whenever the piped doc carries its own frontmatter — so --xref/
224
+ // --supersedes never change the slug and a frontmattered doc gets its
225
+ // heading-derived slug on every path.
190
226
  const result = await writeMarkdownAsset({
191
227
  type: "knowledge",
192
- content,
228
+ content: mergeXrefsIntoContent(content, xrefs),
193
229
  name: args.name ?? (isHttpUrl(args.source) ? preferredName : undefined),
194
230
  fallbackPrefix: "knowledge",
195
- preferredName,
231
+ preferredName: preferredName ?? inferAssetName(parseFrontmatter(content).content, "knowledge"),
196
232
  force: args.force,
197
233
  target: args.target,
198
234
  path: args.path,
235
+ supersedes,
199
236
  });
200
237
  appendEvent({
201
238
  eventType: "import",
@@ -7,8 +7,8 @@
7
7
  * Ships a well-tuned multi-cadence task set so a typical single-developer
8
8
  * install is correct with zero manual config. Registration is **idempotent**:
9
9
  * running `akm setup` / `akm tasks init` twice yields the same task set with no
10
- * duplicates. Each default task is a shell-command task (`akm improve --profile
11
- * <name>`), so the profile is overridable in `config.json` without editing the
10
+ * duplicates. Each default task is a shell-command task (`akm improve --strategy
11
+ * <name>`), so the strategy is overridable in `config.json` without editing the
12
12
  * task definition.
13
13
  *
14
14
  * The OS-scheduler-touching primitives (`add` / `setEnabled` / `list`) are
@@ -27,40 +27,40 @@ import { akmTasksAdd, akmTasksList } from "./tasks.js";
27
27
  export const DEFAULT_IMPROVE_TASKS = [
28
28
  {
29
29
  id: "akm-improve-frequent",
30
- profile: "frequent",
31
- command: "akm improve --profile frequent --auto-accept safe",
30
+ strategy: "frequent",
31
+ command: "akm improve --strategy frequent --auto-accept safe",
32
32
  schedule: "0 * * * *",
33
33
  description: "Frequent extract + inference pass (every 60 min)",
34
34
  enableMode: "always",
35
35
  },
36
36
  {
37
37
  id: "akm-improve-consolidate",
38
- profile: "consolidate",
39
- command: "akm improve --profile consolidate --auto-accept safe",
38
+ strategy: "consolidate",
39
+ command: "akm improve --strategy consolidate --auto-accept safe",
40
40
  schedule: "0 */4 * * *",
41
41
  description: "Consolidation-only pass (every 4h)",
42
42
  enableMode: "always",
43
43
  },
44
44
  {
45
45
  id: "akm-improve-nightly",
46
- profile: "thorough",
47
- command: "akm improve --profile thorough --auto-accept safe",
46
+ strategy: "thorough",
47
+ command: "akm improve --strategy thorough --auto-accept safe",
48
48
  schedule: "0 2 * * *",
49
49
  description: "Full nightly quality sweep (daily 2am)",
50
50
  enableMode: "server",
51
51
  },
52
52
  {
53
53
  id: "akm-improve-catchup",
54
- profile: "catchup",
55
- command: "akm improve --profile catchup --auto-accept safe",
54
+ strategy: "catchup",
55
+ command: "akm improve --strategy catchup --auto-accept safe",
56
56
  schedule: null,
57
57
  description: "Manual recovery — consolidation + triage drain (run on demand)",
58
58
  enableMode: "manual",
59
59
  },
60
60
  {
61
61
  id: "akm-graph-refresh-weekly",
62
- profile: "graph-refresh",
63
- command: "akm improve --profile graph-refresh --auto-accept safe",
62
+ strategy: "graph-refresh",
63
+ command: "akm improve --strategy graph-refresh --auto-accept safe",
64
64
  schedule: "0 3 * * 0",
65
65
  description: "Full-corpus graph rebuild (weekly Sunday 3am)",
66
66
  enableMode: "always",
@@ -31,7 +31,9 @@ const tasksAddCommand = defineJsonCommand({
31
31
  type: "string",
32
32
  description: 'Shell command to run on the schedule (no AI agent), e.g. "akm improve --auto-accept safe". Split on whitespace; quote the whole flag value.',
33
33
  },
34
- profile: { type: "string", description: "Agent profile to use for prompt targets (default: defaults.agent)" },
34
+ engine: { type: "string", description: "Engine to use for prompt targets (default: defaults.engine)" },
35
+ model: { type: "string", description: "Model override for prompt targets" },
36
+ "timeout-ms": { type: "string", description: "Positive timeout in milliseconds for prompt or command targets" },
35
37
  params: { type: "string", description: "Workflow params as a JSON object" },
36
38
  name: { type: "string", description: "Human-readable name for the task" },
37
39
  "when-to-use": { type: "string", description: "Guidance on when this task runs or should be used" },
@@ -47,7 +49,9 @@ const tasksAddCommand = defineJsonCommand({
47
49
  workflow: args.workflow,
48
50
  prompt: args.prompt,
49
51
  command: args.command,
50
- profile: args.profile,
52
+ engine: args.engine,
53
+ model: args.model,
54
+ timeoutMs: args["timeout-ms"] === undefined ? undefined : parsePositiveIntFlag(args["timeout-ms"]),
51
55
  params: args.params,
52
56
  name: args.name,
53
57
  when_to_use: args["when-to-use"],
@@ -179,7 +183,7 @@ export const tasksCommand = defineGroupCommand({
179
183
  meta: {
180
184
  name: "tasks",
181
185
  alias: "task",
182
- description: "Schedule workflows or prompts via the OS-native scheduler (cron / launchd / schtasks)",
186
+ description: "Schedule version-2 workflows, prompts, or commands via the OS-native scheduler (cron / launchd / schtasks)",
183
187
  },
184
188
  subCommands: {
185
189
  add: tasksAddCommand,