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
@@ -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,
@@ -15,7 +15,7 @@
15
15
  * never touch a real platform.
16
16
  * - Bounded LLM call wrapped by {@link tryLlmFeature} under the
17
17
  * `session_extraction` gate (default-on; opt out via
18
- * `profiles.improve.default.processes.extract.enabled: false`).
18
+ * `improve.strategies.<name>.processes.extract.enabled: false`).
19
19
  * - Proposals routed via `createProposal({ source: "extract", ... })` — the
20
20
  * same review queue as reflect / distill / consolidate. Never direct-write.
21
21
  * - Per-candidate body assembly merges description (+ when_to_use for lessons)
@@ -27,16 +27,18 @@ import fs from "node:fs";
27
27
  import path from "node:path";
28
28
  import { assembleAsset } from "../../core/asset/asset-serialize.js";
29
29
  import { resolveStashDir, timestampForFilename } from "../../core/common.js";
30
- import { getDefaultLlmConfig, getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
30
+ import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
31
31
  import { ConfigError, UsageError } from "../../core/errors.js";
32
32
  import { appendEvent } from "../../core/events.js";
33
- import { probeLock, releaseLock, tryAcquireLockSync } from "../../core/file-lock.js";
33
+ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
34
+ import { tryAcquireMaintenanceBarrier } from "../../core/maintenance-barrier.js";
34
35
  import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
35
36
  import { getStateDbPath, openStateDatabase, withStateDb } from "../../core/state-db.js";
36
37
  import { repairTruncatedDescription } from "../../core/text-truncation.js";
37
38
  import { warn } from "../../core/warn.js";
38
39
  import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
39
- import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
40
+ import { resolveLlmEngineUse } from "../../integrations/agent/engine-resolution.js";
41
+ import { materializeLlmRunnerConnection, resolveImproveProcessRunner, } from "../../integrations/agent/runner.js";
40
42
  import { normalizeHarnessId } from "../../integrations/harnesses/index.js";
41
43
  import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
42
44
  import { preFilterSession } from "../../integrations/session-logs/pre-filter.js";
@@ -48,7 +50,7 @@ import { getExtractedSessionsMap, getLastExtractRunAt, shouldSkipAlreadyExtracte
48
50
  import { createProposal, isProposalSkipped } from "../proposal/repository.js";
49
51
  import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload } from "./extract-prompt.js";
50
52
  import { buildHotProbationFrontmatter } from "./hot-probation.js";
51
- import { resolveProcessEnabled } from "./improve-profiles.js";
53
+ import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
52
54
  import { applySchemaSimilarityPenalty, loadDerivedLayerEmbeddings, } from "./schema-similarity-gate.js";
53
55
  import { buildSessionSummaryPrompt, parseSessionSummary, SESSION_SUMMARY_JSON_SCHEMA, sessionMeetsDurationGate, writeSessionAsset, } from "./session-asset.js";
54
56
  import { resolveTriageConfig, scoreSessionTriage } from "./triage.js";
@@ -123,20 +125,75 @@ function getExtractSessionLockPath(harness, sessionId, stateDbPath) {
123
125
  * extraction outright.
124
126
  */
125
127
  function acquireExtractSessionLock(lockPath) {
128
+ const releaseBarrier = tryAcquireMaintenanceBarrier();
129
+ if (!releaseBarrier)
130
+ return { proceed: false };
126
131
  try {
127
132
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
128
- if (tryAcquireLockSync(lockPath, String(process.pid)))
129
- return true;
133
+ let ownership = tryAcquireLockSync(lockPath, createLockPayload());
134
+ if (ownership)
135
+ return { proceed: true, ownership };
130
136
  const probe = probeLock(lockPath, { staleAfterMs: EXTRACT_SESSION_LOCK_STALE_MS });
131
137
  if (probe.state === "held")
132
- return false;
133
- // absent (released between attempt + probe) or stale → reclaim and retry once.
134
- releaseLock(lockPath);
135
- return tryAcquireLockSync(lockPath, String(process.pid));
138
+ return { proceed: false };
139
+ // Absent (released between attempt + probe) or successfully reclaimed stale lock → retry once.
140
+ if (probe.state === "stale" && !reclaimStaleLock(lockPath, probe))
141
+ return { proceed: false };
142
+ ownership = tryAcquireLockSync(lockPath, createLockPayload());
143
+ return ownership ? { proceed: true, ownership } : { proceed: false };
136
144
  }
137
145
  catch {
138
- return true;
146
+ return { proceed: true };
139
147
  }
148
+ finally {
149
+ releaseBarrier();
150
+ }
151
+ }
152
+ function cloneAndFreeze(value) {
153
+ const clone = structuredClone(value);
154
+ const freeze = (item) => {
155
+ if (typeof item !== "object" || item === null || Object.isFrozen(item))
156
+ return;
157
+ for (const child of Object.values(item))
158
+ freeze(child);
159
+ Object.freeze(item);
160
+ };
161
+ freeze(clone);
162
+ return clone;
163
+ }
164
+ /** Resolve standalone extract selection once before discovery, auto iteration, or watch startup. */
165
+ export function resolveStandaloneExtractPlan(config, selection) {
166
+ if (selection.engine && selection.strategy) {
167
+ throw new UsageError("--engine and --strategy are mutually exclusive. Pick one.", "INVALID_FLAG_VALUE");
168
+ }
169
+ const selected = resolveImproveStrategy(selection.strategy, config);
170
+ const process = cloneAndFreeze(getImproveProcessConfig(config, "extract", selected.config) ?? {});
171
+ const invocation = {
172
+ ...(selection.engine ? { engine: selection.engine } : {}),
173
+ ...(Object.hasOwn(selection, "timeoutMs") ? { timeoutMs: selection.timeoutMs ?? null } : {}),
174
+ };
175
+ const resolved = resolveLlmEngineUse(config, [selected.config, process, invocation], { optional: true });
176
+ if (!resolved) {
177
+ throw new ConfigError("No LLM engine configured for extract. Set defaults.llmEngine, pass --engine, or select an improve strategy with processes.extract.engine.", "LLM_NOT_CONFIGURED");
178
+ }
179
+ const runner = {
180
+ kind: "llm",
181
+ engine: resolved.engine,
182
+ connection: resolved.connection,
183
+ ...(resolved.credential ? { credential: resolved.credential } : {}),
184
+ timeoutMs: resolved.timeoutMs,
185
+ };
186
+ return Object.freeze({
187
+ strategy: selected.name,
188
+ engine: resolved.engine,
189
+ // `akm extract` is an explicit operation. The strategy supplies behavior,
190
+ // but its improve-stage enablement gate does not disable this command.
191
+ enabled: true,
192
+ process,
193
+ runner: cloneAndFreeze(runner),
194
+ timeoutMs: resolved.timeoutMs,
195
+ embeddingConfig: cloneAndFreeze(config.embedding),
196
+ });
140
197
  }
141
198
  // ── Helpers ──────────────────────────────────────────────────────────────────
142
199
  /**
@@ -245,11 +302,11 @@ export function hashSessionContent(data) {
245
302
  * proposal validation failure) the session result records a warning and
246
303
  * keeps going — one session's bad luck never aborts a multi-session run.
247
304
  */
248
- async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars,
305
+ async function processSession(harness, sessionRef, stashDir, config, getLlmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars,
249
306
  // #626 — pre-LLM heuristic triage gate. Default-off (enabled:false) takes the
250
307
  // exact pre-change path (no scorer call, no new skipReason).
251
308
  // #641 — proceduralAwareFloor is opt-in, DEFAULT OFF.
252
- triage, sessionIndexing, schemaSimilarityCtx,
309
+ triage, sessionIndexing, schemaSimilarityCtx, hotProbationEnabled,
253
310
  // #602 — already-extracted skip moved INSIDE processSession: the content hash
254
311
  // can only be computed after readSession, so the skip decision lives here. The
255
312
  // prior row + bypass flags are threaded in from the caller. Skipping here still
@@ -390,7 +447,7 @@ standardsContext) {
390
447
  };
391
448
  let llmRaw = "";
392
449
  const llmResult = await tryLlmFeature("session_extraction", config, async () => {
393
- llmRaw = await chat(llmConfig, [{ role: "user", content: prompt }], {
450
+ llmRaw = await chat(getLlmConfig(), [{ role: "user", content: prompt }], {
394
451
  timeoutMs,
395
452
  responseSchema: EXTRACT_JSON_SCHEMA,
396
453
  });
@@ -451,12 +508,7 @@ standardsContext) {
451
508
  // When enabled, system-generated extractions enter captureMode: hot-probation
452
509
  // so they spend ONE consolidation cycle in probation before the deterministic
453
510
  // dedup+quality pass promotes them. Default OFF.
454
- // Reads the `default` profile deliberately: this per-session hot-probation
455
- // flag lives inside the 18-arg `processSession`, which has no handle to the
456
- // active profile, and threading a 19th positional arg for a DEFAULT-OFF flag
457
- // isn't worth the param bloat. The extract STAGE toggle (in `akmExtract`,
458
- // below) already honors `--profile`.
459
- const hotProbationEnabled = getImproveProcessConfig(config, "extract")?.hotProbation?.enabled === true;
511
+ // The invocation boundary resolves this from the frozen extract process.
460
512
  for (const candidate of payload.candidates) {
461
513
  if (dryRun) {
462
514
  proposalIds.push(`dry-run:${candidate.type}:${candidate.name}`);
@@ -555,20 +607,19 @@ export async function akmExtract(options) {
555
607
  const stashDir = options.stashDir ?? resolveStashDir();
556
608
  const dryRun = options.dryRun ?? false;
557
609
  const sourceRun = options.sourceRun ?? `extract-${timestampForFilename()}`;
558
- // Read the per-process extract config + enabled-state from the ACTIVE improve
559
- // profile when `akmImprove` threaded it; otherwise fall back to the `default`
560
- // profile (the explicit `akm extract` path, which has no active profile). This
561
- // is what stops a non-default profile's `extract.enabled` from being silently
562
- // overridden by the default profile and vice-versa.
563
- const activeProfile = options.improveProfile;
564
- const extractProcess = activeProfile ? activeProfile.processes?.extract : getImproveProcessConfig(config, "extract");
610
+ // Read process behavior from the frozen standalone plan or the active improve
611
+ // strategy. This prevents config changes during watch mode from changing later
612
+ // triggers and prevents one improve strategy from overriding another.
613
+ const activeProfile = options.improveProfile ?? (options.resolvedPlan ? undefined : resolveImproveStrategy(undefined, config).config);
614
+ const extractProcess = options.resolvedPlan?.process ?? getImproveProcessConfig(config, "extract", activeProfile);
565
615
  // The `extract.enabled` process toggle gates extract as a STAGE of `akm improve`
566
616
  // (the activeProfile path) — consistent with #593/#594 where the active profile,
567
617
  // not `default`, is the source of truth. An EXPLICIT `akm extract` invocation
568
618
  // (no activeProfile) is a direct user/cron action and always runs; gating it on
569
619
  // the default improve profile's stage toggle was a footgun — dropping extract
570
620
  // from the daily improve profile would silently disable the standalone command.
571
- const extractEnabled = activeProfile ? resolveProcessEnabled("extract", activeProfile) : true;
621
+ const extractEnabled = options.resolvedPlan?.enabled ??
622
+ (options.improveProfile ? resolveProcessEnabled("extract", options.improveProfile) : true);
572
623
  // Feature-gate early so we get a clean "skipped because disabled" envelope.
573
624
  if (!extractEnabled) {
574
625
  return {
@@ -582,35 +633,29 @@ export async function akmExtract(options) {
582
633
  candidatesCreated: 0,
583
634
  proposals: [],
584
635
  sessions: [],
585
- warnings: [
586
- "session_extraction feature disabled — set profiles.improve.default.processes.extract.enabled: true to use",
587
- ],
636
+ warnings: ["extract is disabled by the selected improve strategy"],
588
637
  durationMs: Date.now() - startMs,
589
638
  };
590
639
  }
591
- // Resolve the LLM connection. Priority order:
592
- // 1. Options.config.profiles.improve.default.processes.extract.profile
593
- // (per-process override, matches reflect/distill/consolidate)
594
- // 2. config.defaults.llm (the default LLM profile)
595
- // 3. throw extract requires an LLM.
596
- let llmConfig;
597
- const runnerSpec = resolveImproveProcessRunnerFromProfile(extractProcess, config);
598
- if (runnerSpec) {
599
- if (!runnerIsLlm(runnerSpec)) {
600
- throw new ConfigError(`Extract only supports mode: "llm" (in-tree LLM call). Got mode: "${runnerSpec.kind}" from profiles.improve.default.processes.extract — change it to "llm" or remove the override.`, "INVALID_CONFIG_FILE");
601
- }
602
- llmConfig = runnerSpec.connection;
603
- }
604
- else {
605
- llmConfig = getDefaultLlmConfig(config) ?? undefined;
606
- }
607
- if (!llmConfig) {
608
- throw new ConfigError("No LLM connection configured for extract. Set profiles.llm + defaults.llm, or set profiles.improve.default.processes.extract.profile to a configured LLM profile.");
640
+ // Improve supplies its invocation-owned connection. Standalone extract
641
+ // resolves the selected process engine, then defaults.llmEngine.
642
+ const runnerSpec = options.resolvedPlan
643
+ ? options.resolvedPlan.runner
644
+ : resolveImproveProcessRunner(activeProfile, "extract", config);
645
+ const fixedLlmConfig = options.resolvedPlan ? undefined : options.llmConfig;
646
+ if (!runnerSpec && !fixedLlmConfig) {
647
+ throw new ConfigError("No LLM engine configured for extract. Set defaults.llmEngine or improve.strategies.<name>.processes.extract.engine.", "LLM_NOT_CONFIGURED");
609
648
  }
610
- // Honor per-process timeoutMs override; fall back to options.timeoutMs; then 60s.
611
- const timeoutMs = options.timeoutMs ??
612
- (typeof extractProcess?.timeoutMs === "number" ? extractProcess.timeoutMs : undefined) ??
613
- 60_000;
649
+ const timeoutMs = options.resolvedPlan
650
+ ? options.resolvedPlan.timeoutMs
651
+ : Object.hasOwn(options, "timeoutMs")
652
+ ? (options.timeoutMs ?? null)
653
+ : runnerSpec?.timeoutMs !== undefined
654
+ ? runnerSpec.timeoutMs
655
+ : fixedLlmConfig && Object.hasOwn(fixedLlmConfig, "timeoutMs")
656
+ ? (fixedLlmConfig.timeoutMs ?? null)
657
+ : 600_000;
658
+ const getLlmConfig = () => runnerSpec ? materializeLlmRunnerConnection(runnerSpec) : fixedLlmConfig;
614
659
  // Pre-filter budget — process config can raise it for large-context models.
615
660
  const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
616
661
  // #595/#596 — minimum raw session size; sessions below it skip the LLM call
@@ -643,7 +688,7 @@ export async function akmExtract(options) {
643
688
  const defaultSessionSummaryGenerator = async (data) => {
644
689
  let raw = "";
645
690
  await tryLlmFeature("session_extraction", config, async () => {
646
- raw = await chatForSummary(llmConfig, [{ role: "user", content: buildSessionSummaryPrompt(data) }], {
691
+ raw = await chatForSummary(getLlmConfig(), [{ role: "user", content: buildSessionSummaryPrompt(data) }], {
647
692
  timeoutMs,
648
693
  responseSchema: SESSION_SUMMARY_JSON_SCHEMA,
649
694
  });
@@ -768,13 +813,14 @@ export async function akmExtract(options) {
768
813
  // processes.extract.schemaSimilarity.enabled: false. The gate is inert in
769
814
  // practice when no derived-layer embeddings exist (empty ctx → no penalty).
770
815
  const schemaSimilarityCfg = extractProcess?.schemaSimilarity;
816
+ const hotProbationEnabled = extractProcess?.hotProbation?.enabled === true;
771
817
  let schemaSimilarityCtx = null;
772
818
  if (schemaSimilarityCfg?.enabled !== false) {
773
819
  const derivedEmbeddings = options.schemaSimilarityEmbeddings ?? loadDerivedLayerEmbeddings();
774
820
  schemaSimilarityCtx = {
775
821
  config: { ...schemaSimilarityCfg, enabled: true },
776
822
  derivedEmbeddings,
777
- embeddingConfig: config.embedding,
823
+ embeddingConfig: options.resolvedPlan?.embeddingConfig ?? config.embedding,
778
824
  embedFn: options.schemaSimilarityEmbedFn,
779
825
  };
780
826
  }
@@ -802,10 +848,11 @@ export async function akmExtract(options) {
802
848
  // runs (those that open their own state.db): dry-run is read-only, an
803
849
  // injected `stateDb` handle is an in-process/test scenario with no cross-
804
850
  // process race, and skip-tracking-off opts out entirely.
805
- let sessionLockPath;
851
+ let sessionLockOwnership;
806
852
  if (trackingEnabled && !dryRun && !options.stateDb) {
807
- sessionLockPath = getExtractSessionLockPath(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath());
808
- if (!acquireExtractSessionLock(sessionLockPath)) {
853
+ const sessionLockPath = getExtractSessionLockPath(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath());
854
+ const sessionLock = acquireExtractSessionLock(sessionLockPath);
855
+ if (!sessionLock.proceed) {
809
856
  sessions.push({
810
857
  sessionId: summary.sessionId,
811
858
  harness: harness.name,
@@ -819,9 +866,10 @@ export async function akmExtract(options) {
819
866
  skippedCount += 1;
820
867
  continue;
821
868
  }
869
+ sessionLockOwnership = sessionLock.ownership;
822
870
  }
823
871
  try {
824
- const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, schemaSimilarityCtx, prior, options.force === true, extractStandardsContext);
872
+ const result = await processSession(harness, summary, stashDir, config, getLlmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, schemaSimilarityCtx, hotProbationEnabled, prior, options.force === true, extractStandardsContext);
825
873
  sessions.push(result);
826
874
  // #626 — triage aggregation. A session reached the triage gate only when it
827
875
  // was NOT already preempted by an earlier skip (read_failed / too_short /
@@ -916,8 +964,8 @@ export async function akmExtract(options) {
916
964
  skippedCount += 1;
917
965
  }
918
966
  finally {
919
- if (sessionLockPath)
920
- releaseLock(sessionLockPath);
967
+ if (sessionLockOwnership)
968
+ releaseLock(sessionLockOwnership);
921
969
  }
922
970
  }
923
971
  // Close the state.db connection we opened. Callers that injected stateDb