akm-cli 0.9.30 → 0.9.31

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 (161) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +10 -10
  3. package/ROADMAP.md +112 -0
  4. package/dist/commands/improve/improve-cli.js +33 -12
  5. package/dist/core/file-lock.js +10 -1
  6. package/dist/llm/client.js +33 -3
  7. package/dist/scripts/akm-migrate-node.js +5 -1
  8. package/dist/scripts/akm-migrate.js +5 -1
  9. package/docs/README.md +42 -41
  10. package/docs/agents/README.md +15 -0
  11. package/docs/agents/agent-install.md +14 -0
  12. package/docs/architecture/README.md +67 -0
  13. package/docs/architecture/adapters.md +272 -0
  14. package/docs/architecture/akm-architecture-decision-history.md +2055 -0
  15. package/docs/architecture/akm-core-principles.md +126 -0
  16. package/docs/architecture/architecture.md +579 -0
  17. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +550 -0
  18. package/docs/architecture/decisions/0001-no-interpolation-attached-structured-context.md +72 -0
  19. package/docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md +142 -0
  20. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +189 -0
  21. package/docs/architecture/decisions/0004-task-input-contract-and-flag-coercion.md +97 -0
  22. package/docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md +108 -0
  23. package/docs/architecture/decisions/0006-task-source-version-routing.md +124 -0
  24. package/docs/architecture/decisions/0007-workflow-composition-bounds.md +83 -0
  25. package/docs/architecture/decisions/0008-task-binding-normalization.md +88 -0
  26. package/docs/architecture/decisions/0009-child-run-publication-column-parity.md +80 -0
  27. package/docs/architecture/decisions/0010-driver-protocol-cut.md +36 -0
  28. package/docs/architecture/decisions/0011-engine-run-loop-invariants.md +125 -0
  29. package/docs/architecture/decisions/README.md +62 -0
  30. package/docs/architecture/improvement.md +440 -0
  31. package/docs/architecture/internals/classification.md +154 -0
  32. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +40 -0
  33. package/docs/architecture/internals/functional-contract-patterns.md +340 -0
  34. package/docs/architecture/internals/health-advisories.md +110 -0
  35. package/docs/architecture/internals/improve-workflow.md +713 -0
  36. package/docs/architecture/internals/indexing.md +498 -0
  37. package/docs/architecture/internals/registry-network-boundary.md +57 -0
  38. package/docs/architecture/internals/search.md +249 -0
  39. package/docs/architecture/internals/storage-locations.md +943 -0
  40. package/docs/architecture/persisted-data-compat.md +59 -0
  41. package/docs/architecture/reviews/env-secret-access.md +319 -0
  42. package/docs/architecture/runtime-boundary-design.md +153 -0
  43. package/docs/architecture/specs/0.9.0-close-out-plan.md +361 -0
  44. package/docs/architecture/specs/0.9.0-decisions.md +528 -0
  45. package/docs/architecture/specs/0.9.0-docs-code-drift-register.md +745 -0
  46. package/docs/architecture/specs/0.9.0-open-items-register.md +135 -0
  47. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +740 -0
  48. package/docs/architecture/specs/0.9.0-release-surface-review.md +636 -0
  49. package/docs/architecture/specs/agent-command-engine-model-design.md +513 -0
  50. package/docs/architecture/specs/akm-0.9.0-bundle-adapter-spec.md +501 -0
  51. package/docs/architecture/specs/akm-0.9.0-ref-grammar-decision.md +291 -0
  52. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +2059 -0
  53. package/docs/architecture/specs/di-seams-plan.md +539 -0
  54. package/docs/architecture/specs/driver-protocol-keep-or-cut.md +275 -0
  55. package/docs/architecture/specs/fact-asset-type.md +142 -0
  56. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +634 -0
  57. package/docs/architecture/specs/okf-support.md +148 -0
  58. package/docs/architecture/specs/ref.md +329 -0
  59. package/docs/architecture/specs/reviews/task-workflow-unification/README.md +72 -0
  60. package/docs/architecture/specs/reviews/task-workflow-unification/findings-consolidated.md +152 -0
  61. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-alternatives.md +352 -0
  62. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-bunqueue.md +300 -0
  63. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-openworkflow.md +402 -0
  64. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-1.md +758 -0
  65. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-2.md +645 -0
  66. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-3.md +602 -0
  67. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-adversarial.md +27 -0
  68. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-architecture.md +27 -0
  69. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-coverage.md +74 -0
  70. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-security.md +15 -0
  71. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-adversarial.md +132 -0
  72. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-verification.md +147 -0
  73. package/docs/architecture/specs/stash-conventions-code-spec.md +275 -0
  74. package/docs/architecture/specs/stash-organization-conventions.md +377 -0
  75. package/docs/architecture/specs/task-workflow-format-unification.md +721 -0
  76. package/docs/architecture/specs/workflow-engine-buy-vs-build.md +231 -0
  77. package/docs/architecture/specs/workflow-format-unification.md +437 -0
  78. package/docs/architecture/testing/manual-testing-checklist.md +3420 -0
  79. package/docs/architecture/testing/okf-v0.2-conformance-runbook.md +924 -0
  80. package/docs/architecture/testing/task-workflow-manual-tests.md +1817 -0
  81. package/docs/architecture/testing/testing-workflow.md +446 -0
  82. package/docs/architecture/workflow-engine.md +274 -0
  83. package/docs/guides/README.md +33 -0
  84. package/docs/guides/author-bundles.md +310 -0
  85. package/docs/guides/author-workflows.md +520 -0
  86. package/docs/guides/bundles.md +321 -0
  87. package/docs/guides/capture-knowledge.md +136 -0
  88. package/docs/guides/claude-code-vs-akm-workflows.md +30 -0
  89. package/docs/guides/concepts.md +93 -0
  90. package/docs/guides/discover-and-load.md +158 -0
  91. package/docs/guides/getting-started.md +240 -0
  92. package/docs/guides/improve-the-library.md +246 -0
  93. package/docs/guides/knowledge-management.md +18 -0
  94. package/docs/guides/recipes/headless-install.md +97 -0
  95. package/docs/guides/recipes/website-source.md +28 -0
  96. package/docs/guides/run-workflows.md +279 -0
  97. package/docs/guides/scheduling.md +123 -0
  98. package/docs/guides/use-with-any-agent.md +129 -0
  99. package/docs/guides/wikis.md +81 -0
  100. package/docs/maintainers/README.md +19 -0
  101. package/docs/maintainers/curate-workmap.md +336 -0
  102. package/docs/maintainers/eval.md +3 -0
  103. package/docs/maintainers/local-development.md +108 -0
  104. package/docs/maintainers/release-checklist.md +103 -0
  105. package/docs/migration/README.md +2 -2
  106. package/docs/migration/release-notes/0.0.13.md +1 -1
  107. package/docs/migration/release-notes/0.1.0.md +1 -1
  108. package/docs/migration/release-notes/0.2.0.md +1 -1
  109. package/docs/migration/release-notes/0.3.0.md +1 -1
  110. package/docs/migration/release-notes/0.5.0.md +1 -1
  111. package/docs/migration/release-notes/0.6.0.md +1 -1
  112. package/docs/migration/release-notes/0.7.0.md +1 -1
  113. package/docs/migration/release-notes/0.7.3.md +2 -2
  114. package/docs/migration/release-notes/0.7.4.md +1 -1
  115. package/docs/migration/release-notes/0.7.5.md +1 -1
  116. package/docs/migration/release-notes/0.8.0.md +1 -1
  117. package/docs/migration/release-notes/0.9.0.md +1 -1
  118. package/docs/migration/release-notes/0.9.14.md +1 -1
  119. package/docs/migration/release-notes/0.9.15.md +1 -1
  120. package/docs/migration/release-notes/0.9.16.md +1 -1
  121. package/docs/migration/release-notes/0.9.17.md +1 -1
  122. package/docs/migration/release-notes/0.9.19.md +1 -1
  123. package/docs/migration/release-notes/0.9.2.md +1 -1
  124. package/docs/migration/release-notes/README.md +27 -31
  125. package/docs/migration/v0.5-to-v0.6.md +759 -0
  126. package/docs/migration/v0.8-to-v0.9.md +1 -1
  127. package/docs/plans/0.9.2-agent-command-workflow-plan.md +423 -0
  128. package/docs/posts/README.md +30 -0
  129. package/docs/posts/akm-command-tour.md +495 -0
  130. package/docs/posts/akm-logo-sized.webp +0 -0
  131. package/docs/posts/belief-aware-memory-12.md +139 -0
  132. package/docs/posts/config-v2-lessons-16.md +191 -0
  133. package/docs/posts/improve-loop-autonomous-10.md +225 -0
  134. package/docs/posts/improve-pipeline-debugging-0803.md +179 -0
  135. package/docs/posts/intro-part-01.md +76 -0
  136. package/docs/posts/intro-part-02.md +221 -0
  137. package/docs/posts/intro-part-03.md +190 -0
  138. package/docs/posts/intro-part-04.md +221 -0
  139. package/docs/posts/llm-mode-reflect-13.md +166 -0
  140. package/docs/posts/openpalm-built-on-akm.md +86 -0
  141. package/docs/posts/progressive-disclosure-06.md +129 -0
  142. package/docs/posts/proposal-safety-net-15.md +112 -0
  143. package/docs/posts/release-0.5.0.md +247 -0
  144. package/docs/posts/release-0.6.0.md +117 -0
  145. package/docs/posts/release-0.7.0.md +159 -0
  146. package/docs/posts/release-0.8.0.md +332 -0
  147. package/docs/posts/scattered-skills-05.md +100 -0
  148. package/docs/posts/task-assets-persistent-workflows-11.md +222 -0
  149. package/docs/posts/team-skills-07.md +198 -0
  150. package/docs/posts/wiki-agent-knowledge-08.md +223 -0
  151. package/docs/posts/workflows-vaults-09.md +321 -0
  152. package/docs/product-surface.md +54 -0
  153. package/docs/reference/README.md +1 -2
  154. package/docs/reference/asset-types.md +168 -0
  155. package/docs/reference/cli.md +5 -6
  156. package/docs/reference/env-and-secrets.md +126 -0
  157. package/docs/reference/memory.md +124 -0
  158. package/docs/reference/refs.md +156 -0
  159. package/docs/reference/registry.md +489 -0
  160. package/docs/reference/website-sources.md +141 -0
  161. package/package.json +12 -15
