UNPKG

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

529 lines (427 loc) • 28.7 kB
# 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`](./0.9.0-release-surface-review.md) — the full review and the prioritized action list. - [`ref.md`](./ref.md) — the normative ref grammar. - [`../../../STABILITY.md`](../../../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](./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](./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`](../../migration/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](./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 `pageKind`s, `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](./ref.md#ref-prefix-enumeration-browse). --- ## 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](./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`](./okf-support.md); [`akm-0.9.0-bundle-adapter-spec.md`](./akm-0.9.0-bundle-adapter-spec.md); [`ref.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`](./akm-0.9.0-bundle-adapter-spec.md) §2; `src/core/adapter/bundle-adapter.ts` (interface + its closing comment on deferred facets, same framing).