akm docs

Adapters

A BundleAdapter (src/core/adapter/bundle-adapter.ts) is the code that decides what a bundle's files are, how they're indexed, how akm lint validates them, and where a new item would be placed. 0.9.0 ships 11 built-in adapters — one per format in Supported Formats. This page is the write-path and internals reference; that page is the compatibility table.

For read-only native tool bundles, recognition is runtime translation, not conversion: the adapter projects native files into AKM's index/runtime shapes while the source file remains authoritative. AKM does not synchronize native formats, maintain translated canonical copies, or write agent/command assets back through the Claude/OpenCode adapters. See Agent, Command, Engine, and Model Resolution.

Stability note. The adapter set, bundle-recognition rules, and the bundles config shape are still evolving — see STABILITY.md.


Probe order

Each bundle root is owned by exactly one adapter, decided once at install/index time:

  1. If the bundle's config entry sets components.<id>.adapter explicitly, that adapter wins — no detection runs.
  2. Otherwise, every built-in adapter's looksLikeRoot(root) probe runs in a fixed order, most-specific markers first. The first probe that returns true claims the root.
  3. If no probe fires, the bundle falls back to the akm adapter.

The order is pinned by BUILTIN_ADAPTERS (src/core/adapter/adapters/index.ts) and enforced by the conformance suite's ordered-owner matrix — it, not any prose spec listing, is authoritative:

website-snapshot   -- root manifest.json {url, fetchedAt}
agent-skills       -- a <name>/SKILL.md package at the bundle root
claude             -- root CLAUDE.md + commands/|agents/|skills/
opencode           -- opencode.json(c), or root AGENTS.md + a tool dir
dotenv             -- every top-level dir is env/ and/or secrets/
akm-workflow       -- a top-level .md with explicit type: workflow
akm-task           -- a top-level valid task source v4 .yml (schedule optional)
llm-wiki           -- root schema.md + pages/
akm                -- .stash marker, or 2+ native subdirs, or fallback
okf                -- root index.md, or any .md with frontmatter type
generic-files      -- never auto-selected; explicit config only

The rationale for the ordering:


BundleAdapter interface contract

Transcribed from src/core/adapter/bundle-adapter.ts:

interface BundleAdapter {
  readonly id: string;
  readonly version: string;              // feeds incrementality + fingerprints
  readonly extensions: readonly string[]; // longest-match stripping + collision priority

  // REQUIRED — single-file recognition primitive.
  recognize(c: BundleComponent, file: FileContext): IndexDocument | null;

  // OPTIONAL — full-component scan for non-per-file layouts (website
  // snapshots, llm-wiki multi-file semantics). Absent -> the core walk
  // (git-aware, symlink-safe, skip-dirs) drives recognize() per file.
  index?(inst: BundleInstallation, c: BundleComponent): AsyncIterable<IndexDocument>;

  // OPTIONAL — item-scoped incrementality. Default: identity (one file = one item).
  affectedItems?(c: BundleComponent, changedPaths: string[]): string[];

  // REQUIRED — native validation (change-transaction pre-commit + lint --fix).
  // Adapters MUST NOT write and MUST NOT read the live filesystem: ctx serves
  // a run snapshot with pending changes overlaid, plus a read-only
  // resolveRef for link/xref existence.
  validate(c: BundleComponent, changes: FileChange[], ctx: ValidateContext): Promise<Diagnostic[]>;

  // OPTIONAL — the CLOSED-FORM set of existing-file placements/read aliases
  // that could own `conceptId`, used for first-owner arbitration without
  // reading authored bytes or walking the bundle.
  readCandidates?(c: BundleComponent, conceptId: string): Array<{ path: string; conceptId: string }>;

  // OPTIONAL — placement / discovery.
  placeNew?(c: BundleComponent, conceptId: string): string;   // where a new item would live
  directoryList?(c: BundleComponent): string[];               // owned dirs; feeds git exact-path staging
  looksLikeRoot?(root: string): boolean;                      // install-time probe
}

recognize and validate are required on every adapter; index, affectedItems, readCandidates, placeNew, directoryList, and looksLikeRoot are optional capability methods. An adapter overriding index() must keep recognize() coherent with it (conformance: index() == fold of recognize() over the core walk) or declare component-level incrementality.

