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,12 +29,13 @@ import { openStateDatabase, withStateDb } from "../../core/state-db.js";
29
29
  import { warn } from "../../core/warn.js";
30
30
  import { commitWriteTargetBoundary, deleteAssetFromSource, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
31
31
  import { closeDatabase, findEntryIdByRef, getAllEntries, getEntryById, getNeighborsByEntryId, openExistingDatabase, } from "../../indexer/db/db.js";
32
- import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
32
+ import { materializeLlmRunnerConnection, resolveImproveProcessRunner } from "../../integrations/agent/runner.js";
33
33
  import { chatCompletion } from "../../llm/client.js";
34
34
  import { cosineSimilarity, embedBatch, resolveEmbeddingModelId } from "../../llm/embedder.js";
35
- import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
35
+ import { tryLlmFeature } from "../../llm/feature-gate.js";
36
36
  import { getConsolidationJudgedMap, upsertConsolidationJudged, } from "../../storage/repositories/consolidation-repository.js";
37
37
  import { getBodyEmbeddings, upsertBodyEmbeddings } from "../../storage/repositories/embeddings-repository.js";
38
+ import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
38
39
  // Chunk sizing + per-chunk prompt assembly live in ./consolidate/chunking.
39
40
  // Imported for internal use by the orchestrator and re-exported for importers.
40
41
  import { buildChunkPrompt, computeSafeChunkSize, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
@@ -493,13 +494,13 @@ function archiveMemory(filePath, stashDir, ref, reason, opIndex, supersededBy, w
493
494
  *
494
495
  * Priority order (mirrors extract / reflect / distill — see
495
496
  * `src/commands/extract.ts:421-438` and the canonical
496
- * `resolveImproveProcessRunnerFromProfile` pattern):
497
+ * `resolveImproveProcessRunner` pattern):
497
498
  *
498
- * 1. `profiles.improve.default.processes.consolidate.profile` (or `mode`)
499
- * via {@link resolveImproveProcessRunnerFromProfile}. Lets the user pin
499
+ * 1. `improve.strategies.<name>.processes.consolidate.engine`
500
+ * via {@link resolveImproveProcessRunner}. Lets the user pin
500
501
  * a dedicated model (e.g. `ministral-3b`) for consolidation instead of
501
- * whatever `defaults.llm` happens to be.
502
- * 2. `getDefaultLlmConfig(config)` — the baseline default LLM profile.
502
+ * whatever `defaults.llmEngine` happens to be.
503
+ * 2. `getDefaultLlmConfig(config)` — the baseline default LLM engine.
503
504
  *
504
505
  * Regression guard (2026-05-26): before this resolver, `akmConsolidate`
505
506
  * called `getDefaultLlmConfig` directly and silently ignored a configured
@@ -509,13 +510,9 @@ function archiveMemory(filePath, stashDir, ref, reason, opIndex, supersededBy, w
509
510
  * `/tmp/akm-health-investigations/consolidation-no-op.md`.
510
511
  */
511
512
  function resolveConsolidateLlmConfig(config, activeProfile) {
512
- const consolidateProcess = getImproveProcessConfig(config, "consolidate", activeProfile);
513
- const runnerSpec = resolveImproveProcessRunnerFromProfile(consolidateProcess, config);
514
- if (runnerSpec && runnerIsLlm(runnerSpec)) {
515
- return runnerSpec.connection;
516
- }
517
- // Non-LLM runner modes (agent/sdk) don't apply to consolidate's HTTP path;
518
- // fall back to the default LLM profile rather than disabling the pass.
513
+ const runnerSpec = resolveImproveProcessRunner(activeProfile, "consolidate", config);
514
+ if (runnerSpec)
515
+ return materializeLlmRunnerConnection(runnerSpec);
519
516
  return getDefaultLlmConfig(config);
520
517
  }
521
518
  // ── Judged-state cache (#581) ────────────────────────────────────────────────
@@ -578,8 +575,9 @@ export async function akmConsolidate(opts = {}) {
578
575
  // standalone `akm consolidate` gets a self-contained token.
579
576
  const sourceRun = opts.sourceRun ?? `consolidate-${startMs}`;
580
577
  const config = opts.config ?? loadConfig();
578
+ opts = { ...opts, improveProfile: opts.improveProfile ?? resolveImproveStrategy(undefined, config).config };
581
579
  const stashDir = opts.stashDir ?? resolveStashDir();
582
- if (!isLlmFeatureEnabled(config, "memory_consolidation")) {
580
+ if (!resolveProcessEnabled("consolidate", opts.improveProfile ?? resolveImproveStrategy(undefined, config).config)) {
583
581
  return makeConsolidateResult({
584
582
  dryRun: opts.dryRun ?? false,
585
583
  target: opts.target ?? stashDir,
@@ -868,9 +866,11 @@ async function planConsolidation(opts, config, stashDir, startMs, memories, warn
868
866
  // CLI. The agent CLI is for interactive agent sessions (reflect, propose);
869
867
  // structured JSON generation works better and faster via HTTP.
870
868
  //
871
- // Honor `profiles.improve.default.processes.consolidate.profile` first; fall
872
- // back to the default LLM. See {@link resolveConsolidateLlmConfig}.
873
- const llmConfig = resolveConsolidateLlmConfig(config, opts.improveProfile);
869
+ // Improve supplies a frozen connection; standalone consolidate resolves its
870
+ // selected strategy/default engine here.
871
+ const llmConfig = Object.hasOwn(opts, "llmConfig")
872
+ ? (opts.llmConfig ?? undefined)
873
+ : resolveConsolidateLlmConfig(config, opts.improveProfile);
874
874
  const isHttpPath = !!llmConfig;
875
875
  // Chunk sizing: derive a safe chunk size from the configured model context
876
876
  // window so that the full prompt (system prompt + chunk user prompt) never
@@ -1087,7 +1087,7 @@ async function planConsolidation(opts, config, stashDir, startMs, memories, warn
1087
1087
  catch (e) {
1088
1088
  return { ok: false, error: String(e) };
1089
1089
  }
1090
- }, { ok: false, error: fallbackError });
1090
+ }, { ok: false, error: fallbackError }, { enabled: true });
1091
1091
  let raw = await callChunkLlm(`chunk ${chunkIdx + 1} failed`);
1092
1092
  if (!raw.ok) {
1093
1093
  // Single retry with 2s backoff before recording chunk as lost.
@@ -1230,7 +1230,7 @@ async function planConsolidation(opts, config, stashDir, startMs, memories, warn
1230
1230
  * the op-handlers' `pushSkipReason`. Behavior-identical to the former inlined
1231
1231
  * write block. Never invoked on the dry-run or aborted-confirm paths.
1232
1232
  */
1233
- async function applyConsolidationPlan(config, stashDir, sourceRun, memories, warnings, allOps, accounting, dedupCollapsed, activeProfile) {
1233
+ async function applyConsolidationPlan(config, stashDir, sourceRun, memories, warnings, allOps, accounting, dedupCollapsed, activeProfile, llmConfig) {
1234
1234
  // -- Phase B + writes -------------------------------------------------------
1235
1235
  const target = resolveWriteTarget(config);
1236
1236
  const timestamp = timestampForFilename();
@@ -1258,6 +1258,7 @@ async function applyConsolidationPlan(config, stashDir, sourceRun, memories, war
1258
1258
  const opCtx = {
1259
1259
  config,
1260
1260
  improveProfile: activeProfile,
1261
+ llmConfig: llmConfig ?? null,
1261
1262
  stashDir,
1262
1263
  sourceRun,
1263
1264
  target,
@@ -1380,7 +1381,9 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, w
1380
1381
  }
1381
1382
  }
1382
1383
  // -- Pass 3: execute the plan against the filesystem ------------------------
1383
- const { merged, deleted, contradicted, mergeFloorViolations, mergedSecondaries, promoted } = await applyConsolidationPlan(config, stashDir, sourceRun, memories, warnings, allOps, accounting, dedupCollapsed, opts.improveProfile);
1384
+ const { merged, deleted, contradicted, mergeFloorViolations, mergedSecondaries, promoted } = await applyConsolidationPlan(config, stashDir, sourceRun, memories, warnings, allOps, accounting, dedupCollapsed, opts.improveProfile, Object.hasOwn(opts, "llmConfig")
1385
+ ? (opts.llmConfig ?? null)
1386
+ : (resolveConsolidateLlmConfig(config, opts.improveProfile) ?? null));
1384
1387
  const runDurationMs = Date.now() - startMs;
1385
1388
  const budgetFraction = opts.runBudgetMs !== undefined && opts.runBudgetMs > 0 ? runDurationMs / opts.runBudgetMs : undefined;
1386
1389
  return {
@@ -1502,7 +1505,7 @@ export async function handleMergeOp(op, opIndex, ctx) {
1502
1505
  emitMergeFailureSkips("merge_read_failed");
1503
1506
  return;
1504
1507
  }
1505
- const mergeResult = await generateMergedContent(config, op.primary, primaryBody, op.secondaries, memoryByRef, ctx.improveProfile);
1508
+ const mergeResult = await generateMergedContent(config, op.primary, primaryBody, op.secondaries, memoryByRef, ctx.improveProfile, ctx.llmConfig);
1506
1509
  if ("error" in mergeResult) {
1507
1510
  warnings.push(`Merge: ${mergeResult.error} for ${mergeResult.detail}.`);
1508
1511
  emitMergeFailureSkips(mergeResult.error);
@@ -2147,7 +2150,7 @@ function loadMemoriesForSource(source, stashDir, warnings) {
2147
2150
  }
2148
2151
  return memories;
2149
2152
  }
2150
- async function generateMergedContent(config, primaryRef, primaryBody, secondaryRefs, memoryByRef, activeProfile) {
2153
+ async function generateMergedContent(config, primaryRef, primaryBody, secondaryRefs, memoryByRef, activeProfile, resolvedLlmConfig) {
2151
2154
  // Only handle single-secondary merges per design (one call per merge op)
2152
2155
  const secRef = secondaryRefs[0];
2153
2156
  const secEntry = memoryByRef.get(secRef);
@@ -2191,7 +2194,7 @@ async function generateMergedContent(config, primaryRef, primaryBody, secondaryR
2191
2194
  .join("\n");
2192
2195
  // Use the same per-process profile resolution as the chunk-plan call above
2193
2196
  // so the merge generation step doesn't silently revert to the default LLM.
2194
- const llmConfig = resolveConsolidateLlmConfig(config, activeProfile);
2197
+ const llmConfig = resolvedLlmConfig === null ? undefined : (resolvedLlmConfig ?? resolveConsolidateLlmConfig(config, activeProfile));
2195
2198
  const result = await tryLlmFeature("memory_consolidation", config, async () => {
2196
2199
  if (!llmConfig)
2197
2200
  return { ok: false, error: "No LLM configured for consolidation" };
@@ -2204,7 +2207,7 @@ async function generateMergedContent(config, primaryRef, primaryBody, secondaryR
2204
2207
  catch (e) {
2205
2208
  return { ok: false, error: String(e) };
2206
2209
  }
2207
- }, { ok: false, error: `merge content generation failed for ${primaryRef}` });
2210
+ }, { ok: false, error: `merge content generation failed for ${primaryRef}` }, { enabled: true });
2208
2211
  if (!result.ok) {
2209
2212
  return {
2210
2213
  error: "merge_transport_failed",
@@ -18,11 +18,8 @@
18
18
  */
19
19
  import fs from "node:fs";
20
20
  import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
21
- import { getDefaultLlmConfig } from "../../../core/config/config.js";
22
- import { ConfigError } from "../../../core/errors.js";
23
21
  import { appendEvent } from "../../../core/events.js";
24
22
  import { parseEmbeddedJsonResponse } from "../../../llm/client.js";
25
- import { isLlmFeatureEnabled } from "../../../llm/feature-gate.js";
26
23
  import { createProposal, isProposalSkipped } from "../../proposal/repository.js";
27
24
  import { assessMemoryKnowledgePromotionCandidate } from "../distill-promotion-policy.js";
28
25
  import { persistOutputEncodingSalience, runLessonQualityJudge, writeQualityRejection } from "./quality-gate.js";
@@ -62,7 +59,7 @@ export async function promoteMemoryToKnowledge(ctx) {
62
59
  }
63
60
  })()
64
61
  : null;
65
- if (existingKnowledgeContent && config && getDefaultLlmConfig(config)) {
62
+ if (existingKnowledgeContent && ctx.llmConfig) {
66
63
  // Existing content found: call LLM for contradiction-resolution merge.
67
64
  const mergePrompt = [
68
65
  "You are merging two versions of a knowledge document.",
@@ -81,11 +78,7 @@ export async function promoteMemoryToKnowledge(ctx) {
81
78
  "```",
82
79
  ].join("\n");
83
80
  try {
84
- const mergeLlm = getDefaultLlmConfig(config);
85
- if (!mergeLlm) {
86
- throw new ConfigError("LLM is not configured for distillation merge.", "LLM_NOT_CONFIGURED");
87
- }
88
- const mergeResponse = await chat(mergeLlm, [
81
+ const mergeResponse = await chat(ctx.llmConfig, [
89
82
  { role: "system", content: "Return only valid JSON. No prose." },
90
83
  { role: "user", content: mergePrompt },
91
84
  ]);
@@ -122,7 +115,7 @@ export async function promoteMemoryToKnowledge(ctx) {
122
115
  // The reviewer will see both versions in the proposal diff.
123
116
  }
124
117
  }
125
- else if (existingKnowledgeContent && config && !getDefaultLlmConfig(config)) {
118
+ else if (existingKnowledgeContent) {
126
119
  // No LLM configured: include existing content as context in the proposal
127
120
  // so the reviewer can do the contradiction resolution manually.
128
121
  resolvedPromotionContent = [
@@ -141,10 +134,10 @@ export async function promoteMemoryToKnowledge(ctx) {
141
134
  // D-5 / #388: Three-band system — review_needed band queues to proposal
142
135
  // queue with review_needed outcome rather than auto-rejecting.
143
136
  let knowledgeJudgeConfidence;
144
- if (isLlmFeatureEnabled(config, "lesson_quality_gate")) {
137
+ if (ctx.strategy?.processes?.distill?.qualityGate?.enabled ?? true) {
145
138
  // D-4 / #390: retrieve top-3 similar lessons for dedup check in judge.
146
139
  const similarLessons = await fetchSimilarLessonsFn(resolvedPromotionContent.slice(0, 500), 3);
147
- const judgeResult = await runLessonQualityJudge(config, resolvedPromotionContent, assetContent ?? "", chat, similarLessons.length > 0 ? similarLessons : undefined);
140
+ const judgeResult = await runLessonQualityJudge(config, resolvedPromotionContent, assetContent ?? "", chat, similarLessons.length > 0 ? similarLessons : undefined, ctx.llmConfig);
148
141
  if (!judgeResult.pass) {
149
142
  if (judgeResult.reviewNeeded) {
150
143
  // Uncertainty band (2.5–3.5): queue as review_needed instead of rejecting.
@@ -102,10 +102,8 @@ export function buildJudgePrompt(lessonContent, sourceContent, similarLessons) {
102
102
  * Run the LLM-as-judge quality gate on a proposal's content.
103
103
  *
104
104
  * Exported so reflect.ts can apply the same gate to reflect proposals (R-5 / #374).
105
- * Gated by the flag name `lesson_quality_gate` (or its alias
106
- * `proposal_quality_gate`) via {@link isLlmFeatureEnabled} which reads
107
- * `profiles.improve.default.processes.distill.qualityGate.enabled` (and the
108
- * corresponding `.reflect.qualityGate.enabled` for proposals).
105
+ * The selected strategy's distill/reflect quality-gate setting is resolved by
106
+ * the caller before this function runs.
109
107
  *
110
108
  * Fail-CLOSED (07 P0-2): returns `pass: false` (score -1) on timeout, parse
111
109
  * failure, or missing LLM. Minted content that cannot be judged is rejected,
@@ -114,12 +112,12 @@ export function buildJudgePrompt(lessonContent, sourceContent, similarLessons) {
114
112
  */
115
113
  export async function runLessonQualityJudge(config, lessonContent, sourceContent, chat,
116
114
  /** D-4 / #390: top-3 similar existing lessons for dedup check. */
117
- similarLessons) {
118
- const llmConfig = getDefaultLlmConfig(config);
115
+ similarLessons, llmConfigOverride) {
116
+ const llmConfig = llmConfigOverride ?? getDefaultLlmConfig(config);
119
117
  if (!llmConfig) {
120
118
  return { pass: false, score: -1, reason: "no LLM configured — cannot judge, failing closed" };
121
119
  }
122
- const judgeLlmConfig = llmConfig.judgeModel ? { ...llmConfig, model: llmConfig.judgeModel } : llmConfig;
120
+ const judgeLlmConfig = llmConfig;
123
121
  const JUDGE_TIMEOUT_MS = 8_000;
124
122
  try {
125
123
  const raw = await Promise.race([
@@ -69,8 +69,9 @@ import { withStateDb } from "../../core/state-db.js";
69
69
  import { warnVerbose } from "../../core/warn.js";
70
70
  import { closeDatabase, getAllEntries, openIndexDatabase } from "../../indexer/db/db.js";
71
71
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
72
+ import { materializeLlmRunnerConnection, resolveImproveProcessRunner } from "../../integrations/agent/runner.js";
72
73
  import { chatCompletion, parseEmbeddedJsonResponse } from "../../llm/client.js";
73
- import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
74
+ import { tryLlmFeature } from "../../llm/feature-gate.js";
74
75
  import { createProposal, isProposalSkipped, listProposals, } from "../proposal/repository.js";
75
76
  import { stripFrontmatterBody as stripBodyForFidelity } from "./dedup.js";
76
77
  import { autoRepairLessonFrontmatter, autoSwapDescriptionWhenToUse, collectLessonQualityFindings, repairLessonDescriptionTruncation, } from "./distill/content-repair.js";
@@ -79,6 +80,7 @@ import { fetchTopSimilarLessons, persistOutputEncodingSalience, runLessonQuality
79
80
  import { buildClsContext, checkDistillFidelity } from "./distill-guards.js";
80
81
  import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
81
82
  import { buildRefVocabulary, scoreEncodingSalience } from "./encoding-salience.js";
83
+ import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
82
84
  import { computeSalience, upsertAssetSalience } from "./salience.js";
83
85
  // Re-exported for `reflect.ts`, which applies the same LLM-as-judge gate to
84
86
  // reflect proposals (R-5 / #374).
@@ -474,8 +476,15 @@ export async function akmDistill(options) {
474
476
  };
475
477
  }
476
478
  const config = options.config ?? loadConfig();
479
+ options = { ...options, improveProfile: options.improveProfile ?? resolveImproveStrategy(undefined, config).config };
477
480
  const stash = options.stashDir ?? resolveStashDir();
478
481
  const chat = options.chat ?? chatCompletion;
482
+ const distillLlm = Object.hasOwn(options, "llmConfig")
483
+ ? (options.llmConfig ?? undefined)
484
+ : (() => {
485
+ const runner = resolveImproveProcessRunner(options.improveProfile, "distill", config);
486
+ return runner ? materializeLlmRunnerConnection(runner) : getDefaultLlmConfig(config);
487
+ })();
479
488
  const lookup = options.lookupFn ?? defaultLookup;
480
489
  const readEventsImpl = options.readEventsFn ?? readEvents;
481
490
  // R1 opt-out must flow into every computeSalience call this command makes so
@@ -602,6 +611,8 @@ export async function akmDistill(options) {
602
611
  assetContent,
603
612
  filteredEvents,
604
613
  config,
614
+ strategy: options.improveProfile,
615
+ llmConfig: distillLlm,
605
616
  chat,
606
617
  stash,
607
618
  lookup,
@@ -684,12 +695,11 @@ export async function akmDistill(options) {
684
695
  const distillSchema = effectiveProposalKind === "knowledge" ? DISTILL_KNOWLEDGE_JSON_SCHEMA : DISTILL_LESSON_JSON_SCHEMA;
685
696
  let fallbackReason;
686
697
  const raw = await tryLlmFeature("distill", config, async () => {
687
- const distillLlm = getDefaultLlmConfig(config);
688
698
  if (!distillLlm) {
689
699
  // No LLM connection configured — treat as gate-disabled. Throwing
690
700
  // here lets `tryLlmFeature` route us through the "error" fallback,
691
701
  // which is the same graceful skipped path.
692
- throw new ConfigError("No LLM connection configured. Set `defaults.llm` and a profile under `profiles.llm`.", "LLM_NOT_CONFIGURED");
702
+ throw new ConfigError("No LLM engine configured. Set defaults.llmEngine or improve.strategies.<name>.processes.distill.engine.", "LLM_NOT_CONFIGURED");
693
703
  }
694
704
  // Production path: pass the JSON schema so providers that honour
695
705
  // `response_format: json_schema` enforce shape upstream. Providers that
@@ -701,6 +711,7 @@ export async function akmDistill(options) {
701
711
  // functions (which return markdown strings) continue to work.
702
712
  return chat(distillLlm, messages);
703
713
  }, null, {
714
+ enabled: resolveProcessEnabled("distill", options.improveProfile ?? resolveImproveStrategy(undefined, config).config),
704
715
  onFallback: (evt) => {
705
716
  fallbackReason = evt.reason;
706
717
  // Log the fallback reason; the caller (raw === null path) handles
@@ -822,10 +833,10 @@ export async function akmDistill(options) {
822
833
  // D-5 / #388: Three-band system — review_needed band queues a proposal
823
834
  // with review_needed outcome rather than auto-rejecting.
824
835
  let lessonJudgeConfidence;
825
- if (isLlmFeatureEnabled(config, "lesson_quality_gate")) {
836
+ if (options.improveProfile?.processes?.distill?.qualityGate?.enabled ?? true) {
826
837
  // D-4 / #390: retrieve top-3 similar lessons for dedup check in judge.
827
838
  const similarLessons = await fetchSimilarLessonsFn(content.slice(0, 500), 3);
828
- const judgeResult = await runLessonQualityJudge(config, content, assetContent ?? "", chat, similarLessons.length > 0 ? similarLessons : undefined);
839
+ const judgeResult = await runLessonQualityJudge(config, content, assetContent ?? "", chat, similarLessons.length > 0 ? similarLessons : undefined, distillLlm);
829
840
  if (!judgeResult.pass) {
830
841
  if (judgeResult.reviewNeeded) {
831
842
  return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, {
@@ -8,11 +8,11 @@ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
8
8
  import { isAssetType } from "../../core/common.js";
9
9
  import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
10
10
  import { readEvents } from "../../core/events.js";
11
- import { closeDatabase, getAllEntries, getUtilityScoresByIds, openExistingDatabase } from "../../indexer/db/db.js";
11
+ import { closeDatabase, getAllEntries, getUtilityScoresByIds, openExistingDatabase, openReadonlyExistingDatabase, } from "../../indexer/db/db.js";
12
12
  import { getWritableStashDirs, resolveSourceEntries } from "../../indexer/search/search-source.js";
13
13
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
14
14
  import { isDistillRefusedInputType } from "./distill.js";
15
- import { isProfileFilteredForAllPasses } from "./improve-profiles.js";
15
+ import { isStrategyFilteredForAllPasses } from "./improve-strategies.js";
16
16
  // Eligibility / candidate-selection predicates for improve. Free functions
17
17
  // (no akmImprove closure state) extracted from improve.ts to shrink the
18
18
  // orchestrator and make candidate selection independently testable.
@@ -49,6 +49,13 @@ export function dedupeRefs(refs) {
49
49
  return out;
50
50
  }
51
51
  export async function collectEligibleRefs(scope, stashDir, improveProfile) {
52
+ return collectEligibleRefsFromIndex(scope, stashDir, improveProfile, false);
53
+ }
54
+ /** Dry-run planner path: query an existing index without creating or mutating it. */
55
+ export async function collectEligibleRefsReadOnly(scope, stashDir, improveProfile) {
56
+ return collectEligibleRefsFromIndex(scope, stashDir, improveProfile, true);
57
+ }
58
+ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, readOnly) {
52
59
  if (scope.mode === "ref" && scope.value) {
53
60
  const parsed = parseAssetRef(scope.value);
54
61
  const writableDirs = new Set(getWritableStashDirs(stashDir).map((dir) => path.resolve(dir)));
@@ -57,7 +64,7 @@ export async function collectEligibleRefs(scope, stashDir, improveProfile) {
57
64
  return {
58
65
  plannedRefs: [],
59
66
  memorySummary: { eligible: 0, derived: 0 },
60
- profileFilteredRefs: [],
67
+ strategyFilteredRefs: [],
61
68
  };
62
69
  }
63
70
  return {
@@ -66,7 +73,7 @@ export async function collectEligibleRefs(scope, stashDir, improveProfile) {
66
73
  eligible: parsed.type === "memory" ? 1 : 0,
67
74
  derived: parsed.type === "memory" && parsed.name.endsWith(".derived") ? 1 : 0,
68
75
  },
69
- profileFilteredRefs: [],
76
+ strategyFilteredRefs: [],
70
77
  };
71
78
  }
72
79
  let sources;
@@ -74,10 +81,10 @@ export async function collectEligibleRefs(scope, stashDir, improveProfile) {
74
81
  sources = resolveSourceEntries(stashDir);
75
82
  }
76
83
  catch {
77
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, profileFilteredRefs: [] };
84
+ return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
78
85
  }
79
86
  if (sources.length === 0) {
80
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, profileFilteredRefs: [] };
87
+ return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
81
88
  }
82
89
  // Only operate on writable sources — never mutate read-only registry caches
83
90
  // or remote stashes that the user did not mark writable.
@@ -91,7 +98,10 @@ export async function collectEligibleRefs(scope, stashDir, improveProfile) {
91
98
  const writableDirSet = new Set(writableDirs.map((d) => path.resolve(d)));
92
99
  let db;
93
100
  try {
94
- db = openExistingDatabase();
101
+ db = readOnly ? openReadonlyExistingDatabase() : openExistingDatabase();
102
+ if (!db) {
103
+ return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
104
+ }
95
105
  const entries = getAllEntries(db, scope.mode === "type" ? scope.value : undefined).filter((indexed) => {
96
106
  // First apply the existing stashDir-scope filter (no-op when stashDir is unset).
97
107
  if (!isEntryInScope(indexed.stashDir, indexed.filePath, stashDir))
@@ -117,14 +127,14 @@ export async function collectEligibleRefs(scope, stashDir, improveProfile) {
117
127
  // 2026-05-27: extend the .derived precedent to profile-incompatible
118
128
  // refs. If every per-ref pass (reflect + distill) on the active
119
129
  // profile would refuse this ref, drop it from `plannedRefs`. The
120
- // caller emits `improve_skipped { reason: profile_filtered_all_passes }`
130
+ // caller emits `improve_skipped { reason: strategy_filtered_all_passes }`
121
131
  // once `eventsCtx` is available so the audit trail is preserved in a
122
132
  // single event per ref instead of 2× synthetic actions per run.
123
133
  // Background: see /tmp/akm-health-investigations/planner-profile-metrics-deep-analysis.md
124
- if (improveProfile && isProfileFilteredForAllPasses(ref, improveProfile)) {
134
+ if (improveProfile && isStrategyFilteredForAllPasses(ref, improveProfile)) {
125
135
  profileFiltered.set(ref, {
126
136
  ref,
127
- reason: "profile_filtered_all_passes",
137
+ reason: "strategy_filtered_all_passes",
128
138
  filePath: indexed.filePath,
129
139
  });
130
140
  }
@@ -145,14 +155,14 @@ export async function collectEligibleRefs(scope, stashDir, improveProfile) {
145
155
  return {
146
156
  plannedRefs: [...planned.values()],
147
157
  memorySummary: { eligible: memoryEligible, derived: memoryDerived },
148
- profileFilteredRefs: [...profileFiltered.values()],
158
+ strategyFilteredRefs: [...profileFiltered.values()],
149
159
  };
150
160
  }
151
161
  catch (error) {
152
162
  // The bun-test isolation guard must never be downgraded to "empty plan".
153
163
  rethrowIfTestIsolationError(error);
154
164
  if (error instanceof NotFoundError || error instanceof Error) {
155
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, profileFilteredRefs: [] };
165
+ return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
156
166
  }
157
167
  throw error;
158
168
  }
@@ -18,9 +18,10 @@ import fs from "node:fs";
18
18
  import path from "node:path";
19
19
  import { getStringArg } from "../../cli/parse-args.js";
20
20
  import { defineJsonCommand, EXIT_CODES, output } from "../../cli/shared.js";
21
+ import { loadConfig } from "../../core/config/config.js";
21
22
  import { UsageError } from "../../core/errors.js";
22
23
  import { getAvailableHarnesses, getWatchTargets } from "../../integrations/session-logs/index.js";
23
- import { akmExtract } from "./extract.js";
24
+ import { akmExtract, resolveStandaloneExtractPlan, } from "./extract.js";
24
25
  import { akmExtractWatch } from "./extract-watch.js";
25
26
  export const extractCommand = defineJsonCommand({
26
27
  meta: {
@@ -61,7 +62,15 @@ export const extractCommand = defineJsonCommand({
61
62
  },
62
63
  "timeout-ms": {
63
64
  type: "string",
64
- description: "Per-session LLM timeout in ms (default 60000).",
65
+ description: "Per-session LLM timeout in ms (default 600000).",
66
+ },
67
+ engine: {
68
+ type: "string",
69
+ description: "Named LLM engine for this invocation. Mutually exclusive with --strategy.",
70
+ },
71
+ strategy: {
72
+ type: "string",
73
+ description: "Improve strategy supplying extract behavior and engine. Mutually exclusive with --engine.",
65
74
  },
66
75
  watch: {
67
76
  type: "boolean",
@@ -81,12 +90,17 @@ export const extractCommand = defineJsonCommand({
81
90
  const auto = args.auto === true;
82
91
  const dryRun = args["dry-run"] === true;
83
92
  const force = args.force === true;
93
+ const engine = getStringArg(args, "engine");
94
+ const strategy = getStringArg(args, "strategy");
84
95
  const timeoutMs = typeof args["timeout-ms"] === "string" && args["timeout-ms"] !== ""
85
96
  ? Number.parseInt(args["timeout-ms"], 10)
86
97
  : undefined;
87
98
  if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
88
99
  throw new UsageError(`--timeout-ms must be a positive integer (got "${args["timeout-ms"]}").`, "INVALID_FLAG_VALUE");
89
100
  }
101
+ if (engine && strategy) {
102
+ throw new UsageError("--engine and --strategy are mutually exclusive. Pick one.", "INVALID_FLAG_VALUE");
103
+ }
90
104
  const watch = args.watch === true;
91
105
  const debounceMs = typeof args["debounce-ms"] === "string" && args["debounce-ms"] !== ""
92
106
  ? Number.parseInt(args["debounce-ms"], 10)
@@ -94,24 +108,31 @@ export const extractCommand = defineJsonCommand({
94
108
  if (watch && (!Number.isFinite(debounceMs) || debounceMs <= 0)) {
95
109
  throw new UsageError(`--debounce-ms must be a positive integer (got "${args["debounce-ms"]}").`, "INVALID_FLAG_VALUE");
96
110
  }
97
- if (watch) {
98
- await runWatchMode({ debounceMs, dryRun, force, ...(since ? { since } : {}) });
99
- return;
100
- }
101
- if (auto && type) {
111
+ if (!watch && auto && type) {
102
112
  throw new UsageError("--auto and --type are mutually exclusive. Pick one.", "INVALID_FLAG_VALUE");
103
113
  }
104
- if (!auto && !type) {
114
+ if (!watch && !auto && !type) {
105
115
  throw new UsageError("--type is required (or pass --auto to try every available harness).", "MISSING_REQUIRED_ARGUMENT");
106
116
  }
107
- const commonOptions = {
117
+ const config = loadConfig();
118
+ const resolvedPlan = resolveStandaloneExtractPlan(config, {
119
+ ...(engine ? { engine } : {}),
120
+ ...(strategy ? { strategy } : {}),
121
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
122
+ });
123
+ if (watch) {
124
+ await runWatchMode({ debounceMs, dryRun, force, config, resolvedPlan, ...(since ? { since } : {}) });
125
+ return;
126
+ }
127
+ const commonOptions = Object.freeze({
108
128
  ...(sessionId ? { sessionId } : {}),
109
129
  ...(location ? { location } : {}),
110
130
  ...(since ? { since } : {}),
111
131
  dryRun,
112
132
  force,
113
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
114
- };
133
+ config,
134
+ resolvedPlan,
135
+ });
115
136
  if (auto) {
116
137
  const harnesses = getAvailableHarnesses();
117
138
  if (harnesses.length === 0) {
@@ -196,6 +217,13 @@ function createFsWatchEventSource(roots) {
196
217
  },
197
218
  };
198
219
  }
220
+ /** Snapshot the CLI-resolved watch options once and reuse them for every debounced trigger. */
221
+ export function createExtractWatchTrigger(options, extractFn = akmExtract) {
222
+ const snapshot = Object.freeze({ ...options });
223
+ return async (harnessName) => {
224
+ await extractFn({ type: harnessName, ...snapshot });
225
+ };
226
+ }
199
227
  /**
200
228
  * Run `akm extract --watch`: watch every available harness's session-log
201
229
  * roots and run extract (debounced, per-harness) on change. Stays alive until
@@ -216,18 +244,18 @@ async function runWatchMode(opts) {
216
244
  }
217
245
  const allRoots = targets.flatMap((t) => t.roots);
218
246
  const eventSource = createFsWatchEventSource(allRoots);
247
+ const onTrigger = createExtractWatchTrigger({
248
+ dryRun: opts.dryRun,
249
+ force: opts.force,
250
+ config: opts.config,
251
+ resolvedPlan: opts.resolvedPlan,
252
+ ...(opts.since ? { since: opts.since } : {}),
253
+ });
219
254
  const handle = akmExtractWatch({
220
255
  roots: targets,
221
256
  eventSource,
222
257
  debounceMs: opts.debounceMs,
223
- onTrigger: async (harnessName) => {
224
- await akmExtract({
225
- type: harnessName,
226
- dryRun: opts.dryRun,
227
- force: opts.force,
228
- ...(opts.since ? { since: opts.since } : {}),
229
- });
230
- },
258
+ onTrigger,
231
259
  });
232
260
  output("extract", {
233
261
  schemaVersion: 1,