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
@@ -29,13 +29,14 @@
29
29
  * tags: [scheduled, backup]
30
30
  * ```
31
31
  *
32
- * Validation lives in {@link validateTaskDocument}. The parser only enforces
33
- * shape; cron syntax, target reachability, and profile availability are
32
+ * Validation lives in {@link validateTaskDocument}. The parser enforces the
33
+ * strict source shape; cron syntax and target reachability are
34
34
  * checked separately so callers can choose how strictly to surface errors.
35
35
  */
36
36
  import path from "node:path";
37
37
  import { parse as parseYaml } from "yaml";
38
38
  import { UsageError } from "../core/errors.js";
39
+ import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-params.js";
39
40
  import { TASK_SCHEMA_VERSION } from "./schema.js";
40
41
  export function parseTaskDocument(input) {
41
42
  const { yaml, filePath, id } = input;
@@ -52,15 +53,17 @@ export function parseTaskDocument(input) {
52
53
  throw err;
53
54
  throw new UsageError(`Task "${id}" has invalid YAML: ${err instanceof Error ? err.message : String(err)}. File: ${filePath}`, "INVALID_FLAG_VALUE");
54
55
  }
55
- const schedule = readString(data.schedule, "schedule", filePath);
56
+ requireVersion(data, id, filePath);
57
+ rejectUnknownKeys(data, id, filePath);
58
+ const schedule = requireString(data.schedule, "schedule", filePath);
56
59
  if (!schedule) {
57
60
  throw new UsageError(`Task "${id}" is missing a schedule (YAML key "schedule"). File: ${filePath}`, "MISSING_REQUIRED_ARGUMENT");
58
61
  }
59
- const enabled = data.enabled === undefined ? true : data.enabled === true;
60
- const name = readString(data.name, "name", filePath);
61
- const description = readString(data.description, "description", filePath);
62
- const when_to_use = readString(data.when_to_use, "when_to_use", filePath);
63
- const tags = readStringArray(data.tags);
62
+ const enabled = readEnabled(data.enabled, filePath);
63
+ const name = optionalString(data.name, "name", filePath);
64
+ const description = optionalString(data.description, "description", filePath);
65
+ const when_to_use = optionalString(data.when_to_use, "when_to_use", filePath);
66
+ const tags = readTags(data.tags, filePath);
64
67
  const hasWorkflow = "workflow" in data && data.workflow !== "" && data.workflow != null;
65
68
  const hasPrompt = "prompt" in data && data.prompt !== "" && data.prompt != null;
66
69
  const hasCommand = "command" in data && data.command !== "" && data.command != null;
@@ -73,7 +76,8 @@ export function parseTaskDocument(input) {
73
76
  }
74
77
  let target;
75
78
  if (hasWorkflow) {
76
- const ref = readString(data.workflow, "workflow", filePath);
79
+ rejectTargetFields(data, ["params"], id, filePath);
80
+ const ref = requireString(data.workflow, "workflow", filePath);
77
81
  if (!ref) {
78
82
  throw new UsageError(`Task "${id}" has empty \`workflow\`. File: ${filePath}`, "INVALID_FLAG_VALUE");
79
83
  }
@@ -84,48 +88,81 @@ export function parseTaskDocument(input) {
84
88
  };
85
89
  }
86
90
  else if (hasCommand) {
91
+ rejectTargetFields(data, ["timeoutMs"], id, filePath);
87
92
  const cmd = readCommand(data.command, filePath, id);
88
93
  target = { kind: "command", cmd };
89
94
  }
90
95
  else {
91
- const promptRaw = readString(data.prompt, "prompt", filePath);
96
+ rejectTargetFields(data, ["engine", "model", "timeoutMs", "llm"], id, filePath);
97
+ const promptRaw = requireString(data.prompt, "prompt", filePath);
92
98
  if (!promptRaw) {
93
99
  throw new UsageError(`Task "${id}" has empty \`prompt\`. File: ${filePath}`, "INVALID_FLAG_VALUE");
94
100
  }
95
- const profile = readString(data.profile, "profile", filePath);
101
+ const engine = optionalString(data.engine, "engine", filePath);
102
+ const model = optionalString(data.model, "model", filePath);
103
+ const timeoutMs = readTimeout(data.timeoutMs, filePath);
104
+ const llm = readLlmOverrides(data.llm, filePath);
96
105
  target = {
97
106
  kind: "prompt",
98
107
  source: resolvePromptSource(promptRaw, filePath, id),
99
- profile: profile && profile.length > 0 ? profile : undefined,
108
+ ...(engine ? { engine } : {}),
109
+ ...(model ? { model } : {}),
110
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
111
+ ...(llm ? { llm } : {}),
100
112
  };
101
113
  }
