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
bundlesconfig shape are still evolving — see STABILITY.md.
Probe order
Each bundle root is owned by exactly one adapter, decided once at install/index time:
- If the bundle's config entry sets
components.<id>.adapterexplicitly, that adapter wins — no detection runs. - Otherwise, every built-in adapter's
looksLikeRoot(root)probe runs in a fixed order, most-specific markers first. The first probe that returnstrueclaims the root. - If no probe fires, the bundle falls back to the
akmadapter.
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:
- The three loosest probes —
llm-wiki,akm, andokf— stay last. Native AKM evidence wins before OKF because AKM Markdown is an OKF superset; theakmprobe is strict enough not to claim an OKF bundle merely because it contains one familiar directory name. akm.looksLikeRootfires on any root carrying a bundle-subdir-named directory — which includes a.claude/.opencodetool dir (commands/,agents/,skills/) and a dotenv bundle (env/,secrets/). Soclaude/opencode/dotenvcome ahead ofakm; their tighter markers claim those roots first, andakmstill wins its own workspace root (which those tighter probes reject).website-snapshot,agent-skills,akm-workflow, andakm-taskcarry disjoint, specific markers that fire on none of the other roots, so they sit at the front for clarity.generic-filesis explicit-config-only — itslooksLikeRootnever fires — so it's last and can never shadow anything.
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:
agent-skillsrecognition/validation are decoupled. The per-change half ofvalidate()only inspects changes that resolve to an actualSKILL.md, so an invalid skill is still indexed asskill(with its raw, invalid name projected) and its field violations surface only for files that exist.missing-skill-mdis the exception and runs as a separate directory pass overValidateContext.list: a top-level directory that holds files but noSKILL.mdanywhere within three levels is flagged. A package's own resource dirs (pdf-processing/reference/) are part of the item, not candidate packages, and are never considered.claude/opencodeshare one codec (tool-dir-shared.ts) and differ only in instruction filename and adapter/component ids. Both accept only the canonical plural tool directories.dotenvredaction is a hard, adapter-level contract. No frontmattertype:override can bypass it:enventries surface only key names,secretentries surface only the file name, never content. A.envfile placed undersecrets/is a secret (name-only), not an env group — the directory gate wins over the.envsuffix.- Task adapters share one task source parser. A standalone
akm-taskbundle and task files owned by the nativeakmadapter both route throughparseTaskSource, which accepts onlyversion: 4(task source v4) — aversion: 3orversion: 2document fails closed withTASK_SCHEMA_VERSION_UNSUPPORTEDand is never indexed, validated, or run. Task source v4 requires exactly oneusesorrunexecutable selector, but itsschedule:is OPTIONAL. It recognizesakm/command,commands/,workflows/, andscripts/targets, refuses the github-actionuses:variant entirely (removed in v4 — no equivalent spelling), and refuses agent/task refs and unsupported actions..yamlis a collection hint only: recognition still gates on.yml, so the near miss is diagnosed but never indexed or scheduled. See the Tasks reference. llm-wiki,akm, andokfall reserveindex.md/log.mdas structural files — never indexed as concepts, never valid write targets — at any depth.okf's lenient wording is not a lenient gate.missing-typeand broken-link findings are labelledinfo:/warning:in the diagnostic text, but diagnostics carry no severity field:akm lintfunnels every finding into oneflaggedlist, and--fail-on-flaggedfails on any non-empty list.generic-filesis the only adapter with no auto-detection at all.looksLikeRootalways returnsfalse; a bundle only gets this adapter through an explicitcomponents.<id>.adapter: "generic-files"override.
See also
- Supported Formats — the compatibility table this page's internals back
- Asset Types — the
akmadapter's 14 native asset types - Concepts — the retrieval loop and execution boundary these adapters serve
- Architecture — sources, refs, the search pipeline, and the write-source chokepoint