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
@@ -17,7 +17,6 @@ import { loadConfig } from "../../core/config/config.js";
17
17
  import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
18
18
  import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
19
19
  import { commitWriteTargetBoundary, deleteAssetFromSource, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
20
- import { listAgentProfileNames } from "../../integrations/agent/index.js";
21
20
  import { resolveAssetPath } from "../../sources/resolve.js";
22
21
  import { backendNameForPlatform, selectBackend } from "../../tasks/backends/index.js";
23
22
  import { parseTaskDocument } from "../../tasks/parser.js";
@@ -25,7 +24,7 @@ import { resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
25
24
  import { exitCodeForStatus, readTaskHistory, runTask } from "../../tasks/runner.js";
26
25
  import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT, translateToCron } from "../../tasks/schedule.js";
27
26
  import { validateTaskDocument } from "../../tasks/validator.js";
28
- import { resolveImproveProfile } from "../improve/improve-profiles.js";
27
+ import { resolveImproveStrategy } from "../improve/improve-strategies.js";
29
28
  export async function akmTasksAdd(input) {
30
29
  const id = normaliseTaskId(input.id);
31
30
  const hasCommand = input.command !== undefined &&
@@ -36,6 +35,12 @@ export async function akmTasksAdd(input) {
36
35
  if (targetCount !== 1) {
37
36
  throw new UsageError("Pass exactly one of --workflow <ref>, --prompt <asset-ref|./file.md|text>, or --command <shell-command>.", "INVALID_FLAG_VALUE");
38
37
  }
38
+ if (input.workflow && (input.engine !== undefined || input.model !== undefined || input.timeoutMs !== undefined)) {
39
+ throw new UsageError("Workflow tasks accept only --params; engine, model, and timeout are prompt-task fields.", "INVALID_FLAG_VALUE");
40
+ }
41
+ if (hasCommand && (input.engine !== undefined || input.model !== undefined)) {
42
+ throw new UsageError("Command tasks accept --timeout-ms but not --engine or --model.", "INVALID_FLAG_VALUE");
43
+ }
39
44
  // Validate the schedule for the active backend before writing anything.
40
45
  const backend = backendNameForPlatform();
41
46
  parseSchedule(input.schedule, backend);
@@ -56,7 +61,9 @@ export async function akmTasksAdd(input) {
56
61
  workflow: input.workflow,
57
62
  prompt: input.prompt,
58
63
  command: input.command,
59
- profile: input.profile,
64
+ engine: input.engine,
65
+ model: input.model,
66
+ timeoutMs: input.timeoutMs,
60
67
  params: input.params,
61
68
  name: input.name,
62
69
  description: input.description,
@@ -137,11 +144,12 @@ export async function akmTasksList() {
137
144
  const stashDir = resolveStashDir();
138
145
  const typeRoot = path.join(stashDir, "tasks");
139
146
  if (!fs.existsSync(typeRoot))
140
- return { tasks: [] };
147
+ return { tasks: [], stale: [] };
141
148
  const entries = fs.readdirSync(typeRoot);
142
149
  warnLegacyMdTaskFiles(typeRoot);
143
150
  const files = entries.filter((f) => f.endsWith(".yml"));
144
151
  const tasks = [];
152
+ const stale = [];
145
153
  for (const file of files) {
146
154
  const id = file.slice(0, -4);
147
155
  const filePath = path.join(typeRoot, file);
@@ -149,7 +157,9 @@ export async function akmTasksList() {
149
157
  try {
150
158
  task = parseTaskDocument({ yaml: fs.readFileSync(filePath, "utf8"), filePath, id });
151
159
  }
152
- catch {
160
+ catch (err) {
161
+ if (isStaleTaskError(err))
162
+ stale.push(id);
153
163
  continue; // skip malformed files; `akm tasks show <id>` will surface the error
154
164
  }
155
165
  tasks.push({
@@ -165,7 +175,9 @@ export async function akmTasksList() {
165
175
  tags: task.tags,
166
176
  });
167
177
  }
168
- return { tasks };
178
+ if (stale.length > 0)
179
+ warnStaleTaskFiles(stale);
180
+ return { tasks, stale };
169
181
  }
170
182
  export async function akmTasksShow(id) {
171
183
  const normalised = normaliseTaskId(id);
@@ -234,6 +246,9 @@ export async function akmTasksSetEnabled(id, enabled) {
234
246
  warnLegacyMdTaskFiles(typeRoot);
235
247
  const filePath = await resolveAssetPath(stashDir, "task", normalised);
236
248
  const yaml = fs.readFileSync(filePath, "utf8");
249
+ // Parse before writing so stale v1 tasks are diagnosed without changing the
250
+ // source file or its installed scheduler entry.
251
+ parseTaskDocument({ yaml, filePath, id: normalised });
237
252
  const updated = setEnabledInYaml(yaml, enabled);
238
253
  const ref = taskAssetRef(normalised);
239
254
  await writeAssetToSource(target.source, target.config, ref, updated);
@@ -374,16 +389,15 @@ export async function akmTasksDoctor() {
374
389
  }
375
390
  const backend = backendNameForPlatform();
376
391
  const config = loadConfig();
377
- // v2: prefer profiles.agent / defaults.agent; fall back to legacy agent.default
378
- const defaultProfile = config.defaults?.agent;
379
- const profiles = config.profiles?.agent ? Object.keys(config.profiles.agent) : listAgentProfileNames(config);
392
+ const defaultEngine = config.defaults?.engine;
393
+ const engines = Object.keys(config.engines ?? {});
380
394
  // §6.1: surface the effective triage settings for the default improve
381
- // profile. The struct is a fixed shape, so this is a deliberate addition.
382
- const improveProfileName = typeof config.defaults?.improve === "string" ? config.defaults.improve : "default";
383
- const triage = resolveImproveProfile(config.defaults?.improve, config).processes?.triage;
395
+ // strategy. The struct is a fixed shape, so this is a deliberate addition.
396
+ const improveStrategyName = typeof config.defaults?.improveStrategy === "string" ? config.defaults.improveStrategy : "default";
397
+ const triage = resolveImproveStrategy(config.defaults?.improveStrategy, config).config.processes?.triage;
384
398
  const improveTriage = triage
385
399
  ? {
386
- defaultProfile: improveProfileName,
400
+ defaultStrategy: improveStrategyName,
387
401
  enabled: triage.enabled === true,
388
402
  applyMode: triage.applyMode ?? "queue",
389
403
  policy: triage.policy ?? "personal-stash",
@@ -394,7 +408,9 @@ export async function akmTasksDoctor() {
394
408
  akm: invocation,
395
409
  logDir: getTaskLogDir(),
396
410
  historyDir: getTaskHistoryDir(),
397
- agent: { defaultProfile, available: profiles },
411
+ engine: { defaultEngine, available: engines },
412
+ stale: collectStaleTaskIds(),
413
+ staleGeneratedCommands: collectStaleGeneratedCommands(),
398
414
  scheduleSubset: SCHEDULE_SUPPORTED_SUBSET_HINT,
399
415
  warnings,
400
416
  ...(improveTriage ? { improveTriage } : {}),
@@ -421,7 +437,7 @@ function resolveTaskWriteTarget() {
421
437
  return resolveWriteTarget(loadConfig());
422
438
  }
423
439
  function renderTaskYaml(input) {
424
- const obj = { schedule: input.schedule };
440
+ const obj = { version: 2, schedule: input.schedule, enabled: input.enabled };
425
441
  if (input.workflow) {
426
442
  obj.workflow = input.workflow;
427
443
  if (input.params) {
@@ -430,16 +446,21 @@ function renderTaskYaml(input) {
430
446
  }
431
447
  else if (input.prompt) {
432
448
  obj.prompt = input.prompt;
433
- if (input.profile)
434
- obj.profile = input.profile;
449
+ if (input.engine)
450
+ obj.engine = input.engine;
451
+ if (input.model)
452
+ obj.model = input.model;
453
+ if (input.timeoutMs !== undefined)
454
+ obj.timeoutMs = input.timeoutMs;
435
455
  }
436
456
  else if (input.command !== undefined) {
437
457
  // Emit a string when given a string, an array when given an array. The
438
458
  // parser accepts both forms; preserving the caller's shape keeps the YAML
439
459
  // ergonomic for humans editing the file later.
440
460
  obj.command = input.command;
461
+ if (input.timeoutMs !== undefined)
462
+ obj.timeoutMs = input.timeoutMs;
441
463
  }
442
- obj.enabled = input.enabled;
443
464
  if (input.name)
444
465
  obj.name = input.name;
445
466
  if (input.description)
@@ -450,6 +471,80 @@ function renderTaskYaml(input) {
450
471
  obj.tags = input.tags;
451
472
  return yamlStringify(obj);
452
473
  }
474
+ function isStaleTaskError(err) {
475
+ return err instanceof UsageError && err.code === "TASK_SCHEMA_VERSION_UNSUPPORTED";
476
+ }
477
+ function warnStaleTaskFiles(ids) {
478
+ process.stderr.write(`WARNING: ${ids.length} task file(s) use retired task schema v1 and were not loaded.\n` +
479
+ ` Rewrite them with version: 2 and replace profile with engine. See docs/migration/v0.8-to-v0.9.md#task-assets.\n` +
480
+ ` Affected: ${ids.map((id) => `tasks/${id}.yml`).join(", ")}\n`);
481
+ }
482
+ function collectStaleTaskIds() {
483
+ const typeRoot = path.join(resolveStashDir(), "tasks");
484
+ if (!fs.existsSync(typeRoot))
485
+ return [];
486
+ const stale = [];
487
+ for (const file of fs.readdirSync(typeRoot)) {
488
+ if (!file.endsWith(".yml"))
489
+ continue;
490
+ const id = file.slice(0, -4);
491
+ try {
492
+ parseTaskDocument({
493
+ yaml: fs.readFileSync(path.join(typeRoot, file), "utf8"),
494
+ filePath: path.join(typeRoot, file),
495
+ id,
496
+ });
497
+ }
498
+ catch (err) {
499
+ if (isStaleTaskError(err))
500
+ stale.push(id);
501
+ }
502
+ }
503
+ return stale;
504
+ }
505
+ const STALE_GENERATED_COMMANDS = {
506
+ "akm-improve-frequent": {
507
+ command: "akm improve --profile frequent --auto-accept safe",
508
+ replacement: "akm improve --strategy frequent --auto-accept safe",
509
+ },
510
+ "akm-improve-consolidate": {
511
+ command: "akm improve --profile consolidate --auto-accept safe",
512
+ replacement: "akm improve --strategy consolidate --auto-accept safe",
513
+ },
514
+ "akm-improve-nightly": {
515
+ command: "akm improve --profile thorough --auto-accept safe",
516
+ replacement: "akm improve --strategy thorough --auto-accept safe",
517
+ },
518
+ "akm-improve-catchup": {
519
+ command: "akm improve --profile catchup --auto-accept safe",
520
+ replacement: "akm improve --strategy catchup --auto-accept safe",
521
+ },
522
+ "akm-graph-refresh-weekly": {
523
+ command: "akm improve --profile graph-refresh --auto-accept safe",
524
+ replacement: "akm improve --strategy graph-refresh --auto-accept safe",
525
+ },
526
+ };
527
+ function collectStaleGeneratedCommands() {
528
+ const typeRoot = path.join(resolveStashDir(), "tasks");
529
+ if (!fs.existsSync(typeRoot))
530
+ return [];
531
+ const stale = [];
532
+ for (const [id, expected] of Object.entries(STALE_GENERATED_COMMANDS)) {
533
+ const filePath = path.join(typeRoot, `${id}.yml`);
534
+ if (!fs.existsSync(filePath))
535
+ continue;
536
+ try {
537
+ const task = parseTaskDocument({ yaml: fs.readFileSync(filePath, "utf8"), filePath, id });
538
+ if (task.target.kind === "command" && task.target.cmd.join(" ") === expected.command) {
539
+ stale.push({ id, replacement: expected.replacement });
540
+ }
541
+ }
542
+ catch {
543
+ // Stale-schema reporting owns files that cannot be parsed.
544
+ }
545
+ }
546
+ return stale;
547
+ }
453
548
  function parseJsonObjectArg(raw) {
454
549
  let parsed;
455
550
  try {
@@ -220,13 +220,13 @@ const wikiLintCommand = defineCommand({
220
220
  const wikiIngestCommand = defineJsonCommand({
221
221
  meta: {
222
222
  name: "ingest",
223
- description: "Dispatch an agent to execute the ingest workflow for this wiki. Uses --profile or config.defaults.agent.",
223
+ description: "Dispatch an agent to execute the ingest workflow for this wiki. Uses --engine or config.defaults.engine.",
224
224
  },
225
225
  args: {
226
226
  name: { type: "positional", description: "Wiki name", required: true },
227
- profile: {
227
+ engine: {
228
228
  type: "string",
229
- description: "Agent profile to use (default: config.defaults.agent).",
229
+ description: "Agent engine to use (default: config.defaults.engine).",
230
230
  },
231
231
  model: {
232
232
  type: "string",
@@ -239,17 +239,15 @@ const wikiIngestCommand = defineJsonCommand({
239
239
  const stashDir = resolveStashDir();
240
240
  const built = buildIngestWorkflow(stashDir, args.name);
241
241
  const config = loadConfig();
242
- const profileName = getStringArg(args, "profile") ?? config.defaults?.agent;
243
- if (!profileName) {
244
- throw new UsageError("akm wiki ingest requires an agent profile. Pass --profile <name> or set defaults.agent in config.", "MISSING_REQUIRED_ARGUMENT", "Available profiles are listed under profiles.agent in your config. Run `akm config get profiles.agent` to inspect.");
242
+ const engine = getStringArg(args, "engine") ?? config.defaults?.engine;
243
+ if (!engine) {
244
+ throw new UsageError("akm wiki ingest requires an agent engine. Pass --engine <name> or set defaults.engine in config.", "MISSING_REQUIRED_ARGUMENT", "Available engines are listed under engines in your config. Run `akm config get engines` to inspect.");
245
245
  }
246
246
  const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
247
247
  const model = getStringArg(args, "model");
248
- const { getDefaultLlmConfig } = await import("../core/config/config.js");
249
248
  const dispatchResult = await akmAgentDispatch({
250
- profileName,
249
+ engine,
251
250
  agentConfig: config,
252
- llmConfig: getDefaultLlmConfig(config),
253
251
  prompt: built.workflow,
254
252
  dispatch: {
255
253
  prompt: built.workflow,
@@ -258,11 +256,12 @@ const wikiIngestCommand = defineJsonCommand({
258
256
  ...(timeoutMs !== undefined && Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
259
257
  });
260
258
  output("wiki-ingest", {
259
+ schemaVersion: 2,
261
260
  wiki: built.wiki,
262
261
  path: built.path,
263
262
  schemaPath: built.schemaPath,
264
263
  dispatched: true,
265
- profile: profileName,
264
+ engine,
266
265
  agentResult: dispatchResult,
267
266
  });
268
267
  },
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import fs from "node:fs";
12
12
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
13
- import { assembleAsset } from "./asset-serialize.js";
13
+ import { assembleAsset, serializeFrontmatter } from "./asset-serialize.js";
14
14
  /**
15
15
  * Parse YAML frontmatter from a Markdown (or similar) string.
16
16
  *
@@ -118,6 +118,13 @@ function parseFrontmatterLenient(frontmatter) {
118
118
  * or `null` to skip the write entirely (e.g. for idempotent no-ops). The body
119
119
  * content is preserved from the parse.
120
120
  *
121
+ * A frontmatter mutation is a METADATA edit, not a content edit: when the file
122
+ * already has a frontmatter block, only that block is replaced and the body
123
+ * bytes are kept verbatim (routing through `assembleAsset` would strip the
124
+ * body's leading blank lines and force a trailing newline, silently reshaping
125
+ * assets whose writer used a different separator style). A file gaining its
126
+ * FIRST frontmatter block goes through the canonical `assembleAsset` shape.
127
+ *
121
128
  * @returns `true` if a write occurred, `false` if the mutator returned `null`.
122
129
  */
123
130
  export function mutateFrontmatter(filePath, mutator) {
@@ -126,7 +133,10 @@ export function mutateFrontmatter(filePath, mutator) {
126
133
  const nextFrontmatter = mutator(parsed);
127
134
  if (nextFrontmatter === null)
128
135
  return false;
129
- fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
136
+ const next = parsed.frontmatter !== null
137
+ ? `---\n${serializeFrontmatter(nextFrontmatter)}\n---\n${parsed.content}`
138
+ : assembleAsset(nextFrontmatter, parsed.content);
139
+ fs.writeFileSync(filePath, next, "utf8");
130
140
  return true;
131
141
  }
132
142
  export function parseFrontmatterBlock(raw) {
@@ -243,11 +243,12 @@ function normalizeFsPathForComparison(value) {
243
243
  */
244
244
  export async function fetchWithTimeout(url, opts, timeoutMs = 30_000, signal) {
245
245
  const controller = new AbortController();
246
- const timer = setTimeout(() => controller.abort(), timeoutMs);
246
+ const timer = timeoutMs === null ? undefined : setTimeout(() => controller.abort(), timeoutMs);
247
247
  const abortExternal = () => controller.abort(signal?.reason);
248
248
  if (signal) {
249
249
  if (signal.aborted) {
250
- clearTimeout(timer);
250
+ if (timer)
251
+ clearTimeout(timer);
251
252
  controller.abort(signal.reason);
252
253
  }
253
254
  else {
@@ -269,7 +270,8 @@ export async function fetchWithTimeout(url, opts, timeoutMs = 30_000, signal) {
269
270
  finally {
270
271
  if (signal)
271
272
  signal.removeEventListener("abort", abortExternal);
272
- clearTimeout(timer);
273
+ if (timer)
274
+ clearTimeout(timer);
273
275
  }
274
276
  }
275
277
  /**
@@ -17,7 +17,7 @@ import path from "node:path";
17
17
  import { sleepSync } from "../../runtime.js";
18
18
  import { writeFileAtomic } from "../common.js";
19
19
  import { ConfigError } from "../errors.js";
20
- import { probeLock, releaseLock, tryAcquireLockSync } from "../file-lock.js";
20
+ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "../file-lock.js";
21
21
  import { getCacheDir, getConfigDir } from "../paths.js";
22
22
  /**
23
23
  * Read the raw text of a config file. Returns `undefined` when the file does
@@ -80,7 +80,7 @@ export function writeConfigAtomic(configPath, config) {
80
80
  }
81
81
  /** Maximum number of timestamped config backups to retain (#459). */
82
82
  const MAX_CONFIG_BACKUPS = 5;
83
- export function backupExistingConfig(configPath) {
83
+ export function backupExistingConfig(configPath, now = new Date()) {
84
84
  if (!fs.existsSync(configPath))
85
85
  return undefined;
86
86
  const backupDir = path.join(getCacheDir(), "config-backups");
@@ -89,10 +89,22 @@ export function backupExistingConfig(configPath) {
89
89
  // the copy→chmod window. chmod again to tighten a dir from an older version.
90
90
  fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
91
91
  fs.chmodSync(backupDir, 0o700);
92
- const timestamp = new Date().toISOString().replace(/[.:]/g, "-");
93
- const timestamped = path.join(backupDir, `config-${timestamp}.json`);
92
+ const timestamp = now.toISOString().replace(/[.:]/g, "-");
93
+ let sequence = 0;
94
+ let timestamped;
95
+ while (true) {
96
+ timestamped = path.join(backupDir, `config-${timestamp}${sequence === 0 ? "" : `-${sequence}`}.json`);
97
+ try {
98
+ fs.copyFileSync(configPath, timestamped, fs.constants.COPYFILE_EXCL);
99
+ break;
100
+ }
101
+ catch (error) {
102
+ if (error.code !== "EEXIST")
103
+ throw error;
104
+ sequence++;
105
+ }
106
+ }
94
107
  const latest = path.join(backupDir, "config.latest.json");
95
- fs.copyFileSync(configPath, timestamped);
96
108
  fs.copyFileSync(configPath, latest);
97
109
  // 08-F4: a config backup carries the same sensitive fields as the live config
98
110
  // (endpoints, tokens). `copyFileSync` inherits the source's (often 0644) mode,
@@ -141,7 +153,7 @@ function pruneOldBackups(backupDir) {
141
153
  * predictable for debugging. Uses $CONFIG (not $DATA) because config.json
142
154
  * itself lives in $CONFIG — they should fail together if the dir is read-only.
143
155
  */
144
- function getConfigLockPath() {
156
+ export function getConfigLockPath() {
145
157
  return path.join(getConfigDir(), "config.json.lck");
146
158
  }
147
159
  const CONFIG_LOCK_MAX_RETRIES = 10;
@@ -157,8 +169,8 @@ function sleepSyncMs(ms) {
157
169
  /**
158
170
  * Acquire an exclusive sentinel around config writes.
159
171
  *
160
- * Returns a release function. Best-effort: when all retries are exhausted the
161
- * write proceeds unlocked rather than erroring (same posture as lockfile.ts).
172
+ * Returns a release function. Acquisition is fail-closed: config mutation may
173
+ * never continue without owning the lock that protects its read/merge/write.
162
174
  */
163
175
  export function acquireConfigLock() {
164
176
  const lockPath = getConfigLockPath();
@@ -170,16 +182,16 @@ export function acquireConfigLock() {
170
182
  }
171
183
  for (let attempt = 0; attempt < CONFIG_LOCK_MAX_RETRIES; attempt++) {
172
184
  try {
173
- if (tryAcquireLockSync(lockPath, String(process.pid))) {
174
- return () => releaseLock(lockPath);
185
+ const ownership = tryAcquireLockSync(lockPath, createLockPayload());
186
+ if (ownership) {
187
+ return () => releaseLock(ownership);
175
188
  }
176
189
  }
177
- catch {
178
- // Non-EEXIST error (permissions, etc.) bail out and proceed unlocked.
179
- break;
190
+ catch (error) {
191
+ throw new ConfigError(`Unable to acquire config lock at ${lockPath}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
180
192
  }
181
- if (probeLock(lockPath).state === "stale") {
182
- releaseLock(lockPath);
193
+ const probe = probeLock(lockPath);
194
+ if (probe.state === "stale" && reclaimStaleLock(lockPath, probe)) {
183
195
  continue; // Reclaimed — retry immediately.
184
196
  }
185
197
  if (attempt < CONFIG_LOCK_MAX_RETRIES - 1) {
@@ -195,8 +207,7 @@ export function acquireConfigLock() {
195
207
  sleepSyncMs(CONFIG_LOCK_RETRY_DELAY_MS);
196
208
  }
197
209
  }
198
- // Best-effort: proceed without lock.
199
- return () => { };
210
+ throw new ConfigError(`Timed out waiting for config lock at ${lockPath}. Another AKM process may be updating config.`, "INVALID_CONFIG_FILE");
200
211
  }
201
212
  /**
202
213
  * Run `fn` inside the config write lock. Always releases the lock.