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
@@ -10,13 +10,15 @@ import { rethrowIfTestIsolationError } from "../../core/errors.js";
10
10
  import { appendEvent, readEvents } from "../../core/events.js";
11
11
  import { classifyImproveAction, foldDistillSkipped } from "../../core/improve-types.js";
12
12
  import { getDbPath, getStateDbPathInDataDir } from "../../core/paths.js";
13
+ import { redactSensitiveText } from "../../core/redaction.js";
13
14
  import { openStateDatabase } from "../../core/state-db.js";
14
15
  import { info, warn } from "../../core/warn.js";
15
16
  import { closeDatabase, getEntryCount, openExistingDatabase } from "../../indexer/db/db.js";
16
17
  import { ensureIndex } from "../../indexer/ensure-index.js";
17
18
  import { akmIndex } from "../../indexer/indexer.js";
18
19
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
19
- import { resolveTriageJudgmentRunner } from "../../integrations/agent/runner.js";
20
+ import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
21
+ import { materializeLlmRunnerConnection } from "../../integrations/agent/runner.js";
20
22
  import { installLlmUsagePersistence } from "../../llm/usage-persist.js";
21
23
  import { withLlmStage } from "../../llm/usage-telemetry.js";
22
24
  import { isGitBackedStash, resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
@@ -24,9 +26,9 @@ import { drainProposals } from "../proposal/drain.js";
24
26
  import { resolveDrainPolicy } from "../proposal/drain-policies.js";
25
27
  import { akmDistill } from "./distill.js";
26
28
  // Eligibility / candidate-selection predicates live in ./eligibility.
27
- import { buildLatestProposalTsMap, collectEligibleRefs, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
29
+ import { buildLatestProposalTsMap, collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
28
30
  import { countEvalCases } from "./eval-cases.js";
29
- import { resolveImproveProfile, resolveProcessEnabled } from "./improve-profiles.js";
31
+ import { resolveImprovePlan } from "./improve-strategies.js";
30
32
  // #607 per-process lock primitives live in ./locks. Imported for internal use;
31
33
  // resetHeldProcessLocks is re-exported (the test seam imports it from here).
32
34
  import { PROCESS_LOCK_DEFS, processLockPath, releaseAllProcessLocks, releaseHeldLocksIfOwned, releaseProcessLock, tryAcquireProcessLock, withOptionalProcessLock, } from "./locks.js";
@@ -91,19 +93,28 @@ export async function akmImprove(options = {}) {
91
93
  const reindexFn = options.reindexFn ?? akmIndex;
92
94
  const drainProposalsFn = options.drainProposalsFn ?? drainProposals;
93
95
  // #616 multi-cycle test seams. Default to the real module-local fns.
94
- const collectEligibleRefsImpl = options.collectEligibleRefsFn ?? collectEligibleRefs;
96
+ const collectEligibleRefsImpl = options.collectEligibleRefsFn ?? (options.dryRun ? collectEligibleRefsReadOnly : collectEligibleRefs);
95
97
  const runImprovePreparationStageImpl = options.runImprovePreparationStageFn ?? runImprovePreparationStage;
96
98
  const runImproveLoopStageImpl = options.runImproveLoopStageFn ?? runImproveLoopStage;
97
99
  const runImprovePostLoopStageImpl = options.runImprovePostLoopStageFn ?? runImprovePostLoopStage;
98
100
  // Resolve the improve profile for this run. Profile drives type filtering,
99
101
  // process gating, and default autoAccept/limit values.
100
102
  const _earlyConfig = options.config ?? loadConfig();
101
- const improveProfile = resolveImproveProfile(options.profile, _earlyConfig);
103
+ const resolvedPlan = options.resolvedPlan ??
104
+ resolveImprovePlan(options.strategy, _earlyConfig, {
105
+ repairValidationFailures: options.repairValidationFailures,
106
+ });
107
+ const selectedStrategy = resolvedPlan.strategy;
108
+ const improveSensitiveValues = collectEngineCredentialValues(_earlyConfig);
109
+ const improveProfile = selectedStrategy.config;
102
110
  // Apply profile defaults — CLI flags take precedence over profile defaults.
103
111
  // Rebuild options with effective values so all downstream stage functions
104
112
  // automatically pick up the profile-driven defaults.
105
113
  options = {
106
114
  ...options,
115
+ // Pin nested calls and quality gates to the same config snapshot as the
116
+ // invocation plan. They must never reload a changed config mid-run.
117
+ config: _earlyConfig,
107
118
  autoAccept: options.autoAccept ?? improveProfile.autoAccept,
108
119
  // Profile-level limit, then process-level reflect.limit as fallback.
109
120
  // CLI --limit takes precedence over both.
@@ -161,7 +172,7 @@ export async function akmImprove(options = {}) {
161
172
  // (cycle 1, in the first try) always overwrites them before any read.
162
173
  let plannedRefs = [];
163
174
  let memorySummary = { eligible: 0, derived: 0 };
164
- let profileFilteredRefs = [];
175
+ let strategyFilteredRefs = [];
165
176
  let memoryCleanupPlan;
166
177
  let guidance;
167
178
  let triageDrain;
@@ -170,7 +181,7 @@ export async function akmImprove(options = {}) {
170
181
  // first try) and is re-run at the top of each subsequent multi-cycle cycle.
171
182
  // Re-running ensureIndex between cycles makes cycle N's gate-promoted
172
183
  // proposals visible to cycle N+1's collectEligibleRefs. Mutates the
173
- // outer-scope plannedRefs/memorySummary/profileFilteredRefs/memoryCleanupPlan/
184
+ // outer-scope plannedRefs/memorySummary/strategyFilteredRefs/memoryCleanupPlan/
174
185
  // guidance so for maxCycles:1 the body is byte-identical to pre-#616.
175
186
  const runIndexAndCollect = async () => {
176
187
  // #339 fix: ensureIndex MUST run BEFORE collectEligibleRefs. The eligible-ref
@@ -178,7 +189,7 @@ export async function akmImprove(options = {}) {
178
189
  // table (or the index is otherwise empty), the prior run order silently
179
190
  // returned plannedRefs=[] and the improve loop no-op'd. Hoisting the call
180
191
  // here repopulates the index first so the subsequent query sees fresh data.
181
- if (primaryStashDir) {
192
+ if (primaryStashDir && !options.dryRun) {
182
193
  // Probe pre-ensureIndex entry count to drive the loud-fail warning below.
183
194
  // Best-effort: a missing DB / unreadable schema is the fresh-install case
184
195
  // and not a bug — we silently skip the probe.
@@ -230,15 +241,24 @@ export async function akmImprove(options = {}) {
230
241
  }
231
242
  }
232
243
  }
233
- ({ plannedRefs, memorySummary, profileFilteredRefs } = await collectEligibleRefsImpl(scope, options.stashDir, improveProfile));
244
+ ({
245
+ plannedRefs,
246
+ memorySummary,
247
+ strategyFilteredRefs = [],
248
+ } = await collectEligibleRefsImpl(scope, options.stashDir, improveProfile));
234
249
  const cleanupParentRef = memoryCleanupParentRef(scope, options.stashDir);
235
250
  // M-1 (#367): Run contradiction-detection BEFORE analyzeMemoryCleanup so
236
251
  // the SCC resolver in resolveFamilyContradictions has edges to work on.
237
252
  // Best-effort: failures are warnings, never fatal.
238
- if (primaryStashDir && shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir)) {
253
+ if (!options.dryRun &&
254
+ primaryStashDir &&
255
+ shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir)) {
239
256
  try {
240
257
  // Reuse the config resolved at the top of the run instead of a second load.
241
- await withLlmStage("memory-contradiction", () => detectAndWriteContradictions(primaryStashDir, _earlyConfig));
258
+ const contradictionDetectionFn = options.contradictionDetectionFn ?? detectAndWriteContradictions;
259
+ await withLlmStage("memory-contradiction", () => contradictionDetectionFn(primaryStashDir, _earlyConfig, undefined, improveProfile, resolvedPlan.processes.consolidate.runner
260
+ ? materializeLlmRunnerConnection(resolvedPlan.processes.consolidate.runner)
261
+ : null), { engine: resolvedPlan.processes.consolidate.runner?.engine, process: "consolidate" });
242
262
  }
243
263
  catch (err) {
244
264
  // Non-fatal: contradiction detection is a best-effort pass.
@@ -264,7 +284,7 @@ export async function akmImprove(options = {}) {
264
284
  if (!options.dryRun) {
265
285
  // Backstop release on process.exit() (signal handler / budget watchdog),
266
286
  // which skips the finally below. Removed in that finally on the normal path.
267
- const releaseAllOnExit = () => releaseHeldLocksIfOwned(process.pid);
287
+ const releaseAllOnExit = () => releaseHeldLocksIfOwned();
268
288
  exitBackstop = releaseAllOnExit;
269
289
  process.on("exit", releaseAllOnExit);
270
290
  // #607 triage pre-pass: acquire triage.lock, drain the standing pending
@@ -272,14 +292,14 @@ export async function akmImprove(options = {}) {
272
292
  // a cleared queue (no `duplicate_pending` collisions) and ensureIndex
273
293
  // absorbs triage's promotions for free. Release immediately after —
274
294
  // triage.lock is not needed again until the next improve run.
275
- if (primaryStashDir && resolveProcessEnabled("triage", improveProfile)) {
295
+ if (primaryStashDir && resolvedPlan.processes.triage.enabled) {
276
296
  if (scope.mode === "ref") {
277
297
  warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
278
298
  }
279
299
  else {
280
300
  const triageLPath = processLockPath(lockBaseDir, "triage");
281
301
  const triageResult = tryAcquireProcessLock(triageLPath, PROCESS_LOCK_DEFS.triage.staleAfterMs, options.skipIfLocked, "triage");
282
- if (triageResult === "skipped") {
302
+ if (triageResult.state === "skipped") {
283
303
  triageDrain = undefined;
284
304
  }
285
305
  else {
@@ -288,9 +308,6 @@ export async function akmImprove(options = {}) {
288
308
  const policy = resolveDrainPolicy(triageConfig?.policy);
289
309
  const applyMode = triageConfig?.applyMode ?? "queue";
290
310
  const maxAccepts = triageConfig?.maxAcceptsPerRun ?? 25;
291
- const judgment = triageConfig?.judgment
292
- ? resolveTriageJudgmentRunner(triageConfig.judgment, _earlyConfig)
293
- : null;
294
311
  triageDrain = await drainProposalsFn({
295
312
  stashDir: primaryStashDir,
296
313
  policy,
@@ -299,14 +316,14 @@ export async function akmImprove(options = {}) {
299
316
  dryRun: false,
300
317
  excludeIds: new Set(),
301
318
  ...(triageConfig?.maxDiffLines !== undefined ? { maxDiffLines: triageConfig.maxDiffLines } : {}),
302
- judgment,
319
+ judgment: resolvedPlan.triageJudgment,
303
320
  });
304
321
  }
305
322
  catch (err) {
306
323
  warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
307
324
  }
308
325
  finally {
309
- releaseProcessLock(triageLPath);
326
+ releaseProcessLock(triageResult.ownership);
310
327
  }
311
328
  }
312
329
  }
@@ -318,15 +335,16 @@ export async function akmImprove(options = {}) {
318
335
  await runIndexAndCollect();
319
336
  if (options.dryRun) {
320
337
  const result = {
321
- schemaVersion: 1,
338
+ schemaVersion: 2,
322
339
  ok: true,
340
+ strategy: selectedStrategy.name,
323
341
  scope,
324
342
  dryRun: true,
325
343
  ...(guidance ? { guidance } : {}),
326
344
  memorySummary,
327
345
  ...(memoryCleanupPlan ? { memoryCleanup: shapeMemoryCleanup(memoryCleanupPlan) } : {}),
328
346
  plannedRefs,
329
- ...(profileFilteredRefs.length > 0 ? { profileFilteredRefs } : {}),
347
+ ...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
330
348
  };
331
349
  return result;
332
350
  }
@@ -459,14 +477,15 @@ export async function akmImprove(options = {}) {
459
477
  // event per ref (#592) — the per-ref loop caused O(n) sequential state.db
460
478
  // writes that consumed ~500 s on a 9 000-ref stash. No downstream consumer
461
479
  // needs the per-ref audit trail: health's skip histogram reads the
462
- // `profile_filtered_all_passes` counters from `improve_completed` metadata.
463
- if (profileFilteredRefs.length > 0) {
480
+ // `strategy_filtered_all_passes` counters from `improve_completed` metadata.
481
+ if (strategyFilteredRefs.length > 0) {
464
482
  appendEvent({
465
483
  eventType: "improve_skipped",
466
484
  ref: undefined,
467
485
  metadata: {
468
- reason: "profile_filtered_all_passes",
469
- count: profileFilteredRefs.length,
486
+ strategy: selectedStrategy.name,
487
+ reason: "strategy_filtered_all_passes",
488
+ count: strategyFilteredRefs.length,
470
489
  },
471
490
  }, eventsCtx);
472
491
  }
@@ -534,11 +553,15 @@ export async function akmImprove(options = {}) {
534
553
  if (cycleIndex > 0) {
535
554
  await runIndexAndCollect();
536
555
  // Re-emit the profile-filtered audit summary for this cycle's selection.
537
- if (profileFilteredRefs.length > 0) {
556
+ if (strategyFilteredRefs.length > 0) {
538
557
  appendEvent({
539
558
  eventType: "improve_skipped",
540
559
  ref: undefined,
541
- metadata: { reason: "profile_filtered_all_passes", count: profileFilteredRefs.length },
560
+ metadata: {
561
+ strategy: selectedStrategy.name,
562
+ reason: "strategy_filtered_all_passes",
563
+ count: strategyFilteredRefs.length,
564
+ },
542
565
  }, eventsCtx);
543
566
  }
544
567
  }
@@ -563,6 +586,8 @@ export async function akmImprove(options = {}) {
563
586
  eventsCtx,
564
587
  initialCleanupWarnings: preEnsureCleanupWarnings,
565
588
  improveProfile,
589
+ resolvedPlan,
590
+ strategyName: selectedStrategy.name,
566
591
  budgetSignal: budgetAbortController.signal,
567
592
  }));
568
593
  prepGateCount += preparation.gateAutoAcceptedCount;
@@ -624,6 +649,7 @@ export async function akmImprove(options = {}) {
624
649
  budgetMs,
625
650
  eventsCtx,
626
651
  improveProfile,
652
+ resolvedPlan,
627
653
  budgetSignal: budgetAbortController.signal,
628
654
  });
629
655
  });
@@ -655,6 +681,7 @@ export async function akmImprove(options = {}) {
655
681
  eventsCtx,
656
682
  budgetSignal: budgetAbortController.signal,
657
683
  improveProfile,
684
+ resolvedPlan,
658
685
  consolidationRan: preparation.consolidationRan,
659
686
  // R5: floor violations from this run's consolidate pass + the
660
687
  // auto-accepted volume so far (prep + loop gates) for churn detection.
@@ -709,8 +736,9 @@ export async function akmImprove(options = {}) {
709
736
  // below still read `finalActions` (reflect skips are not folded).
710
737
  const { actions: persistedActions, aggregate: distillSkippedAggregate } = foldDistillSkipped(finalActions);
711
738
  const result = {
712
- schemaVersion: 1,
739
+ schemaVersion: 2,
713
740
  ok: true,
741
+ strategy: selectedStrategy.name,
714
742
  scope,
715
743
  dryRun: false,
716
744
  ...(guidance ? { guidance } : {}),
@@ -737,7 +765,7 @@ export async function akmImprove(options = {}) {
737
765
  }
738
766
  : {}),
739
767
  plannedRefs: preparation.actionableRefs,
740
- ...(profileFilteredRefs.length > 0 ? { profileFilteredRefs } : {}),
768
+ ...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
741
769
  actions: persistedActions,
742
770
  ...(distillSkippedAggregate ? { distillSkipped: distillSkippedAggregate } : {}),
743
771
  ...(preparation.validationFailures.length > 0 ? { validationFailures: preparation.validationFailures } : {}),
@@ -826,7 +854,8 @@ export async function akmImprove(options = {}) {
826
854
  eventType: "improve_failed",
827
855
  ref: scope.mode === "ref" ? scope.value : `improve:${scope.mode}:${scope.value ?? "all"}`,
828
856
  metadata: {
829
- error: errMessage(err),
857
+ strategy: selectedStrategy.name,
858
+ error: redactSensitiveText(errMessage(err), improveSensitiveValues),
830
859
  durationMs: Date.now() - startMs,
831
860
  },
832
861
  }, eventsCtx);
@@ -942,6 +971,7 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
942
971
  ? result.scope.value
943
972
  : `improve:${result.scope.mode}:${result.scope.value ?? "all"}`,
944
973
  metadata: {
974
+ strategy: result.strategy,
945
975
  plannedRefs: result.plannedRefs.length,
946
976
  reflectActions: actionCounts.reflect,
947
977
  distillActions: actionCounts.distill,
@@ -5,7 +5,8 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { ConfigError } from "../../core/errors.js";
7
7
  import { appendEvent } from "../../core/events.js";
8
- import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "../../core/file-lock.js";
8
+ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
9
+ import { withMaintenanceStartBarrier } from "../../core/maintenance-barrier.js";
9
10
  import { warn } from "../../core/warn.js";
10
11
  // #607 Lock Decomposition: fine-grained per-process locks replace the single
11
12
  // `improve.lock`. Three independent locks allow concurrent improve runs when
@@ -32,11 +33,27 @@ export function processLockPath(lockBaseDir, lockName) {
32
33
  return path.join(lockBaseDir, PROCESS_LOCK_DEFS[lockName].fileName);
33
34
  }
34
35
  export function tryAcquireProcessLock(lockPath, staleAfterMs, skipIfLocked, lockLabel) {
36
+ let recoveryEvent;
37
+ const result = withMaintenanceStartBarrier(() => tryAcquireProcessLockUnlocked(lockPath, staleAfterMs, skipIfLocked, lockLabel, (event) => {
38
+ recoveryEvent = event;
39
+ }));
40
+ if (recoveryEvent) {
41
+ try {
42
+ appendEvent(recoveryEvent);
43
+ }
44
+ catch {
45
+ /* event emission is best-effort; never block lock recovery */
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ function tryAcquireProcessLockUnlocked(lockPath, staleAfterMs, skipIfLocked, lockLabel, onRecovered) {
35
51
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
36
- const lockPayload = () => JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() });
37
- if (tryAcquireLockSync(lockPath, lockPayload())) {
38
- heldProcessLocks.add(lockPath);
39
- return "acquired";
52
+ const lockPayload = () => createLockPayload({ startedAt: new Date().toISOString() });
53
+ let ownership = tryAcquireLockSync(lockPath, lockPayload());
54
+ if (ownership) {
55
+ heldProcessLocks.add(ownership);
56
+ return { state: "acquired", ownership };
40
57
  }
41
58
  const probe = probeLock(lockPath, { staleAfterMs });
42
59
  // Race: the holder released the lock between our failed `tryAcquireLockSync`
@@ -45,9 +62,10 @@ export function tryAcquireProcessLock(lockPath, staleAfterMs, skipIfLocked, lock
45
62
  // would warn/throw with a null PID for a lock that nobody actually holds.
46
63
  // (Mirrors the absent/stale reclaim-and-retry in `acquireExtractSessionLock`.)
47
64
  if (probe.state === "absent") {
48
- if (tryAcquireLockSync(lockPath, lockPayload())) {
49
- heldProcessLocks.add(lockPath);
50
- return "acquired";
65
+ ownership = tryAcquireLockSync(lockPath, lockPayload());
66
+ if (ownership) {
67
+ heldProcessLocks.add(ownership);
68
+ return { state: "acquired", ownership };
51
69
  }
52
70
  // Re-grabbed by another racer in the window — fall through and treat as held.
53
71
  }
@@ -63,52 +81,55 @@ export function tryAcquireProcessLock(lockPath, staleAfterMs, skipIfLocked, lock
63
81
  })()
64
82
  : null;
65
83
  if (probe.state === "stale") {
66
- try {
67
- appendEvent({
68
- eventType: "improve_lock_recovered",
69
- metadata: {
70
- lockName: lockLabel,
71
- stalePid: lock?.pid ?? null,
72
- lockedAt: lock?.startedAt ?? null,
73
- recoveredAt: new Date().toISOString(),
74
- lockAgeMs: probe.ageMs ?? null,
75
- reason: probe.reason === "pid_dead" ? "pid_not_alive" : probe.reason,
76
- },
77
- });
78
- }
79
- catch {
80
- /* event emission is best-effort; never block lock recovery */
84
+ if (!reclaimStaleLock(lockPath, probe)) {
85
+ if (skipIfLocked) {
86
+ warn(`[improve] ${lockLabel} lock changed ownership during stale recovery; skipping (--skip-if-locked)`);
87
+ return { state: "skipped" };
88
+ }
89
+ throw new ConfigError(`akm improve ${lockLabel} is already running. Delete ${lockPath} to force.`, "INVALID_CONFIG_FILE");
81
90
  }
82
- releaseLock(lockPath);
83
- if (tryAcquireLockSync(lockPath, lockPayload())) {
84
- heldProcessLocks.add(lockPath);
85
- return "acquired";
91
+ onRecovered({
92
+ eventType: "improve_lock_recovered",
93
+ metadata: {
94
+ lockName: lockLabel,
95
+ stalePid: lock?.pid ?? null,
96
+ lockedAt: lock?.startedAt ?? null,
97
+ recoveredAt: new Date().toISOString(),
98
+ lockAgeMs: probe.ageMs ?? null,
99
+ reason: probe.reason === "pid_dead" ? "pid_not_alive" : probe.reason,
100
+ },
101
+ });
102
+ ownership = tryAcquireLockSync(lockPath, lockPayload());
103
+ if (ownership) {
104
+ heldProcessLocks.add(ownership);
105
+ return { state: "acquired", ownership };
86
106
  }
87
107
  if (skipIfLocked) {
88
108
  warn(`[improve] ${lockLabel} lock acquired by another run during stale recovery; skipping (--skip-if-locked)`);
89
- return "skipped";
109
+ return { state: "skipped" };
90
110
  }
91
111
  throw new ConfigError(`akm improve ${lockLabel} is already running. Delete ${lockPath} to force.`, "INVALID_CONFIG_FILE");
92
112
  }
93
113
  if (skipIfLocked) {
94
114
  warn(`[improve] ${lockLabel} lock held by another run (PID ${lock?.pid}, started ${lock?.startedAt}); skipping (--skip-if-locked)`);
95
- return "skipped";
115
+ return { state: "skipped" };
96
116
  }
97
117
  throw new ConfigError(`akm improve ${lockLabel} is already running (PID ${lock?.pid}, started ${lock?.startedAt}). Delete ${lockPath} to force.`, "INVALID_CONFIG_FILE");
98
118
  }
99
- export function releaseProcessLock(lockPath) {
119
+ export function releaseProcessLock(ownership) {
120
+ if (!heldProcessLocks.has(ownership))
121
+ return;
100
122
  try {
101
- fs.unlinkSync(lockPath);
123
+ releaseLock(ownership);
102
124
  }
103
- catch {
104
- // ignore
125
+ finally {
126
+ heldProcessLocks.delete(ownership);
105
127
  }
106
- heldProcessLocks.delete(lockPath);
107
128
  }
108
129
  export function releaseAllProcessLocks() {
109
- for (const p of heldProcessLocks) {
130
+ for (const ownership of heldProcessLocks) {
110
131
  try {
111
- fs.unlinkSync(p);
132
+ releaseLock(ownership);
112
133
  }
113
134
  catch {
114
135
  // ignore
@@ -119,12 +140,12 @@ export function releaseAllProcessLocks() {
119
140
  /**
120
141
  * Ownership-safe release of every currently-held lock, for the `process.on("exit")`
121
142
  * backstop (signal handler / budget watchdog paths that skip the normal finally).
122
- * Uses `releaseLockIfOwned` so a lock another PID legitimately re-acquired after a
123
- * stale recovery is never deleted. Does NOT clear the Set (the process is exiting).
143
+ * Uses exact ownership handles so a lock legitimately re-acquired after stale
144
+ * recovery is never deleted. Does NOT clear the Set (the process is exiting).
124
145
  */
125
- export function releaseHeldLocksIfOwned(pid) {
126
- for (const p of heldProcessLocks) {
127
- releaseLockIfOwned(p, pid);
146
+ export function releaseHeldLocksIfOwned() {
147
+ for (const ownership of heldProcessLocks) {
148
+ releaseLock(ownership);
128
149
  }
129
150
  }
130
151
  /**
@@ -136,19 +157,19 @@ export function releaseHeldLocksIfOwned(pid) {
136
157
  * Behaviour matches the hand-rolled `acquired = tryAcquire(...) === "acquired";
137
158
  * …run stage…; if (acquired) release` idiom it replaces:
138
159
  * - When the lock is held and `skipIfLocked` is set, `tryAcquireProcessLock`
139
- * returns "skipped" → the stage still runs (unlocked), nothing to release.
160
+ * returns `state: "skipped"` → the stage still runs (unlocked), nothing to release.
140
161
  * - When the lock is held and `skipIfLocked` is NOT set, `tryAcquireProcessLock`
141
162
  * throws (propagated here before `body` runs; nothing acquired, nothing released).
142
163
  * - The process-exit backstop (`releaseHeldLocksIfOwned`) still covers a
143
164
  * `process.exit` that skips this `finally`.
144
165
  */
145
166
  export async function withOptionalProcessLock(opts, body) {
146
- const acquired = tryAcquireProcessLock(opts.lockPath, opts.staleAfterMs, opts.skipIfLocked, opts.label) === "acquired";
167
+ const acquisition = tryAcquireProcessLock(opts.lockPath, opts.staleAfterMs, opts.skipIfLocked, opts.label);
147
168
  try {
148
169
  return await body();
149
170
  }
150
171
  finally {
151
- if (acquired)
152
- releaseProcessLock(opts.lockPath);
172
+ if (acquisition.state === "acquired")
173
+ releaseProcessLock(acquisition.ownership);
153
174
  }
154
175
  }