readCandidates is intentionally distinct from both extensions and placeNew: extensions are non-exhaustive walk hints, while placement is a write-normalization policy. Read candidates preserve canonical directory manifests (for example skills/<name>/SKILL.md) so lookup, index-backed show, and workflow runtime share one physical-owner decision. Each candidate carries its canonical concept identity, so a byte-denying ownership probe never has to infer identity from the query. Path-derived recognition (toCanonicalName/toAssetPath, src/core/asset/asset-placement.ts) is a deterministic function of a file's path, so its inverse — every physical spelling that could own one conceptId — is computable in CLOSED FORM (#857): the canonical placement under the type's own bundle subdir, the loose off-canonical placement (an authored path elsewhere in the bundle, keyed off its full path relative to the bundle root instead of the type subdir — this is what readCandidates returns for it), and any type-specific duality (for example env's .env/<name>.env spellings for the "default" alias). readCandidates enumerates the complete closed-form set directly from the conceptId; no bundle walk is needed, so no scan bound applies. The shared authority (src/indexer/lookup/adapter-concept-owner.ts) still probes every candidate with a byte-denying FileContext before trusting it, so an adapter returning a candidate the file does not actually own is harmless — it is simply filtered out.

The core walk — the live indexer's per-dir walk, drained by drainDirDocuments — is one implementation carrying the security policy (symlink-safety, skip-dirs, git-awareness); adapters never reimplement it.

Cross-component ref existence is a core base check, not an adapter concern.


placeNew() wiring status

The interface declares placeNew() as an optional capability method, and 9 of the 11 built-in adapters already implement it — all but okf and website-snapshot (claude and opencode inherit theirs from the shared tool-dir factory). Nothing in the write path calls it yet: AKM-native writes still resolve through AKM's own flat type→directory placement table (src/core/asset/asset-placement.ts's PLACEMENT_SPECS), not through the owning adapter's placeNew().

Placement for every existing bundle is already correct today — this is a deliberately sequenced routing change, not unfinished behavior. It's scoped out of 0.9.0 by D12 and deferred to 0.10; nothing user-visible changes in 0.10 for this alone. See STABILITY.md ("On the horizon") for the up-to-date status if this changes.


Write allowlists

placeNew() wiring is a separate question from which bundles AKM-native write commands are allowed to touch at all — that's decided today by a small allowlist check, assertAkmAssetWrite (src/core/write-source.ts), run before any write command touches the filesystem. It compares the target bundle's detected (or configured) adapter id against an allowlist and throws a UsageError on a miss.

The default allowlist is ["akm"]. Two command families widen it explicitly:

Command Allowlist adds
akm env create / env remove / secret set dotenv
akm workflow create akm-workflow

Every other adapter — including every adapter that already implements placeNew() (agent-skills, claude, opencode, akm-task, generic-files) — is rejected by every AKM-native write command in 0.9.0. akm task add in particular uses the default allowlist (akm only), so a standalone akm-task bundle is read-only through AKM-native write commands even though akm-task's own placeNew() is implemented.

Reading, searching, and akm lint are unaffected by this allowlist — only creating, editing, or deleting through AKM's own commands is restricted. "Read-only" in Supported Formats means this write-command restriction, not a filesystem permission.

This allowlist boundary is also where AKM's execution boundary principle shows up on the write side: AKM retrieves and validates every supported format, but it only lets its own write commands mutate a narrow, explicitly-declared set of targets — it does not widen write access to a format just because that format's adapter happens to expose a placeNew() implementation.


Task adapter

The native and standalone akm-task paths delegate to the authoritative task source parser, parseTaskSource, which accepts only version: 4 (task source v4). A task source v4 document declares exactly one executable selector (uses or run), but its scheduling is OPTIONAL — a document with no schedule: is valid and runnable manually. A version: 3 or version: 2 document is no longer read by src at all: it fails closed with TASK_SCHEMA_VERSION_UNSUPPORTED, naming akm migrate as the path forward. See Tasks for the public source contract and the migration path.


Implementation caveats

A few adapter-specific behaviors are easy to assume differently from how they actually work:


See also