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:
0.9.0-release-surface-review.md— the full review and the prioritized action list.ref.md— the normative ref grammar.../../../STABILITY.md— the stability contract.
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:
-
akm healthno longer branches on--formatat all. The first landing kept a format-driven READ (--format htmlperformed 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 --reportfetches 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 htmlalone now renders generically — use--report; the html-only--compareflag is replaced by--window-compare. -
The citty flag mis-capture is fixed at the root, so the
syncandenv unsetworkarounds 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/--outputon everydefineJsonCommandleaf (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. -
akm graph export's artifact payload follows the--outextension (.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).