akm-cli 0.9.30 → 0.10.26101001-alpha

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 (353) hide show
  1. package/CHANGELOG.md +282 -0
  2. package/README.md +10 -10
  3. package/ROADMAP.md +112 -0
  4. package/STABILITY.md +79 -53
  5. package/dist/api-node.mjs +20 -0
  6. package/dist/api.d.ts +23 -0
  7. package/dist/api.js +45 -0
  8. package/dist/assets/hints/cli-hints-full.md +13 -27
  9. package/dist/assets/hints/cli-hints-short.md +1 -1
  10. package/dist/assets/improve-strategies/default.json +3 -3
  11. package/dist/assets/prompts/consolidate-pair.md +9 -7
  12. package/dist/cli/shared.js +31 -48
  13. package/dist/cli/unknown-flags.js +6 -43
  14. package/dist/cli.js +8 -34
  15. package/dist/commands/agent/contribute-cli.js +1 -1
  16. package/dist/commands/completions.js +1 -2
  17. package/dist/commands/config-cli.js +5 -28
  18. package/dist/commands/env/env-cli.js +5 -5
  19. package/dist/commands/env/secret-cli.js +2 -2
  20. package/dist/commands/health/checks.js +41 -16
  21. package/dist/commands/health/plugin-staleness.js +52 -22
  22. package/dist/commands/improve/autonomy-gate.js +5 -3
  23. package/dist/commands/improve/consolidate/pair-pass.js +7 -1
  24. package/dist/commands/improve/consolidate.js +6 -35
  25. package/dist/commands/improve/distill/lesson-update.js +91 -0
  26. package/dist/commands/improve/distill-guards.js +1 -43
  27. package/dist/commands/improve/distill.js +140 -20
  28. package/dist/commands/improve/extract-cli.js +1 -1
  29. package/dist/commands/improve/improve-cli.js +120 -114
  30. package/dist/commands/improve/improve-strategies.js +8 -12
  31. package/dist/commands/improve/loop-stages.js +0 -1
  32. package/dist/commands/improve/planner.js +0 -1
  33. package/dist/commands/improve/preparation.js +40 -99
  34. package/dist/commands/improve/proactive-maintenance.js +2 -2
  35. package/dist/commands/improve/reflect-noise.js +3 -55
  36. package/dist/commands/improve/reflect.js +3 -8
  37. package/dist/commands/improve/retrieval-scope.js +2 -2
  38. package/dist/commands/improve/salience.js +2 -9
  39. package/dist/commands/improve/stage.js +14 -9
  40. package/dist/commands/lint/index.js +5 -20
  41. package/dist/commands/observability-cli.js +3 -9
  42. package/dist/commands/proposal/drain.js +39 -6
  43. package/dist/commands/proposal/proposal-cli.js +10 -30
  44. package/dist/commands/proposal/repository.js +3 -3
  45. package/dist/commands/read/knowledge.js +3 -3
  46. package/dist/commands/read/remember-cli.js +0 -13
  47. package/dist/commands/read/search-cli.js +29 -97
  48. package/dist/commands/read/search.js +0 -11
  49. package/dist/commands/read/show.js +0 -19
  50. package/dist/commands/sources/plugin-upgrade.js +164 -101
  51. package/dist/commands/sources/sources-cli.js +1 -15
  52. package/dist/commands/sources/stash-cli.js +4 -12
  53. package/dist/commands/tasks/tasks-cli.js +4 -115
  54. package/dist/commands/workflow/plan.js +11 -33
  55. package/dist/commands/workflow-cli.js +2 -7
  56. package/dist/core/adapter/adapter-ids.js +1 -2
  57. package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
  58. package/dist/core/adapter/adapters/akm-adapter.js +9 -48
  59. package/dist/core/adapter/adapters/akm-lint.js +10 -26
  60. package/dist/core/adapter/adapters/akm-metadata.js +2 -72
  61. package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
  62. package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
  63. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
  64. package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
  65. package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
  66. package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
  67. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
  68. package/dist/core/adapter/recognize-match.js +3 -4
  69. package/dist/core/common.js +18 -2
  70. package/dist/core/config/config-schema.js +12 -15
  71. package/dist/core/config/config-types.js +6 -15
  72. package/dist/core/config/config-walker.js +6 -23
  73. package/dist/core/config/config.js +17 -80
  74. package/dist/core/config/engine-semantics.js +7 -3
  75. package/dist/core/config/schema/engines.js +7 -7
  76. package/dist/core/config/schema/improve-processes.js +8 -61
  77. package/dist/core/config/schema/improve.js +2 -21
  78. package/dist/core/config/schema/index-config.js +6 -39
  79. package/dist/core/env-secret-ref.js +2 -45
  80. package/dist/core/errors.js +1 -2
  81. package/dist/core/extra-params.js +2 -86
  82. package/dist/core/file-lock.js +10 -1
  83. package/dist/core/improve-result.js +11 -1
  84. package/dist/core/mutation-target.js +2 -2
  85. package/dist/core/recognition-util.js +4 -5
  86. package/dist/core/warn.js +21 -4
  87. package/dist/core/write-source.js +3 -3
  88. package/dist/execution/target-ref.js +1 -2
  89. package/dist/indexer/search/db-search.js +4 -19
  90. package/dist/indexer/search/fts-query.js +0 -19
  91. package/dist/integrations/agent/engine-resolution.js +5 -2
  92. package/dist/integrations/agent/index.js +1 -1
  93. package/dist/integrations/agent/profiles.js +41 -98
  94. package/dist/integrations/agent/request-lowering.js +3 -3
  95. package/dist/integrations/harnesses/aider/index.js +5 -0
  96. package/dist/integrations/harnesses/amazonq/index.js +1 -0
  97. package/dist/integrations/harnesses/claude/config-import.js +2 -3
  98. package/dist/integrations/harnesses/claude/index.js +5 -0
  99. package/dist/integrations/harnesses/codex/index.js +1 -0
  100. package/dist/integrations/harnesses/copilot/index.js +1 -0
  101. package/dist/integrations/harnesses/gemini/index.js +1 -0
  102. package/dist/integrations/harnesses/index.js +18 -39
  103. package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
  104. package/dist/integrations/harnesses/opencode/config-import.js +121 -52
  105. package/dist/integrations/harnesses/opencode/index.js +22 -0
  106. package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
  107. package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
  108. package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
  109. package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
  110. package/dist/integrations/harnesses/opencode/session-log.js +104 -115
  111. package/dist/integrations/harnesses/opencode/version.js +98 -0
  112. package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
  113. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
  114. package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
  115. package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +228 -0
  116. package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
  117. package/dist/integrations/harnesses/openhands/index.js +1 -0
  118. package/dist/integrations/harnesses/pi/index.js +1 -0
  119. package/dist/integrations/harnesses/shared.js +2 -0
  120. package/dist/integrations/harnesses/types.js +2 -0
  121. package/dist/llm/client.js +74 -14
  122. package/dist/output/cli-hints.js +1 -1
  123. package/dist/output/context.js +10 -19
  124. package/dist/output/format-exempt.js +5 -8
  125. package/dist/output/shapes/helpers.js +1 -22
  126. package/dist/output/shapes/passthrough.js +0 -1
  127. package/dist/output/shapes.js +3 -31
  128. package/dist/output/stdout.js +5 -1
  129. package/dist/output/text/command-format.js +6 -1
  130. package/dist/output/text/workflow-format.js +4 -22
  131. package/dist/scripts/akm-migrate-node.js +15746 -16706
  132. package/dist/scripts/akm-migrate.js +16319 -17275
  133. package/dist/setup/detect.js +10 -4
  134. package/dist/setup/harness-config-import.js +5 -14
  135. package/dist/setup/setup.js +1 -1
  136. package/dist/setup/steps/tasks.js +12 -13
  137. package/dist/storage/repositories/task-history-repository.js +0 -5
  138. package/dist/storage/repositories/workflow-runs-repository.js +5 -15
  139. package/dist/tasks/embedded.js +2 -1
  140. package/dist/tasks/run/task-history.js +0 -2
  141. package/dist/tasks/schedule-gates.js +39 -0
  142. package/dist/tasks/scheduler-binding.js +0 -16
  143. package/dist/tasks/scheduler-sync.js +3 -97
  144. package/dist/tasks/source/bounded-document.js +1 -1
  145. package/dist/tasks/source/task-source-v4.js +2 -12
  146. package/dist/tasks/task-run-reserved-flags.js +2 -31
  147. package/dist/workflows/authoring/authoring.js +6 -6
  148. package/dist/workflows/compile.js +8 -65
  149. package/dist/workflows/exec/child-workflow.js +22 -3
  150. package/dist/workflows/exec/native-executor.js +12 -147
  151. package/dist/workflows/exec/run-workflow.js +31 -103
  152. package/dist/workflows/exec/step-work.js +9 -178
  153. package/dist/workflows/exec/unit-dispatch.js +0 -10
  154. package/dist/workflows/freeze/environment.js +3 -44
  155. package/dist/workflows/freeze/freeze.js +8 -41
  156. package/dist/workflows/freeze/targets/child-workflow.js +27 -63
  157. package/dist/workflows/freeze/targets/command.js +2 -16
  158. package/dist/workflows/freeze/targets/shell.js +0 -2
  159. package/dist/workflows/freeze/task-bindings.js +2 -28
  160. package/dist/workflows/ir/params.js +12 -2
  161. package/dist/workflows/parser.js +82 -232
  162. package/dist/workflows/plan.js +1 -1
  163. package/dist/workflows/renderer.js +10 -19
  164. package/dist/workflows/runtime/run-plan.js +11 -53
  165. package/dist/workflows/runtime/runs.js +4 -27
  166. package/dist/workflows/source-files.js +12 -16
  167. package/dist/workflows/source-semantics.js +1 -93
  168. package/docs/README.md +43 -42
  169. package/docs/agents/README.md +15 -0
  170. package/docs/agents/agent-install.md +14 -0
  171. package/docs/architecture/README.md +67 -0
  172. package/docs/architecture/adapters.md +258 -0
  173. package/docs/architecture/akm-architecture-decision-history.md +2055 -0
  174. package/docs/architecture/akm-core-principles.md +126 -0
  175. package/docs/architecture/architecture.md +578 -0
  176. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +550 -0
  177. package/docs/architecture/decisions/0001-no-interpolation-attached-structured-context.md +72 -0
  178. package/docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md +142 -0
  179. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +189 -0
  180. package/docs/architecture/decisions/0004-task-input-contract-and-flag-coercion.md +97 -0
  181. package/docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md +108 -0
  182. package/docs/architecture/decisions/0006-task-source-version-routing.md +124 -0
  183. package/docs/architecture/decisions/0007-workflow-composition-bounds.md +83 -0
  184. package/docs/architecture/decisions/0008-task-binding-normalization.md +88 -0
  185. package/docs/architecture/decisions/0009-child-run-publication-column-parity.md +80 -0
  186. package/docs/architecture/decisions/0010-driver-protocol-cut.md +36 -0
  187. package/docs/architecture/decisions/0011-engine-run-loop-invariants.md +125 -0
  188. package/docs/architecture/decisions/README.md +62 -0
  189. package/docs/architecture/improvement.md +471 -0
  190. package/docs/architecture/internals/classification.md +154 -0
  191. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +40 -0
  192. package/docs/architecture/internals/functional-contract-patterns.md +340 -0
  193. package/docs/architecture/internals/health-advisories.md +110 -0
  194. package/docs/architecture/internals/improve-workflow.md +720 -0
  195. package/docs/architecture/internals/indexing.md +498 -0
  196. package/docs/architecture/internals/registry-network-boundary.md +57 -0
  197. package/docs/architecture/internals/search.md +248 -0
  198. package/docs/architecture/internals/storage-locations.md +943 -0
  199. package/docs/architecture/persisted-data-compat.md +68 -0
  200. package/docs/architecture/reviews/env-secret-access.md +319 -0
  201. package/docs/architecture/runtime-boundary-design.md +153 -0
  202. package/docs/architecture/specs/0.9.0-close-out-plan.md +361 -0
  203. package/docs/architecture/specs/0.9.0-decisions.md +532 -0
  204. package/docs/architecture/specs/0.9.0-docs-code-drift-register.md +745 -0
  205. package/docs/architecture/specs/0.9.0-open-items-register.md +135 -0
  206. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +740 -0
  207. package/docs/architecture/specs/0.9.0-release-surface-review.md +636 -0
  208. package/docs/architecture/specs/agent-command-engine-model-design.md +513 -0
  209. package/docs/architecture/specs/akm-0.9.0-bundle-adapter-spec.md +501 -0
  210. package/docs/architecture/specs/akm-0.9.0-ref-grammar-decision.md +291 -0
  211. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +2059 -0
  212. package/docs/architecture/specs/di-seams-plan.md +539 -0
  213. package/docs/architecture/specs/driver-protocol-keep-or-cut.md +275 -0
  214. package/docs/architecture/specs/fact-asset-type.md +142 -0
  215. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +634 -0
  216. package/docs/architecture/specs/okf-support.md +148 -0
  217. package/docs/architecture/specs/ref.md +329 -0
  218. package/docs/architecture/specs/reviews/task-workflow-unification/README.md +72 -0
  219. package/docs/architecture/specs/reviews/task-workflow-unification/findings-consolidated.md +152 -0
  220. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-alternatives.md +352 -0
  221. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-bunqueue.md +300 -0
  222. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-openworkflow.md +402 -0
  223. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-1.md +758 -0
  224. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-2.md +645 -0
  225. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-3.md +602 -0
  226. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-adversarial.md +27 -0
  227. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-architecture.md +27 -0
  228. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-coverage.md +74 -0
  229. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-security.md +15 -0
  230. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-adversarial.md +132 -0
  231. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-verification.md +147 -0
  232. package/docs/architecture/specs/stash-conventions-code-spec.md +275 -0
  233. package/docs/architecture/specs/stash-organization-conventions.md +377 -0
  234. package/docs/architecture/specs/task-workflow-format-unification.md +721 -0
  235. package/docs/architecture/specs/workflow-engine-buy-vs-build.md +231 -0
  236. package/docs/architecture/specs/workflow-format-unification.md +432 -0
  237. package/docs/architecture/testing/manual-testing-checklist.md +3411 -0
  238. package/docs/architecture/testing/okf-v0.2-conformance-runbook.md +924 -0
  239. package/docs/architecture/testing/task-workflow-manual-tests.md +1720 -0
  240. package/docs/architecture/testing/testing-workflow.md +446 -0
  241. package/docs/architecture/workflow-engine.md +223 -0
  242. package/docs/guides/README.md +33 -0
  243. package/docs/guides/author-bundles.md +310 -0
  244. package/docs/guides/author-workflows.md +490 -0
  245. package/docs/guides/bundles.md +321 -0
  246. package/docs/guides/capture-knowledge.md +136 -0
  247. package/docs/guides/claude-code-vs-akm-workflows.md +29 -0
  248. package/docs/guides/concepts.md +93 -0
  249. package/docs/guides/discover-and-load.md +158 -0
  250. package/docs/guides/getting-started.md +240 -0
  251. package/docs/guides/improve-the-library.md +246 -0
  252. package/docs/guides/knowledge-management.md +18 -0
  253. package/docs/guides/recipes/headless-install.md +97 -0
  254. package/docs/guides/recipes/website-source.md +28 -0
  255. package/docs/guides/run-workflows.md +279 -0
  256. package/docs/guides/scheduling.md +123 -0
  257. package/docs/guides/use-with-any-agent.md +174 -0
  258. package/docs/guides/wikis.md +81 -0
  259. package/docs/integration/bundling-akm.md +7 -6
  260. package/docs/maintainers/README.md +19 -0
  261. package/docs/maintainers/curate-workmap.md +335 -0
  262. package/docs/maintainers/eval.md +3 -0
  263. package/docs/maintainers/local-development.md +108 -0
  264. package/docs/maintainers/release-checklist.md +132 -0
  265. package/docs/migration/README.md +2 -2
  266. package/docs/migration/release-notes/0.0.13.md +1 -1
  267. package/docs/migration/release-notes/0.1.0.md +1 -1
  268. package/docs/migration/release-notes/0.2.0.md +1 -1
  269. package/docs/migration/release-notes/0.3.0.md +1 -1
  270. package/docs/migration/release-notes/0.5.0.md +1 -1
  271. package/docs/migration/release-notes/0.6.0.md +1 -1
  272. package/docs/migration/release-notes/0.7.0.md +1 -1
  273. package/docs/migration/release-notes/0.7.3.md +2 -2
  274. package/docs/migration/release-notes/0.7.4.md +1 -1
  275. package/docs/migration/release-notes/0.7.5.md +1 -1
  276. package/docs/migration/release-notes/0.8.0.md +1 -1
  277. package/docs/migration/release-notes/0.9.0.md +1 -1
  278. package/docs/migration/release-notes/0.9.14.md +1 -1
  279. package/docs/migration/release-notes/0.9.15.md +1 -1
  280. package/docs/migration/release-notes/0.9.16.md +1 -1
  281. package/docs/migration/release-notes/0.9.17.md +1 -1
  282. package/docs/migration/release-notes/0.9.19.md +1 -1
  283. package/docs/migration/release-notes/0.9.2.md +1 -1
  284. package/docs/migration/release-notes/README.md +27 -31
  285. package/docs/migration/v0.5-to-v0.6.md +759 -0
  286. package/docs/migration/v0.8-to-v0.9.md +1 -1
  287. package/docs/plans/0.9.2-agent-command-workflow-plan.md +423 -0
  288. package/docs/posts/README.md +30 -0
  289. package/docs/posts/akm-command-tour.md +495 -0
  290. package/docs/posts/akm-logo-sized.webp +0 -0
  291. package/docs/posts/belief-aware-memory-12.md +139 -0
  292. package/docs/posts/config-v2-lessons-16.md +191 -0
  293. package/docs/posts/improve-loop-autonomous-10.md +225 -0
  294. package/docs/posts/improve-pipeline-debugging-0803.md +179 -0
  295. package/docs/posts/intro-part-01.md +76 -0
  296. package/docs/posts/intro-part-02.md +221 -0
  297. package/docs/posts/intro-part-03.md +190 -0
  298. package/docs/posts/intro-part-04.md +221 -0
  299. package/docs/posts/llm-mode-reflect-13.md +166 -0
  300. package/docs/posts/openpalm-built-on-akm.md +86 -0
  301. package/docs/posts/progressive-disclosure-06.md +129 -0
  302. package/docs/posts/proposal-safety-net-15.md +112 -0
  303. package/docs/posts/release-0.5.0.md +247 -0
  304. package/docs/posts/release-0.6.0.md +117 -0
  305. package/docs/posts/release-0.7.0.md +159 -0
  306. package/docs/posts/release-0.8.0.md +332 -0
  307. package/docs/posts/scattered-skills-05.md +100 -0
  308. package/docs/posts/task-assets-persistent-workflows-11.md +222 -0
  309. package/docs/posts/team-skills-07.md +198 -0
  310. package/docs/posts/wiki-agent-knowledge-08.md +223 -0
  311. package/docs/posts/workflows-vaults-09.md +321 -0
  312. package/docs/product-surface.md +54 -0
  313. package/docs/reference/README.md +2 -2
  314. package/docs/reference/api.md +65 -0
  315. package/docs/reference/asset-types.md +166 -0
  316. package/docs/reference/bundle-types.md +2 -4
  317. package/docs/reference/cli.md +129 -175
  318. package/docs/reference/configuration.md +84 -19
  319. package/docs/reference/data-and-telemetry.md +1 -1
  320. package/docs/reference/env-and-secrets.md +126 -0
  321. package/docs/reference/memory.md +124 -0
  322. package/docs/reference/refs.md +156 -0
  323. package/docs/reference/registry.md +489 -0
  324. package/docs/reference/supported-formats.md +3 -4
  325. package/docs/reference/tasks.md +12 -19
  326. package/docs/reference/website-sources.md +141 -0
  327. package/docs/reference/workflow-schema.md +89 -317
  328. package/docs/reference/workflows.md +20 -48
  329. package/package.json +23 -16
  330. package/schemas/akm-config.json +2 -243
  331. package/schemas/akm-task.json +2 -3
  332. package/schemas/akm-workflow.json +47 -84
  333. package/dist/assets/improve-strategies/catchup.json +0 -41
  334. package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
  335. package/dist/assets/improve-strategies/quick.json +0 -17
  336. package/dist/assets/improve-strategies/reflect-distill.json +0 -29
  337. package/dist/assets/improve-strategies/thorough.json +0 -49
  338. package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
  339. package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
  340. package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
  341. package/dist/cli/retired-commands.js +0 -118
  342. package/dist/commands/tasks/validate.js +0 -176
  343. package/dist/core/config/legacy-source-shape-shim.js +0 -117
  344. package/dist/integrations/harnesses/ids.js +0 -28
  345. package/dist/tasks/source/task-source-v3-frozen.js +0 -424
  346. package/dist/tasks/source/task-to-v4.js +0 -863
  347. package/dist/workflows/exec/worktree.js +0 -372
  348. package/dist/workflows/freeze/child-output-references.js +0 -83
  349. package/dist/workflows/freeze/identity.js +0 -22
  350. package/dist/workflows/freeze/targets/script.js +0 -50
  351. package/dist/workflows/freeze/targets/task.js +0 -124
  352. package/dist/workflows/github-yaml.js +0 -506
  353. package/dist/workflows/runtime/run-outputs.js +0 -80