@@ -0,0 +1,713 @@
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 fallback lanes (high salience, proactive maintenance) off for the run: they only select and score assets, and a rewrite needs negative feedback. |
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[Fallback lanes\nproactive maintenance, high salience:\nonly retrieved or new material,\nscored and never planned]
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]
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, the optional fidelity check's contradiction (`processes.distill.fidelityCheck.enabled`, off by default; a lesson that contradicts its source reaches a human this way, not through grounding), 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
+ **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).
253
+
254
+ **What it writes:** one durable proposal row in `state.db`. Never writes asset files directly.
255
+
256
+ ### consolidate (akmConsolidate)
257
+
258
+ `akmConsolidate` runs during preparation, before session extraction and the
259
+ per-asset loop.
260
+
261
+ **Gate:** returns immediately (no-op result) if the selected strategy's
262
+ `processes.consolidate.enabled` is false.
263
+
264
+ **Phase A — Plan generation:**
265
+
266
+ 1. Load eligible non-`.derived` memory assets from the SQLite index, minus
267
+ those the improve ledger holds: a memory judged within its 7-day revisit
268
+ window and unchanged since, and a memory whose promotion was accepted or
269
+ rejected and whose body has not changed since (see **Re-eligibility**
270
+ below).
271
+ 2. Chunk memories using the selected strategy's configured limit. For each
272
+ chunk, call `chatCompletion` with the frozen consolidate LLM connection and
273
+ `CONSOLIDATE_SYSTEM_PROMPT`, requesting a JSON plan of `promote`
274
+ operations (the only op the schema offers; `merge`/`delete`/`contradict`
275
+ were removed in 0.9.17-alpha.1 at `e82eec811` — they had run in
276
+ production up to that commit, then were dropped because they cost
277
+ thousands of completion tokens).
278
+ 3. Parse and validate each op, then use `mergePlans` to deduplicate conflicts
279
+ across chunks.
280
+
281
+ **Phase B — Proposal emission:**
282
+
283
+ 1. For each promote op, perform idempotency checks and emit a reviewable
284
+ proposal with `source: "consolidate"`. The checks run cheapest first and
285
+ each records a skip reason: the same knowledge slug or a pending proposal
286
+ for it, an identical body already in `knowledge/` or already pending, and
287
+ then the coverage check (`consolidate/coverage.ts`, #998): the 20
288
+ `knowledge/` docs in the same bundle nearest to the memory by stored vector
289
+ (`getNeighborsByEntryId` scoped to that bundle's knowledge entries, the
290
+ fetch depth the pair pass uses; no similarity floor, only that rank) are
291
+ compared with it, and the memory is skipped as `dedup_covered_by_knowledge`
292
+ when one of them holds at least half of its distinct 5-word shingles. A
293
+ covering doc that ranks lower goes unseen, so the gate removes fewer
294
+ proposals than the rule does against every knowledge doc (122 of 224
295
+ rejected in #998's sample); its recall over the 20 is unmeasured. The rule
296
+ and its evidence are in the module's header comment. With no stored vector
297
+ (semantic search off, or the memory not yet indexed) the check does
298
+ nothing and nothing throws.
299
+ 2. Record what happened in the improve ledger (see **Re-eligibility** below):
300
+ a memory the model judged and left alone, or that a skip reason turned
301
+ away, gets a `judged_no_action` row; a promotion that failed to persist
302
+ gets none, so the next run retries it.
303
+
304
+ **What it writes:**
305
+ - A durable row in the `proposals` table in `state.db` for each emitted
306
+ `promote` op, partitioned by bundle path.
307
+ - An `improve_ledger` row for each memory the pass judged, except a promotion that failed to persist.
308
+
309
+ **Re-eligibility (#998):** the promote pass keys its ledger row by the source
310
+ memory (`memories/<name>`). A memory the model saw and left alone
311
+ (`judged_no_action`) or one with a pending proposal is revisited after 7 days,
312
+ or at once when the file is edited. An accepted or rejected promotion is
313
+ different: the row carries no `next_eligible_at`, and records the body hash
314
+ (`contentHash(_, "body")`, frontmatter excluded) the promotion was decided
315
+ against, taken from the proposal's `promotionSourceHash`. The memory is
316
+ selected again only when its current body hash differs, the same content-driven
317
+ rule the pair pass uses. A rejection is therefore no longer re-asked after 7
318
+ days, and the memory of an accepted promotion that is still on disk (O1 could
319
+ not archive it, or the same text was restored) is no longer eligible again at
320
+ once. A promotion decided before the hash was recorded (an older release's
321
+ proposal) has no hash to compare, and keeps the old windows. An expired
322
+ proposal still waits one day: nobody judged the text.
323
+
324
+ **Promotion retires its source (O1, alpha.9):** when an `akm proposal accept`
325
+ promotes a consolidate `promote` proposal — by a person or by triage
326
+ auto-promotion — it archives the source memory, and its `.derived` twin if
327
+ one exists, through the same path the pair pass uses below
328
+ (`archiveCleanupCandidate`, reason `promoted`, `successorRefs` the new
329
+ knowledge ref). A promotion no longer leaves a memory/knowledge duplicate
330
+ behind. This runs inside `promoteProposal`, not inside `akmConsolidate`
331
+ itself; a failure to archive the source only warns, it does not undo the
332
+ promotion.
333
+
334
+ ### consolidate pair pass (alpha.9)
335
+
336
+ A second pass inside `akmConsolidate`, alongside Phase A/B above, over the
337
+ same enabled gate. It replaces nothing the promote pass does; it adds
338
+ duplicate, subsumed and superseding retirement, review-gated.
339
+
340
+ 1. **Initiators:** memory (base or `.derived`), flat `knowledge/` or lesson
341
+ assets in the bundle the run writes to, in the retrieval scope, and
342
+ content-eligible: no prior pair-pass ledger row (source
343
+ `consolidate-pair`, kept apart from the promote pass's `consolidate`
344
+ rows), or a row whose recorded body hash differs from the asset's current
345
+ one. Eligibility is purely content-driven — the ledger row carries no
346
+ `next_eligible_at` timer at all for this source, so nothing here "expires"
347
+ on a schedule; see step 5.
348
+ 2. **Candidates:** each initiator's nearest neighbours by stored vector
349
+ (`getNeighborsByEntryId`, fetching 20 and keeping the first 5 that clear
350
+ every filter below — a few self/twin/bundle/tier misses in a naive top-5
351
+ used to starve an initiator down to zero real candidates), same bundle,
352
+ memory tier only (structured knowledge in subfolders is excluded), minus
353
+ its own `.derived` twin or parent, at cosine >= `T_PAIR` (0.93). An
354
+ initiator with no prior ledger row needs >= `BACKFILL_FLOOR` (0.95)
355
+ instead, UNLESS it is new material — git first-added within
356
+ `NEW_MATERIAL_DAYS` (7) — which judges at the ordinary `T_PAIR`: new
357
+ content earns the same scrutiny as an edit, not the backlog's higher bar.
358
+ "Older"/"newer" for the judge's own A/B labelling comes from one
359
+ `git log --reverse -M --diff-filter=AR --name-status --format=@%ct` per
360
+ run over the whole bundle (first-add time per path, following renames —
361
+ an `A` sets a path's first-add, an `R` carries the old path's first-add
362
+ to the new one, so a rename never misdates a file as newly added), not
363
+ frontmatter or file mtime; mtime is only the fallback for a path git does
364
+ not track, or a bundle with no `.git` directly inside it. At most
365
+ `MAX_PAIRS_PER_RUN` (300) pairs a run, admitted a whole initiator at a
366
+ time rather than by flat cosine rank across all of them: new-or-changed
367
+ initiators first, then the existing backlog by its own best cosine, each
368
+ admitted only if every one of its own candidate pairs fits in what
369
+ remains of the 300 (first-fit, so a smaller initiator further down still
370
+ fits when a larger one ahead of it does not) — an initiator with a
371
+ pending-blocked pair is skipped entirely rather than spending any of the
372
+ budget on a pair that cannot be judged yet.
373
+ 3. **Judge:** one LLM call per pair (`src/assets/prompts/consolidate-pair.md`)
374
+ through the consolidate process's own engine and concurrency. It first
375
+ lists the durable claims each side alone holds (`onlyInA`, `onlyInB`),
376
+ then labels the pair one of `duplicate`, `subsumed`, `supersedes`,
377
+ `contradicts`, `overlap` or `unrelated`. Each side is shown up to 12,000
378
+ body characters.
379
+ 4. **Outcome:** `duplicate`, `subsumed` and `supersedes` mint one `retire`
380
+ proposal (source `consolidate-pair`, its own generator — see below) for
381
+ the side that does not survive — never a `captureMode: hot` memory, never
382
+ a `.derived` memory whose parent still exists, never across bundles,
383
+ never when either side already has a pending retire proposal (as the
384
+ retired ref or its successor), and never retiring or reusing as a
385
+ successor an asset already spent earlier in the SAME run (the durable
386
+ half of this last guard is at accept time — step below), and never
387
+ retiring a side the judge listed a claim for. `contradicts`,
388
+ `overlap` and `unrelated` are recorded `judged_no_action` with no
389
+ proposal; `contradicts` is counted in the run report and stays a human
390
+ decision. A retirement (`duplicate`, `subsumed` or `supersedes`) whose
391
+ retired side has nothing listed (a `duplicate` must also have nothing listed
392
+ on the kept side; a subsumed or superseding successor may hold more) and no continuity risk gets a second call
393
+ (`consolidate-pair-check.md`: what does the retired note hold that the kept
394
+ one lacks?); an empty answer stages the proposal (gate `consolidate-pair`,
395
+ reason the judge's label), and the triage drain accepts it under its usual
396
+ `applyMode`. Replay precision was 336/360 (duplicate 0.98, subsumed 0.92,
397
+ supersedes 0.875). Every other retire proposal waits for a person.
398
+ 5. **Ledger:** a row is written for an initiator only once every one of its
399
+ own candidates was admitted this run (whole-initiator admission in step 2
400
+ makes this an all-or-nothing membership check) AND actually resolved to a
401
+ verdict — a same-run guard dropping a retire-worthy verdict (the chain
402
+ guard in step 4), a failed mint, a failed or never-sent judge call, all
403
+ count as unresolved, not judged — recording its current body hash and an
404
+ outcome of `proposed` or `judged_no_action`. Neither outcome carries a
405
+ timer for this source: only a later body-hash mismatch (step 1) makes the
406
+ initiator eligible again. An initiator left with any candidate not
407
+ admitted, not judged, or dropped gets NO row at all, so the next run
408
+ reconsiders it rather than treating it as settled — real data: night 1
409
+ alone judged 177 `duplicate` verdicts into only 59 proposals before this,
410
+ the other 118 silently abandoned by the same-run chain guard.
411
+
412
+ Because that "no row" case regenerates ALL of an initiator's candidate
413
+ pairs next run — including ones already decided — a pair the owner
414
+ already declined (rejected, or accepted then reverted) is never re-minted
415
+ while both sides are unchanged: before the judge is ever called, the same
416
+ ref pair and the same two content hashes (checked in both orientations,
417
+ since it is the judge — not yet run — that would decide which side is
418
+ "retired") are checked against every rejected or reverted
419
+ `consolidate-pair` proposal on record, and a match is counted as a settled
420
+ no-action (so the initiator's row still gets written this run) at no LLM
421
+ cost at all. Those keys follow each proposal's current status: `akm
422
+ proposal reopen` (#997) moves a rejected proposal back to `pending`, so it
423
+ is no longer settled, and while it is pending a pair pass leaves both of its
424
+ documents alone (step 4's pending-retire guard) instead of minting the
425
+ pair a second time.
426
+
427
+ **Retire proposals (`akm proposal accept`/`revert`):** minted under their own
428
+ source, `consolidate-pair` — kept apart from the promote pass's
429
+ `consolidate` proposals so a bulk `accept`/`reject --generator consolidate`
430
+ never sweeps a retirement, and the reverse (`--max-diff-lines` counts a
431
+ retire proposal by its target's own current line count, not its empty
432
+ payload). Accepting one first confirms the decision is still fresh — the
433
+ successor still exists, and both sides' recorded body hashes still match
434
+ their current files — refusing cleanly, not partially applying a decision
435
+ something else (an intervening accept, possibly an A→B/B→C chain) made
436
+ stale. It then moves the retired asset (and its `.derived` twin) into the
437
+ existing `.akm/memory-cleanup/archive/` through the generalized
438
+ `archiveCleanupCandidate`, the one retirement encoding (D27) — never
439
+ `.akm/archive/`. A `supersedes` proposal first writes the supersede edge on
440
+ the older asset (`writeSupersededEdge`), then archives it. `akm proposal
441
+ revert` restores the archived file(s), and for the primary asset restores
442
+ the EXACT pre-retire bytes recorded at accept (`backupContent`) rather than
443
+ re-deriving "undo the edge" from the archived copy — so a pre-existing
444
+ human-written edge, YAML comments and key order all survive the round trip.
445
+ Accept records its full intent — `backupContent` and which file is about to
446
+ move — on the still-pending proposal before moving anything; a crash at any
447
+ point after resumes from that recorded intent (skipping whichever file a
448
+ tombstone scan shows an earlier, crashed attempt already archived under this
449
+ proposal's own id) rather than refusing it as stale or re-deriving
450
+ `backupContent` by guessing at what the archived copy would have been.
451
+ Revert works the other way for the same reason: each archive dir's own
452
+ tombstone already names its original and archived paths, so "original
453
+ present, archived copy missing" resumes as an earlier, crashed revert's own
454
+ work — UNLESS that original path's current content does not match the
455
+ `retirement.retiredContentHash` recorded at accept, meaning the path was
456
+ reused by an unrelated file since, which refuses instead of overwriting it.
457
+ Triage auto-accepts a `retire` proposal only when the pair pass staged it
458
+ (gate `consolidate-pair`, see step 4 above) and `applyMode` allows it; every
459
+ other one waits for a person. Review reuses `akm proposal list --generator consolidate-pair` (S4: the
460
+ backlog is reviewed as its own list, not mixed in with every other
461
+ generator's proposals), `show`, `diff` (which renders a retirement as the
462
+ retired file's lines leaving under a `retire` header, with the pair's verdict
463
+ and a note that accept archives and revert restores — not as the file replaced
464
+ by a blank one, #997), and bulk
465
+ `accept --generator consolidate-pair` / `reject --generator
466
+ consolidate-pair`. A proposal carrying `continuityRisk` (below) is excluded
467
+ from that bulk accept, whatever the generator or `--yes` — visible inline in
468
+ `list`'s default output and in the text output of `show` and `diff` (the
469
+ specific failing/unverified queries, not just a count) — bulk reject is unaffected,
470
+ and a person can always accept one by id. A rejection is not final:
471
+ `akm proposal reopen <id>` puts a rejected retire proposal back to `pending`
472
+ (refused if the retired document, or the successor, changed since the pair was
473
+ judged, or if another pending retire proposal now involves either of them),
474
+ and accepting it then archives the file exactly as for any other retire
475
+ proposal.
476
+
477
+ ### Retirement continuity check (rule R3)
478
+
479
+ Before the pair pass mints a `retire` proposal, it replays up to five of the
480
+ retired asset's own past `search`/`curate` queries
481
+ (`loadRetrievalQueries`, `src/commands/improve/retrieval-gate.ts` — the same
482
+ cleaned set the retrieval regression gate replays: `usableRetrievalQueries`
483
+ drops stash-README boilerplate and harness/tool envelopes
484
+ (`nonTaskInput`), pastes over 2,000 characters, and queries that are
485
+ duplicates once whitespace is collapsed, S3a) through akm's own search,
486
+ in-process — the same ranking a user gets, no LLM
487
+ (`src/commands/improve/consolidate/continuity-check.ts`). For every query
488
+ where the retired asset ranked in the top 10, the successor must rank in the
489
+ top 10 too — compared directly (N2): search itself returns at most the top
490
+ 10 hits, so `rankOf` finding the successor among them or not is the whole
491
+ comparison, no generic rank-change-report abstraction needed. No recorded
492
+ queries means no check and no flag — and so does a retired/successor pair
493
+ whose bodies are
494
+ content-identical once whitespace is collapsed (S3b): search's own
495
+ content-dedupe (`src/indexer/search/db-search.ts`) already hides the
496
+ successor behind the retired asset for every such query, so a "successor
497
+ missing" finding there would not be a real risk, just that dedupe working
498
+ as designed.
499
+
500
+ A failing query does not block the mint — it flags. The proposal's
501
+ `retirement.continuityRisk` records the failing query count and, per failing
502
+ query, the retired asset's rank and the successor's (`null` when the
503
+ successor did not rank in the top 10 at all). This is the forgetting-safety
504
+ lane's old purpose (see below), now measured against search rank instead of
505
+ a stash-wide salience rank, and only at the moment an asset would actually
506
+ stop resolving.
507
+
508
+ A query that never ran (the search call threw) or that fell back to
509
+ keyword-only ranking (`mode: "fts-fallback"`) is "unverified" (S2): dropped
510
+ from the rank comparison — its hits are not the ranking a user actually
511
+ gets, so they are never compared — but tracked in
512
+ `retirement.continuityRisk.unverifiedQueries`, and on its own enough to set
513
+ `continuityRisk` even when every verified query passed. A search failure or
514
+ fallback must never look like "no risk found." `createContinuitySearch`
515
+ (`src/commands/improve/consolidate/continuity-check.ts`) is stateful across
516
+ one pair-pass run: the first `fts-fallback` it sees forces
517
+ `semanticSearchMode: "off"` for every later query that same run, so a down
518
+ embedding endpoint pays its failed-connection cost once per run, not once
519
+ per remaining query.
520
+
521
+ **The retired forgetting-safety lane.** Before alpha.9, `scoreSalience`
522
+ compared the whole stash's salience ranking before and after every improve
523
+ run, and a ref that fell from the top 200 to below 500 was injected into that
524
+ run's work set under `eligibilitySource: forgetting-safety`
525
+ (`applyForgettingSafety`, `improve_salience_rank_change` event). It was a
526
+ one-time cutover guard from the June 2026 ranking-formula change, running on
527
+ every run since. R5's 30-day event window (2026-08-30 to 2026-09-29, 47
528
+ `improve_salience_rank_change` events, 5 refs flagged across 4 runs) found no
529
+ marginal pick over the simpler baseline: 4 of the 5 flagged refs were also
530
+ picked that same run by the signal-delta lane, and the 5th has no
531
+ `reflect_invoked`/`distill_invoked` event in the retained history, but that
532
+ run's `improve_runs.plannedRefs` shows it, too, was planned under
533
+ `signal-delta` — just not reflected (a dispatch/budget limit that run, not a
534
+ lane-exclusive pick). All 5 flagged refs were signal-delta picks; zero were
535
+ ever forgetting-safety-only. It also protected `asset_salience.rank_score`,
536
+ which only improve itself ever read. Both the per-run comparison and the
537
+ injection are gone; so is `buildRankChangeReport` itself (N2 — the
538
+ continuity check compares ranks directly, and nothing else called it).
539
+ `forgetting-safety` stays a valid `eligibilitySource`/event-type value so
540
+ old proposals and events still decode.
541
+
542
+ ### Archive purge sweep (step 8)
543
+
544
+ A retirement's archived bytes are not deleted when it is accepted — only the
545
+ tombstone (`cleanup.md`) resolves the ref from then on. `purgeGracedArchive`
546
+ (`src/commands/improve/memory/memory-improve.ts`) deletes them later,
547
+ deterministically and with no LLM, once at the very start of every
548
+ `akm improve` run — before index bootstrap, before triage. For a git-backed
549
+ bundle it walks `.akm/memory-cleanup/archive/`, and for every tombstone whose
550
+ `retiredAt` is more than `RETIRE_GRACE_DAYS` (30) days old AND whose
551
+ directory is entirely git-tracked and clean (one `git ls-files` plus one
552
+ `git status --porcelain -uall` per sweep, not per directory — B1), deletes
553
+ the archived asset file(s) under that tombstone's own directory — never
554
+ `cleanup.md` itself; git history keeps the bytes (D27). `.git` presence
555
+ alone does not prove a retirement was ever committed: `akm proposal accept`
556
+ only commits for a `kind: "git"` write target, and improve's own auto-sync
557
+ only stages the paths that same run wrote, so a directory with even one
558
+ untracked or modified file (the tombstone included) is left whole for a
559
+ later sweep rather than losing the only surviving copy of it. A
560
+ memory-cleanup family-prune archive (not a pair-pass retirement) has no
561
+ `retiredAt` in its tombstone at all, so this sweep never touches that older
562
+ archive class.
563
+
564
+ Every deleted file is journaled individually
565
+ (`src/core/write-provenance.ts`), the same mechanism the archive move itself
566
+ uses, so the end-of-run auto-sync commits the deletion in the run's own
567
+ commit. A bundle with no `.git` of its own is left untouched — there is
568
+ nothing to fall back on if a delete turns out to be wrong — and `akm health`
569
+ reports its archive's size and file count instead (`memory-cleanup-archive`
570
+ advisory, `src/commands/health/archive-usage.ts`), silent whenever the bundle
571
+ is git-backed or the archive is empty or absent.
572
+
573
+ ### improve-owned maintenance
574
+
575
+ After consolidation completes, `akmImprove` runs maintenance steps that own the
576
+ remaining live-write memory/index artifacts previously coupled to indexing.
577
+
578
+ **Memory inference:**
579
+
580
+ 1. Collect the memory refs that completed distill without being promoted to
581
+ `knowledge:` in the same improve run.
582
+ 2. Call `runMemoryInferencePass` with those refs.
583
+ 3. If the pass writes derived memories or marks parents with
584
+ `inferenceProcessed: true`, call `reindexFn({ stashDir })` so SQLite/search
585
+ state reflects the new disk state before any later steps run.
586
+
587
+ ### Proposal queue
588
+
589
+ `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.
590
+
591
+ **Logical proposal shape:**
592
+
593
+ ```json
594
+ {
595
+ "id": "<UUID>",
596
+ "ref": "lessons/skill-deploy-lesson",
597
+ "status": "pending",
598
+ "source": "reflect",
599
+ "sourceRun": "reflect-1715000000000",
600
+ "createdAt": "2026-05-11T00:00:00.000Z",
601
+ "updatedAt": "2026-05-11T00:00:00.000Z",
602
+ "payload": {
603
+ "content": "---\ndescription: ...\n---\n\nbody",
604
+ "frontmatter": { "description": "..." }
605
+ }
606
+ }
607
+ ```
608
+
609
+ 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).
610
+
611
+ ## Scope restrictions
612
+
613
+ `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`, `--plan`) 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.
614
+
615
+ `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.
616
+
617
+ ## Ledger pre-filter (signal delta)
618
+
619
+ 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`).
620
+
621
+ **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:
622
+
623
+ - 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`).
624
+ - A ref that passes only distill, and is a distill candidate, is planned distill-only.
625
+ - A ref with no in-window feedback and no reflect window is left to the fallback lanes (proactive maintenance and high salience), which pick only what retrieval returned or new material (see [Retrieval scope](../improvement.md#retrieval-scope)). The lanes only select and score: a ref they pick is not planned for reflect or distill, so improve does not rewrite assets on a proactive cadence.
626
+ - Every ref the loop does not take is counted in the plan's `signal` gate (or its `retrieval` gate, when the fallback lanes could not pick it for lack of usage evidence) and reported once, in aggregate, as an `improve_skipped` event (`no_new_signal`, `not_retrieved`).
627
+ - 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.
628
+ - 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.
629
+ - 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.
630
+ - The loop's refs are ranked by salience (`scoreSalience` in `preparation.ts`, which also scores the fallback lanes' 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.
631
+
632
+ An explicit ref scope bypasses every gate. Consolidation, extract and schema repair read their own ledger sources in their own stages.
633
+
634
+ ## Strategy process configuration
635
+
636
+ | Process | Config path | Controls |
637
+ |---|---|---|
638
+ | `distill` | `improve.strategies.<name>.processes.distill` | Enables distillation and selects its engine/model/request overrides. |
639
+ | `consolidate` | `improve.strategies.<name>.processes.consolidate` | Enables consolidation and selects its engine/model/request overrides. |
640
+
641
+ Improve process selection is resolved once by `resolveImprovePlan`; the plan
642
+ contains every process's frozen enablement, process config, and resolved runner.
643
+ A process runs on any engine that confines the model-work tool policy; one that
644
+ cannot is refused when the plan is built, never replaced by another engine.
645
+
646
+ ## Output shape
647
+
648
+ `AkmImproveResult` uses `schemaVersion: 2`, identifies the selected `strategy`,
649
+ and can report `ok: false` for terminated runs:
650
+
651
+ | Field | Type | Description |
652
+ |---|---|---|
653
+ | `scope` | `{ mode, value? }` | Resolved scope (`all`, `type`, or `ref`). |
654
+ | `dryRun` | `boolean` | Whether this was a dry run. |
655
+ | `guidance` | `string?` | Human-readable note about memory cleanup when memories are in scope. |
656
+ | `memorySummary` | `{ eligible, derived }` | Count of memory assets in scope and count of `.derived` ones. |
657
+ | `memoryCleanup` | `ImproveMemoryCleanupResult?` | Analysis (always present when eligible > 0) merged with apply results on a live run. Includes `archived`, `transitionLogPath`, `transitionLogEntries`, and `warnings`. |
658
+ | `plannedRefs` | `ImproveEligibleRef[]` | The post-filter, post-cleanup, salience-ranked refs that were (or would be) processed. |
659
+ | `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. |
660
+ | `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. |
661
+ | `validationFailures` | `Array<{ ref, reason }>?` | Refs skipped due to pre-run validation failures (missing file, missing description). |
662
+ | `consolidation` | `ConsolidateResult?` | Result from `akmConsolidate`; omitted when `processed === 0` and no warnings. |
663
+ | `memoryInference` | `MemoryInferenceResult?` | Improve-owned post-consolidation memory inference telemetry. |
664
+
665
+ ## Consolidation Skip Reason Taxonomy
666
+
667
+ 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:
668
+
669
+ | Reason | Meaning |
670
+ |--------|---------|
671
+ | `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. |
672
+ | `promote_pending_proposal_exists` | A pending proposal already exists for the target `knowledge/<slug>`. Clears as triage drains the queue. |
673
+ | `promote_already_exists` | `knowledge/<slug>.md` already exists in the write target. Normal steady-state noise. |
674
+ | `promote_read_failed` | The memory file could not be read. |
675
+ | `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. |
676
+ | `promote_superseded` | The memory has `status: superseded`, which is not promotable knowledge. |
677
+ | `promote_source_too_small` | The memory's body is under 100 characters, too short to make useful knowledge. |
678
+ | `dedup_existing_knowledge` | A `knowledge/` doc in the write target already has an identical body (frontmatter aside). |
679
+ | `dedup_pending_proposal` | A pending consolidate proposal already carries an identical body. Clears as triage drains the queue. |
680
+ | `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. |
681
+ | `promote_invalid_frontmatter` | The description it would use (the model's, else the memory's own) is missing or truncated. |
682
+ | `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. |
683
+ | `promote_create_failed` | `createProposal` threw. The memory gets no ledger row, is retried next run, and is counted in `failedPromotions`. |
684
+
685
+ 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`.
686
+
687
+ ## Reviewed
688
+
689
+ Reviewed against `src/commands/improve/improve.ts`,
690
+ `src/commands/improve/reflect.ts`, and `src/commands/improve/distill.ts`.
691
+
692
+ **Checked:**
693
+ - Diagram branch ordering for lock vs. scope resolution
694
+ - Diagram branch ordering for dry-run early-return
695
+ - Validation sweep placement relative to the limit filter
696
+ - Consolidation placement relative to the per-asset loop
697
+ - Maintenance placement relative to consolidation and reindex
698
+ - Per-asset loop branch ordering (validation skip vs. budget check)
699
+ - Memory cleanup step sequencing (analyzeMemoryCleanup, applyMemoryCleanup, reindexFn)
700
+ - Strategy gating for consolidation (`improve.strategies.<name>.processes.consolidate.enabled`)
701
+ - Dry-run early-return node completeness
702
+ - Budget-exhausted break path
703
+ - Mermaid syntax and subgraph labels
704
+
705
+ **Fixed:**
706
+
707
+ 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.
708
+
709
+ 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.
710
+
711
+ 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.
712
+
713
+ 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.