akm docs

0.9.0 surface decisions

The decision record behind the 0.9.0 surface changes. Each entry states what was decided, why, what it breaks, and where the normative text now lives.

Companion documents:

Governing principle. 0.9.x is the last series that may break freely (STABILITY.md). Every ambiguity we carry past it becomes a migration we owe users later. So the bias throughout is: decide now, break now, and prefer removing a half-built surface over shipping it behind a label.

"Decided" ≠ "shipped." Every entry below records a decision and carries a Status line stating whether it is in the code today. As of the 0.9.0 release every decision is either shipped, partial, or explicitly reversed — but the rule stands for future amendments: do not read a decision as a description of current behavior, and do not copy one into STABILITY.md (the user-facing contract) until its status says shipped.

Decision Status
D1 #fragment section selector Shipped
D2 remove show view grammar Shipped
D3 akm mv stays, Experimental Reversed — mv ships; removed outright 2026-07-30 (see amendment below)
D4 conceptId-prefix browse Shipped
D5 remove akm bundle Shipped
D6 free-form type Shipped at runtime; CLI help text updated
D7 all six formats everywhere Shipped
D8 gate improve autonomy Shipped
D9 --auto-accept warn-and-ignore Shipped (warns, and no longer poisons the scope positional)
D10 extract migration machinery Partial (akm-migrate bin exists; code still in-repo)
D11 OKF foundational Partial (the okf adapter has landed)
D12 placeNew() wiring Deferred to 0.10

D1 — #fragment becomes a section selector for markdown items

Status: SHIPPED, show-only scope for 0.9.0 (ruled 2026-07-26, Q-06). akm show knowledge/guide#auth returns just that section. parseRefInput (src/core/asset/resolve-ref.ts), the ref parser every other ref-consuming command routes through (graph, tasks, improve, proposals, the utility repo, the indexer walk), still rejects any #fragment outright with INVALID_FLAG_VALUE — show works only because it bypasses that parser. Wider rollout across those commands is post-0.9.0.

Decided. The fragment production is implemented rather than merely declared. For markdown-document items the core resolves #fragment as a section selector (heading slug, falling back to case-insensitive heading text). For all other item kinds it stays an adapter-owned selector, opaque to the core, per spec §11.3. Fragments are input-only and never stored.