@@ -0,0 +1,720 @@
1
+ # akm improve — Workflow Reference
2
+
3
+ `akm improve` is the scheduled self-improvement loop that works through the assets with fresh feedback (or a scoped subset), invokes the reflection agent and the LLM distiller on each one, runs memory consolidation across the corpus, and then performs improve-owned maintenance passes such as memory inference. It is the primary mechanism for turning accumulated feedback into queued proposals: a rewrite (reflect) is planned only from negative feedback on an asset, or for an explicit ref, never from a positive signal and never on a proactive cadence. Proposals remain queued until explicit proposal review or the configured drain policy resolves them; events and resolved proposal rows provide the audit trail.
4
+
5
+ ## Command surface
6
+
7
+ | Option | Type | Purpose |
8
+ |---|---|---|
9
+ | `[scope]` (positional) | `string` | Restrict the run to a single ref (`[bundle//]conceptId`), an asset type (`lesson`), or omit for all assets. |
10
+ | `--task` | `string` | Hint forwarded verbatim to the reflection prompt and agent. |
11
+ | `--dry-run` | `boolean` | Compute the plan from the existing index and analyze memory cleanup; emit no events, acquire no lock, call no model, and write nothing. |
12
+ | `--bundle` | `string` | The bundle the run improves and writes to, overriding `defaultWriteTarget` and the working bundle. It is the only bundle whose assets the run plans. |
13
+ | `--limit` | `number` | Cap the number of assets processed, taken from the salience ranking, highest first (refs routed to distill only come last). |
14
+ | `--timeout-ms` | `number` | Wall-clock budget for the entire run. Default: 7 200 000 ms (2 hours). |
15
+ | `--skip-if-locked` | `boolean` | If another improve owns the whole-run lock, return an exit-0 no-op result before triage, indexing, events, or sync. Without the flag, contention is a transient error (`IMPROVE_LOCK_HELD`, exit 75). |
16
+ | `--require-feedback-signal` | `boolean` | Turn the proactive-maintenance lane off for the run, so only assets with recent feedback are planned. |
17
+
18
+ Injected function seams (`reflectFn`, `distillFn`, `ensureIndexFn`, `reindexFn`) replace production defaults in tests.
19
+
20
+ ## High-level flow
21
+
22
+ ```mermaid
23
+ flowchart TD
24
+ A([akm improve invoked]) --> B[resolveImproveScope\nscope mode: all / type / ref]
25
+ B --> C{dryRun?}
26
+ C -- no --> E[Acquire whole-run lock\n$STATE/locks/<stash>/improve.lock]
27
+ C -- yes --> COLLECT[collectEligibleRefs\nquery existing SQLite index, filter to stashDir]
28
+ E --> E1{lock file held?}
29
+ E1 -- yes, skip-if-locked --> SKIP[Return exit-0 no-op\nno triage, index, events, or sync]
30
+ E1 -- yes, no flag --> ERR([throw TransientError: already running\nIMPROVE_LOCK_HELD, exit 75])
31
+ E1 -- no / stale reclaimed --> PURGE[purgeGracedArchive\ngit-backed bundles only: delete archived\nretirement bytes past RETIRE_GRACE_DAYS]
32
+ PURGE --> TRIAGE[Triage pending proposal backlog]
33
+ TRIAGE --> ENSURE[ensureIndex primaryStashDir\nshared deadline signal]
34
+ ENSURE --> COLLECT
35
+ COLLECT --> CLEANUP_ANALYZE{memoryCleanup eligible?}
36
+ CLEANUP_ANALYZE -- yes --> ANALYZE[analyzeMemoryCleanup\nscans .derived memories\nPRE-COMPUTED before dryRun check]
37
+ CLEANUP_ANALYZE -- no --> D
38
+ ANALYZE --> D{dryRun?}
39
+ D -- yes --> DRY[Return dry-run result\nno lock, no events, no writes, no model calls\nincludes memoryCleanupPlan analysis]
40
+ D -- no --> CONSOLIDATE
41
+ EXTRACT[Session extract\nwhen the strategy enables it: queues proposals\nfrom coding-agent session transcripts]
42
+ EXTRACT --> J[applyMemoryCleanup\nautonomy-gated: persist belief-state transitions\narchive prune candidates to .akm/memory-cleanup/archive/]
43
+ J --> K[projectMemoryCleanup\ndrop archived refs from the planned set]
44
+ K --> J2{anything archived or transitioned?}
45
+ J2 -- yes --> J3[push memory-prune actions\nreindexFn: rebuild SQLite index]
46
+ J2 -- no --> O[Pre-run validation sweep\ncheck file exists + lesson description\nschema repair when enabled]
47
+ J3 --> O
48
+ O --> P{validationFailures?}
49
+ P -- yes --> P1[Log failures; refs that still fail\nare excluded from selection]
50
+ P -- no --> L[Signal delta\nreflect: negative feedback, distill: any signal,\nnewer than the last ledger attempt\nand no hard ledger window]
51
+ P1 --> L
52
+ L --> L2[Proactive maintenance lane:\nonly retrieved or new material,\nmaxPerRun picks, due after dueDays]
53
+ L2 --> M[scoreSalience\nsalience vector per ref: encoding, outcome, retrieval\nutility scores from SQLite seed the outcome term]
54
+ M --> N[Sort by salience rank DESC, no-op dampened\ndrop refs missing on disk\napply --limit if set]
55
+ N --> Q
56
+
57
+ subgraph ASSET_LOOP["Per-asset loop"]
58
+ Q{budget exhausted?}
59
+ Q -- yes --> BUDGET([push error action\nbreak loop])
60
+ Q -- no --> REFLECT
61
+
62
+ subgraph REFLECT["reflectFn subprocess"]
63
+ REFLECT_A[appendEvent: reflect_invoked] --> REFLECT_B[lookup ref in FTS index\nread asset file content]
64
+ REFLECT_B --> REFLECT_C[readRecentFeedback\nbuildSchemaHints for lessons]
65
+ REFLECT_C --> REFLECT_D[buildReflectPrompt]
66
+ REFLECT_D --> REFLECT_E[callStageOnce on any engine kind\nllm, agent CLI or SDK\nthe reply's JSON Schema is the output schema]
67
+ REFLECT_E --> REFLECT_F{reply valid?\nparseSchemaReflectOutput}
68
+ REFLECT_F -- no, first reply --> REFLECT_R[one repair turn\nshared across refine passes]
69
+ REFLECT_R --> REFLECT_E
70
+ REFLECT_F -- yes --> REFLECT_G[createProposal\nstate.db proposals row\nsource: reflect]
71
+ REFLECT_G --> REFLECT_H([return AkmReflectResult\nok or failure envelope])
72
+ REFLECT_F -- no, after the repair --> REFLECT_X[parse_error, nothing queued]
73
+ REFLECT_X --> REFLECT_H
74
+ end
75
+
76
+ REFLECT_H --> T{distillable memory?\nmemory ref, not derived or proposed,\nnot cooled by the signal delta}
77
+ T -- no --> NEXT_ASSET
78
+ T -- yes + memory without recent feedback
79
+ --> SKIP_WEAK[push distill-skipped action\nappendEvent improve_skipped\nreason: memory_distill_requires_feedback]
80
+ T -- yes + flagged wrong since its last edit
81
+ --> SKIP_FLAGGED[push distill-skipped action\nimprove_ledger row: unchanged\nappendEvent improve_skipped\nreason: distill_flagged_wrong]
82
+ T -- yes + only positive feedback without a reason
83
+ --> SKIP_BARE[push distill-skipped action\nimprove_ledger row: unchanged\nappendEvent improve_skipped\nreason: distill_positive_without_reason]
84
+ T -- yes --> DISTILL
85
+
86
+ subgraph DISTILL["distillFn subprocess"]
87
+ DISTILL_A[lookup ref file path] --> DISTILL_B[readEvents: feedback for ref\napply excludeFeedbackFromRefs filter]
88
+ DISTILL_B --> DISTILL_C{proposalKind == auto\nAND promotion heuristic passes?}
89
+ DISTILL_C -- yes --> DISTILL_PROMOTE[createProposal knowledge/ref\nsource: distill\nappendEvent: distill_invoked outcome=queued]
90
+ DISTILL_C -- no --> DISTILL_X{lesson already at\nthe target ref?}
91
+ DISTILL_X -- yes --> DISTILL_EXISTS[appendEvent: distill_invoked outcome=skipped\nskipReason lesson_exists, no call, no proposal\nthe loop records unchanged in the ledger]
92
+ DISTILL_X -- no --> DISTILL_D[callStage distill\nplan-resolved runner\n600 s default timeout]
93
+ DISTILL_D --> DISTILL_E{call failed or empty output?}
94
+ DISTILL_E -- yes --> DISTILL_SKIP[appendEvent: distill_invoked outcome=llm_failed\nreturn llm_failed result, no ledger row]
95
+ DISTILL_E -- no --> DISTILL_N{writer found no lesson?\nNONE or decision none}
96
+ DISTILL_N -- yes --> DISTILL_NONE[appendEvent: distill_invoked outcome=skipped\nskipReason nothing_reusable, no proposal, no judge call\nthe loop records unchanged in the ledger]
97
+ DISTILL_N -- no --> DISTILL_F[stripMarkdownFences\nlintLessonContent or validateKnowledgeContent]
98
+ DISTILL_F --> DISTILL_G{findings?}
99
+ DISTILL_G -- yes --> DISTILL_FAIL[appendEvent: outcome=validation_failed\nthrow UsageError]
100
+ DISTILL_G -- no --> DISTILL_J{quality gate judge:\nreusable, non-redundancy, grounding}
101
+ DISTILL_J -- pass --> DISTILL_H[createProposal lessons/slug-lesson\nor knowledge/slug\nsource: distill, gate decision deferred\nappendEvent: outcome=queued]
102
+ DISTILL_J -- mean in the review band --> DISTILL_REVIEW[createProposal, gate decision deferred\nappendEvent: outcome=review_needed]
103
+ DISTILL_J -- any criterion 2 or below, or the mean too low --> DISTILL_REJECT[improve_ledger row, no proposal\nappendEvent: outcome=quality_rejected\nunless a repeated lesson gets an update proposal, step 10]
104
+ DISTILL_PROMOTE --> DISTILL_RETURN
105
+ DISTILL_SKIP --> DISTILL_RETURN
106
+ DISTILL_EXISTS --> DISTILL_RETURN
107
+ DISTILL_NONE --> DISTILL_RETURN
108
+ DISTILL_H --> DISTILL_RETURN([return AkmDistillResult])
109
+ DISTILL_REVIEW --> DISTILL_RETURN
110
+ DISTILL_REJECT --> DISTILL_RETURN
111
+ end
112
+
113
+ SKIP_WEAK --> NEXT_ASSET
114
+ SKIP_FLAGGED --> NEXT_ASSET
115
+ SKIP_BARE --> NEXT_ASSET
116
+ DISTILL_RETURN --> NEXT_ASSET([completedCount++\nlog progress])
117
+ end
118
+
119
+ NEXT_ASSET --> Q
120
+
121
+ BUDGET --> MAINT
122
+ NEXT_ASSET -->|all assets done| MAINT
123
+
124
+ subgraph CONSOLIDATE_SUB["akmConsolidate subprocess"]
125
+ CON_A{selected strategy processes.consolidate.enabled?} -- no --> CON_NOOP([return empty result])
126
+ CON_A -- yes --> CON_C[loadMemoriesForSource\nSQLite DB\nexclude .derived names]
127
+ CON_C --> CON_D{memories == 0?} -- yes --> CON_NOOP
128
+ CON_D -- no --> CON_E
129
+
130
+ subgraph PHASE_A["Phase A — Plan generation (chunked)"]
131
+ CON_E[split into configured chunks] --> CON_F[For each chunk:\nchatCompletion with frozen consolidate connection]
132
+ CON_F --> CON_G[parse and validate ops:\npromote only]
133
+ CON_G --> CON_H{2+ consecutive failures?} -- yes --> CON_ABORT[push warning, break]
134
+ CON_H -- no --> CON_F
135
+ end
136
+
137
+ CON_ABORT --> CON_MERGE
138
+ CON_G --> CON_MERGE[mergePlans: one promotion per\nsource memory, last chunk wins]
139
+ CON_MERGE --> CON_DRY{dryRun?} -- yes --> CON_DRYRESULT([return planned ops, no writes])
140
+ CON_DRY -- no --> PHASE_B
141
+
142
+ subgraph PHASE_B["Phase B — Proposal emission"]
143
+ PHASE_B_PRO[For each promote op:\nidempotency and coverage checks,\nthen emitProposal\nsource: consolidate]
144
+ end
145
+
146
+ PHASE_B_PRO --> CON_DONE
147
+
148
+ subgraph PAIR_PASS["Pair pass (alpha.9), alongside Phase A/B"]
149
+ PAIR_INIT[Initiators: changed-or-new memory,\nflat knowledge or lesson, in retrieval scope]
150
+ PAIR_INIT --> PAIR_CAND[Candidates: k=5 nearest by stored vector,\nsame bundle + memory tier, cosine >= T_pair]
151
+ PAIR_CAND --> PAIR_JUDGE[One LLM call per pair:\nconsolidate-pair.md, 6-label schema]
152
+ PAIR_JUDGE --> PAIR_OUT{judge label}
153
+ PAIR_OUT -- duplicate / subsumed / supersedes --> PAIR_GUARDS[Guards: hot capture, derived parent,\nrejected-pair match, same-run chain]
154
+ PAIR_GUARDS --> PAIR_CONTINUITY[Continuity check R3:\nreplay retired asset's own queries,\nflag continuityRisk, never block]
155
+ PAIR_CONTINUITY --> PAIR_PROPOSE[emitProposal: retire,\nsource: consolidate-pair]
156
+ PAIR_OUT -- overlap / unrelated / contradicts --> PAIR_NOACTION[judged_no_action\nno 7-day timer — content change only]
157
+ end
158
+
159
+ PAIR_PROPOSE --> CON_DONE
160
+ PAIR_NOACTION --> CON_DONE
161
+ CON_DONE[return ConsolidateResult]
162
+ end
163
+
164
+ CONSOLIDATE --> CON_A
165
+ CON_NOOP --> EXTRACT
166
+ CON_DONE --> EXTRACT
167
+ CON_DRYRESULT --> EXTRACT
168
+ CON_ABORT2 --> MAINT
169
+
170
+ subgraph MAINTENANCE["Improve-owned maintenance"]
171
+ MAINT[runImproveMaintenancePasses] --> MI{memory refs queued for inference?}
172
+ MI -- no --> FINAL
173
+ MI -- yes --> MI_RUN[runMemoryInferencePass]
174
+ MI_RUN --> MI_WRITE{wrote derived memories\nor marked parents?}
175
+ MI_WRITE -- yes --> MI_REINDEX[reindexFn\nrefresh SQLite state after inference writes]
176
+ MI_WRITE -- no --> FINAL
177
+ MI_REINDEX --> FINAL
178
+ end
179
+
180
+ FINAL[Assemble AkmImproveResult\nschemaVersion: 2 and sync stash] --> UNLOCK[release whole-run lock\nfinally block]
181
+ UNLOCK --> RETURN([return AkmImproveResult])
182
+ ```
183
+
184
+ ## Subprocess detail
185
+
186
+ ### reflect (akmReflect)
187
+
188
+ `akmReflect` is the agent-invocation subprocess. It always emits a `reflect_invoked` event at entry, regardless of success or failure.
189
+
190
+ Reflect changes only an asset's `description`, `when_to_use` and title. The engine replies with those fields (each `null` when it needs no fix), and akm applies them to the asset it read: the body is kept byte for byte, apart from a `# <title>` heading akm adds when the body has none. A reply that changes nothing creates no proposal.
191
+
192
+ **Internal steps:**
193
+
194
+ 1. Emit `reflect_invoked` event via `appendEvent`.
195
+ 2. Resolve asset content: look up the ref in the FTS index; read the file if found. Index miss is non-fatal. A file in the write target that is not the one a proposal writes (`<root>/<type dir>/<name>.md`, derived from the ref) is refused here as `unsupported_type`, before any model call: a skill's `references/a.md` is the ref `knowledge/skills/<name>/references/a`, and a proposal for it would create a second file (`file_outside_layout`, #1052).
196
+ 3. Resolve the selected strategy's `reflect.engine`, falling back to `defaults.llmEngine`.
197
+ 4. Build the reflection prompt via `buildReflectPrompt` (see Prompt shape below).
198
+ 5. Dispatch the frozen `RunnerSpec` through `callStageOnce` under the
199
+ model-work tool policy
200
+ ([Engines for unattended model work](../../reference/configuration.md#engines-for-unattended-model-work)).
201
+ The engine may be an LLM or an agent that confines the policy, and every
202
+ kind runs the same iteration (`runReflectIteration`). The JSON Schema of
203
+ the reply (`REFLECT_JSON_SCHEMA`, or the unscoped schema when no ref was
204
+ given) is the request's output schema: `response_format` for an LLM, the
205
+ shared schema instruction at the end of the prompt for an agent (`claude`
206
+ also gets `--output-format json`, which akm unwraps). An agent edits only
207
+ its own scratch directory, so it returns the proposal as its reply, as an
208
+ LLM does. An LLM endpoint that rejects JSON Schema gets the framed-markdown
209
+ contract instead.
210
+ 6. Validate the reply with `parseSchemaReflectOutput` (`parseFramedReflectOutput` for the framed contract), which strips `<think>` blocks and code fences, then requires exactly the contract's fields: `confidence` and a `frontmatterPatch` of `description`, `when_to_use` and `title` (each a non-empty single-line string, or `null` for no change), plus `ref` when no target was given. A reply that fails gets one repair turn, shared across self-refine passes: the conversation so far, and a request to reformat the reply in the contract (`reflect-output-repair.md`). A reply still invalid fails with `parse_error` and queues nothing. A failed agent or SDK dispatch is reported as it ran, with its exit code and stderr.
211
+ 7. Apply the patch (`applyReflectPatch`) to the source asset: its frontmatter with the non-null `description` and `when_to_use` set, and its body unchanged. A non-null `title` becomes a `# <title>` heading and one blank line at the top of the body, only when the body has no level-1 heading; otherwise it is ignored. A source that requires a `description` and has none gets one derived from its own text when the patch gave none (#636). A patch that changes nothing (every field `null`, or equal to the source's, and no `description` to derive) is the `no_change` outcome (`reflect_skipped_noop`), as is a source with no content to patch; a result that differs only cosmetically is `reflect_skipped_cosmetic`. Neither creates a proposal.
212
+ 8. Quality gate (`processes.reflect.qualityGate`, on unless disabled): one judge call scores the revision against the source on **need**, **preservation** and **quality** ([Quality judge](../improvement.md#quality-judge)), and passes it only when every criterion scores 4 or more. A score that does not pass is refused (`quality_rejected`, no proposal). A judge that times out, errors or replies unparseably gives no verdict, and the proposal is deferred for review (`judge-error`, gate `quality-gate`). Before the judge, with the gate on or off, reflect refuses (`quality_rejected`, no proposal, no judge call; the rule is the event's `reflectDefect`) a revision that adds placeholder text (`placeholder_added`: "please confirm", "to be confirmed"; not `TODO`, `TBD` or `FIXME`), talks about its own edit (`meta_commentary_added`: "the feedback says", "this revision") or copies frontmatter into its body (`frontmatter_copied_into_body`: `sources:` or `updated:` lines outside code, a `sources` value). Each rule counts only what the revision adds to its source (`findReflectDefect`), and its wording is a list that `processes.reflect.defectFilter` replaces or, when empty, turns off ([Configuration](../../reference/configuration.md#strategies)).
213
+ 9. Retrieval regression gate, after a pass on an existing asset: a revision that grades lower on the asset's own retrieval queries is refused ([Retrieval regression gate](../improvement.md#retrieval-regression-gate)).
214
+ 10. Write the proposal (`mintProposal`). A pass the judge passed is stamped `staged`, and the triage drain accepts it; with the gate off the proposal is minted unstamped for the drain to decide. One made with no judge configured (`no-judge-configured`) is deferred for review (gate `reflect`), and so is one whose judge gave no verdict (`judge-error`). The drain leaves every deferral for a person.
215
+
216
+ **What it writes:** one durable proposal row in `state.db`. It never writes asset files directly.
217
+
218
+ **Prompt shape (`buildReflectPrompt`):** The prompt asks the engine to check the asset's `description`, `when_to_use` and title against its body and the recent feedback signals, and to return a single JSON object `{ confidence, frontmatterPatch }`, with `ref` as well when no target was given. Each patch field is a non-empty single-line string, or `null` when it needs no fix. akm applies the patch to the source it read, so a reply names neither the target nor a body. The goal sentence is the same for every asset type. When `feedback` is empty and a ref is set, the prompt says to fix only a missing or broken field and otherwise return `null` for each. The response contract (`reflect-llm-schema-contract.md`, the same for every engine kind) requires the engine to produce only the JSON object — no prose before or after. Non-empty feedback is always preceded by a caveat (`reflect-feedback-framing.md`) framing it as a signal, not a fact to insert: change a field only when it is missing or broken, or claims more than the body covers, and then only to describe what the body covers; feedback about a task the asset never claims to cover, or asking for information the asset lacks, needs no change. Feedback claims a model treated as ground truth were fabricating whole sections asserting details the asset never contained (#952), and the caveat once offered a `TODO: verify …` placeholder, which a model inserted into a memory from a feedback line reporting that `akm show` had failed; a later distill pass then built a lesson on that line (#999). Previously rejected proposals for the ref are listed with the instruction not to propose the same change again.
219
+
220
+ **Asset content cap:** the asset content section is capped to keep the prompt well under OS ARG_MAX when it travels through CLI argv (agent/SDK runners always use the flat `REFLECT_CONTENT_CAP`, 12 000 chars). The direct-LLM (`kind: "llm"`) path never touches argv, so its cap is instead computed from the resolved engine's `contextLength` (chars-per-token estimate × the reserve actually used by the rest of that prompt, measured per call rather than guessed), halved (the other half is left for the model's reply), and never dropping below the flat floor. When content is truncated, a `REFLECT_TRUNCATION_MARKER` notice is appended after the visible portion (#952).
221
+
222
+ ### distill (akmDistill)
223
+
224
+ `akmDistill` is the bounded in-tree LLM subprocess. It never calls `runAgent`; it issues a direct HTTP chat completion through the configured LLM endpoint. Past the process gate it emits a `distill_invoked` event carrying the outcome.
225
+
226
+ **Internal steps:**
227
+
228
+ 1. Validate the input ref shape (`parseRefInput`, `src/core/asset/resolve-ref.ts`).
229
+ 2. Best-effort load asset content via `lookupFn` (defaults to indexer `lookup`).
230
+ 3. Read feedback events via `readEvents({ ref, type: "feedback" })`. Apply `excludeFeedbackFromRefs` filtering before the LLM sees the events.
231
+ 4. Memory promotion fast path: when `proposalKind` is `"auto"` or `"knowledge"` and `assessMemoryKnowledgePromotionCandidate` returns `promote: true`, create a `knowledge:` proposal immediately without an LLM call.
232
+ - A lesson target that already exists is not regenerated: when the stash the proposal is filed in holds a file at the lesson ref (the path accepting it would write), distill returns `skipped` with `skipReason: "lesson_exists"` and a `distill_invoked` event carrying them, before any LLM call, and mints no proposal and stamps nothing on the input. The lesson ref derives from the memory's name, so a second distill of the same memory proposes the same ref, and accepting that proposal replaces the lesson; all 5 such overwrites recorded by the 2026-10-05 review were rejected. A lesson of that name in another bundle is not an overwrite. As for any `skipped` distill, the loop records the input in the improve ledger as `unchanged`.
233
+ 5. Resolve `improve.strategies.<selected>.processes.distill.engine` (falling
234
+ back to `defaults.llmEngine`), then issue one bounded call.
235
+ - Process gate: disabled if the selected strategy's `processes.distill.enabled` is `false`.
236
+ - Hard timeout: 600 seconds by default, overridden by the resolved invocation timeout.
237
+ - A timeout, an error or empty output returns an `llm_failed` result: a `distill_invoked` event with that outcome, no proposal and exit 0. It is not a ledger attempt, so the ref stays eligible. A disabled process returns `config_disabled` before any event.
238
+ - The prompt carries the memory, its feedback and the lessons, knowledge notes and skills the library already holds near the memory (the top 3 by search, one search per type; `processes.distill.cls.enabled: false` turns them off). The writer is told what a lesson is (a cause and what to do about it, or a rule with its reason) and to state only what the memory and its feedback say.
239
+ - The writer may find no lesson: it answers the word `NONE`, or, in a reply bound to the JSON schema, `decision: "none"` after a one-sentence `reason` written first. A memory that only records what was done, shipped or decided, how a system is set up, the steps of a procedure, or a rule a related asset already states has none. distill returns `skipped` with `skipReason: "nothing_reusable"` and a `distill_invoked` event carrying it (the message holds the writer's reason), mints no proposal and makes no judge call. The loop records the input in the improve ledger as `unchanged`, as for any `skipped` distill. Before this, the prompt and both schemas forced a lesson from every memory: 18 of the 19 memories distilled on 2026-10-05 recorded what was done.
240
+ 6. Strip markdown fences and `<think>` blocks from the raw LLM output.
241
+ 7. Validate: `lintLessonContent` for lesson proposals; `validateKnowledgeContent` for knowledge proposals. Failure emits `distill_invoked` with `outcome: "validation_failed"` and throws `UsageError`.
242
+ 8. Quality gate (`processes.distill.qualityGate`, on unless disabled): one judge call, on distill's engine or the gate's own (`qualityGate.engine`), scores the lesson from 1 to 5 on **reusable**, **non-redundancy** and **grounding**. It is shown what the writer was shown: the source body the lesson was generated from (frontmatter stripped, first 3000 characters), the feedback that carries a reason or a note, and the lessons, knowledge notes and skills nearest the lesson (top 3, 600 characters each).
243
+ - **Reusable** asks for a rule an agent can use on another occasion, with the reason it holds; 1–2 for a lesson that only records what was done, shipped, decided or is pending, or how a system is set up now.
244
+ - **Non-redundancy** asks whether the lesson is new next to the assets listed, never the source memory (a lesson that restates its source is judged on the other two criteria); 1–2 when a listed asset already states the same rule, or most of what the lesson says in broader words; 5 when none is listed.
245
+ - **Grounding** asks whether every cause, step, number, rule and limit in the lesson is stated by the source or its feedback; 4–5 when each is, 3 when one stretches what the source says, 1–2 when any is in neither, when the lesson drops a limit the source states, or when it is about another subject. It is not part of the mean.
246
+ - A criterion at 2 or below is `quality_rejected` whatever the mean is (the reason names the criterion: `grounding 2/5: …`). Otherwise the lesson passes when reusable and non-redundancy both score 4 or more, and a mean of those two of 2.5 or more is `review_needed`. A pass is also queued for a person (step 9), so the rejection is the only thing that keeps a lesson from a reviewer: on 2026-10-05 the old rubric (novelty, non-redundancy, and a grounding that scored 3 for a lesson that "goes beyond the source") passed 10 of the 17 bad lessons, because a lesson that adds to its memory scored as novel. The old novelty question was also the reason the judge rejected a faithful lesson of a lesson-worthy memory. A distill with no source to read (an unindexed ref distilled "from feedback signal alone") gives the judge an empty source, so its lesson is expected to be rejected.
247
+ - `quality_rejected` writes an `improve_ledger` row for the input (30-day distill rejection window) and a `distill_invoked` event carrying `score`, the per-criterion `criteria` and the `reason`. It mints no proposal.
248
+ - `review_needed` mints a pending proposal stamped `deferred` / `quality-gate` for a human; the triage drain leaves it alone. It is the outcome for a non-passing mean of 2.5 or more when no criterion is at 2 or below, a judge that times out or returns something unparseable or incomplete, and a heuristic lesson-quality finding (for example an invalid `description`), which is found before the judge is called.
249
+ 9. Create proposal: `createProposal(stash, { ref: lessonRef, source: "distill", payload })` through `mintProposal`. A pass is minted `deferred` (gate `quality-gate`, reason `distill-review`) with the judge's per-criterion `scores` and `judgeReason`, for a person: the triage drain and its judgment tier leave it alone, and the improve ledger records `review_needed`. It is never `staged`. On 2026-10-05 the gate had staged 12 lessons and 10 were bad (they restated the memory, claimed what it does not say, or filed a dated status as a lesson); no judge score separated them from the two good ones. A promotion to knowledge that passes is deferred the same way. With `processes.distill.qualityGate` off nothing was judged, so the proposal is minted unstamped for the drain to decide.
250
+ 10. Emit `distill_invoked` event with `outcome: "queued"`.
251
+
252
+ **Updating a lesson the memory repeats (#1090):** when the lesson repeats one the library holds, distill extends that lesson instead of only dropping the duplicate. It happens in two places, both with the quality gate on: the judge scored the writer's lesson 2 or below on non-redundancy (step 8), or the writer answered `NONE` (step 5). Either way the update is tried for the related lessons (among the top 3 nearby assets, as for the judge, in the bundle the proposal is filed in) that the writer's `reason`, or the judge's, names by ref or name, or for the only related lesson when it names none. One lesson among several that merely sit near the memory costs no call. A second call shows the writer the memory, its feedback and the candidate lessons' bodies, and asks it to pick the lesson that already states the memory's rule, list in `new_facts` each cause, step, number, limit or consequence the memory states and the lesson lacks (the memory's own incident retold is not one), and return that lesson's whole body with a line added for each; it answers `decision: "none"` when no listed lesson states the rule, when `new_facts` is empty (an update with none is not proposed), or when the memory contradicts a line. Then:
253
+
254
+ - Every existing body line must still be there (compared by words, in any case and spacing). A body that drops or rewords one is not proposed.
255
+ - The extended lesson (existing frontmatter and the whole new body) is judged as one lesson, in one call, by step 8's judge with an edit variant of its rubric: **reusable** and **grounding** read the whole lesson (a line is grounded when the memory, its feedback or the lesson being extended states it), and **non-redundancy** is asked of each added line against the lesson being extended: 1–2 when it only says again what the lesson says (its rule or cause in other words, or the same rule on an example from the memory), 4–5 when it states a fact the lesson lacks, however specific. The lesson being extended is shown to the judge as such, not as an asset the new lesson might repeat. Judging the added lines alone as a new lesson failed `reusable` for a lone fact, and no update passed. A body that adds no line is not proposed; anything short of a judge pass is not proposed.
256
+ - The update is a pending proposal on the existing lesson's ref (`source: distill`), the lesson's frontmatter unchanged except that the memory is added to `xrefs`. Its `beforeHash` is the lesson's as read, so accepting it after the lesson changed is refused as stale. It is minted `deferred` (gate `quality-gate`, reason `distill-update`) with the judge's `scores` and `judgeReason`, so the triage drain and its judgment tier leave it for a person. The result and the `distill_invoked` event are `queued` with `proposalRef` set to the lesson and `updatesExisting: true`.
257
+ - When nothing is proposed, the outcome is what it was before: `quality_rejected` with the judge's reason after a duplicate lesson, `skipped` / `nothing_reusable` after `NONE`.
258
+
259
+ **Lesson-ref derivation rule:** `lessons/<type>-<name>-lesson` where `<type>-<name>` is derived from the input ref with origin stripped and non-alphanumeric characters replaced by `-`. Example: `skills/deploy` → `lessons/skill-deploy-lesson`. A lesson already at that ref is never overwritten (see step 4).
260
+
261
+ **What it writes:** one durable proposal row in `state.db`. Never writes asset files directly.
262
+
263
+ ### consolidate (akmConsolidate)
264
+
265
+ `akmConsolidate` runs during preparation, before session extraction and the
266
+ per-asset loop.
267
+
268
+ **Gate:** returns immediately (no-op result) if the selected strategy's
269
+ `processes.consolidate.enabled` is false.
270
+
271
+ **Phase A — Plan generation:**
272
+
273
+ 1. Load eligible non-`.derived` memory assets from the SQLite index, minus
274
+ those the improve ledger holds: a memory judged within its 7-day revisit
275
+ window and unchanged since, and a memory whose promotion was accepted or
276
+ rejected and whose body has not changed since (see **Re-eligibility**
277
+ below).
278
+ 2. Chunk memories using the selected strategy's configured limit. For each
279
+ chunk, call `chatCompletion` with the frozen consolidate LLM connection and
280
+ `CONSOLIDATE_SYSTEM_PROMPT`, requesting a JSON plan of `promote`
281
+ operations (the only op the schema offers; `merge`/`delete`/`contradict`
282
+ were removed in 0.9.17-alpha.1 at `e82eec811` — they had run in
283
+ production up to that commit, then were dropped because they cost
284
+ thousands of completion tokens).
285
+ 3. Parse and validate each op, then use `mergePlans` to deduplicate conflicts
286
+ across chunks.
287
+
288
+ **Phase B — Proposal emission:**
289
+
290
+ 1. For each promote op, perform idempotency checks and emit a reviewable
291
+ proposal with `source: "consolidate"`. The checks run cheapest first and
292
+ each records a skip reason: the same knowledge slug or a pending proposal
293
+ for it, an identical body already in `knowledge/` or already pending, and
294
+ then the coverage check (`consolidate/coverage.ts`, #998): the 20
295
+ `knowledge/` docs in the same bundle nearest to the memory by stored vector
296
+ (`getNeighborsByEntryId` scoped to that bundle's knowledge entries, the
297
+ fetch depth the pair pass uses; no similarity floor, only that rank) are
298
+ compared with it, and the memory is skipped as `dedup_covered_by_knowledge`
299
+ when one of them holds at least half of its distinct 5-word shingles. A
300
+ covering doc that ranks lower goes unseen, so the gate removes fewer
301
+ proposals than the rule does against every knowledge doc (122 of 224
302
+ rejected in #998's sample); its recall over the 20 is unmeasured. The rule
303
+ and its evidence are in the module's header comment. With no stored vector
304
+ (semantic search off, or the memory not yet indexed) the check does
305
+ nothing and nothing throws.
306
+ 2. Record what happened in the improve ledger (see **Re-eligibility** below):
307
+ a memory the model judged and left alone, or that a skip reason turned
308
+ away, gets a `judged_no_action` row; a promotion that failed to persist
309
+ gets none, so the next run retries it.
310
+
311
+ **What it writes:**
312
+ - A durable row in the `proposals` table in `state.db` for each emitted
313
+ `promote` op, partitioned by bundle path.
314
+ - An `improve_ledger` row for each memory the pass judged, except a promotion that failed to persist.
315
+
316
+ **Re-eligibility (#998):** the promote pass keys its ledger row by the source
317
+ memory (`memories/<name>`). A memory the model saw and left alone
318
+ (`judged_no_action`) or one with a pending proposal is revisited after 7 days,
319
+ or at once when the file is edited. An accepted or rejected promotion is
320
+ different: the row carries no `next_eligible_at`, and records the body hash
321
+ (`contentHash(_, "body")`, frontmatter excluded) the promotion was decided
322
+ against, taken from the proposal's `promotionSourceHash`. The memory is
323
+ selected again only when its current body hash differs, the same content-driven
324
+ rule the pair pass uses. A rejection is therefore no longer re-asked after 7
325
+ days, and the memory of an accepted promotion that is still on disk (O1 could
326
+ not archive it, or the same text was restored) is no longer eligible again at
327
+ once. A promotion decided before the hash was recorded (an older release's
328
+ proposal) has no hash to compare, and keeps the old windows. An expired
329
+ proposal still waits one day: nobody judged the text.
330
+
331
+ **Promotion retires its source (O1, alpha.9):** when an `akm proposal accept`
332
+ promotes a consolidate `promote` proposal — by a person or by triage
333
+ auto-promotion — it archives the source memory, and its `.derived` twin if
334
+ one exists, through the same path the pair pass uses below
335
+ (`archiveCleanupCandidate`, reason `promoted`, `successorRefs` the new
336
+ knowledge ref). A promotion no longer leaves a memory/knowledge duplicate
337
+ behind. This runs inside `promoteProposal`, not inside `akmConsolidate`
338
+ itself; a failure to archive the source only warns, it does not undo the
339
+ promotion.
340
+
341
+ ### consolidate pair pass (alpha.9)
342
+
343
+ A second pass inside `akmConsolidate`, alongside Phase A/B above, over the
344
+ same enabled gate. It replaces nothing the promote pass does; it adds
345
+ duplicate, subsumed and superseding retirement, review-gated.
346
+
347
+ 1. **Initiators:** memory (base or `.derived`), flat `knowledge/` or lesson
348
+ assets in the bundle the run writes to, in the retrieval scope, and
349
+ content-eligible: no prior pair-pass ledger row (source
350
+ `consolidate-pair`, kept apart from the promote pass's `consolidate`
351
+ rows), or a row whose recorded body hash differs from the asset's current
352
+ one. Eligibility is purely content-driven — the ledger row carries no
353
+ `next_eligible_at` timer at all for this source, so nothing here "expires"
354
+ on a schedule; see step 5.
355
+ 2. **Candidates:** each initiator's nearest neighbours by stored vector
356
+ (`getNeighborsByEntryId`, fetching 20 and keeping the first 5 that clear
357
+ every filter below — a few self/twin/bundle/tier misses in a naive top-5
358
+ used to starve an initiator down to zero real candidates), same bundle,
359
+ memory tier only (structured knowledge in subfolders is excluded), minus
360
+ its own `.derived` twin or parent, at cosine >= `T_PAIR` (0.93). An
361
+ initiator with no prior ledger row needs >= `BACKFILL_FLOOR` (0.95)
362
+ instead, UNLESS it is new material — git first-added within
363
+ `NEW_MATERIAL_DAYS` (7) — which judges at the ordinary `T_PAIR`: new
364
+ content earns the same scrutiny as an edit, not the backlog's higher bar.
365
+ "Older"/"newer" for the judge's own A/B labelling comes from one
366
+ `git log --reverse -M --diff-filter=AR --name-status --format=@%ct` per
367
+ run over the whole bundle (first-add time per path, following renames —
368
+ an `A` sets a path's first-add, an `R` carries the old path's first-add
369
+ to the new one, so a rename never misdates a file as newly added), not
370
+ frontmatter or file mtime; mtime is only the fallback for a path git does
371
+ not track, or a bundle with no `.git` directly inside it. At most
372
+ `MAX_PAIRS_PER_RUN` (300) pairs a run, admitted a whole initiator at a
373
+ time rather than by flat cosine rank across all of them: new-or-changed
374
+ initiators first, then the existing backlog by its own best cosine, each
375
+ admitted only if every one of its own candidate pairs fits in what
376
+ remains of the 300 (first-fit, so a smaller initiator further down still
377
+ fits when a larger one ahead of it does not) — an initiator with a
378
+ pending-blocked pair is skipped entirely rather than spending any of the
379
+ budget on a pair that cannot be judged yet.
380
+ 3. **Judge:** one LLM call per pair (`src/assets/prompts/consolidate-pair.md`)
381
+ through the consolidate process's own engine and concurrency. It first
382
+ lists the durable claims each side alone holds (`onlyInA`, `onlyInB`),
383
+ then labels the pair one of `duplicate`, `subsumed`, `supersedes`,
384
+ `contradicts`, `overlap` or `unrelated`. Each side is shown up to 12,000
385
+ body characters.
386
+ 4. **Outcome:** `duplicate`, `subsumed` and `supersedes` mint one `retire`
387
+ proposal (source `consolidate-pair`, its own generator — see below) for
388
+ the side that does not survive — never a `captureMode: hot` memory, never
389
+ a `.derived` memory whose parent still exists, never across bundles,
390
+ never when either side already has a pending retire proposal (as the
391
+ retired ref or its successor), and never retiring or reusing as a
392
+ successor an asset already spent earlier in the SAME run (the durable
393
+ half of this last guard is at accept time — step below), and never
394
+ retiring a side the judge listed a claim for. `contradicts`,
395
+ `overlap` and `unrelated` are recorded `judged_no_action` with no
396
+ proposal; `contradicts` is counted in the run report and stays a human
397
+ decision. A retirement (`duplicate`, `subsumed` or `supersedes`) whose
398
+ retired side has nothing listed (a `duplicate` must also have nothing listed
399
+ on the kept side; a subsumed or superseding successor may hold more) and no continuity risk gets a second call
400
+ (`consolidate-pair-check.md`: what does the retired note hold that the kept
401
+ one lacks?); an empty answer stages the proposal (gate `consolidate-pair`,
402
+ reason the judge's label), and the triage drain accepts it under its usual
403
+ `applyMode`. Replay precision was 336/360 (duplicate 0.98, subsumed 0.92,
404
+ supersedes 0.875). Every other retire proposal waits for a person.
405
+ 5. **Ledger:** a row is written for an initiator only once every one of its
406
+ own candidates was admitted this run (whole-initiator admission in step 2
407
+ makes this an all-or-nothing membership check) AND actually resolved to a
408
+ verdict — a same-run guard dropping a retire-worthy verdict (the chain
409
+ guard in step 4), a failed mint, a failed or never-sent judge call, all
410
+ count as unresolved, not judged — recording its current body hash and an
411
+ outcome of `proposed` or `judged_no_action`. Neither outcome carries a
412
+ timer for this source: only a later body-hash mismatch (step 1) makes the
413
+ initiator eligible again. An initiator left with any candidate not
414
+ admitted, not judged, or dropped gets NO row at all, so the next run
415
+ reconsiders it rather than treating it as settled — real data: night 1
416
+ alone judged 177 `duplicate` verdicts into only 59 proposals before this,
417
+ the other 118 silently abandoned by the same-run chain guard.
418
+
419
+ Because that "no row" case regenerates ALL of an initiator's candidate
420
+ pairs next run — including ones already decided — a pair the owner
421
+ already declined (rejected, or accepted then reverted) is never re-minted
422
+ while both sides are unchanged: before the judge is ever called, the same
423
+ ref pair and the same two content hashes (checked in both orientations,
424
+ since it is the judge — not yet run — that would decide which side is
425
+ "retired") are checked against every rejected or reverted
426
+ `consolidate-pair` proposal on record, and a match is counted as a settled
427
+ no-action (so the initiator's row still gets written this run) at no LLM
428
+ cost at all. Those keys follow each proposal's current status: `akm
429
+ proposal reopen` (#997) moves a rejected proposal back to `pending`, so it
430
+ is no longer settled, and while it is pending a pair pass leaves both of its
431
+ documents alone (step 4's pending-retire guard) instead of minting the
432
+ pair a second time.
433
+
434
+ **Retire proposals (`akm proposal accept`/`revert`):** minted under their own
435
+ source, `consolidate-pair` — kept apart from the promote pass's
436
+ `consolidate` proposals so a bulk `accept`/`reject --generator consolidate`
437
+ never sweeps a retirement, and the reverse (`--max-diff-lines` counts a
438
+ retire proposal by its target's own current line count, not its empty
439
+ payload). Accepting one first confirms the decision is still fresh — the
440
+ successor still exists, and both sides' recorded body hashes still match
441
+ their current files — refusing cleanly, not partially applying a decision
442
+ something else (an intervening accept, possibly an A→B/B→C chain) made
443
+ stale. It then moves the retired asset (and its `.derived` twin) into the
444
+ existing `.akm/memory-cleanup/archive/` through the generalized
445
+ `archiveCleanupCandidate`, the one retirement encoding (D27) — never
446
+ `.akm/archive/`. A `supersedes` proposal first writes the supersede edge on
447
+ the older asset (`writeSupersededEdge`), then archives it. `akm proposal
448
+ revert` restores the archived file(s), and for the primary asset restores
449
+ the EXACT pre-retire bytes recorded at accept (`backupContent`) rather than
450
+ re-deriving "undo the edge" from the archived copy — so a pre-existing
451
+ human-written edge, YAML comments and key order all survive the round trip.
452
+ Accept records its full intent — `backupContent` and which file is about to
453
+ move — on the still-pending proposal before moving anything; a crash at any
454
+ point after resumes from that recorded intent (skipping whichever file a
455
+ tombstone scan shows an earlier, crashed attempt already archived under this
456
+ proposal's own id) rather than refusing it as stale or re-deriving
457
+ `backupContent` by guessing at what the archived copy would have been.
458
+ Revert works the other way for the same reason: each archive dir's own
459
+ tombstone already names its original and archived paths, so "original
460
+ present, archived copy missing" resumes as an earlier, crashed revert's own
461
+ work — UNLESS that original path's current content does not match the
462
+ `retirement.retiredContentHash` recorded at accept, meaning the path was
463
+ reused by an unrelated file since, which refuses instead of overwriting it.
464
+ Triage auto-accepts a `retire` proposal only when the pair pass staged it
465
+ (gate `consolidate-pair`, see step 4 above) and `applyMode` allows it; every
466
+ other one waits for a person. Review reuses `akm proposal list --generator consolidate-pair` (S4: the
467
+ backlog is reviewed as its own list, not mixed in with every other
468
+ generator's proposals), `show`, `diff` (which renders a retirement as the
469
+ retired file's lines leaving under a `retire` header, with the pair's verdict
470
+ and a note that accept archives and revert restores — not as the file replaced
471
+ by a blank one, #997), and bulk
472
+ `accept --generator consolidate-pair` / `reject --generator
473
+ consolidate-pair`. A proposal carrying `continuityRisk` (below) is excluded
474
+ from that bulk accept, whatever the generator or `--yes` — visible inline in
475
+ `list`'s default output and in the text output of `show` and `diff` (the
476
+ specific failing/unverified queries, not just a count) — bulk reject is unaffected,
477
+ and a person can always accept one by id. A rejection is not final:
478
+ `akm proposal reopen <id>` puts a rejected retire proposal back to `pending`
479
+ (refused if the retired document, or the successor, changed since the pair was
480
+ judged, or if another pending retire proposal now involves either of them),
481
+ and accepting it then archives the file exactly as for any other retire
482
+ proposal.
483
+
484
+ ### Retirement continuity check (rule R3)
485
+
486
+ Before the pair pass mints a `retire` proposal, it replays up to five of the
487
+ retired asset's own past `search`/`curate` queries
488
+ (`loadRetrievalQueries`, `src/commands/improve/retrieval-gate.ts` — the same
489
+ cleaned set the retrieval regression gate replays: `usableRetrievalQueries`
490
+ drops stash-README boilerplate and harness/tool envelopes
491
+ (`nonTaskInput`), pastes over 2,000 characters, and queries that are
492
+ duplicates once whitespace is collapsed, S3a) through akm's own search,
493
+ in-process — the same ranking a user gets, no LLM
494
+ (`src/commands/improve/consolidate/continuity-check.ts`). For every query
495
+ where the retired asset ranked in the top 10, the successor must rank in the
496
+ top 10 too — compared directly (N2): search itself returns at most the top
497
+ 10 hits, so `rankOf` finding the successor among them or not is the whole
498
+ comparison, no generic rank-change-report abstraction needed. No recorded
499
+ queries means no check and no flag — and so does a retired/successor pair
500
+ whose bodies are
501
+ content-identical once whitespace is collapsed (S3b): search's own
502
+ content-dedupe (`src/indexer/search/db-search.ts`) already hides the
503
+ successor behind the retired asset for every such query, so a "successor
504
+ missing" finding there would not be a real risk, just that dedupe working
505
+ as designed.
506
+
507
+ A failing query does not block the mint — it flags. The proposal's
508
+ `retirement.continuityRisk` records the failing query count and, per failing
509
+ query, the retired asset's rank and the successor's (`null` when the
510
+ successor did not rank in the top 10 at all). This is the forgetting-safety
511
+ lane's old purpose (see below), now measured against search rank instead of
512
+ a stash-wide salience rank, and only at the moment an asset would actually
513
+ stop resolving.
514
+
515
+ A query that never ran (the search call threw) or that fell back to
516
+ keyword-only ranking (`mode: "fts-fallback"`) is "unverified" (S2): dropped
517
+ from the rank comparison — its hits are not the ranking a user actually
518
+ gets, so they are never compared — but tracked in
519
+ `retirement.continuityRisk.unverifiedQueries`, and on its own enough to set
520
+ `continuityRisk` even when every verified query passed. A search failure or
521
+ fallback must never look like "no risk found." `createContinuitySearch`
522
+ (`src/commands/improve/consolidate/continuity-check.ts`) is stateful across
523
+ one pair-pass run: the first `fts-fallback` it sees forces
524
+ `semanticSearchMode: "off"` for every later query that same run, so a down
525
+ embedding endpoint pays its failed-connection cost once per run, not once
526
+ per remaining query.
527
+
528
+ **The retired forgetting-safety lane.** Before alpha.9, `scoreSalience`
529
+ compared the whole stash's salience ranking before and after every improve
530
+ run, and a ref that fell from the top 200 to below 500 was injected into that
531
+ run's work set under `eligibilitySource: forgetting-safety`
532
+ (`applyForgettingSafety`, `improve_salience_rank_change` event). It was a
533
+ one-time cutover guard from the June 2026 ranking-formula change, running on
534
+ every run since. R5's 30-day event window (2026-08-30 to 2026-09-29, 47
535
+ `improve_salience_rank_change` events, 5 refs flagged across 4 runs) found no
536
+ marginal pick over the simpler baseline: 4 of the 5 flagged refs were also
537
+ picked that same run by the signal-delta lane, and the 5th has no
538
+ `reflect_invoked`/`distill_invoked` event in the retained history, but that
539
+ run's `improve_runs.plannedRefs` shows it, too, was planned under
540
+ `signal-delta` — just not reflected (a dispatch/budget limit that run, not a
541
+ lane-exclusive pick). All 5 flagged refs were signal-delta picks; zero were
542
+ ever forgetting-safety-only. It also protected `asset_salience.rank_score`,
543
+ which only improve itself ever read. Both the per-run comparison and the
544
+ injection are gone; so is `buildRankChangeReport` itself (N2 — the
545
+ continuity check compares ranks directly, and nothing else called it).
546
+ `forgetting-safety` stays a valid `eligibilitySource`/event-type value so
547
+ old proposals and events still decode.
548
+
549
+ ### Archive purge sweep (step 8)
550
+
551
+ A retirement's archived bytes are not deleted when it is accepted — only the
552
+ tombstone (`cleanup.md`) resolves the ref from then on. `purgeGracedArchive`
553
+ (`src/commands/improve/memory/memory-improve.ts`) deletes them later,
554
+ deterministically and with no LLM, once at the very start of every
555
+ `akm improve` run — before index bootstrap, before triage. For a git-backed
556
+ bundle it walks `.akm/memory-cleanup/archive/`, and for every tombstone whose
557
+ `retiredAt` is more than `RETIRE_GRACE_DAYS` (30) days old AND whose
558
+ directory is entirely git-tracked and clean (one `git ls-files` plus one
559
+ `git status --porcelain -uall` per sweep, not per directory — B1), deletes
560
+ the archived asset file(s) under that tombstone's own directory — never
561
+ `cleanup.md` itself; git history keeps the bytes (D27). `.git` presence
562
+ alone does not prove a retirement was ever committed: `akm proposal accept`
563
+ only commits for a `kind: "git"` write target, and improve's own auto-sync
564
+ only stages the paths that same run wrote, so a directory with even one
565
+ untracked or modified file (the tombstone included) is left whole for a
566
+ later sweep rather than losing the only surviving copy of it. A
567
+ memory-cleanup family-prune archive (not a pair-pass retirement) has no
568
+ `retiredAt` in its tombstone at all, so this sweep never touches that older
569
+ archive class.
570
+
571
+ Every deleted file is journaled individually
572
+ (`src/core/write-provenance.ts`), the same mechanism the archive move itself
573
+ uses, so the end-of-run auto-sync commits the deletion in the run's own
574
+ commit. A bundle with no `.git` of its own is left untouched — there is
575
+ nothing to fall back on if a delete turns out to be wrong — and `akm health`
576
+ reports its archive's size and file count instead (`memory-cleanup-archive`
577
+ advisory, `src/commands/health/archive-usage.ts`), silent whenever the bundle
578
+ is git-backed or the archive is empty or absent.
579
+
580
+ ### improve-owned maintenance
581
+
582
+ After consolidation completes, `akmImprove` runs maintenance steps that own the
583
+ remaining live-write memory/index artifacts previously coupled to indexing.
584
+
585
+ **Memory inference:**
586
+
587
+ 1. Collect the memory refs that completed distill without being promoted to
588
+ `knowledge:` in the same improve run.
589
+ 2. Call `runMemoryInferencePass` with those refs.
590
+ 3. If the pass writes derived memories or marks parents with
591
+ `inferenceProcessed: true`, call `reindexFn({ stashDir })` so SQLite/search
592
+ state reflects the new disk state before any later steps run.
593
+
594
+ ### Proposal queue
595
+
596
+ `createProposal` is the write point used by reflect, distill, consolidate (promote), extract, schema repair and `akm proposal new`; the pair pass's retire proposals go through `createRetireProposal` instead. Both write the canonical `proposals` table in `state.db`; rows are partitioned by `stash_dir`, and pending/accepted/rejected/reverted are statuses on the same durable record (`akm proposal reopen` moves a rejected row back to pending). The retired `<stash>/.akm/proposals/` tree is neither read nor written.
597
+
598
+ **Logical proposal shape:**
599
+
600
+ ```json
601
+ {
602
+ "id": "<UUID>",
603
+ "ref": "lessons/skill-deploy-lesson",
604
+ "status": "pending",
605
+ "source": "reflect",
606
+ "sourceRun": "reflect-1715000000000",
607
+ "createdAt": "2026-05-11T00:00:00.000Z",
608
+ "updatedAt": "2026-05-11T00:00:00.000Z",
609
+ "payload": {
610
+ "content": "---\ndescription: ...\n---\n\nbody",
611
+ "frontmatter": { "description": "..." }
612
+ }
613
+ }
614
+ ```
615
+
616
+ Two proposals can share the same `ref`; their UUID primary keys prevent collisions. Nothing in the loop scans the queue before it runs reflect or distill. The improve ledger keeps a stage from proposing the same ref again: an attempt that left a pending proposal is revisited after 7 days, or sooner when feedback newer than the attempt arrives (see **Ledger pre-filter (signal delta)** below).
617
+
618
+ ## Scope restrictions
619
+
620
+ `akm improve` reads and writes one bundle, its write target: `--bundle`, else `defaultWriteTarget`, else the working bundle. The candidate set is that bundle's assets and nothing else, because every proposal is filed in the write target. An asset that another writable bundle owns is never planned: reflect would read it from its own bundle and the proposal would land in this one, either forking the asset as a `create` or overwriting this bundle's copy with content taken from the other's (#1000). A bare ref scope (`akm improve skills/x`) resolves inside the write target only, and distill's memory-to-knowledge promotion merges only with a doc that already exists there. To improve another writable bundle, select it with `--bundle team` or a bundle-qualified scope such as `team//skills/code-review`; a scheduled run therefore covers only its write target, and each other bundle needs its own scheduled `akm improve --bundle <name>` run. A dry run (`--dry-run`) resolves the bundle the way a live run does, with the working bundle starting from `AKM_BUNDLE_DIR` before `defaultBundle`, so it previews the bundle a live run improves. As a second line of defence, `createProposal` refuses a proposal whose `itemRef` names an asset owned by a different configured bundle than the queue's.
621
+
622
+ `akm improve` and `akm lint` only operate on writable bundle sources (sources with `writable: true`). Read-only sources (git, npm, website) are excluded from the candidate set before any other filtering; a read-only write target plans nothing.
623
+
624
+ ## Ledger pre-filter (signal delta)
625
+
626
+ Every stage reads the improve ledger (`improve_ledger` in `state.db`, one row per `(stash_dir, ref, source)`; `source` is `reflect`, `distill`, `consolidate`, `consolidate-pair`, `extract`, `schema-repair` or `propose`, the last written by `akm proposal new` through `createProposal`) before it spends a model call, and writes it after. It replaced the per-stage cooldown constants and the event reads behind them (`reflect_invoked`, `distill_invoked`, `consolidate_completed`, rejected-proposal rows, `proposal_fingerprints`). When a ref may be tried again is decided in one place, `nextEligibleAt`, from the row's source and outcome: a rejected or quality-rejected attempt waits 14 days (reflect), 30 days (distill) or 7 days (any other source), an expired proposal 1 day, and an attempt that left a pending proposal, needed review, judged no action or changed nothing is revisited after 7 days; an accepted or failed attempt has no window. The consolidate pair pass and a decided consolidate promotion have no clock at all: their rows hold a body hash and the ref waits until its body differs (see **Re-eligibility** above). `rejected`, `quality_rejected` and `expired` are hard windows; every other window is a revisit cadence that a newer signal (new feedback, or for consolidation an edit) lifts (`isLedgerBlocked`).
627
+
628
+ **Selection.** Before the per-asset loop, `buildSnapshotManifest` reads the feedback events once and the ledger's `reflect` and `distill` rows once for every candidate, and `partitionBySignalDelta` (both in `preparation.ts`) sorts the refs. Reflect *passes* a ref when it has negative feedback, and distill when it has feedback carrying a signal or a note (a positive one included): dated within the last 30 days, newer than that stage's last ledger attempt for the ref, with no hard window blocking it. A positive or note-only signal never plans a reflect, and neither does a negative that came with an exact fix (`akm feedback --replace/--with`, `--outdated`, `--superseded-by`) once a `feedback` proposal accepted since its event has applied the fix: it is already acted on. Then:
629
+
630
+ - A ref that passes reflect is planned for the loop. If it does not pass distill, distill is skipped for it (a `distill-skipped` action and an `improve_skipped` event with reason `distill_no_new_signal`).
631
+ - A ref that passes only distill, and is a distill candidate, is planned distill-only.
632
+ - A ref with no in-window feedback and no reflect window is left to the proactive-maintenance lane, which picks only what retrieval returned or new material (see [Retrieval scope](../improvement.md#retrieval-scope)) and plans its picks with the feedback-bearing refs. The cap against a proposal flood: `maxPerRun` (15) picks per run, an asset due only after `dueDays` (30) without a reflect or distill, and reflect's `limit` (25).
633
+ - Every ref the loop does not take is counted in the plan's `signal` gate (or its `retrieval` gate, when the proactive lane could not pick it for lack of usage evidence) and reported once, in aggregate, as an `improve_skipped` event (`no_new_signal`, `not_retrieved`).
634
+ - A memory flagged wrong is not distilled: when a negative feedback in the 30-day window recorded the hash of the body it judged (`contentHash`, the `body` hash of `content-hash.ts`) and the file's body still has it, the loop skips distill for it (a `distill-skipped` action and an `improve_skipped` event with reason `distill_flagged_wrong`) and records the attempt in the ledger as `unchanged`, so the ref waits for newer feedback. Reflect still plans it. A write that leaves the body alone (an inference stamp, a frontmatter repair) does not lift the flag; changing the body does. Feedback recorded without a hash keeps the earlier test: it flags the memory while it is newer than the file's last write, whose modification time is the edit signal, as for the retrieval scope's new material.
635
+ - A memory whose only feedback in the 30-day window is positive without a reason or note (`hasOnlyBarePositiveFeedback` in `preparation.ts`) is not distilled either: the loop skips distill for it (a `distill-skipped` action with the reason "only positive feedback, without a reason" and an `improve_skipped` event with reason `distill_positive_without_reason`) and records the attempt in the ledger as `unchanged`, so it waits for newer feedback. A bare `--positive` only records that a note helped, which gives the writer nothing to distil: 10 of the 11 lessons made from such a memory were rejected on 2026-10-05, and the 11th was good. A reason, a note or a negative signal among the feedback, or an explicit ref scope, lets it through.
636
+ - A memory whose frontmatter says `beliefState: deprecated` or `superseded` (`isDeprecatedOrSuperseded` in `preparation.ts`) is not distilled either: the loop skips distill for it (a `distill-skipped` action with the reason "marked deprecated or superseded" and an `improve_skipped` event with reason `distill_deprecated_or_superseded`) and records the attempt in the ledger as `unchanged`. An explicit `--scope` ref still runs. `contradicted` is not skipped: it marks an open conflict, not a retired note.
637
+ - The loop's refs are ranked by salience (`scoreSalience` in `preparation.ts`, which also scores the lane's picks and computes each vector with `computeSalience` from `salience.ts`): encoding, outcome, and retrieval frequency and recency, discounted for file size, with a ref that was repeatedly skipped as a no-op ranked lower. Refs missing on disk are dropped, and `--limit` cuts the list: reflect-path refs first, then distill-only refs.
638
+
639
+ An explicit ref scope bypasses every gate. Consolidation, extract and schema repair read their own ledger sources in their own stages.
640
+
641
+ ## Strategy process configuration
642
+
643
+ | Process | Config path | Controls |
644
+ |---|---|---|
645
+ | `distill` | `improve.strategies.<name>.processes.distill` | Enables distillation and selects its engine/model/request overrides. |
646
+ | `consolidate` | `improve.strategies.<name>.processes.consolidate` | Enables consolidation and selects its engine/model/request overrides. |
647
+
648
+ Improve process selection is resolved once by `resolveImprovePlan`; the plan
649
+ contains every process's frozen enablement, process config, and resolved runner.
650
+ A process runs on any engine that confines the model-work tool policy; one that
651
+ cannot is refused when the plan is built, never replaced by another engine.
652
+
653
+ ## Output shape
654
+
655
+ `AkmImproveResult` uses `schemaVersion: 2`, identifies the selected `strategy`,
656
+ and can report `ok: false` for terminated runs:
657
+
658
+ | Field | Type | Description |
659
+ |---|---|---|
660
+ | `scope` | `{ mode, value? }` | Resolved scope (`all`, `type`, or `ref`). |
661
+ | `dryRun` | `boolean` | Whether this was a dry run. |
662
+ | `guidance` | `string?` | Human-readable note about memory cleanup when memories are in scope. |
663
+ | `memorySummary` | `{ eligible, derived }` | Count of memory assets in scope and count of `.derived` ones. |
664
+ | `memoryCleanup` | `ImproveMemoryCleanupResult?` | Analysis (always present when eligible > 0) merged with apply results on a live run. Includes `archived`, `transitionLogPath`, `transitionLogEntries`, and `warnings`. |
665
+ | `plannedRefs` | `ImproveEligibleRef[]` | The post-filter, post-cleanup, salience-ranked refs that were (or would be) processed. |
666
+ | `actions` | `ImproveActionResult[]?` | Per-asset action records `{ ref, mode, result }`, where `mode` is an `ImproveActionMode` (`src/core/improve-types.ts`). A run emits `reflect` (a reflect proposal was queued), `reflect-skipped` (the strategy filtered the ref, or reflect declined it as `unsupported_type` or `no_change`), `reflect-guard-rejected` (a content-policy guard rejected the rewrite), `reflect-failed` (any other reflect failure, a quality rejection included), `distill` (the distill result, a validation failure included), `memory-prune` (a memory archived by cleanup), `memory-inference` (one `memories/_inference` row for the inference pass) and `error` (a loop failure other than a distill validation failure, or the wall-clock budget running out). `distill-skipped` is also emitted per ref, but `foldDistillSkipped` moves those rows into `distillSkipped` before the result is built, so none appears here. `reflect-cooldown` stays in the union for older rows and counters; nothing emits it. Absent on dry-run. |
667
+ | `distillSkipped` | `{ total, byReason, samples }?` | The folded `distill-skipped` actions: their total, a count per skip reason, and up to three sample refs per reason. Omitted when none were skipped. |
668
+ | `validationFailures` | `Array<{ ref, reason }>?` | Refs skipped due to pre-run validation failures (missing file, missing description). |
669
+ | `consolidation` | `ConsolidateResult?` | Result from `akmConsolidate`; omitted when `processed === 0` and no warnings. |
670
+ | `memoryInference` | `MemoryInferenceResult?` | Improve-owned post-consolidation memory inference telemetry. |
671
+
672
+ ## Consolidation Skip Reason Taxonomy
673
+
674
+ The promote pass records a structured skip for each memory the model proposed to promote and the pass then declined to queue. They appear in `consolidation.skipReasons` of the improve result as `{ ref, skips: [{ op: "promote", reason }] }`, one entry per memory (`ConsolidateResult`, `src/core/improve-types.ts`); `emitPromotionProposal` in `src/commands/improve/consolidate.ts` is the only producer. Each skip also adds a warning line and moves the memory from `judgedNoAction` into the skip count, which keeps the accounting invariant `processed == promoted + judgedNoAction + Σ(skipReasons) + failedChunkMemories`. A skipped memory is written to the ledger as `judged_no_action` (revisited after 7 days, or at once on an edit), except a memory already promoted this run, which keeps the `proposed` row its proposal wrote, and `promote_create_failed`, which leaves no row so the next run retries it. The checks run in this order and the first that applies wins:
675
+
676
+ | Reason | Meaning |
677
+ |--------|---------|
678
+ | `promote_already_promoted_this_run` | Defensive guard: the memory was already promoted earlier in this run. `mergePlans` leaves one operation per memory, so it should not fire. |
679
+ | `promote_pending_proposal_exists` | A pending proposal already exists for the target `knowledge/<slug>`. Clears as triage drains the queue. |
680
+ | `promote_already_exists` | `knowledge/<slug>.md` already exists in the write target. Normal steady-state noise. |
681
+ | `promote_read_failed` | The memory file could not be read. |
682
+ | `promote_sanitization_failed` | The memory could not be re-serialized as an asset (an unbalanced code fence, missing or malformed frontmatter, frontmatter that is not valid YAML or not a mapping); the warning carries the reason code. |
683
+ | `promote_superseded` | The memory has `status: superseded`, which is not promotable knowledge. |
684
+ | `promote_source_too_small` | The memory's body is under 100 characters, too short to make useful knowledge. |
685
+ | `dedup_existing_knowledge` | A `knowledge/` doc in the write target already has an identical body (frontmatter aside). |
686
+ | `dedup_pending_proposal` | A pending consolidate proposal already carries an identical body. Clears as triage drains the queue. |
687
+ | `dedup_covered_by_knowledge` | A `knowledge/` doc among the 20 nearest to the memory in its bundle already holds at least half of its 5-word shingles (#998), so promoting it would queue a near-copy. |
688
+ | `promote_invalid_frontmatter` | The description it would use (the model's, else the memory's own) is missing or truncated. |
689
+ | `promote_dedup_window` | The target slug is a variant of a pending consolidate proposal's slug (dates, counters and word order folded), so it would queue a near-copy. |
690
+ | `promote_create_failed` | `createProposal` threw. The memory gets no ledger row, is retried next run, and is counted in `failedPromotions`. |
691
+
692
+ The dedup and eligibility reasons are ordinary; `promote_read_failed`, `promote_sanitization_failed`, `promote_invalid_frontmatter` and `promote_create_failed` point at a broken memory or a failing write and are worth a look when they recur. The consolidate `merge`, `delete` and `contradict` operations, and the `merge_*` and `captureMode_hot_refused` skip reasons that went with them, were removed in 0.9.17-alpha.1 (see Phase A above); the promote pass emits none of them. The pair pass (`consolidate-pair`) reports through its own result, not through `skipReasons`.
693
+
694
+ ## Reviewed
695
+
696
+ Reviewed against `src/commands/improve/improve.ts`,
697
+ `src/commands/improve/reflect.ts`, and `src/commands/improve/distill.ts`.
698
+
699
+ **Checked:**
700
+ - Diagram branch ordering for lock vs. scope resolution
701
+ - Diagram branch ordering for dry-run early-return
702
+ - Validation sweep placement relative to the limit filter
703
+ - Consolidation placement relative to the per-asset loop
704
+ - Maintenance placement relative to consolidation and reindex
705
+ - Per-asset loop branch ordering (validation skip vs. budget check)
706
+ - Memory cleanup step sequencing (analyzeMemoryCleanup, applyMemoryCleanup, reindexFn)
707
+ - Strategy gating for consolidation (`improve.strategies.<name>.processes.consolidate.enabled`)
708
+ - Dry-run early-return node completeness
709
+ - Budget-exhausted break path
710
+ - Mermaid syntax and subgraph labels
711
+
712
+ **Fixed:**
713
+
714
+ 1. **`analyzeMemoryCleanup` placement (critical accuracy bug):** The original diagram showed `analyzeMemoryCleanup` happening after lock acquisition (`E3 → F → G → H{memoryCleanup eligible?} → I`). In the actual code (`improve.ts` lines 251–253), `memoryCleanupPlan` is computed unconditionally before the `dryRun` check (line 259) and before lock acquisition (line 272). Moved `analyzeMemoryCleanup` to before the `dryRun?` diamond, and updated the DRY node to note it includes the pre-computed analysis.
715
+
716
+ 2. **Per-asset loop (critical accuracy bug):** The loop no longer checks validation failures. A ref that fails the pre-run validation sweep (and is not repaired) is excluded before selection (`validationFailureRefs` in `runImprovePreparationStage`, `preparation.ts`), so the first check in `runImproveLoopStage` is the wall-clock budget. The diagram's `S{ref in validationFailures?}` branch, which had also reused the `SKIP` node id of the lock-held exit, is gone.
717
+
718
+ 3. **Preparation order (accuracy bug):** `runImprovePreparationStage` (`preparation.ts`) runs consolidation, session extract, memory cleanup (`applyMemoryCleanup`, `projectMemoryCleanup`, then the `memory-prune` actions and `reindexFn` when something was archived), the validation sweep and schema repair, and only then selection (signal delta, fallback lanes, salience ranking, disk check, `--limit`). The diagram follows that order.
719
+
720
+ 4. **Post-loop maintenance placement (accuracy bug):** Improve now runs memory inference after consolidation, not before it (the same fix originally applied to graph extraction, retired in 0.9.17-alpha.9). The workflow now documents the maintenance stage and the reindex after inference writes.