akm-cli 0.9.17-alpha.7 → 0.9.17-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/STABILITY.md +9 -8
  3. package/dist/akm +55 -22
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  15. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  16. package/dist/assets/templates/html/health.html +3 -5
  17. package/dist/cli/retired-commands.js +1 -1
  18. package/dist/commands/health/archive-usage.js +98 -0
  19. package/dist/commands/health/data-dir-usage.js +25 -13
  20. package/dist/commands/health/html-report.js +1 -4
  21. package/dist/commands/health/improve-metrics.js +0 -25
  22. package/dist/commands/health/md-report.js +1 -6
  23. package/dist/commands/health/report-view-model.js +4 -14
  24. package/dist/commands/health/windows.js +0 -1
  25. package/dist/commands/health.js +13 -0
  26. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  27. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  28. package/dist/commands/improve/consolidate.js +38 -63
  29. package/dist/commands/improve/extract-prompt.js +1 -2
  30. package/dist/commands/improve/improve-cli.js +1 -1
  31. package/dist/commands/improve/improve-strategies.js +23 -5
  32. package/dist/commands/improve/improve.js +19 -30
  33. package/dist/commands/improve/ledger.js +3 -2
  34. package/dist/commands/improve/loop-stages.js +5 -84
  35. package/dist/commands/improve/memory/memory-belief.js +3 -1
  36. package/dist/commands/improve/memory/memory-improve.js +269 -11
  37. package/dist/commands/improve/planner.js +0 -5
  38. package/dist/commands/improve/preparation.js +20 -135
  39. package/dist/commands/improve/retrieval-scope.js +19 -4
  40. package/dist/commands/improve/salience.js +1 -14
  41. package/dist/commands/improve/stage.js +0 -1
  42. package/dist/commands/lint/base-linter.js +19 -11
  43. package/dist/commands/proposal/drain.js +8 -1
  44. package/dist/commands/proposal/proposal-cli.js +16 -2
  45. package/dist/commands/proposal/proposal-types.js +7 -0
  46. package/dist/commands/proposal/proposal.js +37 -6
  47. package/dist/commands/proposal/repository.js +613 -4
  48. package/dist/commands/proposal/validators/proposals.js +9 -0
  49. package/dist/commands/read/curate.js +40 -13
  50. package/dist/commands/read/knowledge.js +3 -2
  51. package/dist/commands/read/show.js +55 -16
  52. package/dist/commands/sources/info.js +3 -0
  53. package/dist/commands/sources/stash-cli.js +2 -2
  54. package/dist/core/adapter/adapters/akm-adapter.js +2 -0
  55. package/dist/core/adapter/adapters/akm-metadata.js +31 -0
  56. package/dist/core/bundle-rename.js +1 -7
  57. package/dist/core/config/config-schema.js +8 -1
  58. package/dist/core/config/config.js +23 -48
  59. package/dist/core/config/engine-semantics.js +0 -2
  60. package/dist/core/config/schema/improve-processes.js +17 -42
  61. package/dist/core/config/schema/index-config.js +5 -25
  62. package/dist/core/file-change.js +13 -5
  63. package/dist/core/improve-result.js +16 -5
  64. package/dist/core/improve-types.js +0 -1
  65. package/dist/core/loopback.js +7 -12
  66. package/dist/core/parse.js +13 -16
  67. package/dist/core/state/migrations.js +15 -0
  68. package/dist/core/time.js +0 -20
  69. package/dist/indexer/db/llm-cache.js +2 -2
  70. package/dist/indexer/ensure-index.js +2 -2
  71. package/dist/indexer/index-written-assets.js +2 -3
  72. package/dist/indexer/indexer.js +18 -418
  73. package/dist/indexer/links/declared-links.js +90 -0
  74. package/dist/indexer/passes/metadata.js +0 -19
  75. package/dist/indexer/scan/doc-to-entry.js +1 -0
  76. package/dist/indexer/walk/walker.js +3 -4
  77. package/dist/llm/client.js +8 -10
  78. package/dist/llm/embedders/remote.js +1 -2
  79. package/dist/llm/feature-gate.js +0 -5
  80. package/dist/output/shapes/helpers.js +23 -4
  81. package/dist/output/text/command-format.js +0 -8
  82. package/dist/output/text/proposal-format.js +47 -1
  83. package/dist/output/text/show-format.js +13 -17
  84. package/dist/scripts/akm-migrate-node.js +2754 -2836
  85. package/dist/scripts/akm-migrate.js +2754 -2836
  86. package/dist/setup/steps/connection.js +5 -6
  87. package/dist/setup/steps/platforms.js +2 -2
  88. package/dist/sources/providers/git-stash.js +55 -4
  89. package/dist/storage/repositories/improve-ledger-repository.js +48 -7
  90. package/dist/storage/repositories/index-entries-repository.js +16 -13
  91. package/dist/storage/repositories/index-entry-schema.js +22 -3
  92. package/dist/storage/repositories/index-links-repository.js +143 -0
  93. package/dist/storage/repositories/index-llm-cache-repository.js +7 -26
  94. package/dist/storage/repositories/index-schema.js +82 -104
  95. package/dist/storage/repositories/proposals-repository.js +61 -0
  96. package/dist/storage/repositories/salience-repository.js +1 -19
  97. package/dist/tasks/source/task-to-v4.js +462 -74
  98. package/docs/migration/release-notes/0.9.17.md +7 -5
  99. package/docs/reference/cli.md +33 -21
  100. package/docs/reference/configuration.md +21 -12
  101. package/docs/reference/data-and-telemetry.md +0 -1
  102. package/package.json +1 -1
  103. package/schemas/akm-config.json +0 -342
  104. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  105. package/dist/assets/prompts/contradiction-judge.md +0 -33
  106. package/dist/assets/prompts/graph-extract-system.md +0 -1
  107. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  108. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  109. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  110. package/dist/indexer/db/graph-db.js +0 -431
  111. package/dist/indexer/graph/graph-extraction.js +0 -807
  112. package/dist/indexer/graph/graph-related.js +0 -131
  113. package/dist/indexer/graph/graph-types.js +0 -4
  114. package/dist/llm/graph-extract.js +0 -903
  115. package/dist/llm/metadata-enhance.js +0 -95
  116. package/dist/tasks/source/task-to-v3.js +0 -453