Why. STABILITY.md declared [bundle//]conceptId[#fragment] Stable while every input boundary rejected fragments outright and nothing consumed them — a production that was stable on paper and unusable in practice. Two ways out: delete it from the grammar, or give it a meaning. Deleting it forecloses the exports/bindings design that spec §11.3 reserves it for; implementing the markdown case costs almost nothing (the parser already returns the fragment and the resolver already carries it) and retires a competing grammar in the same move (D2).

Breaks. Nothing — fragments were previously rejected everywhere, so this is purely additive at the input boundary. The spec text changes (§11.3 gains the markdown carve-out).

Normative text. ref.md § Fragments.


D2 — the akm show view-mode grammar is removed

Status: SHIPPED. normalizeShowArgv and the KnowledgeView type are gone; a positional after the ref is a usage error naming #fragment.

Decided. akm show <ref> toc|section "H"|lines A B|frontmatter|full is deleted. #fragment (D1) replaces section; no fragment means the whole item, replacing full; an unmatched fragment lists the available slugs, replacing toc. lines and frontmatter are dropped outright.

Why. It was a second argv parser: normalizeShowArgv rewrote process.argv before citty saw it and injected hidden --akmView / --akmHeading / --akmStart / --akmEnd flags (which were never rejected when passed directly). Its keywords were reserved words in ref position. With D1 in place it is redundant for the one mode that carried real weight.

Breaks. A documented Stable read-command spelling. Requires a CHANGELOG migration note. lines has no replacement — every response carries path, so callers slice the file themselves. frontmatter has no replacement; if a raw-YAML projection proves necessary it returns as a --shape value, which STABILITY.md already designates as the projection axis.

Normative text. ref.md § Superseded: the akm show view-mode grammar.


D3 — renames are delete + create; akm mv ships Experimental

Status: REVERSED. akm mv ships in 0.9.0. src/cli.ts registers mvCommand and src/commands/mv-cli.ts ships in full. What 0.9.0 changes is the command's classification — Experimental, outside the stability contract — and the recommended rename procedure.

Amended 2026-07-30. akm mv was removed outright before the 0.9.0 release — src/cli.ts no longer registers mvCommand and src/commands/mv-cli.ts no longer exists; there is no alias or stub. The Experimental classification recorded above never got the chance to matter in practice. The recommended rename procedure below (plain filesystem move plus akm index and akm lint) is now the only procedure. Carrying learned state across a rename is opt-in and out-of-band: bun scripts/rekey-asset-ref.ts <old-ref> <new-ref>, run from a source checkout before akm index, re-keys the entries row plus asset_salience/ asset_outcome/usage_events in place. Automated garbage collection of orphaned rows left by renames that skip that script is tracked as issue #733; until it lands, the script is the only path. See v0.9.0-troubleshooting.md.

Decided. A rename or move produces a new identity. Learned state (utility, salience, outcomes, usage history) does not follow it. Cross-bundle movement is copy/import plus delete. Spec §11.2's "MUST use an explicit state-rekey transaction" is amended accordingly. The recommended rename procedure is a plain filesystem move plus akm index and akm lint; akm mv remains available for callers who want the tooled path and accept Experimental risk, and the identity preservation it advertises is not a contract.

Why Experimental. The command's primary integrity promise is implemented inverted. Spec §11.1 names it directly — inbound-ref rewriting "operate[s] only on these anchored [bundle//conceptId] forms; bare short refs in prose are not recognized as refs" — but buildRewritePatterns emits five patterns, three dead type:name spellings and two bare-conceptId spellings, and no bundle// arm at all. It rewrites precisely what the spec says is not a ref and leaves every real prose ref dangling. Separately, the reserved-filename guard ref.md requires of akm mv does not exist in the file; it hardcodes seven types (MV_SUPPORTED_TYPES) and the primary writable bundle only; it can change bundle provenance by omitting bundleId on its finalizing index call; and its documented failure semantics ("the move still succeeds" with warnings) contradict an implementation that throws after the irreversible filesystem commit.

Cost on the other side of the ledger: 1,452 LOC plus ~2,190 LOC of tests and two permanent function-size-ratchet exemptions — for what the originating spec itself called "the highest-effort, lowest-frequency operation".

Those defects are why delete + create is what the docs prescribe and why the command carries no contract. They are not why it would be deleted: the code is already written and tested, the label bounds what users may rely on, and the redesign below is the real fix.

Breaks. Nothing. akm mv is unchanged code, still registered, still executable. The changes are the Experimental classification and the documented recommendation. The bundle refactor already solved the problems that motivated the command except item rename, and rename is the case where delete+create is defensible: index.db is a regenerable cache, and the row-ID preservation mv performs leaves the embedding stale anyway (the search_text changes without invalidating it).

Future. A narrow same-bundle akm rename is the eventual fix for the defects above and is deferred past 0.9.0. It would resolve through the index, use adapter-owned placement, accept only qualified refs, rewrite only anchored prose refs, apply one qualified old→new state mapping, and rebuild derived index data rather than preserve row IDs.

Normative text. ref.md § Renames and moves; spec §11.2.


D4 — browse uses conceptId prefixes, not <type>:

Status: SHIPPED. parseRefPrefixQuery parses conceptId and bundle// prefixes and consults no type list; enumerateEntries matches on conceptId and bundleId. The scripts/lint-shipped-assets.ts carve-out is removed, so the retired spelling is now an offense in agent-facing assets.

Decided. Subtree enumeration is akm search "memories/", "memories/projecta/", "bundle//", "bundle//skills/". The akm search "<type>:" / "<type>:<prefix>/" grammar is removed.

Why. Three defects, each independently disqualifying. It resurrected the singular type: spelling the release had just removed, as query syntax, in the same release. It validated tokens against the akm adapter's placement types, so items from every other adapter (website, wiki-source, wiki pageKinds, instruction) could not be enumerated at all. And it matched namePrefix against item names while every emitted ref is a conceptId — so copying a ref prefix out of search output and pasting it back in silently degraded to a keyword search.

Breaks. The <type>: browse spelling. Requires a CHANGELOG migration note.

Bonus. bundle// enumeration is the replacement for the removed akm bundle items (D5).

Implementation note. scripts/lint-shipped-assets.ts is a zero-tolerance gate that fails on any dead type:name token in agent-facing shipped assets — but it carries an explicit carve-out permitting the <type>: / <type>:<prefix>/ search grammar because it was "still current". That carve-out must be removed when this lands, so the gate also catches memory:projectA/. Otherwise the retired grammar keeps a sanctioned path back into the material agents read.

Normative text. ref.md § Ref-prefix enumeration.


D5 — akm bundle is removed

Status: SHIPPED. akm bundle is no longer registered.

Decided. The akm bundle {list,show,items} noun group is deleted. Bundles are inspected through akm list (extended additively with the default marker, the desired source descriptor, registryId, the full lock block, components, and per-bundle item counts) and enumerated through akm search "bundle//" (D4).

Why. It was a half-built noun group by its own admission — the source comment records that the lifecycle verbs (create/install/update/remove/sync/export) "stay on their existing top-level commands for 0.9.0" — so akm bundle list coexisted with akm list over the same objects. Leaving that split unresolved means either moving five lifecycle verbs under a noun group after release (expensive) or carrying two competing inventories forever. It is undocumented in docs/reference/cli.md, absent from STABILITY.md, and has no consumers beyond its own CLI file and one test — so removal is nearly free.

Breaks. Three undocumented commands. The unique data they exposed (resolved lock detail, components, per-bundle counts) must land on akm list in the same change or it is lost.


D6 — asset type is free-form and stays unvalidated

Status: SHIPPED. Runtime filtering is unvalidated (src/commands/read/search.ts passes --type straight through), and the search / curate --type help text now describes the open set. Shell completions still offer only the built-ins, which is inherent — a completion list cannot enumerate an open set.

Decided. --type is an exact match against an open set, deliberately not validated. An unrecognized type returns zero hits rather than an error. The help text says so. The closed set survives only where a type selects a write placement (akm propose, placeNew).

Why. This reverses an earlier recommendation in the review. Under the bundle/adapter model IndexDocument.type is an open string by contract, the closed union was deliberately deleted, and adapters legitimately emit types outside the placement set. Validation would reject valid queries against adapter-defined content. The real defect is documentation: the help enum listed 10 of 13 placement types and none of the adapter space.

Breaks. Nothing. Documentation-only.

Normative text. ref.md § Asset types are free-form.


D7 — one output contract, no bespoke renderers

Status: SHIPPED. md and html are registry-driven with generic fallbacks (src/output/render-registry.ts, src/output/generic-render.ts); akm health registers its renderers instead of intercepting, so nothing branches on format before output(); the startup --format html rejection is gone because html is now valid everywhere; graph export's local --format is deleted; and the exempt set is declared in src/output/format-exempt.ts.

Amendments after the first landing. The initial D7 implementation carried two compromises that were subsequently removed rather than kept:

  1. akm health no longer branches on --format at all. The first landing kept a format-driven READ (--format html performed a richer query for trend deltas) plus module-level context bound for the renderer. Both are gone: the full report is a data flag — akm health --report fetches per-run rows, window-compare deltas, and the pending proposal queue, and carries them in the result (runs/deltas/report). The registered md/html renderers are pure functions of that result and fire on its shape, never on the format. This also closed a hole in D7 itself: the report dataset was previously reachable ONLY as html; it is now ordinary data in every format. Breaking: akm health --format html alone now renders generically — use --report; the html-only --compare flag is replaced by --window-compare.

  2. The citty flag mis-capture is fixed at the root, so the sync and env unset workarounds ARE deleted after all. The release review (B2) was right that they should go but wrong about why they existed: they guarded against citty parsing each command level with only its own args, so a root-declared global flag was unknown at the leaf and its space-separated value fell through as a positional. GLOBAL_OUTPUT_ARGS (src/cli/shared.ts) now redeclares --format/--detail/--shape/ --output on every defineJsonCommand leaf (declared args win on collision, e.g. hints --detail), so the parser consumes the values and the workarounds are unnecessary rather than merely inconvenient. The declarations are parse-only; the output mode is still read once from the invocation singleton.

  3. akm graph export's artifact payload follows the --out extension (.jsonl → JSONL, else JSON), not the global --format. The first landing mapped the global flag onto the payload where the 1:1 mapping existed, which still overloaded one flag with two meanings — payload and envelope are separate concerns, and the destination names the payload.

Decided. All six --format values work on every command. Rendering becomes registry-driven (md and html registries mirroring the existing shapes/text registries) with generic fallbacks derived from the shaped envelope. akm health keeps its rich output by registering renderers rather than intercepting before output(). akm graph export --format is deleted in favour of the global flag. Format-exempt commands are declared rather than implicit.

Why. The status quo had three inconsistent failure modes: md silently emitted JSON everywhere except health, html threw everywhere except health, and 37 commands silently emitted JSON under --format text. Silent wrong-format output from a documented Stable contract is the worst of the available behaviors. graph export --format additionally collided with the global flag — one token, two parsers — which is what forced the citty workarounds in sync and env unset.

Breaks. akm graph export --format (use the global --format). The html generic fallback reverses a prior decision (chunk-9 WI-9.4c) that removed the JSON-in-<pre> fallback; that decision is explicitly re-opened here.


D8 — gate improve autonomy, not improve

Status: SHIPPED. experimental.improveAutonomy exists in the schema (src/core/config/schema/experimental.ts), is read through one predicate (src/core/config/experimental.ts), and gates five lanes via src/commands/improve/autonomy-gate.ts. Three are downgraded in the strategy config at resolveImprovePlan; the memory-cleanup and contradiction passes ask the gate directly, because their only prior guard — shouldAnalyzeMemoryCleanup — reads scope and eligible-memory count and no strategy flag at all.

Because the gate runs BEFORE buildImprovePlan's LLM preflight, a gated lane also stops demanding an engine: a strategy whose only model-backed process is gated now resolves with no engine configured. That asymmetry is what tests/improve-plan-autonomy-wiring.test.ts uses to prove the ordering.

Reporting: each downgrade warns naming the lane and the key, appends an improve_skipped event with reason: "autonomy_gated" (which akm health already aggregates by reason, so it needed no new machinery), and is listed by akm tasks doctor under improveAutonomy.gatedLanes. Wiring doctor also fixed a defect the gate would otherwise have introduced: doctor resolved the RAW strategy for improveTriage.applyMode, so a promote strategy under a review-first config would have reported "promote" while the run used "queue" — the diagnostic command misreporting the thing it exists to diagnose. It now resolves the gated strategy.

One extra fix this surfaced. akm improve rejected the global --format outright (akm improve does not accept --format), which made it a fourth inconsistent format behaviour after D7 — neither compliant nor declared exempt, while STABILITY.md claimed all six formats work on every non-exempt command. It does emit an envelope through output() (always under --dry-run, otherwise under --json-to-stdout), so the rejection is removed and --format now applies to that envelope. Progress output stays on stderr either way.

Decided. akm improve stays on by default, review-first, with its schedules intact. A single opt-in — experimental.improveAutonomy — gates the lanes that mutate assets without review: consolidate's merge/delete/contradict, the memory-cleanup and contradiction passes, memory-inference writes, and triage applyMode: "promote". A gated lane never becomes a silent no-op: it emits an event naming the config key, surfaces in akm tasks doctor, and raises a health advisory.

Amendment — sync.push is NOT gated. An earlier draft of this decision put unattended push behind the gate and flipped its default to false. That is reversed: sync.push keeps its true default and stays outside experimental.improveAutonomy. Push is materially different from the other five lanes — it publishes already-committed content to a remote the user configured for exactly that purpose, rather than mutating or deleting assets without review — and it already has two direct, documented opt-outs (improve.strategies.<name>.sync.push: false and --no-push). Gating it would have added a third control over the same behavior while silently stopping a publish step that working setups depend on.

Why. A blanket experimental gate on akm improve would have silently turned installed schedules into no-ops and removed the only normal producer of memory inference and graph extraction — contradicting 0.8's positioning of improve as the unified maintenance entry point, STABILITY.md's Evolving promise, and the roadmap's "harden the 0.8 baseline" framing. But the default strategy is not actually review-only: an audit confirmed six direct-mutation lanes, one of which (memory cleanup / belief-state archiving) is gated by no strategy flag at all and runs on any improve run covering memories. Gating the autonomy resolves the contradiction without removing the feature.

Breaks. Users relying on unattended promotion, direct consolidate merge/delete, memory-inference writes, or the memory-cleanup and contradiction passes must set the key. Nothing changes for sync.push: git-backed bundles with a remote keep pushing after an improve run exactly as they do today.

Scope notes. Three direct writes stay ungated by design and are documented in STABILITY.md: extract's session indexing (additive sessions/** writes), distill's encoding-salience frontmatter stamp (metadata only), and sync.push (publishes already-committed content to a remote the user configured, and already has sync.push: false / --no-push).


D9 — --auto-accept is removed

Status: SHIPPED. akm improve --auto-accept … now warns and names the replacement, and the space-separated form no longer leaves its value in the scope positional (which had reduced the run to a zero-match no-op that exited 0).

Decided. The strict 0.9 runtime does not parse the retired confidence-gate flag. Scheduled commands must remove it rather than silently continuing with a different policy.

Replacement. akm improve && akm proposal drain --promote --yes (defaults: personal-stash policy, 25-accept ceiling), or a triage block with applyMode: "promote". Note the config-only path drains the standing backlog as a pre-pass, so run N's proposals promote at the start of run N+1; closing that one-cycle gap needs an opt-in post-generation drain.


D10 — migration machinery leaves the core

Status: PARTIAL. An akm-migrate binary exists, but the migration code still lives in this repo. Amended 2026-07-27 (R-029 ruling): akm backup is REMOVED from the CLI outright rather than kept as a forwarder — the capability lives at akm-migrate backup / akm-migrate restore, which already existed; a cleaner backup story is deferred past 0.9.0. The migration guide and 0.9.0 release notes now teach the akm-migrate spelling.

Decided. Apply/backup/restore move to a separately published akm-migrate package; akm migrate stays a thin forwarder (akm backup is gone per the amendment above); akm config migrate and the akm-migrate-storage bin entry are removed from the CLI. If the extraction does not make 0.9.0, the declaration still ships: these surfaces are Internal and scheduled for removal at 0.10.

Why. ~8,820 LOC of production code plus ~7,130 LOC of tests, pinned to a one-time 0.8→0.9 cutover (akm backup hard-rejects any --for value except the literal 0.9.0), currently occupying three top-level commands and a second published binary permanently.

Risks to manage. The extracted package shares the runtime boundary (lint-frozen to two files), the cross-process lock protocol, and ordered migration IDs, so it needs an exact-pin peer dependency. The core must keep the pending-operation gate, ledger prefix refusal, and pointer messages.

Breaks. akm config migrate (use akm migrate); the akm-migrate-storage bin (npm-only precedent already exists — install.sh never shipped it).


D11 — OKF is the first-class Markdown baseline; adapters progressively enhance it

Status: PARTIAL. The okf adapter has landed.

Decided. AKM supports OKF through a first-class built-in okf adapter. Every applicable OKF conformance case must pass. OKF supplies the least-common-denominator path identity, open type, content, links, and heading fragment behavior for Markdown concepts. It is not the database schema and does not serialize non-Markdown native formats.

Why. A conformant OKF bundle must work end to end: if search emits okfbundle//tables/customers, show must accept that ref, preserve the OKF concept ID and metadata, and retain OKF links. The current parser's requirement that every concept ID begin with an AKM placement directory is therefore an OKF support bug.

AKM Markdown is an OKF-compatible superset: AKM producers emit the required open type field and retain additional AKM metadata. The selected adapter owns capabilities. The okf adapter applies only generic content/fragment behavior; the akm adapter progressively adds command, script, workflow, task, environment, secret, memory, lesson, and other native handling. Unknown types remain valid data and fall back to generic behavior.

Required changes. Ref resolution accepts opaque adapter concept IDs; identity persists by concept ID rather than type/title; OKF content, metadata, and links survive indexing; heading fragments work through generic show; and AKM-specific write commands reject consumer-only OKF targets before mutation. Adapter ownership is persisted at registration and explicit configuration wins. The akm adapter keeps matcher-based classification while its Markdown writers emit OKF-compatible frontmatter.

Breaks. None intended. This widens valid refs for installed OKF bundles without changing AKM-native refs or classification.

Normative text. okf-support.md; akm-0.9.0-bundle-adapter-spec.md; ref.md.


D12 — BundleAdapter.placeNew() stays unwired until 0.10

Status: DEFERRED to 0.10. The interface declares placeNew(c, conceptId) OPTIONAL (src/core/adapter/bundle-adapter.ts), and 8 of the built-in adapter modules already implement it — generic-files, akm, llm-wiki, agent-skills, dotenv, akm-workflow, akm-task, and the shared tool-dir-shared factory (consumed by the claude and opencode adapters). website-snapshot deliberately has none — it is read-only (Mode A); export (Mode B) is a separate, also-deferred facet. Nothing in src/ calls .placeNew( anywhere.

Decided. 0.9.0 leaves placement on AKM's existing flat type→directory table: resolveAssetFilePath resolves stashDirFor(ref.type) (src/core/write-source.ts) against the PLACEMENT_SPECS table (src/core/asset/asset-placement.ts). Routing writes through per-adapter placeNew() instead is deferred to 0.10 as its own change.

Why. stashDirFor( has 36 call sites across 19 files — the indexer, storage repositories, sources, core, and 7 command modules. That is the write path this release rests on. Cutting it over to per-adapter placement is a large, separable refactor; sequencing it after the rest of the 0.9.0 adapter cutover keeps this release's blast radius bounded. Placement for every existing bundle is already correct today — this is a routing change, not a bug fix, and not unfinished work so much as a deliberately later chunk of the same refactor. (Compare the interface's own closing comment on the Tier-B authoring/export/memory facets, similarly flagged rather than stubbed.)

Breaks. None. No user-visible change: nothing calls placeNew() before 0.10, so write behavior for every existing bundle is unchanged. The 8 adapters that already implement it get no exercise from that code path yet — treat those method bodies as unwired, not battle-tested, until 0.10 routes writes through them.

Normative text. akm-0.9.0-bundle-adapter-spec.md §2; src/core/adapter/bundle-adapter.ts (interface + its closing comment on deferred facets, same framing).