akm-cli 0.10.261016-beta → 0.10.261017-beta
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.
- package/CHANGELOG.md +100 -0
- package/README.md +8 -10
- package/SECURITY.md +7 -99
- package/STABILITY.md +57 -98
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -2
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +11 -17
- package/dist/cli.js +32 -41
- package/dist/commands/feedback-cli.js +5 -4
- package/dist/commands/health.js +15 -16
- package/dist/commands/proposal/proposal-cli.js +1 -1
- package/dist/commands/read/registry-search.js +4 -1
- package/dist/commands/read/search.js +6 -6
- package/dist/commands/sources/self-update.js +9 -5
- package/dist/commands/tasks/tasks.js +12 -13
- package/dist/core/adapter/adapters/agent-skills-adapter.js +7 -7
- package/dist/core/adapter/adapters/akm-task-adapter.js +1 -1
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/adapters/claude-adapter.js +1 -1
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +1 -1
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +1 -1
- package/dist/core/adapter/adapters/opencode-adapter.js +1 -1
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -1
- package/dist/core/bundle-rename.js +17 -7
- package/dist/indexer/bundle-identity-guard.js +6 -6
- package/dist/indexer/index-written-assets.js +3 -3
- package/dist/indexer/indexer.js +12 -8
- package/dist/indexer/materialize-embeddings.js +12 -2
- package/dist/indexer/passes/metadata.js +103 -46
- package/dist/indexer/search/ranking.js +7 -3
- package/dist/indexer/search/search-fields.js +1 -1
- package/dist/indexer/usage/usage-events.js +30 -8
- package/dist/indexer/walk/walker.js +1 -1
- package/dist/output/shapes/helpers.js +3 -3
- package/dist/output/text/command-format.js +5 -6
- package/dist/scripts/akm-migrate-node.js +31 -3
- package/dist/scripts/akm-migrate.js +31 -3
- package/dist/sources/providers/git-provider.js +2 -4
- package/dist/sources/providers/provider-utils.js +8 -13
- package/dist/sources/snapshot-fetchers/website-ingest.js +6 -7
- package/dist/storage/repositories/events-repository.js +15 -0
- package/dist/storage/repositories/improve-ledger-repository.js +13 -0
- package/dist/storage/repositories/index-entries-repository.js +43 -5
- package/dist/storage/repositories/outcome-repository.js +12 -0
- package/dist/storage/repositories/proposals-repository.js +5 -5
- package/dist/storage/repositories/salience-repository.js +13 -0
- package/dist/storage/repositories/task-history-repository.js +8 -7
- package/dist/storage/repositories/workflow-runs-repository.js +10 -8
- package/docs/README.md +24 -60
- package/docs/agents/README.md +3 -8
- package/docs/architecture/README.md +18 -20
- package/docs/architecture/adapters.md +40 -43
- package/docs/architecture/architecture.md +171 -151
- package/docs/architecture/improvement.md +95 -91
- package/docs/architecture/internals/classification.md +101 -119
- package/docs/architecture/internals/fresh-host-rebuild-runbook.md +33 -35
- package/docs/architecture/internals/health-advisories.md +59 -46
- package/docs/architecture/internals/improve-workflow.md +166 -693
- package/docs/architecture/internals/indexing.md +65 -465
- package/docs/architecture/internals/storage-locations.md +301 -848
- package/docs/architecture/persisted-data-compat.md +6 -6
- package/docs/architecture/specs/okf-support.md +67 -95
- package/docs/architecture/specs/ref.md +67 -125
- package/docs/architecture/testing/testing-workflow.md +107 -167
- package/docs/architecture/workflow-engine.md +34 -33
- package/docs/guides/README.md +5 -5
- package/docs/guides/author-bundles.md +72 -98
- package/docs/guides/author-workflows.md +11 -18
- package/docs/guides/bundles.md +91 -123
- package/docs/guides/capture-knowledge.md +10 -9
- package/docs/guides/concepts.md +45 -30
- package/docs/guides/discover-and-load.md +39 -32
- package/docs/guides/getting-started.md +24 -24
- package/docs/guides/improve-the-library.md +98 -80
- package/docs/guides/recipes/headless-install.md +4 -4
- package/docs/guides/recipes/website-source.md +9 -5
- package/docs/guides/run-workflows.md +10 -11
- package/docs/guides/scheduling.md +11 -17
- package/docs/guides/use-with-any-agent.md +15 -14
- package/docs/guides/wikis.md +6 -9
- package/docs/integration/bundling-akm.md +21 -26
- package/docs/maintainers/README.md +2 -2
- package/docs/maintainers/local-development.md +3 -16
- package/docs/migration/README.md +2 -1
- package/docs/migration/release-notes/0.10.0.md +167 -22
- package/docs/migration/release-notes/README.md +2 -2
- package/docs/migration/v0.9-to-v0.10.md +47 -7
- package/docs/product-surface.md +10 -8
- package/docs/reference/README.md +26 -15
- package/docs/reference/asset-types.md +11 -12
- package/docs/reference/bundle-types.md +6 -5
- package/docs/reference/cli.md +52 -28
- package/docs/reference/configuration.md +46 -60
- package/docs/reference/data-and-telemetry.md +204 -421
- package/docs/reference/env-and-secrets.md +80 -83
- package/docs/reference/memory.md +1 -1
- package/docs/reference/refs.md +23 -36
- package/docs/reference/registry.md +138 -348
- package/docs/reference/supported-formats.md +8 -8
- package/docs/reference/tasks.md +13 -19
- package/docs/reference/website-sources.md +23 -15
- package/docs/reference/workflow-schema.md +28 -47
- package/docs/reference/workflows.md +33 -37
- package/package.json +1 -1
|
@@ -1,720 +1,193 @@
|
|
|
1
|
-
# akm improve
|
|
1
|
+
# akm improve: Workflow Reference
|
|
2
2
|
|
|
3
|
-
`akm improve` is the scheduled self-improvement loop
|
|
3
|
+
`akm improve` is the scheduled self-improvement loop. It selects assets that have fresh feedback (or a scoped subset), runs reflect and distill on them, consolidates memories, optionally extracts proposals from coding-agent session transcripts, drains the proposal backlog under the strategy's triage settings, and runs maintenance passes. Everything it produces is a proposal in `state.db`; proposals stay queued until `akm proposal` review or the triage drain resolves them. Reflect is planned from negative feedback on an asset, for an explicit ref, or as a proactive-maintenance pick (an asset due for review with no feedback behind it); a positive signal alone never plans it.
|
|
4
|
+
|
|
5
|
+
The design and rationale are in [Improvement Loop](../improvement.md). This page is the per-step reference. The code is in `src/commands/improve/`.
|
|
4
6
|
|
|
5
7
|
## Command surface
|
|
6
8
|
|
|
7
|
-
| Option |
|
|
8
|
-
|
|
9
|
-
| `[scope]` (positional) |
|
|
10
|
-
| `--
|
|
11
|
-
| `--
|
|
12
|
-
| `--
|
|
13
|
-
| `--
|
|
14
|
-
| `--
|
|
15
|
-
| `--
|
|
16
|
-
| `--
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 a connection or a harness\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: 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 model 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 and one blank line akm inserts when the body has no level-1 heading (the only body change reflect makes). 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 where reflect runs: `improve.processes.reflect`, then `improve.connection` / `improve.harness`, then the default connection (else the default harness), asking for the `fast` model unless a layer names another.
|
|
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
|
-
([Where each process runs](../../reference/configuration.md#where-each-process-runs)).
|
|
201
|
-
The runner may be a connection or a harness (with its tools restricted where
|
|
202
|
-
the harness can be), and both run the same iteration (`runReflectIteration`). The JSON Schema
|
|
203
|
-
of the reply (`REFLECT_JSON_SCHEMA`, or the unscoped schema when no ref was
|
|
204
|
-
given) is the request's output schema: `response_format` for a connection, the
|
|
205
|
-
shared schema instruction at the end of the prompt for a harness. A harness
|
|
206
|
-
is run in its own scratch directory and is asked for the proposal as its
|
|
207
|
-
reply, as a connection is. An endpoint that rejects JSON Schema gets the
|
|
208
|
-
framed-markdown contract instead.
|
|
209
|
-
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 harness dispatch is reported as it ran, with its exit code and stderr.
|
|
210
|
-
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 inserted at the body's first non-blank line, in the body's line ending, only when the body has no level-1 heading; otherwise it is ignored. Leading blank lines and every other body byte are kept. 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.
|
|
211
|
-
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) unless its mean is 3.5 or more: that is uncertain, not bad, and the proposal is deferred for a person (`judge-uncertain`, gate `quality-gate`, with the criterion `scores` and the judge's reason) and records no rejection window. A judge that times out, errors or replies unparseably gives no verdict, and the proposal is deferred for review too (`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)).
|
|
212
|
-
9. 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`) and one in the judge's uncertain band (`judge-uncertain`). `akm proposal list` shows the reason; the drain leaves every deferral for a person.
|
|
213
|
-
|
|
214
|
-
**What it writes:** one durable proposal row in `state.db`. It never writes asset files directly.
|
|
215
|
-
|
|
216
|
-
**Prompt shape (`buildReflectPrompt`):** The prompt asks the model 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 a connection and a harness) requires the model 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.
|
|
217
|
-
|
|
218
|
-
**Asset content cap:** the asset content section is capped to keep the prompt well under OS ARG_MAX when it travels through CLI argv (a harness always uses the flat `REFLECT_CONTENT_CAP`, 12 000 chars). A connection never touches argv, so its cap is instead computed from the resolved connection'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).
|
|
219
|
-
|
|
220
|
-
### distill (akmDistill)
|
|
221
|
-
|
|
222
|
-
`akmDistill` is a bounded single-call subprocess: one model call through `callStage` on the resolved runner, a direct chat completion on a connection or a run on a harness, restricted where the harness can be. Past the process gate it emits a `distill_invoked` event carrying the outcome.
|
|
223
|
-
|
|
224
|
-
**Internal steps:**
|
|
225
|
-
|
|
226
|
-
1. Validate the input ref shape (`parseRefInput`, `src/core/asset/resolve-ref.ts`).
|
|
227
|
-
2. Best-effort load asset content via `lookupFn` (defaults to indexer `lookup`).
|
|
228
|
-
3. Read feedback events via `readEvents({ ref, type: "feedback" })`. Apply `excludeFeedbackFromRefs` filtering before the LLM sees the events.
|
|
229
|
-
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.
|
|
230
|
-
- 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`.
|
|
231
|
-
5. Resolve where distill runs (`improve.processes.distill`, then `improve.connection` /
|
|
232
|
-
`improve.harness`, then the default), then issue one bounded call.
|
|
233
|
-
- Process gate: disabled if the selected strategy's `processes.distill.enabled` is `false`.
|
|
234
|
-
- Hard timeout: 600 seconds by default, overridden by the resolved invocation timeout.
|
|
235
|
-
- 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.
|
|
236
|
-
- 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.
|
|
237
|
-
- 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.
|
|
238
|
-
6. Strip markdown fences and `<think>` blocks from the raw LLM output.
|
|
239
|
-
7. Validate: `lintLessonContent` for lesson proposals; `validateKnowledgeContent` for knowledge proposals. Failure emits `distill_invoked` with `outcome: "validation_failed"` and throws `UsageError`.
|
|
240
|
-
8. Quality gate (`processes.distill.qualityGate`, on unless disabled): one judge call, on the gate's own connection or harness (`improve.qualityGate`, else the default), 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).
|
|
241
|
-
- **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.
|
|
242
|
-
- **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.
|
|
243
|
-
- **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, and not part of the pass test, on purpose (#1173): adding it to the pass bar was measured on replayed judged lessons (akm-eval `distill/pass-bar`, two lab models) and only moved lessons from pass to review (6 good lessons on the 35B, 2 on the 27B). A pass and a review are both queued for a person (step 9), so what a reviewer sees is unchanged, and only the reject line (2 or below) keeps a lesson from one. See `GROUNDING_CRITERION`, `LESSON_REJECT_MAX_SCORE` and `runQualityJudge` in `src/commands/improve/stage.ts`.
|
|
244
|
-
- 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.
|
|
245
|
-
- `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.
|
|
246
|
-
- `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, returns something unparseable or incomplete, or is missing when the lesson or promotion is judged, and a heuristic lesson-quality finding (for example an invalid `description`), which is found before the judge is called.
|
|
247
|
-
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.
|
|
248
|
-
10. Emit `distill_invoked` event with `outcome: "queued"`.
|
|
249
|
-
|
|
250
|
-
**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:
|
|
251
|
-
|
|
252
|
-
- 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.
|
|
253
|
-
- 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.
|
|
254
|
-
- 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`.
|
|
255
|
-
- 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`.
|
|
256
|
-
|
|
257
|
-
**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).
|
|
258
|
-
|
|
259
|
-
**What it writes:** one durable proposal row in `state.db`. Never writes asset files directly.
|
|
260
|
-
|
|
261
|
-
### consolidate (akmConsolidate)
|
|
262
|
-
|
|
263
|
-
`akmConsolidate` runs during preparation, before session extraction and the
|
|
264
|
-
per-asset loop.
|
|
265
|
-
|
|
266
|
-
**Gate:** returns immediately (no-op result) if the selected strategy's
|
|
267
|
-
`processes.consolidate.enabled` is false.
|
|
268
|
-
|
|
269
|
-
**Phase A — Plan generation:**
|
|
270
|
-
|
|
271
|
-
1. Load eligible non-`.derived` memory assets from the SQLite index, minus
|
|
272
|
-
those the improve ledger holds: a memory judged within its 7-day revisit
|
|
273
|
-
window and unchanged since, and a memory whose promotion was accepted or
|
|
274
|
-
rejected and whose body has not changed since (see **Re-eligibility**
|
|
275
|
-
below).
|
|
276
|
-
2. Chunk memories using the selected strategy's configured limit. For each
|
|
277
|
-
chunk, call `chatCompletion` with the frozen consolidate LLM connection and
|
|
278
|
-
`CONSOLIDATE_SYSTEM_PROMPT`, requesting a JSON plan of `promote`
|
|
279
|
-
operations (the only op the schema offers; `merge`/`delete`/`contradict`
|
|
280
|
-
were removed in 0.9.17-alpha.1 at `e82eec811` — they had run in
|
|
281
|
-
production up to that commit, then were dropped because they cost
|
|
282
|
-
thousands of completion tokens).
|
|
283
|
-
3. Parse and validate each op, then use `mergePlans` to deduplicate conflicts
|
|
284
|
-
across chunks.
|
|
285
|
-
|
|
286
|
-
**Phase B — Proposal emission:**
|
|
287
|
-
|
|
288
|
-
1. For each promote op, perform idempotency checks and emit a reviewable
|
|
289
|
-
proposal with `source: "consolidate"`. The checks run cheapest first and
|
|
290
|
-
each records a skip reason: the same knowledge slug or a pending proposal
|
|
291
|
-
for it, an identical body already in `knowledge/` or already pending, and
|
|
292
|
-
then the coverage check (`consolidate/coverage.ts`, #998): the 20
|
|
293
|
-
`knowledge/` docs in the same bundle nearest to the memory by stored vector
|
|
294
|
-
(`getNeighborsByEntryId` scoped to that bundle's knowledge entries, the
|
|
295
|
-
fetch depth the pair pass uses; no similarity floor, only that rank) are
|
|
296
|
-
compared with it, and the memory is skipped as `dedup_covered_by_knowledge`
|
|
297
|
-
when one of them holds at least half of its distinct 5-word shingles. A
|
|
298
|
-
covering doc that ranks lower goes unseen, so the gate removes fewer
|
|
299
|
-
proposals than the rule does against every knowledge doc (122 of 224
|
|
300
|
-
rejected in #998's sample); its recall over the 20 is unmeasured. The rule
|
|
301
|
-
and its evidence are in the module's header comment. With no stored vector
|
|
302
|
-
(semantic search off, or the memory not yet indexed) the check does
|
|
303
|
-
nothing and nothing throws.
|
|
304
|
-
2. Record what happened in the improve ledger (see **Re-eligibility** below):
|
|
305
|
-
a memory the model judged and left alone, or that a skip reason turned
|
|
306
|
-
away, gets a `judged_no_action` row; a promotion that failed to persist
|
|
307
|
-
gets none, so the next run retries it.
|
|
308
|
-
|
|
309
|
-
**What it writes:**
|
|
310
|
-
- A durable row in the `proposals` table in `state.db` for each emitted
|
|
311
|
-
`promote` op, partitioned by bundle path.
|
|
312
|
-
- An `improve_ledger` row for each memory the pass judged, except a promotion that failed to persist.
|
|
313
|
-
|
|
314
|
-
**Re-eligibility (#998):** the promote pass keys its ledger row by the source
|
|
315
|
-
memory (`memories/<name>`). A memory the model saw and left alone
|
|
316
|
-
(`judged_no_action`) or one with a pending proposal is revisited after 7 days,
|
|
317
|
-
or at once when the file is edited. An accepted or rejected promotion is
|
|
318
|
-
different: the row carries no `next_eligible_at`, and records the body hash
|
|
319
|
-
(`contentHash(_, "body")`, frontmatter excluded) the promotion was decided
|
|
320
|
-
against, taken from the proposal's `promotionSourceHash`. The memory is
|
|
321
|
-
selected again only when its current body hash differs, the same content-driven
|
|
322
|
-
rule the pair pass uses. A rejection is therefore no longer re-asked after 7
|
|
323
|
-
days, and the memory of an accepted promotion that is still on disk (O1 could
|
|
324
|
-
not archive it, or the same text was restored) is no longer eligible again at
|
|
325
|
-
once. A promotion decided before the hash was recorded (an older release's
|
|
326
|
-
proposal) has no hash to compare, and keeps the old windows. An expired
|
|
327
|
-
proposal still waits one day: nobody judged the text.
|
|
328
|
-
|
|
329
|
-
**Promotion retires its source (O1, alpha.9):** when an `akm proposal accept`
|
|
330
|
-
promotes a consolidate `promote` proposal — by a person or by triage
|
|
331
|
-
auto-promotion — it archives the source memory, and its `.derived` twin if
|
|
332
|
-
one exists, through the same path the pair pass uses below
|
|
333
|
-
(`archiveCleanupCandidate`, reason `promoted`, `successorRefs` the new
|
|
334
|
-
knowledge ref). A promotion no longer leaves a memory/knowledge duplicate
|
|
335
|
-
behind. This runs inside `promoteProposal`, not inside `akmConsolidate`
|
|
336
|
-
itself; a failure to archive the source only warns, it does not undo the
|
|
337
|
-
promotion.
|
|
338
|
-
|
|
339
|
-
### consolidate pair pass (alpha.9)
|
|
340
|
-
|
|
341
|
-
A second pass inside `akmConsolidate`, alongside Phase A/B above, over the
|
|
342
|
-
same enabled gate. It replaces nothing the promote pass does; it adds
|
|
343
|
-
duplicate, subsumed and superseding retirement, review-gated.
|
|
344
|
-
|
|
345
|
-
1. **Initiators:** memory (base or `.derived`), flat `knowledge/` or lesson
|
|
346
|
-
assets in the bundle the run writes to, in the retrieval scope, and
|
|
347
|
-
content-eligible: no prior pair-pass ledger row (source
|
|
348
|
-
`consolidate-pair`, kept apart from the promote pass's `consolidate`
|
|
349
|
-
rows), or a row whose recorded body hash differs from the asset's current
|
|
350
|
-
one. Eligibility is purely content-driven — the ledger row carries no
|
|
351
|
-
`next_eligible_at` timer at all for this source, so nothing here "expires"
|
|
352
|
-
on a schedule; see step 5.
|
|
353
|
-
2. **Candidates:** each initiator's nearest neighbours by stored vector
|
|
354
|
-
(`getNeighborsByEntryId`, fetching 20 and keeping the first 5 that clear
|
|
355
|
-
every filter below — a few self/twin/bundle/tier misses in a naive top-5
|
|
356
|
-
used to starve an initiator down to zero real candidates), same bundle,
|
|
357
|
-
memory tier only (structured knowledge in subfolders is excluded), minus
|
|
358
|
-
its own `.derived` twin or parent, at cosine >= `T_PAIR` (0.93). An
|
|
359
|
-
initiator with no prior ledger row needs >= `BACKFILL_FLOOR` (0.95)
|
|
360
|
-
instead, UNLESS it is new material — git first-added within
|
|
361
|
-
`NEW_MATERIAL_DAYS` (7) — which judges at the ordinary `T_PAIR`: new
|
|
362
|
-
content earns the same scrutiny as an edit, not the backlog's higher bar.
|
|
363
|
-
"Older"/"newer" for the judge's own A/B labelling comes from one
|
|
364
|
-
`git log --reverse -M --diff-filter=AR --name-status --format=@%ct` per
|
|
365
|
-
run over the whole bundle (first-add time per path, following renames —
|
|
366
|
-
an `A` sets a path's first-add, an `R` carries the old path's first-add
|
|
367
|
-
to the new one, so a rename never misdates a file as newly added), not
|
|
368
|
-
frontmatter or file mtime; mtime is only the fallback for a path git does
|
|
369
|
-
not track, or a bundle with no `.git` directly inside it. At most
|
|
370
|
-
`MAX_PAIRS_PER_RUN` (300) pairs a run, admitted a whole initiator at a
|
|
371
|
-
time rather than by flat cosine rank across all of them: new-or-changed
|
|
372
|
-
initiators first, then the existing backlog by its own best cosine, each
|
|
373
|
-
admitted only if every one of its own candidate pairs fits in what
|
|
374
|
-
remains of the 300 (first-fit, so a smaller initiator further down still
|
|
375
|
-
fits when a larger one ahead of it does not) — an initiator with a
|
|
376
|
-
pending-blocked pair is skipped entirely rather than spending any of the
|
|
377
|
-
budget on a pair that cannot be judged yet.
|
|
378
|
-
3. **Judge:** one LLM call per pair (`src/assets/prompts/consolidate-pair.md`)
|
|
379
|
-
through the consolidate process's own connection or harness and concurrency. It first
|
|
380
|
-
lists the durable claims each side alone holds (`onlyInA`, `onlyInB`),
|
|
381
|
-
then labels the pair one of `duplicate`, `subsumed`, `supersedes`,
|
|
382
|
-
`contradicts`, `overlap` or `unrelated`. Each side is shown up to 12,000
|
|
383
|
-
body characters.
|
|
384
|
-
4. **Outcome:** `duplicate`, `subsumed` and `supersedes` mint one `retire`
|
|
385
|
-
proposal (source `consolidate-pair`, its own generator — see below) for
|
|
386
|
-
the side that does not survive — never a
|
|
387
|
-
`.derived` memory whose parent still exists, never across bundles,
|
|
388
|
-
never when either side already has a pending retire proposal (as the
|
|
389
|
-
retired ref or its successor), and never retiring or reusing as a
|
|
390
|
-
successor an asset already spent earlier in the SAME run (the durable
|
|
391
|
-
half of this last guard is at accept time — step below), and never
|
|
392
|
-
retiring a side the judge listed a claim for. `contradicts`,
|
|
393
|
-
`overlap` and `unrelated` are recorded `judged_no_action` with no
|
|
394
|
-
proposal; `contradicts` is counted in the run report and stays a human
|
|
395
|
-
decision. A retirement (`duplicate`, `subsumed` or `supersedes`) whose
|
|
396
|
-
retired side has nothing listed (a `duplicate` must also have nothing listed
|
|
397
|
-
on the kept side; a subsumed or superseding successor may hold more) and no continuity risk gets a second call
|
|
398
|
-
(`consolidate-pair-check.md`: what does the retired note hold that the kept
|
|
399
|
-
one lacks?); an empty answer stages the proposal (gate `consolidate-pair`,
|
|
400
|
-
reason the judge's label), and the triage drain accepts it under its usual
|
|
401
|
-
`applyMode`. Replay precision was 336/360 (duplicate 0.98, subsumed 0.92,
|
|
402
|
-
supersedes 0.875). Every other retire proposal waits for a person.
|
|
403
|
-
5. **Ledger:** a row is written for an initiator only once every one of its
|
|
404
|
-
own candidates was admitted this run (whole-initiator admission in step 2
|
|
405
|
-
makes this an all-or-nothing membership check) AND actually resolved to a
|
|
406
|
-
verdict — a same-run guard dropping a retire-worthy verdict (the chain
|
|
407
|
-
guard in step 4), a failed mint, a failed or never-sent judge call, all
|
|
408
|
-
count as unresolved, not judged — recording its current body hash and an
|
|
409
|
-
outcome of `proposed` or `judged_no_action`. Neither outcome carries a
|
|
410
|
-
timer for this source: only a later body-hash mismatch (step 1) makes the
|
|
411
|
-
initiator eligible again. An initiator left with any candidate not
|
|
412
|
-
admitted, not judged, or dropped gets NO row at all, so the next run
|
|
413
|
-
reconsiders it rather than treating it as settled — real data: night 1
|
|
414
|
-
alone judged 177 `duplicate` verdicts into only 59 proposals before this,
|
|
415
|
-
the other 118 silently abandoned by the same-run chain guard.
|
|
416
|
-
|
|
417
|
-
Because that "no row" case regenerates ALL of an initiator's candidate
|
|
418
|
-
pairs next run — including ones already decided — a pair the owner
|
|
419
|
-
already declined (rejected, or accepted then reverted) is never re-minted
|
|
420
|
-
while both sides are unchanged: before the judge is ever called, the same
|
|
421
|
-
ref pair and the same two content hashes (checked in both orientations,
|
|
422
|
-
since it is the judge — not yet run — that would decide which side is
|
|
423
|
-
"retired") are checked against every rejected or reverted
|
|
424
|
-
`consolidate-pair` proposal on record, and a match is counted as a settled
|
|
425
|
-
no-action (so the initiator's row still gets written this run) at no LLM
|
|
426
|
-
cost at all. Those keys follow each proposal's current status: `akm
|
|
427
|
-
proposal reopen` (#997) moves a rejected proposal back to `pending`, so it
|
|
428
|
-
is no longer settled, and while it is pending a pair pass leaves both of its
|
|
429
|
-
documents alone (step 4's pending-retire guard) instead of minting the
|
|
430
|
-
pair a second time.
|
|
431
|
-
|
|
432
|
-
**Retire proposals (`akm proposal accept`/`revert`):** minted under their own
|
|
433
|
-
source, `consolidate-pair` — kept apart from the promote pass's
|
|
434
|
-
`consolidate` proposals so a bulk `accept`/`reject --generator consolidate`
|
|
435
|
-
never sweeps a retirement, and the reverse (`--max-diff-lines` counts a
|
|
436
|
-
retire proposal by its target's own current line count, not its empty
|
|
437
|
-
payload). Accepting one first confirms the decision is still fresh — the
|
|
438
|
-
successor still exists, and both sides' recorded body hashes still match
|
|
439
|
-
their current files — refusing cleanly, not partially applying a decision
|
|
440
|
-
something else (an intervening accept, possibly an A→B/B→C chain) made
|
|
441
|
-
stale. It then moves the retired asset (and its `.derived` twin) into the
|
|
442
|
-
existing `.akm/memory-cleanup/archive/` through the generalized
|
|
443
|
-
`archiveCleanupCandidate`, the one retirement encoding (D27) — never
|
|
444
|
-
`.akm/archive/`. A `supersedes` proposal first writes the supersede edge on
|
|
445
|
-
the older asset (`writeSupersededEdge`), then archives it. `akm proposal
|
|
446
|
-
revert` restores the archived file(s), and for the primary asset restores
|
|
447
|
-
the EXACT pre-retire bytes recorded at accept (`backupContent`) rather than
|
|
448
|
-
re-deriving "undo the edge" from the archived copy — so a pre-existing
|
|
449
|
-
human-written edge, YAML comments and key order all survive the round trip.
|
|
450
|
-
Accept records its full intent — `backupContent` and which file is about to
|
|
451
|
-
move — on the still-pending proposal before moving anything; a crash at any
|
|
452
|
-
point after resumes from that recorded intent (skipping whichever file a
|
|
453
|
-
tombstone scan shows an earlier, crashed attempt already archived under this
|
|
454
|
-
proposal's own id) rather than refusing it as stale or re-deriving
|
|
455
|
-
`backupContent` by guessing at what the archived copy would have been.
|
|
456
|
-
Revert works the other way for the same reason: each archive dir's own
|
|
457
|
-
tombstone already names its original and archived paths, so "original
|
|
458
|
-
present, archived copy missing" resumes as an earlier, crashed revert's own
|
|
459
|
-
work — UNLESS that original path's current content does not match the
|
|
460
|
-
`retirement.retiredContentHash` recorded at accept, meaning the path was
|
|
461
|
-
reused by an unrelated file since, which refuses instead of overwriting it.
|
|
462
|
-
Triage auto-accepts a `retire` proposal only when the pair pass staged it
|
|
463
|
-
(gate `consolidate-pair`, see step 4 above) and `applyMode` allows it; every
|
|
464
|
-
other one waits for a person. Review reuses `akm proposal list --generator consolidate-pair` (S4: the
|
|
465
|
-
backlog is reviewed as its own list, not mixed in with every other
|
|
466
|
-
generator's proposals), `show`, `diff` (which renders a retirement as the
|
|
467
|
-
retired file's lines leaving under a `retire` header, with the pair's verdict
|
|
468
|
-
and a note that accept archives and revert restores — not as the file replaced
|
|
469
|
-
by a blank one, #997), and bulk
|
|
470
|
-
`accept --generator consolidate-pair` / `reject --generator
|
|
471
|
-
consolidate-pair`. A proposal carrying `continuityRisk` (below) is excluded
|
|
472
|
-
from that bulk accept, whatever the generator or `--yes` — visible inline in
|
|
473
|
-
`list`'s default output and in the text output of `show` and `diff` (the
|
|
474
|
-
specific failing/unverified queries, not just a count) — bulk reject is unaffected,
|
|
475
|
-
and a person can always accept one by id. A rejection is not final:
|
|
476
|
-
`akm proposal reopen <id>` puts a rejected retire proposal back to `pending`
|
|
477
|
-
(refused if the retired document, or the successor, changed since the pair was
|
|
478
|
-
judged, or if another pending retire proposal now involves either of them),
|
|
479
|
-
and accepting it then archives the file exactly as for any other retire
|
|
480
|
-
proposal.
|
|
481
|
-
|
|
482
|
-
### Retirement continuity check (rule R3)
|
|
483
|
-
|
|
484
|
-
Before the pair pass mints a `retire` proposal, it replays up to five of the
|
|
485
|
-
retired asset's own past `search`/`curate` queries
|
|
486
|
-
(`loadRetrievalQueries`, `src/commands/improve/retrieval-queries.ts`:
|
|
487
|
-
`usableRetrievalQueries`
|
|
488
|
-
drops stash-README boilerplate and harness/tool envelopes
|
|
489
|
-
(`nonTaskInput`), pastes over 2,000 characters, and queries that are
|
|
490
|
-
duplicates once whitespace is collapsed, S3a) through akm's own search,
|
|
491
|
-
in-process — the same ranking a user gets, no LLM
|
|
492
|
-
(`src/commands/improve/consolidate/continuity-check.ts`). For every query
|
|
493
|
-
where the retired asset ranked in the top 10, the successor must rank in the
|
|
494
|
-
top 10 too — compared directly (N2): search itself returns at most the top
|
|
495
|
-
10 hits, so `rankOf` finding the successor among them or not is the whole
|
|
496
|
-
comparison, no generic rank-change-report abstraction needed. No recorded
|
|
497
|
-
queries means no check and no flag — and so does a retired/successor pair
|
|
498
|
-
whose bodies are
|
|
499
|
-
content-identical once whitespace is collapsed (S3b): search's own
|
|
500
|
-
content-dedupe (`src/indexer/search/db-search.ts`) already hides the
|
|
501
|
-
successor behind the retired asset for every such query, so a "successor
|
|
502
|
-
missing" finding there would not be a real risk, just that dedupe working
|
|
503
|
-
as designed.
|
|
504
|
-
|
|
505
|
-
A failing query does not block the mint — it flags. The proposal's
|
|
506
|
-
`retirement.continuityRisk` records the failing query count and, per failing
|
|
507
|
-
query, the retired asset's rank and the successor's (`null` when the
|
|
508
|
-
successor did not rank in the top 10 at all). This is the forgetting-safety
|
|
509
|
-
lane's old purpose (see below), now measured against search rank instead of
|
|
510
|
-
a stash-wide salience rank, and only at the moment an asset would actually
|
|
511
|
-
stop resolving.
|
|
512
|
-
|
|
513
|
-
A query that never ran (the search call threw) or that fell back to
|
|
514
|
-
keyword-only ranking (`mode: "fts-fallback"`) is "unverified" (S2): dropped
|
|
515
|
-
from the rank comparison — its hits are not the ranking a user actually
|
|
516
|
-
gets, so they are never compared — but tracked in
|
|
517
|
-
`retirement.continuityRisk.unverifiedQueries`, and on its own enough to set
|
|
518
|
-
`continuityRisk` even when every verified query passed. A search failure or
|
|
519
|
-
fallback must never look like "no risk found." `createContinuitySearch`
|
|
520
|
-
(`src/commands/improve/consolidate/continuity-check.ts`) is stateful across
|
|
521
|
-
one pair-pass run: the first `fts-fallback` it sees forces
|
|
522
|
-
`semanticSearchMode: "off"` for every later query that same run, so a down
|
|
523
|
-
embedding endpoint pays its failed-connection cost once per run, not once
|
|
524
|
-
per remaining query.
|
|
525
|
-
|
|
526
|
-
**The retired forgetting-safety lane.** Before alpha.9, `scoreSalience`
|
|
527
|
-
compared the whole stash's salience ranking before and after every improve
|
|
528
|
-
run, and a ref that fell from the top 200 to below 500 was injected into that
|
|
529
|
-
run's work set under `eligibilitySource: forgetting-safety`
|
|
530
|
-
(`applyForgettingSafety`, `improve_salience_rank_change` event). It was a
|
|
531
|
-
one-time cutover guard from the June 2026 ranking-formula change, running on
|
|
532
|
-
every run since. R5's 30-day event window (2026-08-30 to 2026-09-29, 47
|
|
533
|
-
`improve_salience_rank_change` events, 5 refs flagged across 4 runs) found no
|
|
534
|
-
marginal pick over the simpler baseline: 4 of the 5 flagged refs were also
|
|
535
|
-
picked that same run by the signal-delta lane, and the 5th has no
|
|
536
|
-
`reflect_invoked`/`distill_invoked` event in the retained history, but that
|
|
537
|
-
run's `improve_runs.plannedRefs` shows it, too, was planned under
|
|
538
|
-
`signal-delta` — just not reflected (a dispatch/budget limit that run, not a
|
|
539
|
-
lane-exclusive pick). All 5 flagged refs were signal-delta picks; zero were
|
|
540
|
-
ever forgetting-safety-only. It also protected `asset_salience.rank_score`,
|
|
541
|
-
which only improve itself ever read. Both the per-run comparison and the
|
|
542
|
-
injection are gone; so is `buildRankChangeReport` itself (N2 — the
|
|
543
|
-
continuity check compares ranks directly, and nothing else called it).
|
|
544
|
-
`forgetting-safety` stays a valid `eligibilitySource`/event-type value so
|
|
545
|
-
old proposals and events still decode.
|
|
546
|
-
|
|
547
|
-
### Archive purge sweep (step 8)
|
|
548
|
-
|
|
549
|
-
A retirement's archived bytes are not deleted when it is accepted — only the
|
|
550
|
-
tombstone (`cleanup.md`) resolves the ref from then on. `purgeGracedArchive`
|
|
551
|
-
(`src/commands/improve/memory/memory-improve.ts`) deletes them later,
|
|
552
|
-
deterministically and with no LLM, once at the very start of every
|
|
553
|
-
`akm improve` run — before index bootstrap, before triage. For a git-backed
|
|
554
|
-
bundle it walks `.akm/memory-cleanup/archive/`, and for every tombstone whose
|
|
555
|
-
`retiredAt` is more than `RETIRE_GRACE_DAYS` (30) days old AND whose
|
|
556
|
-
directory is entirely git-tracked and clean (one `git ls-files` plus one
|
|
557
|
-
`git status --porcelain -uall` per sweep, not per directory — B1), deletes
|
|
558
|
-
the archived asset file(s) under that tombstone's own directory — never
|
|
559
|
-
`cleanup.md` itself; git history keeps the bytes (D27). `.git` presence
|
|
560
|
-
alone does not prove a retirement was ever committed: `akm proposal accept`
|
|
561
|
-
only commits for a `kind: "git"` write target, and improve's own auto-sync
|
|
562
|
-
only stages the paths that same run wrote, so a directory with even one
|
|
563
|
-
untracked or modified file (the tombstone included) is left whole for a
|
|
564
|
-
later sweep rather than losing the only surviving copy of it. A
|
|
565
|
-
memory-cleanup family-prune archive (not a pair-pass retirement) has no
|
|
566
|
-
`retiredAt` in its tombstone at all, so this sweep never touches that older
|
|
567
|
-
archive class.
|
|
568
|
-
|
|
569
|
-
Every deleted file is journaled individually
|
|
570
|
-
(`src/core/write-provenance.ts`), the same mechanism the archive move itself
|
|
571
|
-
uses, so the end-of-run auto-sync commits the deletion in the run's own
|
|
572
|
-
commit. A bundle with no `.git` of its own is left untouched — there is
|
|
573
|
-
nothing to fall back on if a delete turns out to be wrong — and `akm health`
|
|
574
|
-
reports its archive's size and file count instead (`memory-cleanup-archive`
|
|
575
|
-
advisory, `src/commands/health/archive-usage.ts`), silent whenever the bundle
|
|
576
|
-
is git-backed or the archive is empty or absent.
|
|
577
|
-
|
|
578
|
-
### improve-owned maintenance
|
|
579
|
-
|
|
580
|
-
After consolidation completes, `akmImprove` runs maintenance steps that own the
|
|
581
|
-
remaining live-write memory/index artifacts previously coupled to indexing.
|
|
582
|
-
|
|
583
|
-
**Memory inference:**
|
|
584
|
-
|
|
585
|
-
1. Collect the memory refs that completed distill without being promoted to
|
|
586
|
-
`knowledge:` in the same improve run.
|
|
587
|
-
2. Call `runMemoryInferencePass` with those refs.
|
|
588
|
-
3. If the pass writes derived memories or marks parents with
|
|
589
|
-
`inferenceProcessed: true`, call `reindexFn({ stashDir })` so SQLite/search
|
|
590
|
-
state reflects the new disk state before any later steps run.
|
|
591
|
-
|
|
592
|
-
### Proposal queue
|
|
593
|
-
|
|
594
|
-
`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.
|
|
595
|
-
|
|
596
|
-
**Logical proposal shape:**
|
|
597
|
-
|
|
598
|
-
```json
|
|
599
|
-
{
|
|
600
|
-
"id": "<UUID>",
|
|
601
|
-
"ref": "lessons/skill-deploy-lesson",
|
|
602
|
-
"status": "pending",
|
|
603
|
-
"source": "reflect",
|
|
604
|
-
"sourceRun": "reflect-1715000000000",
|
|
605
|
-
"createdAt": "2026-05-11T00:00:00.000Z",
|
|
606
|
-
"updatedAt": "2026-05-11T00:00:00.000Z",
|
|
607
|
-
"payload": {
|
|
608
|
-
"content": "---\ndescription: ...\n---\n\nbody",
|
|
609
|
-
"frontmatter": { "description": "..." }
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
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).
|
|
615
|
-
|
|
616
|
-
## Scope restrictions
|
|
617
|
-
|
|
618
|
-
`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.
|
|
619
|
-
|
|
620
|
-
`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.
|
|
9
|
+
| Option | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `[scope]` (positional) | A single ref (`[bundle//]conceptId`), an asset type, or omitted for all assets. `akm improve report` and `akm improve judge` are subcommands, not scopes. |
|
|
12
|
+
| `--strategy <name>` | A named strategy from `improve.strategies` or a built-in: `default` or `consolidate`. It chooses which processes run, never where. |
|
|
13
|
+
| `--connection <name>` / `--harness <name>` | Run every call of this run, judges included, on this connection or harness instead of the default. They are mutually exclusive. |
|
|
14
|
+
| `--model <alias\|name>` | The model for every call of this run: an alias (`fast`, `balanced`, `reasoning`) or an exact name. |
|
|
15
|
+
| `--task` | Extra guidance for this pass, passed to the reflect prompt. |
|
|
16
|
+
| `--dry-run` | Compute the plan and analyze memory cleanup. Takes no lock, emits no events, calls no model, writes nothing. |
|
|
17
|
+
| `--show-prompt` | Print the composed reflect prompt for one fully-qualified ref and exit. No lock, index write, or model call. |
|
|
18
|
+
| `--bundle` | The bundle the run improves and writes to, overriding `defaultWriteTarget` and the working bundle. Only its assets are planned. |
|
|
19
|
+
| `--limit` | Cap the number of assets processed, highest salience first. |
|
|
20
|
+
| `--timeout-ms` | Wall-clock budget for the whole run. Default 7 200 000 ms (2 hours). |
|
|
21
|
+
| `--skip-if-locked` | If another improve run holds the lock, return an exit-0 no-op. Without it, contention is `IMPROVE_LOCK_HELD`, exit 75. |
|
|
22
|
+
| `--require-feedback-signal` | Turn the proactive-maintenance lane off for the run, so only assets with recent feedback are planned. |
|
|
23
|
+
| `--sync` / `--no-sync`, `--push` / `--no-push` | Override the strategy's `sync` block (the pre-run sync and the end-of-run commit and push for a git-backed bundle). |
|
|
24
|
+
| `--json-to-stdout` | Also print the full persisted run result as JSON. |
|
|
25
|
+
| `--run`, `--since` | Only for `akm improve report`. |
|
|
621
26
|
|
|
622
|
-
|
|
27
|
+
`--require-engines` is retired: it is parsed, warned about once, and ignored. A run already stops at planning (exit 78) when no model-calling process has a connection or harness to run on.
|
|
623
28
|
|
|
624
|
-
|
|
29
|
+
## Run order
|
|
625
30
|
|
|
626
|
-
|
|
31
|
+
A live run:
|
|
627
32
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
33
|
+
1. Resolve the scope and the strategy plan (`resolveImprovePlan`): which processes are enabled and which connection or harness and model each uses.
|
|
34
|
+
2. Take the whole-run lock, `$STATE/locks/<stash>/improve.lock` (see [Storage Locations](storage-locations.md)).
|
|
35
|
+
Then, for a git-backed bundle, sync it as `akm sync` does (fetch, merge, commit, push) so the index and the plan see what another host pushed; a conflict or fetch failure warns and the run continues on the local state (`preRunSync` in the result).
|
|
36
|
+
3. Delete aged retirement archives (`purgeGracedArchive`, git-backed bundles only; see [Archive purge](../improvement.md#archive-purge)).
|
|
37
|
+
4. Bring the index current (`runIndexBootstrapPass`), then drain the pending-proposal backlog under the strategy's `triage` settings (`runTriagePrePass`), then index what triage wrote.
|
|
38
|
+
5. Collect eligible refs from the index.
|
|
39
|
+
6. Preparation (`runImprovePreparationStage`, `preparation.ts`), in this order:
|
|
40
|
+
1. consolidation (promote pass and pair pass);
|
|
41
|
+
2. session extract, when the strategy enables it;
|
|
42
|
+
3. memory cleanup: archive redundant derived memories and write belief-state transitions, then reindex if anything changed;
|
|
43
|
+
4. validation: a ref whose file is missing, or a lesson without a `description`, is excluded; schema repair tries to fix those when `validation` is enabled;
|
|
44
|
+
5. selection: the signal-delta filter, the proactive-maintenance lane, salience ranking, a disk check, and `--limit`.
|
|
45
|
+
7. The per-asset loop (`runImproveLoopStage`, `loop-stages.ts`): for each selected ref, reflect, then distill for a distillable memory. The wall-clock budget is checked before each ref.
|
|
46
|
+
8. Post-loop maintenance (`runImprovePostLoopStage`), skipped when the budget is spent: memory inference (reindex if it wrote anything), proposal hygiene (orphan purge and expiry), orphaned-state GC, the retention purges (events, `improve_runs`, `logs.db`, task log files, and the per-run improve and index log files under `$CACHE/logs/`; `improve.eventRetentionDays`, default 90), and, for a whole-bundle scope, a dead-link check on URLs in knowledge bodies.
|
|
47
|
+
9. Assemble the result, emit `improve_completed`, run the end-of-run sync, release the lock.
|
|
636
48
|
|
|
637
|
-
|
|
49
|
+
A dry run skips steps 2 to 4. It collects refs (step 5), evaluates the preparation gates read-only (`planOnly`), and reports the plan; nothing is dispatched or persisted.
|
|
638
50
|
|
|
639
|
-
|
|
51
|
+
`experimental.improveAutonomy` gates three lanes: memory inference, memory cleanup apply, and triage `applyMode: promote` when the triage judgment tier is on (downgraded to `queue`). A gated lane produces an `improve_skipped` event with reason `autonomy_gated` and a warning that names the config key (`src/commands/improve/autonomy-gate.ts`).
|
|
640
52
|
|
|
641
|
-
|
|
642
|
-
|---|---|---|
|
|
643
|
-
| `distill` | `improve.strategies.<name>.processes.distill` | Enables distillation and its tuning; where it runs is `improve.processes.distill` (connection or harness, model, request overrides). |
|
|
644
|
-
| `consolidate` | `improve.strategies.<name>.processes.consolidate` | Enables consolidation and its tuning; where it runs is `improve.processes.consolidate`. |
|
|
53
|
+
## reflect (akmReflect)
|
|
645
54
|
|
|
646
|
-
|
|
647
|
-
contains every process's frozen enablement, process config, and resolved runner.
|
|
648
|
-
A process runs on a connection, or on a harness: one whose tools cannot be restricted
|
|
649
|
-
runs unrestricted in a scratch directory, with a notice ([How akm runs a
|
|
650
|
-
harness](../../reference/configuration.md#how-akm-runs-a-harness)). A process
|
|
651
|
-
with no connection or harness is skipped and reported in `skippedProcesses`.
|
|
55
|
+
Reflect changes only an asset's `description`, `when_to_use`, and title. The model 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 and one blank line akm inserts when the body has no level-1 heading (the only body change reflect makes). Reflect never writes an asset; it queues a proposal with `source: "reflect"`.
|
|
652
56
|
|
|
653
|
-
|
|
57
|
+
1. `reflect_invoked` is emitted once the dispatch has validated its credentials; every call ends with one `reflect_completed` event.
|
|
58
|
+
2. The ref is looked up in the index and the file read. A file that is not at the path a proposal would write (`<root>/<type dir>/<name>.md`) is refused as `unsupported_type` before any model call; a skill's `references/a.md` is the ref `knowledge/skills/<name>/references/a`.
|
|
59
|
+
3. Where reflect runs: `improve.processes.reflect`, then `improve.connection` / `improve.harness`, then the default connection (else the default harness), asking for the `fast` model unless a layer names another ([Where each process runs](../../reference/configuration.md#where-each-process-runs)). The runner is a connection or a harness (its tools restricted where it can be), and both run the same iteration. The reply's JSON Schema is the request's output schema: `response_format` for a connection, the shared schema instruction at the end of the prompt for a harness.
|
|
60
|
+
4. The reply is validated by `parseSchemaReflectOutput`. An invalid reply gets one repair turn; one still invalid fails with `parse_error` and queues nothing.
|
|
61
|
+
5. `applyReflectPatch` applies the patch; a non-null `title` becomes a `# <title>` heading only when the body has no level-1 heading. A patch that changes nothing is the `no_change` outcome (`reflect_skipped_noop`; `reflect_skipped_cosmetic` for a cosmetic-only change) and queues nothing.
|
|
62
|
+
6. Defect filter (`findReflectDefect`): a revision that adds placeholder text, talks about its own edit, or copies frontmatter into the body is refused as `quality_rejected`. `processes.reflect.defectFilter` replaces or turns off the wording lists.
|
|
63
|
+
7. Quality gate (`processes.reflect.qualityGate`, on unless disabled): a judge scores need, preservation, and quality from 1 to 5 and the revision passes only when every score is 4 or more ([Quality judge](../improvement.md#quality-judge)). A score that does not pass is refused (`quality_rejected`, no proposal) unless its mean is 3.5 or more: that is uncertain, not bad, and the proposal is deferred for a person (`judge-uncertain`, with the scores and the judge's reason) and records no rejection window. A judge that errors or times out defers the proposal for review (`judge-error`).
|
|
64
|
+
8. The proposal is minted (`mintProposal`). A pass the judge passed is stamped `staged` and the triage drain may accept it; with the gate off the proposal is minted unstamped for the drain to decide. One whose gate has no judge configured (`no-judge-configured`), no verdict, or an uncertain verdict is deferred for a person, and the drain leaves every deferral.
|
|
654
65
|
|
|
655
|
-
`
|
|
656
|
-
and can report `ok: false` for terminated runs:
|
|
66
|
+
A harness gets the asset content capped at `REFLECT_CONTENT_CAP` (12 000 characters) because it travels through argv; a connection derives its cap from its `contextLength`.
|
|
657
67
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
The
|
|
68
|
+
## distill (akmDistill)
|
|
69
|
+
|
|
70
|
+
Distill turns a memory and its feedback into a lesson proposal, or into a knowledge proposal for a reinforced memory. It makes one bounded model call (`callStage`) on the resolved runner: a direct chat completion on a connection, or a run on a harness, restricted where the harness can be. Every call emits a `distill_invoked` event with an `outcome`; a disabled process (`config_disabled`) emits none.
|
|
71
|
+
|
|
72
|
+
1. Validate the ref and load the asset through the index.
|
|
73
|
+
2. Read the ref's feedback events, minus those named in `excludeFeedbackFromRefs`.
|
|
74
|
+
3. Promotion fast path: when `proposalKind` is `auto` or `knowledge` and `assessMemoryKnowledgePromotionCandidate` returns `promote: true`, queue a knowledge proposal without a model call.
|
|
75
|
+
4. A lesson already at the target ref is not regenerated: distill returns `skipped` with `skipReason: "lesson_exists"` before any model call.
|
|
76
|
+
5. Call the writer where distill runs (`improve.processes.distill`, then `improve.connection` / `improve.harness`, then the default); a call stops after 600 000 ms unless `timeoutMs` says otherwise. The prompt carries the memory, its feedback, and the lessons, knowledge notes, and skills nearest it (top 3; `processes.distill.cls.enabled: false` turns them off). A timeout, error, or empty reply is `llm_failed` and leaves the ref eligible.
|
|
77
|
+
6. The writer may find no lesson (`NONE`, or `decision: "none"`): distill returns `skipped` with `skipReason: "nothing_reusable"` and makes no judge call.
|
|
78
|
+
7. Strip fences and `<think>` blocks, then validate (`lintLessonContent` for lessons, `validateKnowledgeContent` for knowledge). A failure is `validation_failed` and throws `UsageError`.
|
|
79
|
+
8. Quality gate (`processes.distill.qualityGate`, on unless disabled): one judge call, on the gate's own connection or harness (`improve.qualityGate`, else the default), scores `reusable`, `nonRedundancy`, and `grounding` from 1 to 5. A criterion at 2 or below is `quality_rejected` (no proposal; a ledger row). Otherwise the lesson passes when `reusable` and `nonRedundancy` both score 4 or more, and a mean of those two of 2.5 or more is `review_needed`. `grounding` is left out of the mean and of the pass test on purpose (adding it only moved lessons from pass to review, and both go to a person); it still rejects at 2 or below. A judge that times out, replies unparseably, or is missing also gives `review_needed`. A pass is minted `deferred` for a person (reason `distill-review`), not staged for the drain.
|
|
80
|
+
9. Mint the proposal with `source: "distill"` and emit `distill_invoked` with `outcome: "queued"`.
|
|
81
|
+
|
|
82
|
+
When the lesson repeats one the library holds, distill extends that lesson instead of only dropping the duplicate. This happens with the quality gate on, when the judge scored the writer's lesson 2 or below on `nonRedundancy`, or the writer answered `NONE`. A second call shows the writer the memory, its feedback, and the related lessons (among the top 3 nearby assets, in the bundle the proposal is filed in) that the writer's reason or the judge's names, or the only related lesson when it names none, and asks it for the lesson's whole body with a line added for each fact the memory states and the lesson lacks. The update is proposed only when every existing body line is still there, the extended lesson passes the judge (an edit variant of its rubric), and at least one line was added. It is a pending proposal on the existing lesson's ref, `deferred` for a person (reason `distill-update`), with the memory added to `xrefs` and the lesson's hash as `beforeHash`, so accepting it after the lesson changed is refused as stale. The result and the event are `queued` with `proposalRef` set to the lesson and `updatesExisting: true`. When nothing is proposed, the outcome is what it would have been: `quality_rejected` after a duplicate lesson, `skipped` / `nothing_reusable` after `NONE`.
|
|
83
|
+
|
|
84
|
+
The lesson ref is `lessons/[<scope>/]<type>-<name>-lesson`, derived from the input ref (`deriveLessonRef`, `distill.ts`): the first segment of a nested name is kept as `<scope>/`, and the rest is lower-cased with non-alphanumerics replaced by `-`. `memories/project-a/deploy` becomes `lessons/project-a/memory-deploy-lesson`; `skills/deploy` becomes `lessons/skill-deploy-lesson`.
|
|
85
|
+
|
|
86
|
+
The loop skips distill for a memory (a `distill-skipped` action and an `improve_skipped` event) when:
|
|
87
|
+
|
|
88
|
+
- it has no recent feedback (`memory_distill_requires_feedback`);
|
|
89
|
+
- a negative feedback in the last 30 days judged the body it still has (`distill_flagged_wrong`);
|
|
90
|
+
- its only feedback is positive without a reason or note (`distill_positive_without_reason`);
|
|
91
|
+
- its frontmatter says `beliefState: deprecated` or `superseded` (`distill_deprecated_or_superseded`);
|
|
92
|
+
- no signal is newer than the ref's last distill attempt (`distill_no_new_signal`).
|
|
93
|
+
|
|
94
|
+
An explicit ref scope bypasses these.
|
|
95
|
+
|
|
96
|
+
## consolidate (akmConsolidate)
|
|
97
|
+
|
|
98
|
+
`akmConsolidate` runs during preparation, before session extract and the per-asset loop. It returns immediately when the strategy's `processes.consolidate.enabled` is false.
|
|
99
|
+
|
|
100
|
+
**Promote pass.** It loads eligible non-`.derived` memories, minus those the improve ledger holds (see [Ledger pre-filter](#ledger-pre-filter-signal-delta)). The pool is clustered by embedding similarity, split into chunks (size from the connection's context window, or a default for a harness, capped by `maxChunkSize`, at most 50), and each chunk is sent to the consolidate process's connection or harness with a JSON Schema asking for `promote` operations, the only operation offered. `mergePlans` keeps one promotion per source memory. For each operation, the checks below run cheapest first and the first that applies skips the memory, recording a skip reason; otherwise a `knowledge/` proposal is queued with `source: "consolidate"`. If the remaining wall-clock budget cannot cover every chunk, the oldest memories are taken first and the rest are deferred.
|
|
101
|
+
|
|
102
|
+
Skip reasons, in check order (`consolidation.skipReasons` in the result):
|
|
675
103
|
|
|
676
104
|
| Reason | Meaning |
|
|
677
|
-
|
|
678
|
-
| `promote_already_promoted_this_run` |
|
|
679
|
-
| `promote_pending_proposal_exists` | A pending proposal
|
|
680
|
-
| `promote_already_exists` | `knowledge/<slug>.md` already exists in the write target.
|
|
105
|
+
|---|---|
|
|
106
|
+
| `promote_already_promoted_this_run` | The memory was already promoted earlier in this run. |
|
|
107
|
+
| `promote_pending_proposal_exists` | A pending proposal exists for the target `knowledge/<slug>`. |
|
|
108
|
+
| `promote_already_exists` | `knowledge/<slug>.md` already exists in the write target. |
|
|
681
109
|
| `promote_read_failed` | The memory file could not be read. |
|
|
682
|
-
| `promote_sanitization_failed` | The memory could not be re-serialized as an asset (
|
|
683
|
-
| `promote_superseded` | The memory has `status: superseded
|
|
684
|
-
| `promote_source_too_small` | The memory
|
|
685
|
-
| `dedup_existing_knowledge` | A `knowledge/` doc in the write target
|
|
686
|
-
| `dedup_pending_proposal` | A pending consolidate proposal
|
|
687
|
-
| `dedup_covered_by_knowledge` |
|
|
688
|
-
| `promote_invalid_frontmatter` | The description
|
|
689
|
-
| `promote_dedup_window` | The target slug is a variant of a pending consolidate proposal's slug
|
|
690
|
-
| `promote_create_failed` | `createProposal` threw.
|
|
110
|
+
| `promote_sanitization_failed` | The memory could not be re-serialized as an asset (unbalanced code fence, malformed frontmatter). |
|
|
111
|
+
| `promote_superseded` | The memory has `status: superseded`. |
|
|
112
|
+
| `promote_source_too_small` | The memory body is under 100 characters. |
|
|
113
|
+
| `dedup_existing_knowledge` | A `knowledge/` doc in the write target has an identical body. |
|
|
114
|
+
| `dedup_pending_proposal` | A pending consolidate proposal has an identical body. |
|
|
115
|
+
| `dedup_covered_by_knowledge` | One of the 20 `knowledge/` docs nearest to the memory in its bundle holds at least 30% of the memory's distinct 5-word shingles (`COVERAGE_MIN_CONTAINMENT`, `consolidate/coverage.ts`). Needs a stored vector for the memory. |
|
|
116
|
+
| `promote_invalid_frontmatter` | The description to use is missing or truncated. |
|
|
117
|
+
| `promote_dedup_window` | The target slug is a variant of a pending consolidate proposal's slug. |
|
|
118
|
+
| `promote_create_failed` | `createProposal` threw. No ledger row is written, so the next run retries. |
|
|
119
|
+
|
|
120
|
+
A skipped memory is recorded in the ledger as `judged_no_action`, except one already promoted this run and `promote_create_failed`.
|
|
121
|
+
|
|
122
|
+
Accepting a consolidate promotion archives the source memory (and its `.derived` twin) through `archiveCleanupCandidate` with reason `promoted`. This happens in `promoteProposal`, not in `akmConsolidate`; an archive failure warns and does not undo the promotion.
|
|
691
123
|
|
|
692
|
-
|
|
124
|
+
**Pair pass** (`consolidate/pair-pass.ts`), alongside the promote pass under the same gate. It proposes retiring duplicate, subsumed, and superseded memories, always review-gated.
|
|
693
125
|
|
|
694
|
-
|
|
126
|
+
1. Initiators are memories (base or `.derived`) and flat `knowledge/` or lesson assets in the write-target bundle, in the retrieval scope, that are content-eligible: no ledger row with source `consolidate-pair`, or a row whose recorded body hash differs from the asset's current one. These rows carry no `next_eligible_at`.
|
|
127
|
+
2. For each initiator, the 20 nearest neighbours by stored vector are fetched (`PAIR_NEIGHBOR_FETCH_K`) and the first 5 that clear the filters are kept (`PAIR_NEIGHBOR_K`): same bundle, memory tier only, not its own `.derived` twin or parent, cosine at least `T_PAIR` (0.93). An initiator with no prior ledger row needs `BACKFILL_FLOOR` (0.95) instead, unless it was first added to git within `NEW_MATERIAL_DAYS` (7), in which case `T_PAIR` applies. At most `MAX_PAIRS_PER_RUN` (300) pairs are judged per run, admitted a whole initiator at a time: new or changed initiators first, then the backlog by best cosine.
|
|
128
|
+
3. One judge call per pair (`src/assets/prompts/consolidate-pair.md`), on the consolidate process's own connection or harness, lists the durable claims only each side holds, then labels the pair `duplicate`, `subsumed`, `supersedes`, `contradicts`, `overlap`, or `unrelated`. Each side is shown up to 12 000 body characters.
|
|
129
|
+
4. `duplicate`, `subsumed`, and `supersedes` queue one `retire` proposal with `source: "consolidate-pair"`, subject to guards: not a `.derived` memory whose parent exists, not across bundles, not when either side already has a pending retire proposal, not an asset already spent earlier in the same run, and not a side for which the judge listed a claim. `contradicts`, `overlap`, and `unrelated` are recorded `judged_no_action`; `contradicts` is counted in the run report and left to a person.
|
|
130
|
+
5. A retirement with nothing listed on the retired side, and no continuity risk, gets a second call (`consolidate-pair-check.md`) asking what the retired note holds that the kept one lacks. An empty answer stages the proposal (gate `consolidate-pair`) and the triage drain may accept it under its `applyMode`. Every other retire proposal waits for a person.
|
|
131
|
+
6. A ledger row (the initiator's body hash and outcome `proposed` or `judged_no_action`) is written only when all of the initiator's candidates were admitted and resolved to a verdict. Otherwise no row is written and the next run reconsiders it. A pair a person rejected or reverted is not re-minted while both bodies are unchanged: the pair and its two content hashes are checked against those proposals before the judge is called.
|
|
695
132
|
|
|
696
|
-
|
|
697
|
-
`src/commands/improve/reflect.ts`, and `src/commands/improve/distill.ts`.
|
|
133
|
+
**Continuity check** (`consolidate/continuity-check.ts`). Before a retire proposal is queued, up to five of the retired asset's past `search` and `curate` queries are replayed through akm's own search. For each query where the retired asset ranked in the top 10, the successor must too. A failure does not block the proposal; it is recorded as `retirement.continuityRisk`, and a proposal carrying it is excluded from bulk `accept`. A query that threw or fell back to keyword-only ranking is recorded as unverified and also sets `continuityRisk`. See [Retirement continuity](../improvement.md#retirement-continuity).
|
|
698
134
|
|
|
699
|
-
**
|
|
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
|
|
135
|
+
**Retire proposals** (`akm proposal accept` and `revert`). Accept first checks the successor still exists and both recorded body hashes still match, then moves the retired asset (and its `.derived` twin) into `.akm/memory-cleanup/archive/` through `archiveCleanupCandidate`. A `supersedes` proposal first writes the supersede edge on the older asset. Accept records its intent on the pending proposal before moving anything, so a crashed accept resumes. `revert` restores the archived files and the exact pre-retire bytes recorded as `backupContent`. Review them as their own list: `akm proposal list --generator consolidate-pair`, with bulk `accept` and `reject` taking `--generator consolidate-pair`. `akm proposal reopen <id>` moves a rejected retire proposal back to `pending` unless the retired document or the successor changed, or another pending retire proposal now involves either.
|
|
711
136
|
|
|
712
|
-
|
|
137
|
+
## Proposal queue
|
|
713
138
|
|
|
714
|
-
|
|
139
|
+
`createProposal` is the write point for reflect, distill, consolidate (promote), extract, schema repair, and `akm proposal new`; the pair pass's retire proposals use `createRetireProposal`. Both write the `proposals` table in `state.db`, partitioned by `stash_dir` (see [Storage Locations](storage-locations.md)). Pending, accepted, rejected, and reverted are statuses on one row; `akm proposal reopen` moves a rejected row back to pending.
|
|
715
140
|
|
|
716
|
-
|
|
141
|
+
## Scope
|
|
142
|
+
|
|
143
|
+
`akm improve` reads and writes one bundle, its write target: `--bundle`, else `defaultWriteTarget`, else the working bundle. Only that bundle's assets are planned, because every proposal is filed in the write target. To improve another writable bundle, pass `--bundle <name>` or a bundle-qualified scope such as `team//skills/code-review`; a scheduled run covers only its own write target, so each other bundle needs its own scheduled `akm improve --bundle <name>`. A dry run resolves the bundle the same way a live run does.
|
|
144
|
+
|
|
145
|
+
## Ledger pre-filter (signal delta)
|
|
146
|
+
|
|
147
|
+
Every stage reads the improve ledger (`improve_ledger` in `state.db`; columns in [Storage Locations](storage-locations.md)) before it spends a model call, and writes it after. The ledger `source` is `reflect`, `distill`, `consolidate`, `consolidate-pair`, `extract`, `schema-repair`, or `propose`.
|
|
148
|
+
|
|
149
|
+
`nextEligibleAt` (`src/storage/repositories/improve-ledger-repository.ts`) decides when a ref may be tried again:
|
|
150
|
+
|
|
151
|
+
| Outcome | Window |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `rejected`, `quality_rejected` | 14 days for reflect, 30 for distill, 7 for any other source |
|
|
154
|
+
| `expired` | 1 day |
|
|
155
|
+
| `proposed`, `review_needed`, `judged_no_action`, `unchanged` | 7 days |
|
|
156
|
+
| `accepted`, `failed` | none |
|
|
157
|
+
|
|
158
|
+
The consolidate pair pass, and a consolidate promote-pass row (`accepted`, `rejected`, or `judged_no_action`) that recorded a body hash, have no clock: the ref waits until its body differs. `rejected`, `quality_rejected`, and `expired` are hard windows. Every other window is a revisit cadence that a newer signal lifts (`isLedgerBlocked`).
|
|
159
|
+
|
|
160
|
+
**Selection.** Before the loop, `buildSnapshotManifest` reads the feedback events and the ledger's reflect and distill rows once, and `partitionBySignalDelta` (both in `preparation.ts`) sorts the refs. A ref passes reflect when it has negative feedback dated within the last 30 days and newer than the ref's last reflect attempt, with no hard window blocking it. A positive or note-only signal never plans a reflect. Neither does a negative that came with an exact fix (`akm feedback --replace/--with`, `--outdated`, `--superseded-by`) once an accepted `feedback` proposal has applied the fix. A ref passes distill when it has feedback carrying a signal or a note, positive included, under the same date and ledger rules. Then:
|
|
161
|
+
|
|
162
|
+
- A ref that passes reflect is planned for the loop. If it does not pass distill, distill is skipped for it.
|
|
163
|
+
- A ref that passes only distill, and is a distill candidate, is planned distill-only.
|
|
164
|
+
- A ref with no in-window feedback is left to the proactive-maintenance lane, which picks only what retrieval returned or new material ([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).
|
|
165
|
+
- Every ref the loop does not take is counted in the plan's gates and reported once, in aggregate, as an `improve_skipped` event (`no_new_signal`, `not_retrieved`).
|
|
166
|
+
- Selected refs are ranked by salience (`scoreSalience` in `preparation.ts`, `computeSalience` in `salience.ts`: encoding, outcome, and retrieval frequency and recency, discounted for file size, with repeated no-ops ranked lower). Refs missing on disk are dropped, then `--limit` cuts the list: reflect-path refs first, then distill-only refs.
|
|
167
|
+
|
|
168
|
+
An explicit ref scope bypasses every gate. Consolidation, extract, and schema repair read their own ledger sources in their own stages.
|
|
169
|
+
|
|
170
|
+
## Strategy processes
|
|
171
|
+
|
|
172
|
+
A strategy is `improve.strategies.<name>` (config) or a built-in under `src/assets/improve-strategies/*.json` (`default` and `consolidate`). Its `processes` block configures eight processes (`IMPROVE_PROCESS_NAMES`, `src/core/config/engine-semantics.ts`; schemas in `src/core/config/schema/improve-processes.ts`). Every process takes `enabled`; the table lists the fields that are particular to each. A strategy says which processes run and how each is tuned, never where: where each model-calling process runs is `improve.processes.<name>` (`connection` or `harness`, `model`, `llm`, `timeoutMs`), over `improve.connection` / `improve.harness` and the defaults ([Where each process runs](../../reference/configuration.md#where-each-process-runs)).
|
|
173
|
+
|
|
174
|
+
| Process | Does | Particular fields |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| `reflect` | Revises an asset's `description`, `when_to_use`, and title from negative feedback | `allowedTypes`, `excludeRefPrefixes`, `limit`, `qualityGate`, `defectFilter` |
|
|
177
|
+
| `distill` | Turns a memory and its feedback into a lesson or knowledge proposal | `allowedTypes`, `qualityGate`, `requirePlannedRefs`, `cls` |
|
|
178
|
+
| `consolidate` | Promote pass and pair pass over memories | `allowedTypes`, `limit`, `maxChunkSize` |
|
|
179
|
+
| `memoryInference` | Writes derived memories and marks parents (`inferenceProcessed`); gated by `experimental.improveAutonomy` | `minPendingCount` |
|
|
180
|
+
| `extract` | Queues proposals from coding-agent session transcripts (the same pass as `akm proposal extract`) | `defaultSince`, `maxTotalChars`, `minContentChars`, `triage` (`enabled`, `minScore`), `minNewSessions`, `maxSessionsPerRun`, `indexSessions`, `minSessionDuration` |
|
|
181
|
+
| `validation` | Checks that planned refs exist and lessons have a `description`; where it can run a model, attempts schema repair (proposals with `source: "schema-repair"`) | none beyond `enabled` |
|
|
182
|
+
| `triage` | Drains the pending-proposal backlog before the loop | `applyMode` (`queue` or `promote`), `maxAcceptsPerRun`, `judgment` (boolean or `{ enabled }`; the judgment runs where `improve.processes.triage` says) |
|
|
183
|
+
| `proactiveMaintenance` | Picks assets due for review that have no fresh feedback and plans them with the feedback-bearing refs; makes no model call | `dueDays`, `maxPerRun` |
|
|
184
|
+
|
|
185
|
+
Each strategy also has a `sync` block (`enabled`, `push`, `message`) and a profile-level `limit`. `default` enables reflect, distill, consolidate, memoryInference (which runs only with `experimental.improveAutonomy`), validation, proactive maintenance, and a triage drain in `promote` mode with judgment off and `maxAcceptsPerRun` 200; improve-stage extract is off. `consolidate` is a patch merged onto `default` that turns every process off but consolidate. Any other name is a user-defined strategy that inherits `default`.
|
|
186
|
+
|
|
187
|
+
Process selection is resolved once by `resolveImprovePlan`. The plan holds every process's enablement, config, and resolved runner. A process runs on a connection, or on a harness: one whose tools cannot be restricted runs unrestricted in a scratch directory, with a notice ([How akm runs a harness](../../reference/configuration.md#how-akm-runs-a-harness)). A process with no connection or harness is skipped and reported in `skippedProcesses`; a run in which no model-calling process can run stops at planning (exit 78).
|
|
188
|
+
|
|
189
|
+
## Output shape
|
|
717
190
|
|
|
718
|
-
|
|
191
|
+
The result is `AkmImproveResult` (`src/core/improve-types.ts`), `schemaVersion: 2`. Besides `strategy`, `scope`, `dryRun`, and `ok` it carries: `plannedRefs`, `plan` (the resolved process routing and gates), `actions`, `distillSkipped` (the folded `distill-skipped` actions), `validationFailures`, `schemaRepairs`, `consolidation`, `extract`, `memoryCleanup`, `memoryInference`, `skippedProcesses`, `triage`, `proactiveMaintenance`, `deadUrls`, `writtenPaths`, `preRunSync`, and `sync`. A run that found the lock held returns `skipped: { reason: "lock-held" }`.
|
|
719
192
|
|
|
720
|
-
|
|
193
|
+
`actions` entries are `{ ref, mode, result }` where `mode` is one of `reflect`, `reflect-skipped`, `reflect-guard-rejected`, `reflect-failed`, `distill`, `memory-prune`, `memory-inference`, or `error` (`ImproveActionMode`). `distill-skipped` is emitted per ref but folded into `distillSkipped` before the result is built. `reflect-cooldown` stays in the type for older rows; nothing emits it.
|