102
- // null / 0 / negative → disabled (no timeout). Positive number → override.
103
- // Omitted → undefined (inherits config.agent.timeoutMs).
104
- let timeoutMs;
105
- if ("timeoutMs" in data) {
106
- const raw = data.timeoutMs;
107
- if (raw === null || raw === "null" || raw === 0 || (typeof raw === "number" && raw < 0)) {
108
- timeoutMs = null;
109
- }
110
- else if (typeof raw === "number" && raw > 0) {
111
- timeoutMs = raw;
112
- }
113
- // non-numeric / unrecognised → leave as undefined (inherit)
114
- }
114
+ const timeoutMs = hasCommand ? readTimeout(data.timeoutMs, filePath) : undefined;
115
115
  return {
116
+ version: TASK_SCHEMA_VERSION,
116
117
  schemaVersion: TASK_SCHEMA_VERSION,
117
118
  id,
118
119
  schedule,
119
120
  enabled,
120
121
  target,
121
- name: name && name.length > 0 ? name : undefined,
122
- description: description && description.length > 0 ? description : undefined,
123
- when_to_use: when_to_use && when_to_use.length > 0 ? when_to_use : undefined,
124
- tags: tags && tags.length > 0 ? tags : undefined,
122
+ ...(name ? { name } : {}),
123
+ ...(description ? { description } : {}),
124
+ ...(when_to_use ? { when_to_use } : {}),
125
+ ...(tags ? { tags } : {}),
125
126
  source: { path: filePath },
126
127
  timeoutMs,
127
128
  };
128
129
  }