package/CHANGELOG.md CHANGED
@@ -6,6 +6,479 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.17-alpha.9] - 2026-09-29
10
+
11
+ `akm improve` now forgets, reversibly and under review. A consolidation pair
12
+ pass compares each new or changed memory, flat knowledge file or lesson with
13
+ its nearest neighbours; where an LLM judge calls a pair duplicate, subsumed or
14
+ superseding, it mints a retire proposal that a person reviews (`akm proposal
15
+ list --generator consolidate-pair`). Accepting one archives the older or
16
+ contained copy, and `akm proposal revert` restores it exactly; an accepted
17
+ promotion now retires its source memory, so promotion no longer leaves a
18
+ duplicate. A continuity check flags a retirement whose survivor does not rank
19
+ where the retired asset did for its own past searches, and a flagged proposal
20
+ is never bulk-accepted. Archived files are purged 30 days after retirement,
21
+ only when git holds them unmodified. The per-run forgetting-safety lane, LLM
22
+ metadata enrichment and LLM entity-graph extraction are removed: each measured
23
+ no benefit. Decide pending retire proposals before downgrading to
24
+ 0.9.17-alpha.8.
25
+
26
+ ### Added
27
+
28
+ - **Consolidate pair pass: duplicate, subsumed and superseding memories are
29
+ now retired, review-gated.** A second pass inside `akmConsolidate`,
30
+ alongside the existing promote pass. It walks memory-tier assets (a
31
+ memory, base or `.derived`; a flat `knowledge/` asset; or a lesson) in the
32
+ retrieval scope that are new to the pass or whose body has changed since
33
+ their last full attempt — tracked by content hash, not a time window, so
34
+ an initiator the nightly cap or a pending-proposal collision leaves out
35
+ stays eligible rather than being marked settled, and the pass's own
36
+ ledger rows are never retrieval-scope evidence for the other improve
37
+ lanes — takes each one's nearest neighbours by stored vector (fetching 20,
38
+ keeping the first 5 that clear every filter; same bundle, memory tier
39
+ only — structured knowledge in subfolders is excluded), and judges every
40
+ pair at cosine >= `T_pair` (0.93) with one LLM call using the calibrated
41
+ relation prompt (`src/assets/prompts/consolidate-pair.md`, six labels:
42
+ `duplicate`, `subsumed`, `supersedes`, `contradicts`, `overlap`,
43
+ `unrelated`). An initiator with no prior attempt is held to a higher
44
+ `T_pair` >= 0.95, unless it is new material (git first-added within the
45
+ last 7 days), which judges at the ordinary 0.93. "Older"/"newer" for the
46
+ judge's own A/B labelling comes from one `git log` per run over the
47
+ bundle (first-add time, following renames so a moved or renamed file
48
+ keeps its original date), not frontmatter or file mtime — mtime is only
49
+ the fallback for a file git does not know, or a bundle with no `.git` at
50
+ all. At most 300 pairs are judged a night, admitted a whole initiator at
51
+ a time rather than by flat cosine rank: new-or-changed initiators first,
52
+ then the existing backlog by its own best cosine, each admitted only if
53
+ every one of its candidate pairs fits in what remains of the 300 — so an
54
+ initiator blocked on another pending decision never spends a slot doing
55
+ nothing, and a smaller initiator further down still fits when a larger
56
+ one ahead of it does not. `duplicate`, `subsumed` and `supersedes` mint a
57
+ reviewed `retire` proposal for the losing side (owner-calibrated
58
+ precision 20/22 = 0.91 [0.72, 0.97] against a second-rater baseline of
59
+ 0.17 for `supersedes` alone); `contradicts` is counted but stays a human
60
+ decision, and `overlap`/`unrelated` get no proposal. Guards: never a
61
+ `captureMode: hot` memory, never a `.derived` memory whose parent still
62
+ exists, never a pair where either side already has a pending retire
63
+ proposal (as the retired ref or its successor), and never retiring or
64
+ reusing as a successor an asset already spent earlier in the same run.
65
+ (`src/commands/improve/consolidate/pair-pass.ts`,
66
+ `src/commands/improve/retrieval-scope.ts`,
67
+ `src/storage/repositories/improve-ledger-repository.ts`,
68
+ `src/assets/prompts/consolidate-pair.md`)
69
+ - **Retire proposals.** A pair-pass retire proposal mints under its own
70
+ source, `consolidate-pair` — kept apart from the promote pass's
71
+ `consolidate` proposals, so a bulk `accept`/`reject --generator
72
+ consolidate` never sweeps a retirement, and the reverse; a bare `akm
73
+ proposal accept <ref>` never resolves to one either (it matches the
74
+ newest non-retire proposal for the ref, if any — a retire is reached by
75
+ its own proposal id, or the bulk `--generator consolidate-pair` form),
76
+ and retention expiry never drops a pending one for age alone. A pair the owner
77
+ rejected or reverted is not proposed again while both sides are unchanged. Its primary
78
+ change deletes its target instead of writing content. Accepting one first
79
+ confirms the decision is still fresh — the successor still exists, and
80
+ both sides' recorded body hashes still match their current files, not
81
+ just the retired side's, so a decision a later accept elsewhere made
82
+ stale (an A->B/B->C chain, or A->B/B->A both minted) is refused cleanly
83
+ rather than partially applied — then archives the asset (and its
84
+ `.derived` twin, if one exists) through a generalized
85
+ `archiveCleanupCandidate` (now usable on any memory, knowledge or lesson
86
+ file, not only `.derived` memories): the same
87
+ `.akm/memory-cleanup/archive/` encoding memory cleanup already used,
88
+ never the dead `.akm/archive/`. A `supersedes` judgement first writes the
89
+ `supersededBy` edge on the older asset, then archives it. Triage never
90
+ auto-accepts a retire proposal, whatever `applyMode` says — it waits for
91
+ a direct `akm proposal accept`, reviewed the same way as any other
92
+ proposal (`akm proposal list`, `show`, `diff`, bulk `accept --generator
93
+ consolidate-pair`; `--max-diff-lines` counts a retire by its target's own
94
+ line count). `accept` is crash-safe: it records its full intent —
95
+ `backupContent` and which file is about to move — durably before moving
96
+ anything, so a crash partway through, including between a primary and its
97
+ `.derived` twin, resumes and finishes from what was recorded rather than
98
+ leaving an asset stranded or the decision unrecorded. `revert` needs no
99
+ intent of its own — it resumes from what `accept` already recorded, and
100
+ refuses instead of overwriting a path that was reused by an unrelated file
101
+ since (its current content no longer matching what was retired). `revert`
102
+ restores the archived file(s) byte-for-byte from the bytes recorded at
103
+ accept, even a file that had no trailing newline — YAML comments, key
104
+ order and any human-written edge all survive the round trip. A ref to a retired asset keeps resolving to its tombstone
105
+ (`isArchivedRelPath`) — fixed along the way: that resolver assumed only
106
+ memories are ever archived, so an xref to a retired knowledge or lesson
107
+ asset was wrongly reported `missing-ref` by `akm lint` until now. Pending
108
+ retire proposals must be accepted or rejected before downgrading to
109
+ 0.9.17-alpha.8 or earlier — that release predates the retire shape
110
+ entirely and exits 70 on one in `show`/`diff`/`drain`, and drain's own
111
+ nightly pre-pass failing on the first one it meets stops that run's
112
+ auto-promotion for the whole stash. Downgrading also revives the
113
+ new-material starvation this same branch fixed forward-only: 0.9.17-alpha.8
114
+ counts a pair-pass ledger row as retrieval-scope evidence again, so its own
115
+ nightly attempts crowd new material back out of every other improve lane —
116
+ measured, two nights left only 341 of 2,183 new-only assets still in scope
117
+ once read under alpha.8
118
+ (`docs/architecture/persisted-data-compat.md`).
119
+ (`src/commands/proposal/repository.ts`,
120
+ `src/commands/improve/memory/memory-improve.ts`,
121
+ `src/commands/lint/base-linter.ts`)
122
+ - **A promotion retires its source memory (O1).** When `akm proposal accept`
123
+ promotes a consolidate `promote` proposal — by a person or by triage
124
+ auto-promotion — it now archives the source memory (and its `.derived`
125
+ twin) through the same retire-archive path, tombstoned `reason: promoted`,
126
+ provided the source's body still matches the hash recorded when the
127
+ promotion was minted; an edit since then leaves the source alone (a
128
+ proposal minted before this hash existed is never archived, for the same
129
+ reason). A promotion no longer leaves a memory/knowledge duplicate behind.
130
+ Best-effort: a failure to archive the source only warns; the promotion
131
+ itself is not undone. (`src/commands/improve/consolidate.ts`,
132
+ `src/commands/proposal/repository.ts`)
133
+ - **Retirement continuity check (rule R3).** Before the pair pass mints a
134
+ `retire` proposal, it replays up to five of the retired asset's own past
135
+ `search`/`curate` queries through akm's own search, in-process — the
136
+ ranking a user actually gets, no LLM. For every query where the retired
137
+ asset ranked in the top 10, the successor must too; compared directly,
138
+ since search itself returns at most the top 10 hits. A failing
139
+ query never blocks the mint — the
140
+ proposal's `retirement.continuityRisk` records the failing query count
141
+ and, per failing query, the retired asset's rank and the successor's
142
+ (`null` when the successor did not rank in the top 10 at all). A proposal
143
+ carrying `continuityRisk` is excluded from every bulk accept path (`accept
144
+ --generator …`, with or without `--yes`) but not from bulk reject —
145
+ declining a flagged proposal is always the safe direction; a person can
146
+ always accept one by id. An asset with no recorded queries is not checked
147
+ at all. A query that never ran (the search call threw) or that fell back
148
+ to keyword-only ranking (an unreachable embedding endpoint, most often)
149
+ is never silently trusted or silently dropped either: it counts as
150
+ "unverified" and, on its own, is enough to flag `continuityRisk` — an
151
+ endpoint outage reads as "risk unknown," never as "no risk found," for
152
+ every proposal checked while it stays down, not just the first. The
153
+ first fallback in a pair-pass run forces every later query in that same
154
+ run to skip the semantic attempt entirely, so a dead endpoint costs one
155
+ failed attempt total, not one per remaining query. Two fixes against false
156
+ flags, measured on a real night-1 admission (300 pairs, 6 flags, 3
157
+ spurious): replayed queries are the same cleaned set the retrieval
158
+ regression gate uses (`loadRetrievalQueries`) — stash-README boilerplate,
159
+ harness/tool envelopes, pastes over 2,000 characters, and near-duplicate
160
+ queries (equal once whitespace is collapsed) are dropped before replay,
161
+ not just capped at five raw entries; and the check does not run at all
162
+ when the retired and successor bodies are content-identical once
163
+ whitespace is collapsed — search's own content-dedupe already hides the
164
+ successor behind the retired asset for every such query, so a "successor
165
+ missing" finding would not be a real risk.
166
+ (`src/commands/improve/consolidate/continuity-check.ts`,
167
+ `src/commands/proposal/proposal-types.ts`,
168
+ `src/commands/proposal/proposal.ts`)
169
+ - **Continuity-risk visibility, and a `--generator` filter for `proposal
170
+ list`.** A retire proposal carrying `retirement.continuityRisk` now
171
+ shows `⚠ continuity-risk` inline in the default `akm proposal list`
172
+ output (and `--format text`), not just in `proposal show`. `proposal
173
+ show`'s text output now lists the actual failing query text and rank per
174
+ query, not just a count, and separately reports `unverifiedQueries` when
175
+ the risk is (also, or only) an unverified query rather than a rank
176
+ failure. `akm proposal accept --generator … --dry-run` (and a real bulk
177
+ run) now reports `skippedForContinuityRisk`, the count of otherwise
178
+ matching proposals excluded specifically for this reason, apart from an
179
+ ordinary `--max-diff-lines`/`--older-than` miss. `akm proposal list` gains
180
+ a `--generator <name>` filter, the same value `accept`/`reject
181
+ --generator` already take, so the (potentially large) backlog of one
182
+ generator's retire proposals can be reviewed as its own list.
183
+ (`src/commands/proposal/proposal.ts`, `src/commands/proposal/proposal-cli.ts`,
184
+ `src/output/text/proposal-format.ts`, `src/output/shapes/helpers.ts`)
185
+ - **Archive purge sweep.** Deterministic, no LLM, run once at the very start
186
+ of every `akm improve` invocation, ahead of index bootstrap and triage.
187
+ For a git-backed bundle, deletes the archived asset file(s) of a
188
+ retirement — never its `cleanup.md` tombstone — once `retiredAt` is more
189
+ than 30 days old (`RETIRE_GRACE_DAYS`) AND every file under that
190
+ retirement's archive directory is git-tracked, clean (`git ls-files` plus
191
+ `git status --porcelain -uall`), and verifiable (`git ls-files -v`: a
192
+ file marked `assume-unchanged` or `skip-worktree` hides its own edits
193
+ from `git status`, so it is never trusted as clean either) — each checked
194
+ once per sweep; git history keeps the bytes. `.git` presence alone is not
195
+ enough: `proposal accept` only commits for a `kind: "git"` write target,
196
+ and improve's own auto-sync stages only the paths its own run wrote, so a
197
+ filesystem-kind bundle can carry archived retirements that were never
198
+ committed — the tracked/clean/verifiable check is what keeps the sweep
199
+ from deleting the only surviving copy of those. A directory with even one
200
+ untracked, modified, or unverifiable file (tombstone included) is left
201
+ whole for a later sweep — and so is the ENTIRE archive for that sweep if
202
+ the underlying `git status` or `git ls-files` call itself fails (a broken
203
+ submodule, for instance, can fail `git status` while `git ls-files`
204
+ still succeeds): an empty result from a failed check is never treated as
205
+ "nothing to protect", and the sweep warns once rather than silently
206
+ purging nothing. A memory-cleanup family-prune archive carries no
207
+ `retiredAt`, so this sweep never touches that older archive class. Every
208
+ deleted file is journaled individually, so the end-of-run auto-sync
209
+ commits the removal the same way it commits the archive move itself.
210
+ (`src/commands/improve/memory/memory-improve.ts`,
211
+ `src/sources/providers/git-stash.ts`, `src/commands/improve/improve.ts`)
212
+ - **`akm health`'s `memory-cleanup-archive` advisory now covers every
213
+ bundle**, not just one with no `.git` at all. A bundle with no `.git` of
214
+ its own keeps every retirement's archived bytes forever (there is no
215
+ history to fall back on, so the purge sweep never runs there), and its
216
+ size and file count are reported as before. A git-backed bundle can ALSO
217
+ carry archived bytes the purge sweep will never remove — `.git` presence
218
+ alone never proved a retirement was committed — so this now runs the same
219
+ tracked/clean/verifiable check the purge sweep itself uses and reports
220
+ how many files and bytes of the archive cannot currently be purged
221
+ (untracked, modified, or unverifiable), alongside the total. Silent
222
+ whenever there is nothing to say: the archive is empty or absent, or (for
223
+ a git-backed bundle) every byte in it is purgeable once it ages out.
224
+ (`src/commands/health/archive-usage.ts`, `src/commands/health/data-dir-usage.ts`)
225
+
226
+ ### Removed
227
+
228
+ - **LLM metadata enrichment (`index.metadataEnhance`) is retired.** On 49
229
+ stratified queries, with every eligible candidate enriched (1,968
230
+ entries): search nDCG@10 moved −0.0092 [−0.0324, +0.0165], curate P@5
231
+ +0.000 [−0.037, +0.045], and long prompts lost −0.054 [−0.093, −0.012]. A
232
+ Doc2Query-- filter made it worse (P@5 −0.020 [−0.045, −0.004]). The pass
233
+ replaced authored descriptions on 89% of the entries it rewrote, and a
234
+ full pass costs about 27 B70-hours (RS-D, owner ruling 2026-09-28). It was
235
+ already off by default and off in the maintainer's config. The LLM call
236
+ (`src/llm/metadata-enhance.ts`), its `akm index` dispatch, and the
237
+ `metadata_enhance` feature-gate key are gone; the deterministic metadata
238
+ pass, `quality: "generated"`, and memory inference are unaffected. A
239
+ config that still sets `index.metadataEnhance` loads, named once by the
240
+ same unknown-config-key path every other retired key uses: kept in
241
+ memory, round-trips through ordinary writes, and is dropped only by
242
+ `akm migrate apply`. The pass's `llm_enrichment_cache` rows (the default
243
+ `cache_variant`; graph and memory inference use their own named variants)
244
+ are deleted on the next writable open of `index.db`. An index built while
245
+ enrichment was on keeps its entries' LLM-written descriptions on
246
+ incremental runs — nothing rewrites an unchanged row; run
247
+ `akm index --full` once to replace them with the deterministic ones.
248
+ (`src/indexer/indexer.ts`, `src/llm/feature-gate.ts`,
249
+ `src/core/config/config.ts`, `src/core/config/schema/index-config.ts`,
250
+ `src/storage/repositories/index-schema.ts`)
251
+ - **The LLM entity-graph extraction pass.** `akm improve`'s per-file
252
+ entity/relation extraction, its persisted tables (`graph_meta`,
253
+ `graph_files`, `graph_file_entities`, `graph_file_relations`), and `akm
254
+ show`'s `related` list are gone. On the navigation eval, vector kNN beat
255
+ the LLM `related` list by 0.157 P@5 [0.051, 0.260]; the ranking boost it
256
+ once fed was already removed in 0.9.17-alpha.4. Declared links (#935,
257
+ alpha.8) are the only navigation surface `akm show` has now, and curate's
258
+ support refs already came from them, not the graph. index.db is a
259
+ regenerable cache, so the graph tables are dropped unconditionally on the
260
+ next writable open — nothing migrates or backs them up.
261
+ - **The `graph-refresh` improve strategy** and the `akm-graph-refresh-weekly`
262
+ task template are deleted. Naming `graph-refresh` via `--strategy` or a task
263
+ now fails with a message naming the retirement, unconditionally — even when
264
+ `improve.strategies["graph-refresh"]` still has a leftover override block
265
+ from customizing the built-in (the message names it; `akm migrate apply`
266
+ drops it). `defaults.improveStrategy: "graph-refresh"` still loads config
267
+ successfully — the refusal happens lazily, when the strategy is actually
268
+ resolved, not at every command's config load.
269
+ - **Retired config keys:** `index.graph.*` and every strategy's
270
+ `processes.graphExtraction.*`. An old config that still sets them keeps
271
+ loading and the keys are unread, but `index.graph` is now also named once
272
+ by the unknown-config-key warning (it previously validated silently
273
+ against the generic per-pass catchall) and, like any other retired key,
274
+ is dropped only by `akm migrate apply` — not by an ordinary config write.
275
+ - **`akm health` drops every graph metric** — the KPI card, summary-table
276
+ rows, per-run duration/entity/relation columns, and the
277
+ `improve.graphExtraction.failures` window-compare delta. `--window-compare`
278
+ and `--group-by run` still count every run, including a pre-alpha.9 one:
279
+ its stored `graphExtraction`/`graphExtractionDurationMs` result fields and
280
+ its `graph-extraction` `plan.stages` / `graphExtraction` `plan.processes`
281
+ entries still decode, read-only, same as any other retired field (AGENTS.md
282
+ "Reading persisted data") — they are just no longer rendered. The
283
+ `improve_completed` event's `graphExtractionExtractedFiles`,
284
+ `graphExtractionDurationMs`, `graphCoverage`, `graphDensity`, and
285
+ `graphEntities` metadata fields are no longer emitted.
286
+ - **The per-run forgetting-safety lane.** `scoreSalience`'s stash-wide
287
+ salience-rank comparison, `applyForgettingSafety`, and the
288
+ `improve_salience_rank_change` event are gone. It was a one-time WS-1
289
+ cutover guard from the June 2026 ranking-formula change that had kept
290
+ running on every improve run since; the last 30 days of events
291
+ (2026-08-30 to 2026-09-29: 47 `improve_salience_rank_change` events, 5
292
+ refs flagged across 4 runs — 09-05, 09-08 x2, 09-19, 09-28) showed no
293
+ marginal pick over the signal-delta lane and the retrieval scope: 4 of
294
+ the 5 flagged refs were also picked that same run by signal-delta
295
+ (adjacent event ids/timestamps, 2–26 minutes after the rank-change
296
+ event), and the 5th (`workflows/create-github-issues-from-spec`, flagged
297
+ 09-05) has no `reflect_invoked` or `distill_invoked` event in the
298
+ retained history, but that run's `improve_runs.plannedRefs` shows it,
299
+ too, was planned under `signal-delta` — just not reflected (a
300
+ dispatch/budget limit that run, not a lane-exclusive pick). All 5
301
+ flagged refs were signal-delta picks; zero were forgetting-safety-only.
302
+ It also protected
303
+ `asset_salience.rank_score`, which only improve itself ever read — a rank
304
+ drop could not hide anything from search. `buildRankChangeReport` (its
305
+ comparator) is also gone: the new retirement continuity check (see
306
+ Added) compares ranks directly instead, and nothing else called it.
307
+ `forgetting-safety` stays a valid `eligibilitySource`/event-type
308
+ value so old proposals and events still decode, but nothing assigns or
309
+ emits it any more. (`src/commands/improve/preparation.ts`,
310
+ `src/commands/improve/salience.ts`, `src/core/events.ts`,
311
+ `src/storage/repositories/salience-repository.ts`)
312
+ - **`improve.strategies.<name>.processes.consolidate.incrementalSince` and
313
+ `.neighborsPerChanged`.** The consolidate pair pass is now the candidate
314
+ generator, narrowing per initiator through the improve ledger rather than
315
+ a global time window; neither key was set anywhere in the owner's config.
316
+ `narrowToIncrementalCandidates` goes with them, along with its
317
+ now-orphaned `parseSinceToIsoLenient` helper. A config that still sets
318
+ either key keeps loading under the retired-key contract: named once as
319
+ unknown, it survives an ordinary config write, and only `akm migrate
320
+ apply` drops it. (`src/core/config/schema/improve-processes.ts`,
321
+ `src/commands/improve/consolidate.ts`, `src/core/time.ts`,
322
+ `docs/reference/configuration.md`)
323
+
324
+ ### Fixed
325
+
326
+ - **Stale "advisory merge/delete/contradict" documentation.** Consolidation
327
+ stopped executing its merge/delete/contradict operations at `e82eec811`
328
+ (2026-07, #732; they had run in production until then, not "never
329
+ executed" as a couple of doc comments and `improve-workflow.md` claimed),
330
+ and 0.9.17-alpha.1 (`f4ebd763a`) dropped them from the prompt and schema,
331
+ which have offered `promote` only since. But `docs/architecture/
332
+ improvement.md`, `STABILITY.md` and `docs/architecture/internals/
333
+ improve-workflow.md` still described them as advisory planned output.
334
+ Corrected, and `improve-workflow.md` gains a section documenting the pair
335
+ pass, retire proposals and O1. Also corrected: the `default` strategy's
336
+ "advisory consolidation" description, two comments that still credited a
337
+ `beliefState` ranking boost alpha.4 removed (`memory-belief.ts`,
338
+ `knowledge.ts`), and D27's stale `archiveMemory` naming in the
339
+ architecture decision history. Deleted the unused
340
+ `src/assets/prompts/contradiction-judge.md` (no reader since
341
+ `e82eec811`).
342
+
343
+ ## [0.9.17-alpha.8] - 2026-09-28
344
+
345
+ `akm index` now records the links a bundle already declares (`xrefs`,
346
+ `supersededBy`, a `.derived` memory's parent, wiki sources, page links, and
347
+ workflow and task targets) as typed links, with no model. `akm show` lists
348
+ them, and curate's support refs come from them instead of the LLM entity
349
+ graph. The index moves to layout 26 in place on its first writable open;
350
+ 0.9.17-alpha.4 through alpha.7 cannot open it. `index.graph.enabled: false`
351
+ now stops graph extraction in `akm improve`, and a partly failed extraction is
352
+ retried. `akm migrate` converts a v2 or v3 task file to v4 in one pass, and
353
+ the launchers no longer lose a signal that arrives before their child starts.
354
+
355
+ ### Added
356
+
357
+ - **Declared links (#935).** The relations a bundle already declares are now
358
+ stored as typed links: `xrefs:` (`xref`), `supersededBy:`
359
+ (`superseded_by`), `contradictedBy:` (`contradicted_by`),
360
+ `currentBeliefRefs:` (`belief_peer`), a `.derived` memory's parent
361
+ (`derived_from`), wiki `sources:` that name an asset (`cites`), the page
362
+ links an llm-wiki or OKF bundle resolves (`links_to`), and a workflow step's
363
+ or a task's target (`uses`). `akm index` reads them from what it already
364
+ parses, with no model, so an install without an LLM engine gets them. They
365
+ live in their own table (`asset_links`), apart from the LLM entity graph:
366
+ each link belongs to the entry that declares it and is written, replaced
367
+ and deleted with it, including on incremental and write-path (`akm
368
+ remember`) indexing. A target resolves when it is indexed, not when its
369
+ citer was, so a note that cites something added later links to it without
370
+ being reindexed. Retired spellings convert in memory (`memory:<name>`,
371
+ `wiki:<wiki>/<page>`, a `.md` suffix), and, as lint already allows (#882),
372
+ a memory whose own file is gone resolves to its `.derived` child.
373
+ `akm show` lists an asset's links grouped by kind: `outgoing`, `incoming`
374
+ and the `unresolved` tokens it names, at most 10 per kind with a `total`.
375
+ `akm info` reports links per kind with how many are unresolved. Links do
376
+ not change search ranking, and `related` is unchanged: on the retrieval
377
+ suite, against 0.9.17-alpha.7 on the same index with two runs per arm,
378
+ search nDCG@10 moved +0.002 [−0.005, +0.009] (a rerun of alpha.7 alone
379
+ moved +0.006) and curate P@5 +0.000 [−0.001, +0.001].
380
+ On the retrieval snapshot of the maintainer's 21 bundles (23,979 entries)
381
+ there are 14,917 links: 7,393 `contradicted_by`, 4,401 `xref`, 2,577
382
+ `derived_from`, 540 `cites` and 6 `superseded_by`. 1,801 are unresolved;
383
+ 1,744 of those are `.derived` memories whose parent memory no longer
384
+ exists. (`src/indexer/links/declared-links.ts`,
385
+ `src/storage/repositories/index-links-repository.ts`,
386
+ `src/commands/read/show.ts`, `src/commands/sources/info.ts`)
387
+
388
+ ### Changed
389
+
390
+ - **`akm migrate` converts a v2 or v3 task file straight to v4 in one pass.**
391
+ The chain that read every file as v2, converted it to an intermediate v3
392
+ shape, then converted that to v4 (`src/tasks/source/task-to-v3.ts` ->
393
+ `task-to-v4.ts`, composed by `scripts/akm-migrate/migrate/task-files.ts`)
394
+ is now one planner: `task-to-v4.ts` reads a file once and, for v2, builds
395
+ the v3-shape record in memory — never written to disk or reported as its
396
+ own outcome — before hoisting it to v4 through the same code path a real
397
+ v3 file goes through. `akm migrate status`/`apply` output shapes, and
398
+ every blocked/changed reason code, are unchanged, checked fixture by
399
+ fixture against the prior two-hop chain's actual output (one exception:
400
+ a v3 document that fails only the typed pre-check's own "exactly one
401
+ scheduling source" rule — unreachable through the real chain, which
402
+ always ran that same pre-check first — now reports `invalid-v3-task`
403
+ instead of the raw hoist stage's own `ambiguous-scheduling-source`,
404
+ matching what `akm migrate apply` already returned end to end). The
405
+ `already-v3` and `pending-v2-to-v3-migration` intermediate states are
406
+ gone with the generation split that produced them.
407
+ `src/tasks/source/task-to-v3.ts` (500 lines) is deleted; its logic moved
408
+ into `task-to-v4.ts`, which also drops the duplicate raw-YAML reader and
409
+ outcome-base helpers the two files each carried their own copy of.
410
+ (`src/tasks/source/task-to-v4.ts`, `scripts/akm-migrate/migrate/task-files.ts`)
411
+ - **Curate's support refs come from declared links.** Each curated item's
412
+ support refs (at most two) are now assets its declared links name: what
413
+ it links to, then what links to it, in the order `akm show` lists them,
414
+ skipping assets curate already selected. They no longer come from the LLM
415
+ entity graph's `related` list. On the retrieval snapshot, `related` offered
416
+ support refs for 47 of 867 curate items (5.4%) and declared links for 257
417
+ (29.6%). The retrieval judge graded 157 of those items, asking whether each
418
+ support ref is worth opening next: 56% of declared support refs were useful
419
+ against 68% of `related`'s, so curate attaches about 24 useful support refs
420
+ per 100 items instead of 7. Curate's items are unchanged.
421
+ (`src/commands/read/curate.ts`)
422
+ - **Index layout 26.** The first writable open of an older index derives
423
+ every entry's links from its stored `document_json` in place, reading no
424
+ file: on the 23,979-entry snapshot index that takes 0.8 s and adds 3.5 MB.
425
+ Earlier layouts never stored a workflow's or a task's targets, so only the
426
+ directories holding workflows and tasks re-read on the next `akm index`.
427
+ The open leaves `index_meta.vacuumPending` like every layout migration.
428
+ 0.9.17-alpha.4 through alpha.7 refuse an index at layout 26
429
+ (`INDEX_SCHEMA_INCOMPATIBLE`, naming the upgrade), for writing as well as
430
+ reading, so going back to one of them needs a new index: move `index.db`
431
+ aside and run `akm index` under that release (the LLM graph and enrichment
432
+ cache it held are not rebuilt by `akm index`).
433
+ (`src/storage/repositories/index-schema.ts`,
434
+ `src/storage/repositories/index-entry-schema.ts`)
435
+
436
+ ### Fixed
437
+
438
+ - **`index.graph.enabled: false` stops graph extraction in `akm improve`.**
439
+ The switch was read only when improve had no strategy plan, which is never
440
+ the case in a real run, so the nightly and weekly graph tasks kept
441
+ extracting with it set. Improve now skips its graph extraction stage
442
+ whenever `index.graph.enabled` is `false`, whatever the strategy enables.
443
+ This also makes the graph ablation harness's "graph off" arm, which sets
444
+ exactly this key, turn extraction off. Improve still does not read
445
+ `index.defaults` when it picks the engine for graph extraction.
446
+ (`src/commands/improve/loop-stages.ts`)
447
+ - **Graph extraction never has more calls in flight than the run's
448
+ concurrency.** Batches run side by side up to the runner's concurrency, and
449
+ each batch also sent its per-file calls (long bodies, a non-array response)
450
+ up to that limit at once, so at a concurrency of 2 four calls could be in
451
+ flight. A batch now makes its per-file calls one at a time. At the default
452
+ concurrency of 1 nothing changes. (`src/llm/graph-extract.ts`)
453
+ - **A long document whose extraction partly failed is extracted again.** A
454
+ body over 1,600 characters is extracted in chunks. When some chunks failed
455
+ (a timeout, an error, an empty response) and others found entities, the
456
+ file was recorded and cached as extracted, so the failed chunks were never
457
+ retried. Such a file is now recorded as failed and not cached, and the next
458
+ run extracts it again, every chunk: partial failures are rare outside
459
+ provider outages, and keeping per-chunk results to skip the chunks that
460
+ succeeded would need a second cache. Until then the entities the other
461
+ chunks found are stored when the file had no graph rows yet; a file with
462
+ rows keeps them. (`src/llm/graph-extract.ts`)
463
+ - **`scripts/node-runtime/akm` could die from a raw signal instead of
464
+ forwarding it to its child.** The launcher registered its
465
+ SIGTERM/SIGINT/SIGHUP forwarding listeners only after spawning the child;
466
+ under load, a signal could arrive in that window and fall through to the
467
+ runtime's default (process-terminating) disposition, killing the launcher
468
+ before the child ever saw it. The listeners now go up before anything else
469
+ runs, with a small queue for a signal that arrives before the child exists.
470
+ - **`scripts/node-runtime/akm-migrate` carried the same pre-spawn signal
471
+ race** as `scripts/node-runtime/akm` above, for the same reason (listeners
472
+ registered only after `spawn()`), with no test covering it. Fixed the same
473
+ way, and added `tests/integration/akm-migrate-signal-forwarding.test.ts`
474
+ (modelled on `launcher-signal-forwarding.test.ts`) for its forwarding.
475
+ - **The launcher signal tests failed intermittently because of their own
476
+ fixture.** The fake child wrote its ready file before it registered its
477
+ signal handler, so a forwarded signal could reach it in between and kill it,
478
+ and the launcher then reported that signal. Each fixture now registers its
479
+ handler first. The launcher's pre-spawn window above could not cause this:
480
+ the tests signal only after the child is running.
481
+
9
482
  ## [0.9.17-alpha.7] - 2026-09-28
10
483
 
11
484
  A scheduled task is now just a command and a schedule. Each native row
package/STABILITY.md CHANGED
@@ -366,8 +366,8 @@ for scripted use.
366
366
  ### `akm improve` autonomy — opt-in in 0.9.0
367
367
 
368
368
  **`akm improve` is review-first by default in 0.9.0.** The command itself is ON
369
- — its schedules, reflect/distill proposals, and graph extraction all run — but
370
- the lanes that mutate assets *without* review require an explicit opt-in:
369
+ — its schedules and reflect/distill proposals all run — but the lanes that
370
+ mutate assets *without* review require an explicit opt-in:
371
371
 
372
372
  ```sh
373
373
  akm config set experimental.improveAutonomy true
@@ -388,9 +388,10 @@ review-first config correctly shows `queue`.
388
388
  | memory cleanup | Belief-state frontmatter rewrites, archive moves | analyzed but not applied |
389
389
  | `triage` `applyMode: "promote"` | Auto-accepts queued proposals into the bundle | downgraded to `queue` — triage still runs, it just does not auto-accept |
390
390
 
391
- Consolidation remains enabled with autonomy off because merge, delete, and
392
- contradiction operations are advisory; promotion only emits a reviewable
393
- proposal.
391
+ Consolidation remains enabled with autonomy off: both its passes (promotion,
392
+ and the pair pass's duplicate/subsumed/supersedes judging) only ever emit a
393
+ reviewable proposal, and a pair-pass `retire` proposal is never auto-accepted
394
+ by `triage` `applyMode: "promote"` regardless of this gate.
394
395
 
395
396
  Because the gate is applied before the LLM preflight, a review-first workspace
396
397
  also needs fewer engines configured: a strategy whose only model-backed process
@@ -409,9 +410,9 @@ Autonomy is never inferred: an absent `experimental` section, an absent key, and
409
410
  an explicit `false` all read as off, so a partially-written or older config is
410
411
  review-first rather than accidentally permissive.
411
412
 
412
- Reflect, distill, extract candidates, validation, proactive-maintenance
413
- selection, and graph extraction are proposal-only and never write assets
414
- directly. Two further direct writes are ungated by design: `extract`'s session
413
+ Reflect, distill, extract candidates, validation, and proactive-maintenance
414
+ selection are proposal-only and never write assets directly. Two further
415
+ direct writes are ungated by design: `extract`'s session
415
416
  indexing (additive `sessions/**` writes,
416
417
  `processes.extract.indexSessions`, default on) and distill's
417
418
  encoding-salience frontmatter stamp (metadata only).
package/dist/akm CHANGED
@@ -6,6 +6,48 @@
6
6
  import { spawn, spawnSync } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
+ // #956 round 3: install the signal-forwarding scaffolding before ANYTHING
10
+ // else in this process — env var setup, the bun-version probe, spawning the
11
+ // child — gets a chance to run. The three `process.once` listeners used to
12
+ // go up only after spawn() (below) returned; a scheduler preemption right
13
+ // after that syscall was enough for a signal to arrive with no listener yet,
14
+ // and the runtime's default (process-terminating) disposition killed the
15
+ // launcher outright, never reaching the child at all. `child` starts
16
+ // unset: a signal that arrives before it exists is queued and flushed the
17
+ // moment it does; if this process ends up never spawning one (the
18
+ // direct-import branch below), the queued signal is re-raised against
19
+ // ourselves once we hand default disposition back, so it behaves exactly
20
+ // like the runtime's own default rather than being silently swallowed.
21
+ let child;
22
+ let childExited = false;
23
+ const pendingSignals = [];
24
+ const forwardHandlers = new Map();
25
+ const forwardSignal = (signal) => {
26
+ if (childExited) return;
27
+ if (!child) {
28
+ pendingSignals.push(signal);
29
+ return;
30
+ }
31
+ try {
32
+ child.kill(signal);
33
+ } catch {
34
+ // Child exited in the race between the check above and here.
35
+ }
36
+ };
37
+ // `.once`, not `.on`: Node/Bun suppress a signal's default
38
+ // (process-terminating) disposition for as long as ANY listener stays
39
+ // registered for it. A persistent `.on` listener would still be registered
40
+ // when the `process.kill(process.pid, result.signal)` re-raise below runs at
41
+ // shutdown, swallowing it and leaving this launcher exiting 0 instead of
42
+ // reflecting the child's signal. `.once` consumes only the
43
+ // externally-delivered signal that triggers the forward, so the re-raise
44
+ // correctly falls through to the OS default.
45
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
46
+ const handler = () => forwardSignal(signal);
47
+ forwardHandlers.set(signal, handler);
48
+ process.once(signal, handler);
49
+ }
50
+
9
51
  // A scheduled row sets its environment itself (a cron `VAR=value` prefix, a
10
52
  // launchd plist's EnvironmentVariables), and the scheduler supplies PATH, so
11
53
  // runtime selection below needs nothing from it. A row written by 0.9.0 –
@@ -42,13 +84,21 @@ const bunEntry = fileURLToPath(new URL("./cli.js", import.meta.url));
42
84
  const nodeEntry = fileURLToPath(new URL("./cli-node.mjs", import.meta.url));
43
85
 
44
86
  if (!useBun && !process.versions.bun) {
45
- await import("./cli-node.mjs");
87
+ // No child is ever spawned on this path — the imported module runs
88
+ // in-process, so it IS the work, and the runtime's ordinary default signal
89
+ // disposition is exactly correct for it. Hand that back (the forwarding
90
+ // listeners above no longer apply here), re-raising anything that already
91
+ // arrived so it is not silently swallowed by a listener that now does
92
+ // nothing.
93
+ for (const [signal, handler] of forwardHandlers) process.removeListener(signal, handler);
94
+ if (pendingSignals.length > 0) process.kill(process.pid, pendingSignals[0]);
95
+ else await import("./cli-node.mjs");
46
96
  } else {
47
97
  const command = useBun ? (process.versions.bun ? process.execPath : "bun") : "node";
48
98
  const entry = useBun ? bunEntry : nodeEntry;
49
99
  const runtime = useBun ? "Bun" : "Node.js";
50
100
  const result = await new Promise((resolve) => {
51
- const child = spawn(command, [entry, ...process.argv.slice(2)], {
101
+ child = spawn(command, [entry, ...process.argv.slice(2)], {
52
102
  stdio: "inherit",
53
103
  env: process.env,
54
104
  // #956: give the child its OWN process group on POSIX (`setsid()` —
@@ -75,29 +125,12 @@ if (!useBun && !process.versions.bun) {
75
125
  // forward once the child has already exited: forwarding to a
76
126
  // dead/replaced pid would be at best a no-op and at worst a signal to
77
127
  // an unrelated process that reused the pid.
78
- let childExited = false;
79
128
  child.once("exit", () => {
80
129
  childExited = true;
81
130
  });
82
- const forwardSignal = (signal) => {
83
- if (childExited) return;
84
- try {
85
- child.kill(signal);
86
- } catch {
87
- // Child exited in the race between the check above and here.
88
- }
89
- };
90
- // `.once`, not `.on`: Node/Bun suppress a signal's default
91
- // (process-terminating) disposition for as long as ANY listener stays
92
- // registered for it. A persistent `.on` listener would still be
93
- // registered when the `process.kill(process.pid, result.signal)`
94
- // re-raise below runs at shutdown, swallowing it and leaving this
95
- // launcher exiting 0 instead of reflecting the child's signal. `.once`
96
- // consumes only the externally-delivered signal that triggers the
97
- // forward, so the re-raise correctly falls through to the OS default.
98
- for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
99
- process.once(signal, () => forwardSignal(signal));
100
- }
131
+ // Flush whatever arrived in the (now much smaller) window between the
132
+ // listeners going up and the child existing to receive them.
133
+ for (const signal of pendingSignals.splice(0)) forwardSignal(signal);
101
134
  child.once("error", (error) => resolve({ error }));
102
135
  child.once("exit", (code, signal) => resolve({ code, signal }));
103
136
  });