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
330 lines (262 loc) • 16 kB
Markdown
# Ref Format
A `ref` is the identifier that `akm search` returns for items and `akm show`
consumes.
Agents should not parse refs or construct them by hand. The intended flow is:
```text
search -> pick a hit -> pass its ref to show
```
> **Status.** This document is normative for 0.9.0 and is the target the
> implementation is being brought to. Sections marked **[0.9.0 change]**
> describe decisions taken during the 0.9.0 surface review that the code does
> not yet fully implement; see
> [`0.9.0-release-surface-review.md`](./0.9.0-release-surface-review.md) and
> [`0.9.0-decisions.md`](./0.9.0-decisions.md).
## Item Refs
Item refs use this wire format:
```text
[bundle//]conceptId[#fragment]
```
| Part | Required | Description |
| --- | --- | --- |
| `bundle` | no | Workspace bundle slug (e.g. `personal`, `team-catalog`) that owns the item. Separated from the rest of the ref by `//`. When omitted, the ref resolves against the containing bundle (content-internal refs) or, for CLI/API input, against `defaultBundle` and then the remaining bundles in installation-priority order. |
| `conceptId` | yes | Adapter-defined path within the bundle, `/`-separated and canonicalized by that adapter. Examples: `knowledge/http-caching`, `skills/code-review`, `scripts/db/migrate/run.sh`, `tables/customers`. |
| `fragment` | no | Selector for a part of the item. For markdown-document items the core interprets it as a **section selector**; for every other item kind it is an adapter-owned selector, opaque to the core. See [Fragments](#fragments). |
`type` is **no longer part of a ref**. Identity is a path, not a `type:name`
pair, and a conceptId is an **opaque path** — akm does not parse meaning out of
its segments. `skills/deploy` and `workflows/deploy` are distinct concepts that
never collide because their paths differ, not because their first segment is a
recognized word.
An item's `type` is an **attribute of the resolved item**, supplied by the
selected adapter, never a predicate parsed out of the ref string. The `okf`
adapter reads it from OKF frontmatter; the `akm` adapter derives it from its
native matcher and placement rules. This is why a ref into any bundle —
`okfbundle//tables/customers`, `wiki//pages/attention` — is just as addressable
as `personal//memories/vpn-note`. See
[`okf-support.md`](./okf-support.md).
Refs are parsed by `parseBundleRef` in `src/core/asset/asset-ref.ts`. The
grammar (normative spec §11.1) is:
```text
ref := [ bundle "//" ] concept-id [ "#" fragment ]
bundle := any run of non-space chars excluding : . # /
concept-id := adapter-canonical path within the bundle, / -separated, NFC, case-sensitive
fragment := section slug (markdown-document items) | adapter-owned selector
```
The bundle slug excludes `:`, `.`, `#`, `/`, and whitespace so a `bundle//conceptId`
token is lexically distinct from a URL (whose scheme carries a `:` before `//`) and
so the first `//` unambiguously bounds the bundle.
### AKM concept ids and adapter layouts
The subdirectory prefix is the item's placement directory:
| Subdir | Holds | Example conceptId |
| --- | --- | --- |
| `scripts/` | Executable scripts | `scripts/deploy.sh` |
| `skills/` | Skill directories (`SKILL.md`) | `skills/code-review` |
| `commands/` | Slash-command templates | `commands/release` |
| `agents/` | Agent definitions | `agents/reviewer` |
| `knowledge/` | Reference documents | `knowledge/api-guide` |
| `workflows/` | Workflow documents / programs | `workflows/ship-release` |
| `memories/` | Recalled context fragments | `memories/deployment-notes` |
| `lessons/` | Distilled feedback lessons | `lessons/retry-backoff` |
| `facts/` | Durable bundle-level facts | `facts/team/tool-stack` |
| `sessions/` | Indexed agent sessions | `sessions/claude/2026-07-24-abc` |
| `env/` | `.env` configuration groups | `env/prod` |
| `secrets/` | Single sensitive values | `secrets/deploy-token` |
| `tasks/` | Scheduled / on-demand tasks | `tasks/nightly-sync` |
These are the subdirs the **`akm` adapter** places into and interprets according
to its own matcher rules. Other bundles use whatever paths their adapters
declare — an OKF bundle emits `tables/customers`, an LLM wiki emits
`pages/attention`, and a website snapshot emits its crawled path. Every one of
those is an equally valid conceptId; the core does not require the leading
segment to be an AKM placement directory.
### Examples
- `scripts/deploy.sh`
- `skills/code-review`
- `knowledge/api-guide`
- `commands/release`
- `agents/reviewer`
- `memories/deployment-notes`
- `env/prod`
- `personal//knowledge/http-caching`
- `team-catalog//workflows/release`
- `knowledge/api-guide#authentication` (section selector)
### Rejected
- `viking://skills/deploy` (URI scheme — a `:` before `//` is not a bundle slug)
- `skills/../../../etc/passwd` (path traversal)
- `github:owner/repo` (this is an install ref, parsed elsewhere)
- the pre-0.9.0 `<type>:<name>` grammar (e.g. the old colon-typed spelling) — dead;
parsed only by the frozen migrator
## Fragments
**Scope: `show` only in 0.9.0 (ruled 2026-07-26, Q-06).** `parseRefInput`
still rejects any `#fragment` outright; `show` is the only command that
accepts one, by bypassing that shared parser. Every other ref-consuming
command (graph, tasks, improve, proposals, the utility repo, the indexer
walk) routes through `parseRefInput` and does not accept fragments yet.
Wider rollout is post-0.9.0.
**[0.9.0 change]** Before 0.9.0 the fragment production existed in the grammar
and in this document, but every input boundary rejected it
(`parseRefInput` threw `INVALID_FLAG_VALUE` for any `#fragment`) and nothing
consumed it. It was stable on paper and unusable in practice. 0.9.0 resolves
that by giving it one concrete meaning the core implements, and keeping the
adapter-owned meaning for everything else.
**Markdown-document items — section selector (core-implemented).** For an item
whose adapter presents it as a markdown document, the fragment names a section:
```text
akm show knowledge/api-guide#authentication
```
Resolution is: slugify each heading in the document (lowercase, non-alphanumeric
runs collapsed to `-`, trimmed) and match the fragment against those slugs;
fall back to a case-insensitive match against raw heading text. The selected
region runs from that heading to the next heading of the same or higher level.
A fragment that matches no heading is an error that **lists the available
fragment slugs** — that error is the discovery mechanism, replacing the removed
`toc` view mode.
**Every other item kind — adapter-owned selector.** Per normative spec §11.3,
when one item exposes multiple exports the adapter MAY append a stable
fragment (`team//tools/toolbox#deploy`). The fragment is adapter-owned and
opaque to the core. The two meanings coexist because the core only interprets
fragments for items whose adapter declares them markdown documents; for
everything else it passes the fragment through untouched.
**Fragments are input-only.** Durable state — index rows, state keys, proposal
targets, bindings — never stores a fragment. A stored ref carrying one is
invalid.
### Superseded: the `akm show` view-mode grammar
**[0.9.0 change]** `akm show <ref> toc|section "H"|lines A B|frontmatter|full`
is removed. It was a second argv parser (`normalizeShowArgv` rewrote
`process.argv` and injected hidden `--akmView` / `--akmHeading` / `--akmStart` /
`--akmEnd` flags), and its view keywords were reserved words in ref position.
| Old | New |
| --- | --- |
| `akm show X section "Auth"` | `akm show X#auth` |
| `akm show X full` | `akm show X` (no fragment = whole item) |
| `akm show X toc` | `akm show X#<unmatched>` lists available fragment slugs |
| `akm show X lines 10 30` | removed — every response carries `path`; slice the file |
| `akm show X frontmatter` | removed — if a raw-YAML projection proves necessary it returns as a `--shape` value, the designated projection axis |
## Ref-prefix enumeration (browse)
**[0.9.0 change]** Browsing a subtree uses a **conceptId prefix**, matching the
grammar refs are actually emitted in:
```text
akm search "memories/" # every memory in scope
akm search "memories/projecta/" # one subtree
akm search "team-catalog//" # every item in one bundle
akm search "team-catalog//skills/" # one subtree of one bundle
```
A trailing `/` is required for a non-empty prefix, giving exact `/`-boundary
subtree semantics (`projecta/` cannot match a sibling `projectalpha/`). A query
with interior whitespace is prose, not a prefix — it stays an ordinary keyword
search.
This replaces the pre-0.9.0 `akm search "<type>:"` / `"<type>:<prefix>/"`
grammar, which had three defects: it resurrected the singular `type:` spelling
the release removed; it validated against the `akm` adapter's placement types,
so items from every other adapter could not be enumerated at all; and it
matched against item *names* while every displayed ref is a conceptId — so
copying a ref prefix out of search output and pasting it back in silently
degraded to a keyword search.
Because enumeration is now prefix-matching over conceptIds, it covers every
adapter's items uniformly, and `bundle//` enumeration replaces the removed
`akm bundle items` command.
## Asset types are free-form
`type` is adapter-supplied metadata that presents, ranks, and filters — it
never executes and never identifies. The OKF adapter reads the open OKF `type`
field and defaults it according to OKF adapter rules; other adapters derive a
type from their native formats. `IndexDocument.type` is an open string by
contract, and adapters emit types outside the `akm` adapter's placement set
(`website`, `wiki-source`, an LLM-wiki page's `pageKind`, `instruction`, and
anything an OKF bundle author writes).
Consequently `akm search --type <t>` is an **exact string match against an open
set, deliberately unvalidated**: an unrecognized type returns zero hits rather
than an error, because "unrecognized" is not a decidable property. Callers that
want a closed set should filter on conceptId prefix instead (see above).
The closed set still applies where a type must select a *write placement* —
`akm proposal new <type> <name>`, `placeNew` — because those need a directory to write to.
## Reserved structural files
For the OKF adapter, `index.md` (directory listing / progressive disclosure)
and `log.md` (update history) are reserved structural files at every level and
are never concepts. The `akm` and `llm-wiki` adapters independently reserve
the same names as part of their own format contracts. Other adapters own their
reserved-name rules; the cross-format ref grammar does not impose OKF
structure on them. Item writes must follow the selected adapter's rules.
## Install Refs (distinct grammar)
`akm add` and one-shot `akm clone` accept a different ref grammar. Install
refs locate an upstream kit to fetch; they are **not** item refs and are
parsed by `parseRegistryRef` in `src/registry/resolve.ts`.
```text
install-ref := github-ref | git-url | npm-pkg | https-url | skills-sh-slug | local-path
```
Examples: `github:owner/repo#v1.2.3`, `git+https://gitlab.com/org/kit`,
`@scope/kit`, `https://docs.example.com`, `skills.sh:code-review`,
`./path/to/kit`.
The two parsers are intentionally distinct — each rejects the other's inputs.
Item refs never carry URI schemes; install refs are not addressable through
`akm show`.
## Bundle prefix
When a ref includes a bundle prefix, `akm show` narrows lookup to that bundle:
```text
team-catalog//scripts/deploy.sh
personal//knowledge/my-notes
```
When absent, a short ref from CLI/API input resolves to `defaultBundle` if the
conceptId exists there, otherwise to the first bundle containing it in installation
priority order (first match wins, deterministically). A short ref inside bundle
content resolves against its **containing** bundle. To pin lookup to a single
bundle programmatically, use `resolveRef(input, { only: bundleId })` — there is no
ref spelling for "primary only".
## Refs in prose
Refs embedded in prose MUST use the fully-qualified `bundle//conceptId` form —
the bundle-slug charset makes such a token lexically anchored — or a native
link form owned by the adapter (OKF links, wiki links). **Bare short refs in
prose are not refs**: `memories/foo` in a sentence is ordinary text, and no
tool may rewrite it.
Only lint's missing-ref scan honours this rule today. The other consumer,
`akm mv`'s inbound-ref rewriting, was defective: it implemented the rule
exactly inverted (it rewrote bare conceptIds and matched no `bundle//` pattern
at all), so it rewrote prose that is not a ref while leaving real anchored
refs dangling. That defect is why `akm mv` shipped **Experimental** earlier in
the 0.9.0 cycle and was never covered by the stability contract.
**Amended 2026-07-30.** `akm mv` was removed outright before the 0.9.0
release (no alias, no stub — see [`0.9.0-decisions.md` §D3](./0.9.0-decisions.md#d3--renames-are-delete--create-akm-mv-ships-experimental)).
There is no tool today that rewrites inbound refs on rename; see "Renames and
moves" below for the current procedure.
## Renames and moves
**[0.9.0 change]** A rename or move is **delete plus create**. The destination
is a new identity: it gets a fresh index row, and learned state (utility,
salience, outcomes, usage history) does not follow it.
```sh
mv ~/akm/memories/old-note.md ~/akm/memories/new-note.md
# update any intentional refs to it
akm index
akm lint # confirms nothing dangles
```
Moving an item **between bundles** is copy/import followed by deletion from the
source. Both the bundle and the concept identity change; there is no
identity-preserving cross-bundle move.
This supersedes the pre-0.9.0 rule that a rename "MUST use an explicit
state-rekey transaction". That rule was a product choice made during the bundle
refactor, not a constraint the architecture imposes. `akm mv`, which implemented
the superseded rule, shipped briefly as an **Experimental** surface outside
the stability contract, because its inbound-ref rewriting was inverted (above),
then was removed outright before the 0.9.0 release. The procedure above —
plain filesystem move, then `akm index`, then `akm lint` — is the only
supported path today. If preserving learned state through renames later proves
materially valuable, a narrow same-bundle `akm rename` can return — resolving
through the index, using adapter-owned placement, accepting qualified refs,
rewriting only anchored prose refs, applying one qualified old→new state
mapping, and rebuilding derived index data rather than preserving row IDs.
**Amended 2026-07-30.** Carrying learned state (utility, salience, usage
history) across a rename is opt-in and out-of-band: run
`bun scripts/rekey-asset-ref.ts <old-ref> <new-ref>` from a source checkout
**before** `akm index`, and it re-keys the `entries` row plus
`asset_salience`/`asset_outcome`/`usage_events` in place rather than letting
`akm index` mint a fresh row. Automated garbage collection of orphaned rows
left by renames that skip this 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) for
the full walkthrough.
## Usage Notes
Consumers should use structured fields like `conceptId` and `bundle` for display,
and pass the full `ref` string back to `show` as the lookup token.
## Canonical Form
Refs are emitted in canonical form: the bundle slug (when qualified), the
adapter-canonical conceptId, and an optional `#fragment`. The adapter decides
whether an on-disk extension is stripped: OKF and AKM Markdown items strip
`.md`, scripts retain executable extensions, and directory-items such as
skills resolve on the directory path (`skills/<dir>`, not
`skills/<dir>/SKILL`).