130
+ const TASK_KEYS = new Set([
131
+ "version",
132
+ "name",
133
+ "description",
134
+ "when_to_use",
135
+ "tags",
136
+ "schedule",
137
+ "enabled",
138
+ "workflow",
139
+ "prompt",
140
+ "command",
141
+ "params",
142
+ "engine",
143
+ "model",
144
+ "timeoutMs",
145
+ "llm",
146
+ ]);
147
+ const SHARED_KEYS = new Set(["version", "name", "description", "when_to_use", "tags", "schedule", "enabled"]);
148
+ function requireVersion(data, id, filePath) {
149
+ if (data.version === TASK_SCHEMA_VERSION)
150
+ return;
151
+ const actual = data.version === undefined ? "missing" : JSON.stringify(data.version);
152
+ throw new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task "${id}" uses task schema version ${actual}; version: 2 is required. File: ${filePath}`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Rewrite the task using version: 2 and replace profile with engine.");
153
+ }
154
+ function rejectUnknownKeys(data, id, filePath) {
155
+ const unknown = Object.keys(data).filter((key) => !TASK_KEYS.has(key));
156
+ if (unknown.length > 0) {
157
+ throw new UsageError(`Task "${id}" has unknown key(s): ${unknown.join(", ")}. File: ${filePath}`, "INVALID_FLAG_VALUE");
158
+ }
159
+ }
160
+ function rejectTargetFields(data, allowed, id, filePath) {
161
+ const forbidden = Object.keys(data).filter((key) => !SHARED_KEYS.has(key) && !allowed.includes(key) && !["workflow", "prompt", "command"].includes(key));
162
+ if (forbidden.length > 0) {
163
+ throw new UsageError(`Task "${id}" has field(s) not valid for this target: ${forbidden.join(", ")}. File: ${filePath}`, "INVALID_FLAG_VALUE");
164
+ }
165
+ }
129
166
  /**
130
167
  * Resolve a `prompt:` value into a {@link TaskPromptSource} variant.
131
168
  *
@@ -150,32 +187,44 @@ function resolvePromptSource(raw, filePath, id) {
150
187
  }
151
188
  return { kind: "inline", text: trimmed };
152
189
  }
153
- function readString(value, key, filePath) {
190
+ function optionalString(value, key, filePath) {
154
191
  if (value === undefined || value === null)
155
192
  return undefined;
156
193
  if (typeof value === "string")
157
- return value.trim();
158
- if (typeof value === "number" || typeof value === "boolean")
159
- return String(value);
194
+ return value;
160
195
  throw new UsageError(`Key "${key}" must be a string. File: ${filePath}`, "INVALID_FLAG_VALUE");
161
196
  }
162
- function readStringArray(value) {
197
+ function requireString(value, key, filePath) {
198
+ const result = optionalString(value, key, filePath);
199
+ if (result === undefined || result.length === 0) {
200
+ throw new UsageError(`Key "${key}" must be a non-empty string. File: ${filePath}`, "INVALID_FLAG_VALUE");
201
+ }
202
+ return result;
203
+ }
204
+ function readEnabled(value, filePath) {
205
+ if (value === undefined)
206
+ return true;
207
+ if (typeof value !== "boolean")
208
+ throw new UsageError(`Key "enabled" must be a boolean. File: ${filePath}`, "INVALID_FLAG_VALUE");
209
+ return value;
210
+ }
211
+ function readTags(value, filePath) {
163
212
  if (value === undefined || value === null)
164
213
  return undefined;
165
- if (typeof value === "string") {
166
- return value
167
- .split(/[\s,]+/)
168
- .map((s) => s.trim())
169
- .filter(Boolean);
170
- }
171
214
  if (Array.isArray(value)) {
172
- return value.filter((v) => typeof v === "string" && v.trim().length > 0);
215
+ if (!value.every((entry) => typeof entry === "string" && entry.length > 0)) {
216
+ throw new UsageError(`Key "tags" must be an array of non-empty strings. File: ${filePath}`, "INVALID_FLAG_VALUE");
217
+ }
218
+ return value;
173
219
  }
174
- return undefined;
220
+ throw new UsageError(`Key "tags" must be an array of strings. File: ${filePath}`, "INVALID_FLAG_VALUE");
175
221
  }
176
222
  function readCommand(value, filePath, id) {
177
223
  if (Array.isArray(value)) {
178
- const parts = value.filter((v) => typeof v === "string" && v.trim().length > 0);
224
+ if (!value.every((entry) => typeof entry === "string" && entry.length > 0)) {
225
+ throw new UsageError(`Task "${id}" command array must contain non-empty strings. File: ${filePath}`, "INVALID_FLAG_VALUE");
226
+ }
227
+ const parts = value;
179
228
  if (parts.length === 0) {
180
229
  throw new UsageError(`Task "${id}" has empty \`command\` array. File: ${filePath}`, "INVALID_FLAG_VALUE");
181
230
  }
