akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
362 lines (308 loc) • 19.9 kB
Markdown
# akm 0.9.0 — milestone close-out plan
**Status:** PLANNED (2026-07-30). Owner ruling applied: all four open issues
ship in 0.9.0.
The release sits at `0.9.0-rc.11` with the CLI overhaul (#734) merged and the
[open-items register](0.9.0-open-items-register.md) recording its close-out
state. This document is the implementation plan for the four issues remaining
in the 0.9.0 milestone, in landing order.
## Milestone triage
All five open issues across 0.9.0 and 0.9.1 were reviewed. An initial
recommendation deferred #733 and #730 to 0.9.1; the owner ruled both stay in
0.9.0. Final state:
| Issue | Milestone | Scope in one line |
| --- | --- | --- |
| #692 salience boost | 0.9.0 | Remove the R2 contributor from default ranking; the removal also deletes a confirmed 5 s hot-path stall. |
| #672 repositories + guard | 0.9.0 | Move salience/outcome SQL behind `src/storage/repositories/`; add a raw-SQL lint rule. |
| #733 orphan-GC | 0.9.0 | One maintenance pass, one additive migration, one event type, one config gate. Lean by design — see Workstream C. |
| #730 OKF v0.2 | 0.9.0 | Read v0.2 frontmatter; stamp `generated`/`verified` on AKM-native writes via proposals; conformance updated. The `okf` adapter stays consumer-only. |
| #652 write provenance | 0.9.1 | Unchanged — already re-scoped by the rc-5 audit to attribution/concurrency polish. |
Sequencing note: A and B are small and touch the same salience surface — land
them first (A deletes a raw-SQL site B would otherwise relocate). C and D are
independent of each other and of A/B, and can proceed in parallel after.
## Workstream A — #692: remove the salience boost from default ranking
**Decision: fix option 1 (removal), the issue's stated preference.** The R2
contributor multiplies search/curate scores by up to 1.2× from a signal that is
retrieval-dominated (`w_r = 0.60`, `src/commands/improve/salience.ts:356-360`),
warm-starts non-zero with no outcome evidence (`salience.ts:293-297`), has zero
pack coverage, and measures as noise on live data (max ×1.071). No measured
curate-bench delta justifies it, and `STABILITY.md:308` classifies ranking as a
tuning target — removal in 0.9.0 is contractually clean.
**No config gate is added.** A config key for a term being removed would be
dead surface that the 1.0 freeze would then have to carry. If a future
curate-golden-bench delta proves the signal out, reintroduction behind a
default-off gate happens then (the contributor stays exported for exactly that
experiment). `rank_score` itself is untouched — it remains the improve-internal
selection signal (proactive/high-salience lanes, forgetting safety).
The removal also deletes the hot-path defect confirmed in the rc-5 audit
as a side effect rather than tuning around it: today every default search
acquires the maintenance activity synchronously — up to a 5 s spin
(`src/core/maintenance-barrier.ts:91-107`) plus a lock-file create — before the
250 ms SQLite busy-timeout even applies (`src/indexer/search/ranking.ts:102`).
### Changes
1. `src/indexer/search/ranking-contributors.ts:551-554` — drop
`salienceRankingContributor` from `defaultUtilityRankingContributors`
(the list becomes `[utilityRankingContributor]`). The contributor and its
constants stay exported.
2. `src/indexer/search/ranking.ts` — delete `loadSalienceRankScores`
(`:96-139`) and the `:263` default fallback. Its only production caller is
that fallback; tests are the only other importers. New semantics for
`RankEntriesOptions.salienceRankScores`: a `Map` applies the contributor
explicitly (tests/evals); `null`/`undefined` mean off. Update the option's
doc comment (`:58-64`) accordingly.
3. With the loader gone, the search path holds no maintenance-barrier
acquisition and opens no state.db handle. `db-search.ts:502-511` needs no
edit beyond whatever the option-type change forces.
4. Remove the now-dead barrier import from `ranking.ts:6` and confirm the
search layer has no remaining `maintenance-barrier` dependency.
### Tests
- `tests/integration/ranking-salience-boost.test.ts` — keep the contributor
math block (boost exactness at rank 1.0 / 0.5, no-op cases) exercised via
explicit injection. Replace the state.db read-path block: the held-barrier
test (`:111-131`, currently budgeted 10 s) inverts into the release's
regression guard — *search with the maintenance barrier held completes
without waiting*. Keep the "search never creates the state.db file"
assertion (`:101-109`) in its new form: default search never touches
state.db at all.
- `tests/integration/ranking-contributor-ablation.test.ts:52-53` — update for
the one-entry default list; ablation of `"salience-ranking"` against an
explicitly-supplied list still proves the mechanism.
- `tests/integration/curate-golden-eval.test.ts` and
`tests/integration/ranking-regression.test.ts` — expected neutral (live
effect measured as noise); run and record the deltas in the PR description.
### Acceptance
- Bare `akm search` under a held maintenance barrier returns promptly (no 5 s
stall) — covered by the inverted regression test.
- Default search/curate perform zero state.db reads and create no lock files.
- Golden evals within tolerance; CHANGELOG entry under Changed noting removal,
the retained improve-internal role of `rank_score`, and the latency fix.
- Close #692 citing option 1 and the reintroduction path.
## Workstream B — #672: salience/outcome repositories + widened SQL guard
Part 1 of the issue (prep-stage accumulator decomposition) shipped at
`028cc3de`. This lands the remaining part 2, with two corrections to the issue
text discovered against the tree:
- **Location correction.** The issue proposes `src/core/state/salience-repo.ts`
/ `outcome-repo.ts`; the tree's actual convention is
`src/storage/repositories/*-repository.ts` (22 files, including the state.db
precedents `proposals-repository.ts`, `improve-runs-repository.ts`,
`events-repository.ts`, whose shared pattern is "extracted verbatim …
re-exported so existing importers resolve"). Follow the tree.
- **Guard correction.** The issue calls the widening "a one-line allowlist
change", but `scripts/lint-repository-sql.ts` enforces *DB-owner imports and
direct opens* (`RULES` `:55-71`), not raw SQL. `salience.ts` and
`outcome-loop.ts` take `db` parameters and import no owners — they would
pass a widened prefix list today with their SQL intact, and commands
legitimately import owners like `withStateDb`, so prefix-widening alone
cannot express the intended boundary. The faithful implementation is a new
rule.
### Changes
1. `src/storage/repositories/salience-repository.ts` — verbatim moves from
`src/commands/improve/salience.ts`: `AssetSalienceRow` (`:380-393`),
`upsertAssetSalience` (`:425-458`), `getAssetSalience` (`:463-473`),
`getAllRankScores` (`:484-494`), `recordNoOp` (`:509-514`),
`resetConsecutiveNoOps` (`:520-525`), `getConsecutiveNoOps` (`:531-535`).
Add `getTopRetrievalSalience(db, limit)` absorbing the health read
(`src/commands/health/metrics.ts:246-251`).
2. `src/storage/repositories/outcome-repository.ts` — verbatim moves from
`src/commands/improve/outcome-loop.ts`: the upsert SQL inside
`updateAssetOutcome` (`:245-268`; the domain function stays put and calls
the repository), `getAssetOutcome` (`:278-288`), `getAllAssetOutcomes`
(`:293-302`), `getOutcomeScoresByRef` (`:308-323`).
3. Re-export the moved functions from `improve/salience.ts` and
`improve/outcome-loop.ts` per the established precedent, so the six improve
importers and eleven test files do not churn in this PR.
4. `src/commands/health/metrics.ts` — replace the inline
`SELECT retrieval_salience …` with the repository call; the `withStateDb`
handle it already holds is passed through.
5. The fifth raw-SQL site (`ranking.ts:122`) is deleted by Workstream A; if A
has not merged first, its chunked select moves behind
`salience-repository.ts` instead.
6. `scripts/lint-repository-sql.ts` — add a third rule, `state-table-sql`:
flag raw SQL naming `asset_salience` or `asset_outcome` anywhere under
`src/` except `src/storage/repositories/` and
`src/core/state/migrations.ts`. Per-rule scoping (the existing two rules
keep their current `GUARDED_PREFIXES`; the new rule carries its own
scope/exclusions). Workstream C's pass consumes the repositories, so the
rule holds at zero from day one.
### Tests
- `tests/integration/lint-repository-sql.test.ts` — the live-tree-is-zero test
now also proves no raw salience/outcome SQL survives outside repositories.
Add fixtures: raw `asset_salience` SQL under `src/commands/improve/` fires;
the identical string inside `src/storage/repositories/` does not; prose
mentions still do not (comment-stripping already covered). The `:48-53`
assertion that `src/commands/improve/preparation.ts` is unguarded is updated
to reflect the new rule's coverage.
- The improve-side suites
(`tests/integration/commands/improve/{salience,salience-wiring,outcome-loop,outcome-loop-wiring,outcome-invariance}.test.ts`,
`tests/integration/health-ws5-observability.test.ts`) must pass unchanged —
verbatim moves, pure refactor, no behavior delta.
### Acceptance
- `bun run lint` green with the new rule at zero violations.
- No test-behavior changes outside the lint meta-test.
- Close #672 with a comment recording the two corrections above.
## Workstream C — #733: orphan-GC pass (lean)
Deliberately minimal: **one maintenance pass, one additive migration, one
event type, one config gate.** No quarantine archive, no circuit breaker, no
health-advisory plumbing, no new tables. The issue's design constraints are
each satisfied by something that already exists or by the smallest possible
addition:
- **Absent ≠ deleted** is inherited from the indexer, not re-implemented: an
incomplete or failed scan preserves a source's last-known-good `entries`
rows (`src/indexer/indexer.ts:1195-1199`), and mass-wipe is already blocked
upstream (`preserveExistingIndex` + the `fullDelete && scanComplete` gate).
So "ref not present in `entries.item_ref`" *is* the authoritative-deletion
predicate — a temporarily unreachable source never surfaces candidates.
- **Grace window** needs a clock rows don't have (`updated_at` is bumped by
improve itself and `mtime` doesn't exist for rows), so: one additive
migration (`021`, mirroring the 6-line migration-011 precedent) adds
`missing_since INTEGER DEFAULT NULL` to `asset_salience` and
`asset_outcome`. Grace duration is a named constant
(`STATE_GC_GRACE_MS`, 7 days), mirroring `TXN_SWEEP_GRACE_MS`
(`src/core/fs-txn.ts:298`) — not a config knob.
- **Prove before deleting**: the pass always runs and always reports; actual
row deletion is behind a single boolean, `improve.stateGc.collect`
(default **false** for 0.9.0). Every run emits the counts either way, so
live data accumulates proof; flipping the default later is a one-line
change plus a CHANGELOG note.
- **Events**: one new documented `EventType` literal, `asset_state_gc`, with
the sentinel ref `asset_state/_gc` (same convention as
`proposals/_orphan-purge`), emitted only when there is something to say
(the rekey script's no-op-stays-silent precedent).
**Scope cut: `usage_events` is excluded.** Attached rows (`entry_id` set)
already cascade on entry deletion (`deleteUsageEventsByEntryIds`,
`src/storage/repositories/index-entries-repository.ts:607-618`), and the
90-day retention purge (`purgeOldUsageEvents`,
`src/indexer/usage/usage-events.ts:141`) bounds detached remnants. If live
data shows that purge isn't reaching them, extending the pass is a follow-up.
### Changes
1. Migration `021-asset-state-missing-since` in
`src/core/state/migrations.ts` — the two `ALTER TABLE … ADD COLUMN`
statements, nothing else.
2. `runOrphanStateGcPass(ctx)` in `src/commands/improve/loop-stages.ts`,
registered in the maintenance sequence at `:1005-1036` next to
`runOrphanProposalPurgePass` (`:1303`), which it copies structurally
(same `{count, warnings}` return, same borrowed-connection handling via
`withStateDb(…, { borrowed })` — the #585 pattern at `:1400`). Per run:
a. Collect distinct refs from both tables (repository calls — Workstream
B's modules), resolve them against `entries.item_ref` with the same
chunked-probe shape the rekey script uses
(`scripts/rekey-asset-ref.ts:278`), reusing the existing
legacy-spelling normalization from `preparation.ts:2153-2163` so a
live asset keyed under an old spelling is never a candidate.
b. Unresolved and `missing_since IS NULL` → stamp now. Resolved and
`missing_since` set → clear it (the asset came back).
c. If `improve.stateGc.collect` is true: delete rows whose
`missing_since` is older than the grace constant, per-table, in one
transaction each.
d. Emit `asset_state_gc` with `{pending, collected, byTable}` when either
is nonzero; surface the same counts in the pass summary line like the
neighboring purge passes do.
3. Config: `stateGc: { collect: boolean }` added to
`ImproveConfigSchema` (`src/core/config/schema/improve.ts`), default
false; JSON schema regenerated via `scripts/gen-config-schema.ts`.
### Tests
One integration suite, `tests/integration/commands/improve/state-gc.test.ts`:
- Orphaned ref is stamped, not deleted, on first sighting; deleted only after
the grace elapses *and* `collect` is on (fake clock via the stamped value).
- Default config: nothing is ever deleted; counts still reported.
- A live asset whose state row carries a legacy ref spelling is not a
candidate (the normalization case).
- A ref that resolves again after being stamped has `missing_since` cleared.
- The event appears in `akm log` with the expected metadata; a run with
nothing pending emits no event.
- Rekey-race guard: rows stamped but inside grace are untouched, and
`rekey-asset-ref.ts` still moves them intact.
### Acceptance
- Field-failure class reproduced in a fixture (state rows under a retired
source root) is reported immediately and collected only with
`collect: true` after grace.
- No behavior change for any source that fails to scan (entries preserved →
zero candidates).
- CHANGELOG entry under Added; #733 closes with a note that the
`collect` default flips after live runs prove the report clean.
## Workstream D — #730: OKF v0.2
Three parts: read-side adapter support, write-side provenance stamping through
the proposal path, and the conformance/spec updates. One scope decision up
front: **the `okf` adapter stays consumer-only.** AKM markdown is already an
OKF-compatible superset (`okf-support.md` progressive-enhancement contract),
so the new provenance fields are *written* only to AKM-native assets;
runbook §10's write-rejection contract and the tests at
`okf-conformance.test.ts:832-936` are unchanged.
### D1 — read side (`src/core/adapter/adapters/okf-adapter.ts`)
Today `recognize` (`:125-171`) reads exactly five keys (`type`, `title`,
`description`, `tags`, `timestamp`); everything else folds into opaque
`documentJson`. Add, staying lenient (never reject):
1. `updated` ← `generated.at`, falling back to legacy `timestamp` (the v0.2
breaking change, with the fallback v0.2 itself allows).
2. Parse the v0.2 families into new optional `IndexDocument` fields
(`src/core/adapter/types.ts:141-296`): `generated` (`by`/`at`), `verified`
(list; tolerate the single-mapping form), v0.2 `sources` (object list),
`status`, `stale_after`, and `okf_version` (currently read nowhere in
`src/`).
3. **Collision handling** — three AKM fields already occupy adjacent names:
`sources?: string[]` (wiki citations, silently drops non-strings at
`metadata.ts:486-489`), `generation?: number` (consolidation depth), and
`quality: "generated"`. The v0.2 families therefore land under *new,
distinct* field names (recommendation: a single namespaced
`provenance?: { sources, generatedBy, generatedAt, verified }` plus flat
`lifecycleStatus` / `staleAfter`), leaving the existing fields untouched.
Final naming is an owner call at PR review; the plan only fixes that no
existing field is overloaded.
### D2 — write side (AKM-native assets via proposals)
The proposals system already tracks exactly what v0.2 wants on disk —
`source`/`sourceRun` (PROV-DM modeled, `proposal-types.ts:82-120`),
`gateDecision`, `review` — it just never leaves state.db. Project it at
promotion time:
1. `promoteProposal` stamps `generated: { by, at }` into the written asset's
frontmatter, actor per the v0.2 convention (`<producer>/<version>` →
`akm/<pkg-version>` for automated lanes; `human:<id>` when a human
accepted the proposal).
2. A proposal that passed its gate or review appends a
`verified: [{ by, at }]` entry alongside.
3. Where a proposal carries `evidenceSources` (`types.ts:265-268`), project
them as v0.2 `sources` entries (`{ resource }` minimum).
4. `DOCUMENT_JSON_CARRIED_FIELDS` (`akm-adapter.ts:192-213`) gains the new
keys so AKM's own adapter rereads what it wrote; `akm show` surfaces them.
Deeper consumers — `stale_after`-driven re-verification, trust-tier ranking —
are explicitly *not* 0.9.0 scope; they are the 0.9.x improve-tuning track.
### D3 — conformance and spec docs
1. Rewrite `docs/architecture/testing/okf-v0.1-conformance-runbook.md` for
v0.2 and bump the pinned `KC_REF` (`:57`), inspecting the upstream
`okf/SPEC.md` diff first as the runbook itself instructs (`:61-62`).
2. Update the §0.1 OKF↔AKM field-mapping table in
`akm-0.9.0-bundle-adapter-spec.md` (`:22-35`) and the frozen-reference
paragraph (`:480`); add a v0.2 note to `okf-support.md`.
3. Add the missing `tests/fixtures/format-family-goldens/okf/` golden — OKF
is the only registered adapter without one.
### Tests
- `tests/core/adapter/okf-adapter.test.ts` — update the `timestamp`
assertions (`:142`, `:291`, `:300`) to cover `generated.at` precedence and
legacy fallback; add parsing cases for each new family, including the
`verified` single-mapping form and non-string v0.2 `sources` surviving
intact.
- `tests/integration/okf-conformance.test.ts` — v0.2 fixture bundle alongside
the v0.1 one; both must index; write-rejection block unchanged.
- Promotion-path test: an accepted automated proposal produces frontmatter
with `generated` (and `verified` when gated), round-tripping through
`asset-serialize.ts` and re-indexing losslessly.
### Acceptance
- v0.1 and v0.2 bundles both index correctly (v0.1 fixtures byte-identical in
behavior to today).
- AKM-authored assets written through promotion carry `generated`, plus
`verified` when a gate/review confirmed them.
- Conformance runbook passes end to end at the new `KC_REF`; consumer-only
status intact.
- CHANGELOG entries under Added (v0.2 read support; provenance stamping);
#730 closes.
## Sequencing and release
1. **PR 1 — Workstream A** (behavior change; eval evidence + CHANGELOG).
2. **PR 2 — Workstream B** (pure refactor + guard rule; rebases trivially).
3. **PR 3 — Workstream C** and **PR 4 — Workstream D** in parallel (disjoint
surfaces: state.db/maintenance vs adapter/proposal serialization). D is the
largest; its one open naming decision (D1.3) should be settled in review,
not up front.
4. Cut the next rc with all four merged, soak, ship **0.9.0**.
5. On release: revisit milestone due dates (0.9.0's 2026-07-06 has passed;
0.9.1 — now carrying only #652 — is dated 2026-08-08), and schedule the
two deliberate follow-ups: flipping `improve.stateGc.collect` on once live
reports prove clean, and the deeper v0.2 consumers on the improve-tuning
track.