@@ -196,16 +245,53 @@ function readParams(value, filePath) {
196
245
  if (typeof value === "object" && !Array.isArray(value)) {
197
246
  return value;
198
247
  }
199
- if (typeof value === "string" && value.trim()) {
200
- try {
201
- const parsed = JSON.parse(value);
202
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
203
- return parsed;
204
- }
248
+ throw new UsageError(`Key "params" must be a mapping. File: ${filePath}`, "INVALID_FLAG_VALUE");
249
+ }
250
+ function readTimeout(value, filePath) {
251
+ if (value === undefined)
252
+ return undefined;
253
+ if (value === null)
254
+ return null;
255
+ if (typeof value === "number" && Number.isInteger(value) && value > 0)
256
+ return value;
257
+ throw new UsageError(`Key "timeoutMs" must be a positive integer or null. File: ${filePath}`, "INVALID_FLAG_VALUE");
258
+ }
259
+ function readLlmOverrides(value, filePath) {
260
+ if (value === undefined)
261
+ return undefined;
262
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
263
+ throw new UsageError(`Key "llm" must be a mapping. File: ${filePath}`, "INVALID_FLAG_VALUE");
264
+ }
265
+ const data = value;
266
+ const allowed = new Set([
267
+ "temperature",
268
+ "maxTokens",
269
+ "supportsJsonSchema",
270
+ "extraParams",
271
+ "contextLength",
272
+ "enableThinking",
273
+ ]);
274
+ const unknown = Object.keys(data).filter((key) => !allowed.has(key));
275
+ if (unknown.length)
276
+ throw new UsageError(`Key "llm" has unknown field(s): ${unknown.join(", ")}. File: ${filePath}`, "INVALID_FLAG_VALUE");
277
+ if (data.temperature !== undefined && (typeof data.temperature !== "number" || !Number.isFinite(data.temperature))) {
278
+ throw new UsageError(`Key "llm.temperature" must be a finite number. File: ${filePath}`, "INVALID_FLAG_VALUE");
279
+ }
280
+ for (const key of ["maxTokens", "contextLength"]) {
281
+ if (data[key] !== undefined && (!Number.isInteger(data[key]) || data[key] <= 0)) {
282
+ throw new UsageError(`Key "llm.${key}" must be a positive integer. File: ${filePath}`, "INVALID_FLAG_VALUE");
205
283
  }
206
- catch {
207
- // fall through
284
+ }
285
+ for (const key of ["supportsJsonSchema", "enableThinking"]) {
286
+ if (data[key] !== undefined && typeof data[key] !== "boolean") {
287
+ throw new UsageError(`Key "llm.${key}" must be a boolean. File: ${filePath}`, "INVALID_FLAG_VALUE");
288
+ }
289
+ }
290
+ if (data.extraParams !== undefined) {
291
+ const issue = validateExtraParams(data.extraParams)[0];
292
+ if (issue) {
293
+ throw new UsageError(`${formatExtraParamsIssue('Key "llm.extraParams"', issue)}. File: ${filePath}`, "INVALID_FLAG_VALUE");
208
294
  }
209
295
  }
210
- throw new UsageError(`Key "params" must be a mapping or a JSON object. File: ${filePath}`, "INVALID_FLAG_VALUE");
296
+ return data;
211
297
  }
@@ -15,7 +15,7 @@
15
15
  * 3. Refuse to run when `enabled === false` (defense-in-depth).
16
16
  * 4. Dispatch by target kind:
17
17
  * • workflow → `startWorkflowRun(ref, params)`
18
- * • prompt → `runAgent(profile, prompt, { stdio: "captured" })`
18
+ * • prompt → `executeRunner(engine, prompt, { stdio: "captured" })`
19
19
  * 5. Capture stdout / stderr as structured rows in logs.db (task_logs) and,
20
20
  * transitionally, as a flat text tail at `<cacheDir>/tasks/logs/<id>/<ts>.log`
21
21
  * (see docs/technical/logs-audit.md).
@@ -35,17 +35,18 @@ import { buildTaskRunId, insertTaskLogLines, openLogsDatabase, } from "../core/l
35
35
  import { getTaskLogDir } from "../core/paths.js";
36
36
  import { withStateDb } from "../core/state-db.js";
37
37
  import { error } from "../core/warn.js";
38
- import { requireAgentProfile, runAgent } from "../integrations/agent/index.js";
39
- import { resolveProcessAgentProfile } from "../integrations/agent/config.js";
40
- import { resolveRunner } from "../integrations/agent/runner.js";
38
+ import { resolveEngine, resolveLlmEngineUse } from "../integrations/agent/engine-resolution.js";
39
+ import { resolveModel } from "../integrations/agent/model-aliases.js";
40
+ import { executeRunner } from "../integrations/agent/runner-dispatch.js";
41
+ import { chatCompletion } from "../llm/client.js";
41
42
  import { spawn } from "../runtime.js";
42
43
  import { resolveAssetPath } from "../sources/resolve.js";
43
- import { getTaskHistory, queryTaskHistory, upsertTaskHistory } from "../storage/repositories/task-history-repository.js";
44
+ import { decodeTaskHistoryMetadata, getTaskHistory, queryTaskHistory, upsertTaskHistory, } from "../storage/repositories/task-history-repository.js";
44
45
  import { startWorkflowRun } from "../workflows/runtime/runs.js";
45
46
  import { parseTaskDocument } from "./parser.js";
46
47
  export async function runTask(id, options = {}) {
47
48
  const stashDir = options.stashDir ?? resolveStashDir();
48
- const runAgentImpl = options.runAgentImpl ?? runAgent;
49
+ const runAgentImpl = options.runAgentImpl;
49
50
  const startWorkflowRunImpl = options.startWorkflowRunImpl ?? startWorkflowRun;
50
51
  const now = options.now ?? (() => new Date());
51
52
  const logDir = options.logDir ?? getTaskLogDir();
@@ -63,8 +64,8 @@ export async function runTask(id, options = {}) {
63
64
  const disabledTarget = task.target.kind === "workflow"
64
65
  ? { kind: "workflow", ref: task.target.ref }
65
66
  : task.target.kind === "command"
66
- ? { kind: "prompt", profile: undefined }
67
- : { kind: "prompt", profile: task.target.profile };
67
+ ? { kind: "command", cmd: task.target.cmd }
68
+ : { kind: "prompt", engine: task.target.engine ?? null };
68
69
  const result = {
69
70
  id,
70
71
  status: "disabled",
@@ -98,9 +99,6 @@ export async function runTask(id, options = {}) {
98
99
  if (task.target.kind === "command") {
99
100
  return await runCommandTask({ task, logPath, startedAt, now });
100
101
  }
101
- // Resolve config once here so runPromptTask does not call loadConfig()
102
- // on every dispatch in a batch run (Fix C6).
103
- const config = loadConfig();
104
102
  return await runPromptTask({
105
103
  task,
106
104
  stashDir,
@@ -109,8 +107,7 @@ export async function runTask(id, options = {}) {
109
107
  now,
110
108
  runAgentImpl,
111
109
  agentOptions: options.agentOptions,
112
- agentConfig: config,
113
- agentTimeoutMs: undefined,
110
+ chatCompletionImpl: options.chatCompletionImpl ?? chatCompletion,
114
111
  });
115
112
  }
116
113
  // ── command target ──────────────────────────────────────────────────────────
@@ -201,7 +198,7 @@ async function runCommandTask(input) {
201
198
  finishedAt: finishedAt.toISOString(),
202
199
  durationMs: finishedAt.getTime() - startedAt.getTime(),
203
200
  log: logPath,
204
- target: { kind: "prompt", profile: undefined },
201
+ target: { kind: "command", cmd },
205
202
  detail: { exitCode },
206
203
  };
207
204
  appendHistory(result);
@@ -300,70 +297,70 @@ function renderWorkflowLog(input) {
300
297
  }
301
298
  // ── prompt target ───────────────────────────────────────────────────────────
302
299
  async function runPromptTask(input) {
303
- const { task, stashDir, logPath, startedAt, now, runAgentImpl, agentOptions } = input;
300
+ const { task, stashDir, logPath, startedAt, now, agentOptions } = input;
304
301
  if (task.target.kind !== "prompt")
305
302
  throw new Error("invariant: prompt target");
306
- // Use pre-resolved agent config when available to avoid redundant loadConfig()
307
- // calls in batch task runs (Fix C6). Fall back to loadConfig() for callers
308
- // that invoke runPromptTask directly without threading config.
309
- const fullConfig = loadConfig();
310
- const agentCfg = input.agentConfig !== undefined ? input.agentConfig : fullConfig;
311
- // Resolve the profile for this task. When the task doc specifies a profile,
312
- // use it directly. Otherwise fall back to the per-process config for "task"
313
- // (agent.processes["task"]), which itself falls back to agent.default.
314
- let profile;
315
- let processTimeoutMs;
316
- if (task.target.profile) {
317
- // v2: if profiles.agent is configured, resolve through new runner
318
- if (fullConfig.profiles?.agent) {
319
- const mode = task.target.mode ?? "agent";
320
- if (mode !== "llm") {
321
- const runnerSpec = resolveRunner(mode, task.target.profile, fullConfig);
322
- if (runnerSpec.kind === "agent" || runnerSpec.kind === "sdk") {
323
- profile = runnerSpec.profile;
324
- processTimeoutMs = runnerSpec.timeoutMs;
325
- }
326
- else {
327
- profile = requireAgentProfile(agentCfg, task.target.profile);
328
- }
329
- }
330
- else {
331
- profile = requireAgentProfile(agentCfg, task.target.profile);
332
- }
333
- }
334
- else {
335
- // v1: Task doc explicitly names a profile — honour it directly.
336
- profile = requireAgentProfile(agentCfg, task.target.profile);
337
- }
303
+ const promptTarget = task.target;
304
+ const config = loadConfig();
305
+ const engineName = promptTarget.engine ?? config.defaults?.engine;
306
+ if (!engineName)
307
+ throw new NotFoundError(`Task "${task.id}" has no selected engine.`, "ASSET_NOT_FOUND");
308
+ let runner = resolveEngine(engineName, config);
309
+ if (runner.kind === "llm") {
310
+ const resolved = resolveLlmEngineUse(config, [
311
+ {
312
+ engine: engineName,
313
+ ...(promptTarget.model !== undefined ? { model: promptTarget.model } : {}),
314
+ ...(promptTarget.timeoutMs !== undefined ? { timeoutMs: promptTarget.timeoutMs } : {}),
315
+ ...(promptTarget.llm !== undefined ? { llm: promptTarget.llm } : {}),
316
+ },
317
+ ]);
318
+ runner = {
319
+ kind: "llm",
320
+ engine: resolved.engine,
321
+ connection: resolved.connection,
322
+ ...(resolved.credential ? { credential: resolved.credential } : {}),
323
+ timeoutMs: resolved.timeoutMs,
324
+ };
338
325
  }
339
326
  else {
340
- // No per-task profile: use process config for "task" as a fallback.
341
- const resolved = resolveProcessAgentProfile("task", agentCfg);
342
- profile = resolved.profile;
343
- processTimeoutMs = resolved.timeoutMs;
327
+ if (promptTarget.llm !== undefined) {
328
+ throw new NotFoundError(`Task "${task.id}" uses llm overrides with non-LLM engine "${engineName}".`, "ASSET_NOT_FOUND");
329
+ }
330
+ const requestedModel = promptTarget.model;
331
+ const platform = runner.profile.platform;
332
+ if (!platform)
333
+ throw new Error(`Engine "${engineName}" resolved without a platform.`);
334
+ const model = requestedModel
335
+ ? resolveModel(requestedModel, platform, runner.profile.modelAliases, runner.profile.globalModelAliases)
336
+ : runner.profile.model;
337
+ runner = {
338
+ ...runner,
339
+ profile: { ...runner.profile, ...(model ? { model, modelIsExact: true } : {}) },
340
+ ...(promptTarget.timeoutMs !== undefined ? { timeoutMs: promptTarget.timeoutMs } : {}),
341
+ };
344
342
  }
345
- // Task-level timeoutMs (including null = disabled) wins over global config.
346
- // Resolution: task.timeoutMs → process entry timeoutMs → input.agentTimeoutMs → agentCfg.timeoutMs.
347
- const agentTimeoutMs = task.timeoutMs !== undefined
348
- ? task.timeoutMs
349
- : processTimeoutMs !== undefined
350
- ? processTimeoutMs
351
- : input.agentTimeoutMs !== undefined
352
- ? input.agentTimeoutMs
353
- : undefined;
354
343
  const promptText = await resolvePromptText(task, stashDir);
355
- const result = await runAgentImpl(profile, promptText, {
344
+ const result = await executeRunner(runner, promptText, {
356
345
  stdio: "captured",
357
- timeoutMs: agentTimeoutMs,
358
346
  cwd: stashDir,
359
347
  ...agentOptions,
360
348
  // Stamp task-runner provenance for any akm invocation the agent makes
361
349
  // (DRIFT-6: agent-task traffic must not be recorded as user demand).
362
350
  // Caller-supplied env still wins on conflicts.
363
351
  env: { AKM_EVENT_SOURCE: "task", ...agentOptions?.env },
352
+ }, {
353
+ ...(input.runAgentImpl ? { runAgent: input.runAgentImpl } : {}),
354
+ llm: async (spec, prompt, options) => {
355
+ const started = Date.now();
356
+ const stdout = await input.chatCompletionImpl(spec.connection, [{ role: "user", content: prompt }], {
357
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
358
+ });
359
+ return { ok: true, exitCode: 0, stdout, stderr: "", durationMs: Date.now() - started };
360
+ },
364
361
  });
365
362
  const finishedAt = now();
366
- const log = renderPromptLog({ task, profileName: profile.name, result });
363
+ const log = renderPromptLog({ task, engineName, result });
367
364
  persistRunLog({
368
365
  taskId: task.id,
369
366
  startedAtIso: startedAt.toISOString(),
@@ -380,7 +377,7 @@ async function runPromptTask(input) {
380
377
  finishedAt: finishedAt.toISOString(),
381
378
  durationMs: finishedAt.getTime() - startedAt.getTime(),
382
379
  log: logPath,
383
- target: { kind: "prompt", profile: profile.name },
380
+ target: { kind: "prompt", engine: engineName },
384
381
  detail: result.ok
385
382
  ? { exitCode: result.exitCode }
386
383
  : { reason: result.reason, error: result.error, exitCode: result.exitCode },
@@ -410,7 +407,7 @@ async function resolvePromptText(task, stashDir) {
410
407
  function renderPromptLog(input) {
411
408
  const lines = [];
412
409
  const dbLines = [];
413
- const header = `[akm tasks] task=${input.task.id} kind=prompt profile=${input.profileName}`;
410
+ const header = `[akm tasks] task=${input.task.id} kind=prompt engine=${input.engineName}`;
414
411
  const summary = `ok=${input.result.ok} exit_code=${input.result.exitCode ?? "null"} duration_ms=${input.result.durationMs}`;
415
412
  lines.push(header, summary);
416
413
  dbLines.push({ line: header }, { level: input.result.ok ? "info" : "error", line: summary });
@@ -481,9 +478,10 @@ function appendHistory(result) {
481
478
  target_kind: result.target.kind,
482
479
  target_ref: result.target.kind === "workflow" ? result.target.ref : null,
483
480
  metadata_json: JSON.stringify({
481
+ metadataVersion: 2,
484
482
  durationMs: result.durationMs,
485
483
  detail: result.detail ?? null,
486
- profile: result.target.kind === "prompt" ? result.target.profile : undefined,
484
+ ...(result.target.kind === "prompt" ? { engine: result.target.engine } : {}),
487
485
  }),
488
486
  });
489
487
  });
@@ -515,16 +513,14 @@ export function readTaskHistory(options = {}) {
515
513
  * that callers of `readTaskHistory()` expect.
516
514
  */
517
515
  function taskHistoryRowToResult(row) {
518
- let meta = {};
519
- try {
520
- meta = JSON.parse(row.metadata_json);
521
- }
522
- catch {
523
- // ignore corrupt JSON
524
- }
516
+ const meta = decodeTaskHistoryMetadata(row.metadata_json);
525
517
  const target = row.target_kind === "workflow"
526
518
  ? { kind: "workflow", ref: row.target_ref ?? "" }
527
- : { kind: "prompt", profile: meta.profile };
519
+ : row.target_kind === "command"
520
+ ? { kind: "command" }
521
+ : meta.metadataVersion === 2
522
+ ? { kind: "prompt", engine: meta.engine ?? null }
523
+ : { kind: "prompt", engine: null, ...(meta.legacyProfile ? { legacyProfile: meta.legacyProfile } : {}) };
528
524
  return {
529
525
  id: row.task_id,
530
526
  status: row.status,
@@ -533,7 +529,7 @@ function taskHistoryRowToResult(row) {
533
529
  durationMs: meta.durationMs ?? 0,
534
530
  log: row.log_path ?? "",
535
531
  target,
536
- ...(meta.detail !== undefined ? { detail: meta.detail } : {}),
532
+ ...(meta.detail !== undefined && meta.detail !== null ? { detail: meta.detail } : {}),
537
533
  };
538
534
  }
539
535
  /**
@@ -12,4 +12,4 @@
12
12
  * Tasks are stored as pure YAML files at `<stash>/tasks/<id>.yml`. Multi-line
13
13
  * inline prompts use a YAML block scalar (`prompt: |`).
14
14
  */
15
- export const TASK_SCHEMA_VERSION = 1;
15
+ export const TASK_SCHEMA_VERSION = 2;
@@ -8,7 +8,7 @@
8
8
  * • the schedule is parseable and translates to the active backend
9
9
  * • the workflow ref resolves (workflow targets)
10
10
  * • the asset/file source exists (prompt targets)
11
- * • the agent profile resolves (prompt targets)
11
+ * • the selected engine resolves (prompt targets)
12
12
  *
13
13
  * Validation is deliberately split from parsing: callers that only want to
14
14
  * read frontmatter (e.g. `tasks list`) can skip these checks, while
@@ -20,7 +20,7 @@ import { parseAssetRef } from "../core/asset/asset-ref.js";
20
20
  import { resolveStashDir } from "../core/common.js";
21
21
  import { loadConfig } from "../core/config/config.js";
22
22
  import { NotFoundError } from "../core/errors.js";
23
- import { requireAgentProfile } from "../integrations/agent/index.js";
23
+ import { resolveEngine } from "../integrations/agent/engine-resolution.js";
24
24
  import { resolveAssetPath } from "../sources/resolve.js";
25
25
  import { parseSchedule } from "./schedule.js";
26
26
  export async function validateTaskDocument(task, options) {
@@ -38,13 +38,18 @@ export async function validateTaskDocument(task, options) {
38
38
  if (task.target.kind !== "prompt") {
39
39
  return;
40
40
  }
41
- // Prompt target. Resolve the profile unconditionally — when no profile is
42
- // set on the task, requireAgentProfile falls back to defaults.agent and
43
- // throws a clear error if neither is configured. Catching this at
41
+ // Prompt target. Resolve the engine unconditionally — when no engine is
42
+ // set on the task, defaults.engine is required. Catching this at
44
43
  // `tasks add` / `tasks sync` time is much more useful than failing only
45
44
  // when the OS scheduler fires.
46
45
  const config = loadConfig();
47
- requireAgentProfile(config, task.target.profile);
46
+ const engine = task.target.engine ?? config.defaults?.engine;
47
+ if (!engine)
48
+ throw new NotFoundError(`Task "${task.id}" has no selected engine.`, "ASSET_NOT_FOUND");
49
+ const resolved = resolveEngine(engine, config);
50
+ if (task.target.llm !== undefined && resolved.kind !== "llm") {
51
+ throw new NotFoundError(`Task "${task.id}" uses llm overrides with non-LLM engine "${engine}".`, "ASSET_NOT_FOUND");
52
+ }
48
53
  const src = task.target.source;
49
54
  if (src.kind === "asset") {
50
55
  const stashDir = options.stashDir ?? resolveStashDir();
@@ -16,7 +16,7 @@
16
16
  import { readFile } from "node:fs/promises";
17
17
  import { fileURLToPath } from "node:url";
18
18
 
19
- const TEXT_EXTENSIONS = new Set([".md", ".xml", ".txt", ".sql"]);
19
+ const TEXT_EXTENSIONS = new Set([".md", ".xml", ".txt", ".sql", ".yaml", ".yml"]);
20
20
 
21
21
  function isTextImport(url, importAttributes) {
22
22
  if (importAttributes && importAttributes.type === "text") return true;
package/dist/wiki/wiki.js CHANGED
@@ -48,7 +48,7 @@ import ingestWorkflowTemplate from "../assets/wiki/ingest-workflow-template.md"
48
48
  import { akmSearch } from "../commands/read/search.js";
49
49
  import { parseFrontmatter, parseFrontmatterBlock } from "../core/asset/frontmatter.js";
50
50
  import { isWithin, todayIso } from "../core/common.js";
51
- import { getSources, loadUserConfig, saveConfig } from "../core/config/config.js";
51
+ import { getSources, mutateConfig } from "../core/config/config.js";
52
52
  import { NotFoundError, UsageError } from "../core/errors.js";
53
53
  import { resolveSourceEntries } from "../indexer/search/search-source.js";
54
54
  import { buildIndexMd, buildLogMd, buildSchemaMd } from "./wiki-templates.js";
@@ -443,13 +443,14 @@ export function removeWiki(stashDir, name, options = {}) {
443
443
  const external = registeredWikiSources(stashDir).find((source) => source.name === name);
444
444
  const isStashWiki = fs.existsSync(wikiDir) && isRecognizedStashWiki(wikiDir);
445
445
  if (!isStashWiki && external) {
446
- const config = loadUserConfig();
447
- const filteredSources = getSources(config).filter((entry) => entry.wikiName !== name);
448
- const installed = (config.installed ?? []).filter((entry) => entry.wikiName !== name);
449
- saveConfig({
450
- ...config,
451
- sources: filteredSources.length > 0 ? filteredSources : undefined,
452
- installed: installed.length > 0 ? installed : undefined,
446
+ mutateConfig((config) => {
447
+ const filteredSources = getSources(config).filter((entry) => entry.wikiName !== name);
448
+ const installed = (config.installed ?? []).filter((entry) => entry.wikiName !== name);
449
+ return {
450
+ ...config,
451
+ sources: filteredSources.length > 0 ? filteredSources : undefined,
452
+ installed: installed.length > 0 ? installed : undefined,
453
+ };
453
454
  });
454
455
  return {
455
456
  name,