akm docs

CLI Reference

The CLI is called akm (Agent Knowledge Manager). Commands default to structured JSON at --detail brief. Use --format json|jsonl|yaml|text|md|html, --detail brief|normal|full, and --shape human|agent|summary when you want a different presentation. Errors include error and hint fields.

This page is authoritative for the current CLI. For per-release behavior changes, see CHANGELOG.md and docs/migration/. For the bundle formats akm recognizes (detection, ref shapes, indexing, validation, read/write), see Bundle Types.

Global Flags

These flags are accepted by all commands:

Flag Values Default Description
--format json, jsonl, yaml, text, md, html json Output format
--output path (none) Write rendered output to a file instead of stdout (all formats except jsonl)
--detail brief, normal, full brief Output verbosity level
--shape human, agent, summary human Output projection
--quiet / -q boolean false Suppress stderr warnings
--verbose boolean false Enable verbose diagnostics gated behind isVerbose(). Parsed globally before any subcommand runs. The AKM_VERBOSE env var honours the same setting and wins when both are present (see src/core/warn.ts).

--detail controls how much is returned (brief|normal|full); --shape controls the projection (human for people, agent for a token-lean action view, summary for capability discovery).

--format jsonl

Outputs one JSON object per line. For search (including --from registry), each hit is a separate line. For other commands, the entire result is a single line. Useful for streaming consumption by scripts or agents.

--format md and --format html

json, jsonl, and yaml serialize the result envelope; text, md, and html render it. Every result-envelope command supports all six.

A command may register a renderer for a document format when it has something better to say than the generic one: akm health --group-by run --format md emits its per-run table, and akm health --report --format html renders the full report with KPI cards, charts, and advisories. akm metrics always carries its window rows under --format html. The renderers are data-driven — they fire when the result carries the report dataset, never on the format alone, so the same dataset is available as JSON too. Every other command falls back to a generic rendering derived from its own envelope — headings for the top-level keys, a table for an array of uniform objects, lists otherwise. HTML output is a self-contained document with no external references, so it can be redirected to a file and opened directly.

A small set of commands is format-exempt because their output is not a result envelope at all — completions, child-process passthrough (env run and secret run), document payloads (help, help migrate), and env path (a bare filesystem path is the payload, the documented shell-substitution primitive — wrapping it in an envelope would break $(akm env path <ref>) substitutions). Passing --format to one of those warns on stderr and is otherwise ignored; the exempt set is declared in src/output/format-exempt.ts. migrate status/apply invoke the packaged task migrator but are NOT exempt: the CLI parses its task plan and renders it through the normal --format pipeline, so text/md/html/yaml genuinely reformat it.

Scripted setup modes emit a normal format-aware result. Interactive setup is a terminal UI and emits no result document. agent leaves inherited child streams raw, then formats its final agent-result envelope normally.

--shape=agent

Strips output to only action-relevant fields:

For local materialized assets, editHint is added only when editable is false. It is supplemental guidance and does not replace the normal show, run, or use action (or curate followUp). Registry-only results have no local path, editable, or editHint.

The results collection alias

Every list-returning command names its collection field differently — search returns hits, curate returns items, proposal list returns proposals, bundle list returns sources, env list returns envs, secret list returns secrets, registry list returns registries, registry search returns hits, workflow list returns runs, task history returns rows, log list returns events. A caller that does not already know each command's key cannot write one accessor across all of them.

Every one of these commands also carries a results field — the identical array, not a copy — alongside its semantic key, in every --format/--detail combination and both --shape human (the default) and --shape agent. Code written against a single command should keep using its semantic key for clarity; code that needs to handle several list commands uniformly can read results and never maintain a per-command lookup table.

--shape summary

Valid only on akm show. Every other command rejects --shape summary with an INVALID_SHAPE_VALUE usage error (exit 2) — an honest rejection rather than a silent fallback. It returns a compact view suitable for capability discovery:

Exit Codes and Error Envelope

Every command exits with one of the following codes:

Exit code Meaning Error class
0 Success —
1 Not found or command-reported failure NotFoundError, command result
2 Usage / bad input UsageError
4 Health warning (akm health only) —
70 Internal / unclassified error unexpected throw
75 Transient — retry shortly (sysexits EX_TEMPFAIL); another akm process holds a lock or is writing state.db or index.db right now, not a bad command line TransientError
78 Configuration error ConfigError

Failures classified by akm emit a JSON error envelope on stderr before exiting; stdout is normally left empty:

{"ok": false, "error": "<message>", "hint": "<optional hint>"}

The hint field is present only when actionable remediation is available (e.g. a suggested flag or alternate command). Agents should check ok === false on the parsed stderr envelope or a non-zero exit code to detect failure. Scripts can rely on the exit code alone.

Every success envelope produced by the passthrough stamp — config, clone, models, task-*, workflow-*, registry-*, and the rest of that shared handler — also carries ok: true (0.9.12+), so a caller branching on .ok sees the same field on both sides — success and failure — instead of undefined on success. A command that already computes its own ok from a graded outcome (e.g. task run's exit-code mapping, akm lint, akm proposal extract) keeps that value, false included.

env run, secret run, and migrate preserve the spawned process's exact status and raw streams instead of replacing them with an akm failure envelope. task run maps completed, active, and disabled status to 0; blocked and failed status to 1; and configuration errors to 78. It retains a command child's exact status in result.detail.exitCode. agent maps a failed dispatch to 1 while retaining the child status in its formatted result envelope.

Commands

bundle create

Note: akm setup is the recommended entry point — it runs the same directory initialization plus guides you through AI connection configuration. akm bundle create remains available as a low-level building block.

Create the bundle directory structure and persist the working bundle path in config.

akm setup                        # Interactive setup wizard (creates bundle + configures connections)
akm setup --dir ~/custom-bundle   # Initialize at a custom location
akm setup --yes                  # Non-interactive, accepts all defaults

Creates one subdirectory per asset type under the bundle path — currently scripts/, skills/, commands/, agents/, knowledge/, workflows/, instructions/, memories/, env/, secrets/, lessons/, tasks/, sessions/, and facts/. See technical/filesystem.md for config file locations.

akm bundle create                              # Initialize the default bundle (~/akm) and set it as default
akm bundle create --dir ~/scratch-bundle        # Scaffold a secondary bundle WITHOUT changing your default
akm bundle create --dir ~/scratch-bundle --set-default  # Scaffold AND make it the default bundle

--dir <path> scaffolds (and backfills) the target directory. By design it does not change your configured default bundle unless you ask: bundle create updates the primary bundles entry and defaultBundle in config.json only when (a) no --dir is given, (b) no default is configured yet (first-time bootstrap), or (c) you pass --set-default. When a --dir is given and a default already exists without --set-default, your default bundle pointer is left untouched and bundle create prints a note telling you so. This prevents akm bundle create --dir /tmp/throwaway from silently hijacking your real default bundle.

setup

Run the interactive first-run wizard.

akm setup

The setup wizard configures AKM in two steps:

Step 1 — Small model connection (for background processing) Configures the OpenAI-compatible endpoint and model used for akm improve and akm remember --enrich. Supports Ollama, OpenAI, LM Studio, or any custom endpoint. Skipping disables enrichment features.

Step 2 — Agent connection (for agentic commands) Configures how akm improve, akm proposal new, and akm task run dispatch AI sessions. Options:

A feature capability summary is shown at the end of setup.

The wizard also lets you choose a bundle directory, review registries, and add bundle sources. When you save, akm writes the config file, initializes the bundle directory, and builds the search index.

index

Build or refresh the search index.

akm index            # Incremental (only changed directories)
akm index --full     # Re-drain every directory (keeps unchanged embeddings — see below)
akm index --verbose  # Print phase progress to stderr
akm index --clean    # Normal index + remove stale entries from the DB
akm index --clean --dry-run # Report stale entries without deleting
akm index --reembed  # Discard stored vectors and re-embed every entry
akm index --skip-if-locked  # for scheduled/opportunistic runs: skip (exit 0) if a run is already in progress

Returns stats: totalEntries, generatedMetadata, directoriesScanned, directoriesSkipped, verification, optional warnings, and timing breakdown in milliseconds. Use --verbose to print the indexing mode, semantic-search settings, and phase-by-phase progress to stderr while the index is being built. Malformed workflow assets are skipped with file-path warnings instead of aborting the full run.

Progress in non-verbose JSON mode (default output format, #954): even without --verbose, phase-start messages and the embedding heartbeat (Still generating embeddings: X/N stored, F failed; waiting on embedding provider.) are now written to stderr, and a failed embedding batch logs at the default level instead of --verbose-only — a long-running index build against a slow or unresponsive provider is no longer silent until the whole run finishes. Text-mode output keeps its spinner instead (no stderr line growth); JSON stdout output is unaffected either way. The high-frequency per-batch Embedded N/M entries. line stays out of non-verbose stderr (it fires after every committed batch) — pass --verbose for that level of detail.

--clean flag: After indexing completes, verifies every indexed entry's source file still exists on disk. Removes any entries whose file is missing (for local bundle sources only; remote entries are skipped). Returns a clean block in the JSON result with checked, removed, removedRefs arrays, and dryRun flag. Use --clean to resolve the edge case where a deleted file in an unchanged directory lingers in the index across incremental runs. With --dry-run, reports which entries would be removed without modifying the database.

--full does not re-embed unchanged content: a full run re-drains and re-persists every directory, but entry ids are kept, so a vector stays attached to its entry and only entries whose search text changed go back to the embedding provider. An incremental run re-persists only the files that changed.

Embedding model changes: every stored vector records the embedding model it was generated under (embedding.model plus dimension for a remote endpoint, the local model name otherwise). When the configured model changes, the next akm index re-embeds entry by entry, committing each batch; nothing is purged first, an interrupted run resumes where it stopped, and search serves only vectors from the configured model in the meantime.

--reembed flag: Discards every stored vector and re-embeds all entries under the configured model.

--skip-if-locked flag: Every explicit akm index run acquires an opt-in, PID-liveness-only rebuild lock and releases it on exit — this is advisory, never the blocking lock #872 removed (see Locks). A human-typed akm index with no flag is never gated by it: if another run already holds the lock, it warns and proceeds anyway, contending with the existing run. If that contention makes index.db genuinely busy (SQLite database is locked) long enough to exhaust the driver's retry window, the run now fails with exit 75 (TransientError, code INDEX_DB_CONTENDED) instead of the raw driver error at exit 70 — the same retry-shortly contract as STATE_DB_CONTENDED, so a scheduler can branch on it instead of alerting. The rebuild lock itself is registered through a brief internal barrier (getMaintenanceBarrierPath()) shared with every other akm lock/lease; two akm index runs launched close enough together to collide on that registration step retry briefly and then, if it is still busy, also exit 75 (code MAINTENANCE_BARRIER_BUSY) rather than the config-error exit 78 a 2026-09-10 field report found — a busy registration barrier is ordinary contention between two legitimate runs, never a broken config file. --skip-if-locked changes that only for the invocation that passes it: if the lock is already held by a live process, it skips gracefully (exit 0, { ok: true, skipped: { reason: "lock-held", pid, launcherPid, startedAt } } — launcherPid is the holder's launcher pid when known, null otherwise, #956) instead of contending. akm index and akm curate are both safe to call frequently — curate never blocks on a rebuild in progress (read-path indexing stays non-blocking) — but a hook, cron job, or scheduled task that invokes akm index directly should pass --skip-if-locked so it steps aside instead of piling up behind a longer rebuild (the shipped index-refresh task does this).

akm index always rebuilds the search index and keeps metadata in the index, generated deterministically. In text mode, the default CLI UI shows a spinner with processed-versus-total source counts; structured output modes (json, yaml, jsonl) stay clean and machine-readable.

info

Show system capabilities, configuration, and index state.

akm info

Returns a JSON object with:

Field Description
version Current akm version
bundleDir Primary bundle directory — same resolution akm bundle list uses. Falls back to the platform-default location when no bundle resolves.
defaultBundle Name of the primary bundle from config, or null when none is configured
configError Present only when config.json exists but could not be loaded (parse or schema failure); every config-derived field falls back to the same defaults a fresh install reports
bundleDirError Present when a bundle IS configured (an env override or bundles.* in config) but its path doesn't resolve, OR when the platform-default fallback itself can't resolve (e.g. HOME unset) — absent for the ordinary "no bundle created yet" state, where bundleDir needs no explanation
dataDir Resolved data directory (getDataDir())
configDir Resolved config directory (getConfigDir())
cacheDir Resolved cache directory (getCacheDir())
stateDir Resolved state directory (getStateDir())
assetTypes List of recognized asset types
searchModes Active search modes (fts, optionally semantic and hybrid)
semanticSearch Semantic search status: mode, status, and optional reason/message
registries Configured registries
sourceProviders Configured sources (filesystem, git, website, npm)
indexStats Index stats: entryCount, byType (per-asset-type breakdown), links (declared links per kind with total and unresolved; absent when the index holds none), lastBuiltAt, hasEmbeddings, unreadable (index.db exists but the filesystem refuses to read it — a permissions problem), unavailable (index.db is readable but the SQLite-level read didn't complete: locked by another akm process, a newer layout this akm can't understand, or a corrupt file)

akm info never refuses: an unreadable config, an unresolvable bundle directory, or an index.db that's locked, newer, older, corrupt, missing, or empty each degrade the relevant field(s) above instead of failing the command. It also never waits more than about 1.5s on a locked index.db, regardless of the shared 30s lock-wait every write command otherwise uses, and warns rather than refusing on an unrecognized flag.

semanticSearch.status values:

Use akm info to verify that semantic search is working after setup.

Scripts that need akm's resolved paths (for example a health check that differs between a host install and a container) can read them with akm info --format json | jq -r .dataDir instead of hardcoding a path.

health

Check akm runtime health, durable state, and recent improve-loop telemetry.

akm health
akm health --since 24h
akm health --since 7d --format text
akm health --since 2026-05-01T00:00:00Z
akm health --report --format html         # full report: per-run rows, trends, proposal queue
akm health --report --format json         # the same dataset as data
akm health --report --window-compare 7d --format html
Flag Description
--since Rolling window start for task-history, improve, and advisory metrics. Accepts ISO 8601, YYYY-MM-DD, epoch milliseconds, or shorthand like 24h / 7d. Default: last 24 hours.
--report Fetch the full report dataset: per-run rows, trend deltas vs the prior window (default: the --since window, so deltas are like-for-like), and the pending proposal queue. A data flag — the same dataset comes back in every --format; md/html render it as the rich report.
--window-compare Compare the current window against the prior window of the same duration (e.g. 24h, 7d). With --report, overrides the default trend window.
--group-by Group rows by run (one row per improve_runs entry). Omit for the default summary.
--windows Explicit comparison window(s) as name=...,since=ISO,until=ISO (repeatable, up to 4). Mutually exclusive with --window-compare.
--no-probe Skip the default-llm-engine / configured-engines reachability probes, the cli-version update check, and the scheduler-binary version check (for an offline or air-gapped host).

The command reads state.db, verifies that the required tables exist, performs a write-read probe against the events stream, inspects task_history, checks the default agent engine, and summarizes recent improve_* events. Unless --no-probe is given, it also sends a bounded (3s timeout) reachability probe to the default-llm-engine and every configured-engines LLM connection (and an SDK engine's LLM fallback), one probe per distinct endpoint, checks the installed akm-cli version against the latest GitHub release (cli-version), runs the scheduler's recorded akm binary with --version to check it against the running CLI (scheduler-binary).

Primary result fields:

Field Description
status Overall health verdict: pass, warn, or fail
hardChecks Deterministic checks such as state-db-schema, state-db-round-trip, state-db-integrity, state-db-migrations, active-runs, default-engine, model-map-files, default-llm-engine, configured-engines, and active-improve-strategy
advisories Non-fatal warnings including semantic-search-runtime, session-extraction (akmExtract pipeline health), cli-version (installed vs latest release), thinking-control (an enableThinking: false engine whose recorded usage still shows reasoning tokens), and engine-last-used (an engine bound to an enabled improve process with no recorded use in 30 days)
metrics Aggregate task/runtime metrics: taskFailRate, agentFailureRate, stuckActiveRuns
improve Recent improve-loop counts derived from improve_invoked, improve_skipped, and improve_completed events

The improve section includes counts for planned refs, reflect/distill actions, memory-prune actions, memory-inference writes, session-extraction outcomes (sessionsScanned, sessionsExtracted, proposalsCreated), dead-URL detections, and skip reasons observed in the selected time window.

state-db-migrations reports what akm health's own open of state.db applied. Every open applies pending migrations (copying the file to state.db.pre-<id>.bak first when one drops schema), so the check passes and names the applied IDs (evidence.applied) and the copy (evidence.backupPath). It fails only when a pending migration could not be applied — naming it and pointing at akm migrate apply — rather than the command crashing. Read this check's status instead of grepping akm's error text.

default-llm-engine and configured-engines probe reachability (not just configuration) for a kind: "llm" engine — an unreachable endpoint is a hard fail for default-llm-engine and a warn for any other engine. --no-probe skips this. When a required credential is missing from the shell but an env/ asset defines the same variable name, the warn names that asset's ref (never the variable name) and points at akm env run <ref> -- .... active-improve-strategy names the resolved engine per process in its evidence and message, so a strategy-level engine pin that shadows defaults.llmEngine is visible without config archaeology.

cli-version compares the installed akm-cli version against the latest GitHub release — the same source akm upgrade trusts — and warns with the upgrade command when a newer release exists. --no-probe, offline, or a rate-limited request all degrade it to unknown, never a false warn.

engine-last-used checks, for every engine bound to an enabled process in the active improve strategy, whether it has a recorded llm_usage call in the last 30 days (independent of --since). It warns naming the idle engine and its bound process, and stays unknown — not a noisy warn — until at least one improve run has been recorded (started) in that window, so a fresh install is quiet.

The session-extraction advisory is derived from the extract_sessions_seen ledger for the last 7 days — not improve_runs, which the hook-driven akm proposal extract --session-id ... invocation never writes. It reports unknown when nothing was recorded in the window (cannot tell "off on purpose" from "broken"), warn when every session in the window was skipped for an infrastructure reason (llm_unavailable, read_failed, exception, locked_concurrent) — naming the reason and, when recorded, the engine — and pass otherwise, with per-outcome counts.

metrics

Experimental (see STABILITY.md): the report, its JSON shape and the dashboard may change in any release.

Report what akm has recorded locally: asset usage (search, show, curate), feedback, derived utility, LLM tokens, task runs, proposal flow, workflow token spend, and index runs. Read-only, and every number comes from state.db or index.db; nothing is collected that was not already recorded.

akm metrics
akm metrics --since 7d
akm metrics --since 2026-05-01 --format yaml
akm metrics --format html --output metrics.html
Flag Description
--since Window start. Accepts ISO 8601, YYYY-MM-DD, epoch milliseconds, or shorthand like 24h / 7d. Default: 30d.

The window always ends now. Only user-source usage rows are counted (matching utility and retrieval counts), and every ranked list holds its top 20. For a narrower view, open the HTML dashboard and filter by date, bundle, source and event type in the browser.

Result sections (schemaVersion: 1):

Field Description
window, filters The resolved window and the usage source counted
usage Search, show, curate and select totals, selectRate (selects over searches that returned a hit), searchMedianMs, a daily series, top assets, top and zero-result queries, and rows by source
feedback Positive and negative totals, per-asset valence, per-tag counts, and the most recent negatives with their reasons
utility A ten-bucket histogram of utility_scores, the lowest and highest assets, and how many indexed entries were never used
outcomes The assets with the lowest outcome_score
llm akm health's LLM usage aggregate (by stage, process and engine)
index, tasks, proposals, workflows Index runs and median time, task runs and fail rate, proposals by status and accept rate by source, workflow runs and tokens by model
rows The raw usage and LLM rows of the window; see below
notes Anything that limits what the numbers mean

A rate whose denominator is 0 is null, never NaN. A missing state.db gives an empty report and a missing index.db an empty utility section, each with a notes entry, and the exit code stays 0.

rows is present with --format html (the dashboard re-aggregates from it in the browser) and with --detail full in any other format; it is absent otherwise.

Notes you may see:

Search bundle assets, registries, or both.

akm search "deploy"
akm search "deploy" --type script --limit 10
akm search "lint" --from registry
akm search "docker" --from all --detail full

# Multi-tenant scope filtering:
akm search "deploy" --filter user=alice
akm search "deploy" --filter user=alice --filter agent=claude

# Include proposal-queue entries:
akm search "deploy" --include-proposed

# ConceptId-prefix enumeration — list a subtree instead of keyword-matching:
akm search "memories/projectA/"
akm search "knowledge/"
akm search "team-catalog//"
akm search "team-catalog//skills/"

A query ending in / is a conceptId prefix, not a keyword search. It enumerates the entries whose conceptId starts with that prefix: akm search "memories/projectA/" lists exactly the projectA/ subtree of memories (recursive, /-boundary exact — a sibling projectAlpha/ scope does not leak), and akm search "sessions/" lists every session (a prefix is explicit intent, so the default session exclusion — an untyped-path policy — does not apply). A <bundle>// prefix scopes enumeration to one bundle, optionally narrowed further (team-catalog//skills/); <bundle>// alone lists the whole bundle, which is what replaced akm bundle items.

Because the prefix matches the conceptId — the same spelling every emitted ref carries — a ref copied out of search output can be truncated to a prefix and pasted straight back in. Hits carry the fixed browse score 1 in deterministic listing order, matching the empty-query enumeration contract, and compose with --limit, --belief, --filter, and named --from narrowing. A full ref without the trailing slash (memories/projectA/auth-tip) stays an ordinary keyword search — use akm show to resolve a single ref. An explicit --type flag wins over the prefix.

The pre-0.9.0 <type>: / <type>:<prefix>/ spelling was removed. A query in that shape is now an ordinary keyword search, and when it returns nothing the tip names the conceptId spelling that replaces it.

Local search responses include searchMode: semantic when vector ranking ran, keyword when keyword-only search was intentional or semantic search was not ready, and fts-fallback when a ready semantic backend failed during this query. The last case also adds one sanitized, endpoint-naming entry to warnings; it never repeats the provider/runtime error text. Both fields are preserved by --shape agent so machine consumers can lower their confidence instead of treating keyword fallback as healthy semantic ranking.

Ranking fuses two candidate lists by reciprocal rank (k = 60, equal weights): BM25 over whole documents matching any non-stopword query word, and the document vectors nearest to the query embedding, 100 candidates each. A hit's score is its fused score, and equal scores are ordered by ref. The query is embedded with the model's query template (see embedding.queryTemplate in configuration.md); when the embedding takes longer than embedding.queryTimeoutMs (default 3000) or fails, the search is served by keyword ranking alone with fts-fallback and a warning. Filters (--type, --from, --filter, --belief, the default session exclusion, proposed quality) and one-hit-per-file deduplication narrow the fused list without reordering it. Of entries with identical indexed content (the same body saved under another name or in another bundle), only the highest-ranked is kept.

Flag Values Default Description
--type skill, command, agent, knowledge, instruction, workflow, script, memory, env, secret, lesson, task, session, fact, any any Filter by asset type. Free-form and unvalidated — an unknown type returns no hits. Also accepts any adapter-defined type (e.g. website) — see Bundle Types for the open types each adapter emits.
--limit number 20 Maximum results
--from local, registry, all local Where to search
--assets flag false Include asset-level registry results (only meaningful with --from registry|all; folds in the retired akm registry search --assets)
--filter <key>=<value> (none) Scope filter — repeatable. Valid keys: user, agent, run, channel. Example: --filter user=alice --filter channel=ops. Narrows the result set; ranking is unchanged.
--include-proposed flag false Include entries with quality: "proposed" in the result set. Default search excludes them; generated and curated quality entries are always included. Unknown quality values warn once and remain searchable.
--belief all, current, historical all Memory belief filter. current keeps active memory beliefs; historical keeps contradicted/superseded/archived ones.
--include-sessions flag false Include session assets, which are excluded from default results via config.search.defaultExcludeTypes
--format json, jsonl, yaml, text, md, html json Output format
--detail brief, normal, full brief Output verbosity level
--shape human, agent, summary human Output projection. --shape summary is valid only on akm show; passing it here is an INVALID_SHAPE_VALUE usage error (exit 2), like on every other command.

--filter flags AND-join: every supplied key must match the entry's scope for the entry to appear in the result set. Entries without any scope are excluded as soon as a filter is supplied. With no --filter (the default), unfiltered queries continue to surface all entries — including legacy memories that pre-date the scope contract.

Local refs come from the index's canonical fully qualified item_ref; output keeps the short form for the default bundle and qualifies non-default bundles. Local paths are absolute materialized file_path values. Key fields by availability:

The default brief shape is intentionally small. The exact field set per detail level (and per --shape) is authoritative in src/output/shapes/helpers.ts (shapeSearchHit / shapeSearchHitForAgent), assembled into the shape registry by the src/output/shapes.ts barrel:

Level Local bundle hits Registry hits
brief (default) type, name, ref, action, estimatedTokens name, installRef, score
normal type, name, description, action, score, estimatedTokens, optional warnings/quality/keys name, description, action, installRef, score, optional warnings
full full hit object (includes ref, origin, tags, whyMatched, optional warnings, optional quality, timings, bundle metadata) full hit object
--shape agent name, ref, type, path, editable, conditional editHint, description, action, score, optional estimatedTokens/keys no local access fields

--shape summary is not valid on search — see --shape summary above; it is a usage error (exit 2) everywhere except akm show.

There is no registry curated boolean. Renderers surface an optional warnings: string[] field on hits when a provider has non-fatal issues to report; the field is omitted otherwise. Populating warnings does not affect ranking.

Score ranges differ between local and registry hits. Local SearchHit.score is a fixed contract value in [0, 1], higher = better. Registry RegistrySearchHit.score is registry-native: provider-defined and may exceed 1 (the bundled static-index provider can emit values up to ~1.85 from scoreStash()). Use registry scores only for ranking within a single registry — do not compare them numerically against local SearchHit.score values or across registries with different scoring formulas. See docs/architecture/architecture.md for the current type-level distinction.

curate

Pick the assets worth loading for a task. Unlike akm search, curate attaches a preview and run details per hit, adds related support refs, and summarizes the set — the usual starting point for an agent.

akm curate "plan a release"
akm curate "deploy a Bun app" --limit 3
akm curate "review an architecture proposal" --type skill
akm curate "learn the release workflow" --from all --format text
Flag Values Default Description
--type skill, command, agent, knowledge, instruction, workflow, script, memory, env, secret, lesson, task, session, fact, any any Filter curated results by asset type
--limit number 4 Maximum curated results
--from local, registry, all local Where to search before curating

akm curate takes the top --limit hits of one search, in search order, and enriches each with a preview, run details and up to two support refs: the assets the hit's declared links name (xrefs:, supersededBy: and the other kinds akm show lists under links), what it links to before what links to it, skipping assets curate already selected. With search.curateRerank.enabled, a cross-encoder first reorders the top 30 fused candidates (search.curateRerank.topN) by name, description and the start of each asset's indexed content. Curate includes direct follow-up commands such as akm show <ref> or akm bundle add <ref> so you can immediately inspect or install what it found. --detail and --shape agent both work on curate output; --shape summary does not. Curate preserves the underlying search's searchMode and warnings. Agent-shaped local items include ref, path, and editable, plus editHint only for read-only items. Their followUp remains akm show <ref> rather than being replaced by clone guidance. Use --type workflow when you want curated step-by-step procedures instead of individual scripts, skills, or docs. Curate returns no items, on purpose, when the input is not a task: a harness or tool envelope (input that starts with an XML-style tag and contains a closing tag, such as <task-notification>…</task-notification>) or the stash README boilerplate. The summary then starts with Curate abstained and names the reason, and tip says how to curate the task instead. Long input is curated like any other. akm curate is safe to call frequently, including from a hook that fires on every prompt: it only ever reads the index as it currently stands (the same non-blocking ensureIndex() path search uses) and never waits on or contends with a full akm index rebuild in progress.

show

Display an asset by ref. On a markdown document #fragment selects one section by heading slug (falling back to case-insensitive heading text); an unmatched fragment lists the available slugs.

akm show scripts/deploy.sh
akm show skills/code-review
akm show agents/architect
akm show commands/release
akm show workflows/ship-release
akm show knowledge/guide                 # the whole document
akm show knowledge/guide#authentication  # just that section
akm show '<search-result-ref>' --context lead --max-tokens 800
akm show knowledge/guide#nope            # lists the available fragment slugs

# Bundle .meta/ orientation docs — direct-read, not indexed:
akm show meta                       # working bundle's .meta/index.md
akm show meta:about                 # working bundle's .meta/about.md
akm show akm//meta                  # the primary bundle explicitly
akm show github:owner/repo//meta    # an installed bundle's .meta/index.md

# Multi-tenant scope filtering:
akm show memories/retro --filter user=alice
akm show memories/retro --filter user=alice --filter agent=claude
Flag Values Default Description
--context exact, lead exact Fragment presentation. exact preserves the selected-section behavior. lead returns the indexed-safe first fragment followed by [Selected matching fragment] and the selected fragment.
--max-chars positive integer 3200 for lead Hard contextual content budget in characters; requires --context lead and is mutually exclusive with --max-tokens.
--max-tokens positive integer (none) Approximate contextual budget using four characters per token; requires --context lead and is mutually exclusive with --max-chars.
--filter <key>=<value> (none) Repeatable scope filter (user, agent, run, channel).

meta is not an asset type — [<origin>//]meta[:<name>] direct-reads a human-authored orientation doc from a bundle's optional .meta/ directory (<name> defaults to index; .meta/<name>.md is tried before an extensionless .meta/<name>). These files are never indexed, so they do not appear in akm search. See concepts.md for the full convention.

--filter accepts the same <key>=<value> shape as akm search --filter — one spelling for the scope-narrowing axis on both commands (--scope was removed in 0.9.0) (repeatable; valid keys: user, agent, run, channel). When supplied, the resolved asset's frontmatter scope_* keys must match every supplied filter. A mismatch (or absent scope) returns NotFoundError so the caller cannot accidentally read out-of-scope content.

The default show JSON includes the asset body when applicable. Canonical ref is always present, in every --shape (human, agent, and summary alike) and at every --detail level. Absolute path and editable are always present too, at every --detail level, in the human (default) and agent shapes — --shape summary omits both, since it is a compact capability-discovery view, not an edit-target view. None of ref/path/ editable are gated behind --detail full. Use --detail brief for a reduced metadata-first view without content/template/prompt; --detail full adds verbose extras such as schemaVersion and, when editable is false, editHint; --shape agent strips non-action metadata (e.g. origin, tags) down to the action-relevant field set while still including ref/path/editable; --shape summary returns a compact view with type, name, ref, description, tags, parameters, workflowTitle, action, run, origin, and keys, plus the optional fragment metadata described below.

links lists the asset's declared links, grouped by kind: outgoing (the assets its own xrefs:, supersededBy:, contradictedBy:, currentBeliefRefs:, wiki sources:, .derived parent, page links, or workflow and task targets name), incoming (the assets that name it), and unresolved (tokens it names that match no indexed asset, as written). Each kind is { "total": n, "refs": [...] } with at most 10 refs; total counts them all. The field is omitted when nothing links either way. Links are read from frontmatter and parsed structure at index time, with no model; they do not affect search ranking.

Opaque fragment shows and --context lead keep ref as the canonical parent identity and add selectedRef, parentRef, one-based fragmentOrdinal, fragmentCount, startLine, endLine, optional previousRef/nextRef, and separate fragment/parent character and token estimates. Contextual shows also report contextMode, contextMaxChars, and contextTruncated. Heading aliases are canonicalized in contextual selectedRef to the resolved opaque indexed selector. Default exact shows through a friendly #heading retain their source-live body and do not attach indexed-safe provenance that could describe a different revision or projection.

--context lead is opt-in and accepts only fragment-qualified indexed Markdown assets. Both the lead and selected content come from the same line-preserving, safe indexed revision used by fragment search, even when the source file changes between search and show. The selected block is labelled and kept last. When the budget is tight, AKM clips or omits lead content before clipping selected evidence; contextTruncated reports either case.

Returns type-specific payloads:

Type Key fields
script run, setup, cwd
skill content (full SKILL.md)
command template, description
agent prompt, description, modelHint
knowledge content — the whole document, or one section via #fragment
workflow workflowTitle, workflowParameters, steps (each step's orchestration summary names its engine/model, or — for an exec step — its exec.command and no engine at all)
memory content
env keys (key names only — values and comment text never returned)
lesson content plus when_to_use surfaced from frontmatter

editable means current AKM source policy authorizes direct in-place modification of that exact path. It is computed from current source ownership and effective writable policy, not persisted in the index; unknown paths fail closed. editHint is present only when editable is false. akm show uses the local index and materialized disk path, with no remote-provider fallback. If the ref points to a package origin that is not installed, it returns guidance to run akm bundle add <origin> first.

workflow

Author, inspect, and execute structured workflow assets.

akm workflow create ship-release --print
akm workflow create ship-release
akm workflow create ship-release --from ./ship-release.md
akm workflow run workflows/ship-release --version 1.2.3
akm workflow run <run-id>                  # continue an active partial run
akm workflow run workflows/ship-release --new  # start a fresh run even if one is already active
akm workflow status <run-id>
akm workflow status workflows/ship-release
akm workflow status 7c115132               # 8+ char run-id prefix also works
akm workflow resume <run-id>
akm workflow abandon <run-id>
akm workflow list --active
akm workflow list --children               # also list child workflow runs
akm workflow list --all-scopes             # include runs started from a different working directory
akm workflow plan workflows/ship-release    # compile+freeze preview, zero writes

Bare akm workflow (no subcommand) is a usage error (exit 2), the canonical bare-group behavior — name a subcommand.

Subcommands:

Subcommand Description
create <name> Validate and write a Markdown workflow under workflows/. --path <dir> places it in a subdirectory; --from <file> imports content; --force (requires --from or --reset) overwrites; --print prints the template that would be written instead of writing it
run <run-id|ref> Stable canonical start/resume/execute command. A ref starts a run or resumes the active run in the current scope (announced as resumed: true, see below); a run id continues that exact active run. --new starts a fresh run even when one is already active. Executes until completion, failure, verification rejection, interruption, or an explicit limit
status <run-id|ref> Show the full run state, including all step statuses. --units also lists per-unit rows from the run journal (diagnostics only). Renders a children: tree when the run composes child workflows. --all-scopes widens the ref-fallthrough lookup (only reached when the target does not resolve to a run id) to every scope instead of just the current one
list List workflow runs (optionally filtered by --ref; --active shows only status=active runs, excluding blocked/failed/completed). Child workflow runs are excluded unless --children is passed. --all-scopes searches every scope instead of only the current one (#942)
resume <run-id> Flip a blocked or failed run back to active. Completed runs cannot be resumed
abandon <run-id> Mark a run failed so it stops counting as active (resume can reopen it)
plan <ref> Evolving. Compile and freeze a workflow WITHOUT publishing a run: the canonical step graph, per-step frozen target kinds, task/child expansion, input bindings, source read set, and lowering notices — zero durable writes. Returns the full JSON envelope by default, like every other command; pass --format text for a human-readable summary

Everywhere a run id is accepted (run, status, resume, abandon), a unique run-id prefix of 8 or more characters works too — the same convention akm proposal accept/reject use for proposal UUIDs. A prefix matching more than one run is a usage error listing every candidate; a prefix matching none is a not-found error. Only strings shaped like a run id (hex digits and hyphens, 8+ characters) are ever treated as a prefix, so a workflow ref is never mistaken for one.

The public workflow start, next, and complete lifecycle was removed in 0.9, along with the experimental brief/report external-driver protocol. Use workflow run for execution and workflow status for inspection. The removed commands fail with an UNKNOWN_COMMAND envelope and a migration hint; there are no compatibility aliases.

There is also no akm workflow template, validate, or watch. workflow create --print prints a starter, akm lint --type workflows validates it, and akm log --run <id> --since '@offset:<id>' provides durable event polling.

workflow run

akm workflow run workflows/ship-release --version 1.2.3
akm workflow run workflows/review --files a.ts --files b.ts
akm workflow run <run-id> --max-steps 3
akm workflow run <run-id> --max-retries 2 --timeout 10m
akm workflow run <run-id> --skip-if-locked   # for scheduled runs: skip (exit 0) instead of failing on contention

Parameter flags must follow the target and exactly match keys declared in the workflow's params frontmatter. AKM coerces each value from the declared JSON Schema before persisting the run:

A bare boolean flag means true. Hyphen/underscore aliases are not inferred: declared include_processes requires --include_processes, not --include-processes. Parameters can be supplied only when a new run is created; a later invocation against an active run rejects parameter flags. The old --params <json> bag is removed.

Flag Description
--max-steps <n> Stop once this many steps have finished, leaving a partial run active. Must be at least 1.
--max-retries <n> When a step fails, reopen the same run and retry the failed step up to this many additional times. Range: 0 through 100; default 0. Gate rejection and interruption are not retried.
--timeout <duration> Abort the whole invocation after N, Nms, Ns, or Nm; bare N is milliseconds. The active step remains resumable.
--new Start a fresh run even when one is already active for this ref, instead of resuming it. The existing active run is left untouched — it is never abandoned automatically. A workflow ref only: passing a run id with --new is a usage error (exit 2). Parameter flags are allowed together with --new, since it is starting a new run.
--skip-if-locked If another akm process is already driving this run (it holds the run's lock file: RUN_LEASE_HELD), or state.db is busy with another writer (STATE_DB_CONTENDED), skip gracefully (exit 0) instead of failing (exit 75, TransientError). The envelope reports { skipped: { reason: "lock-held" | "state-db-contended", message } }. Every other failure (a bad flag, an unresolvable target) still fails loudly regardless of this flag. Use for high-frequency scheduled runs so they don't pile up failures while a longer-running invocation is in progress — same family as improve --skip-if-locked.

Resuming an active run is announced, not silent. Passing a ref that already has an active run in the current scope resumes that run rather than starting a new one (unchanged since #485) — but the envelope now carries resumed: true alongside the resumed run's run.id, and the default text output leads with resuming existing run <id> for <ref>; pass --new to start a fresh run. Pass --new to start a second, independent run instead.

The result includes the current run, an executed step report list, a stepsProcessed count of the steps that finished, and optional done, gateRejection, aborted, or timedOut markers. A failed run, rejected verification gate, timeout, or interrupt exits nonzero. SIGINT and SIGTERM map to 130 and 143; a timeout maps to exit 1. Reaching --max-steps with an active resumable run is successful. A --timeout that lands during the run's final bookkeeping is not reported as a timeout: a run that reached completed has nothing left to abort and nothing left to resume.

What --max-steps counts. The budget is spent by the steps that finish — completed, failed, or gate-rejected with the loop budget spent — not by entries in the executed report, which gains one per gate-loop iteration and one per route-skip. So a step's whole bounded gate.max_loops loop costs one, a route-skipped step costs nothing (no work was dispatched for it), and a step the invocation left unfinished — an abort, a verification-judge outage — costs nothing either, because the next invocation still owes that work. --max-retries subtracts the same way: a reopened run's remaining budget is not shrunk by loops or skips.

The budget is checked between steps, so it does not bound what any single step dispatches — a step's gate loop runs to its own limit no matter how little budget is left. gate.max_loops (1–100) is the per-step ceiling; budget.max_units and budget.max_tokens are the whole-run ceilings, seeded from the unit journal so they hold across resumes.

run is Stable and does not consult experimental.workflowEngine. Every non-empty ### gate requires workflow.judgeEngine to name a configured LLM or agent engine before a new run can be frozen. Gate evaluation is fail-closed.

Workflow runs are scoped to the current working context, not globally across all repos or directories. akm resolves that context from the nearest .akm/config.json ancestor when present, otherwise the nearest git root, otherwise the bundle root when the cwd is inside it, otherwise the cwd itself. In practice this means:

workflow create

akm workflow create ship-release
akm workflow create ship-release --from ./ship-release.md
akm workflow create ship-release --from ./ship-release.md --force
akm workflow create ship-release --force --reset
akm workflow create ship --path release          # writes workflows/release/ship.md
Flag Description
--path <dir> Relative subdirectory under workflows/ to place the workflow in. The filename comes from <name>.
--from <file> Import and validate a Markdown workflow from an existing file
--force Overwrite an existing workflow. Requires --from or --reset.
--reset Explicitly replace an existing workflow with a fresh template (use with --force)
--print Print the Markdown template without creating anything

--force requires either --from <file> (replace from a source file) or --reset (explicitly acknowledge you are overwriting in place). Without one of these, --force is rejected to prevent silent template overwrites.

<name> itself must be flat — ^[a-z0-9][a-z0-9._/-]*$ after combining with --path, but the bare --name positional is rejected if it contains a /. Hierarchical placement (release/ship) goes through --path release --name ship, the same convention every other create command (knowledge, env, secret, …) uses — akm workflow create release/ship directly is a usage error (exit 2).

Snapshot isolation: workflow run compiles and freezes the workflow plan when it creates a run. Edits to the source workflow after that point do not affect the in-flight run.

workflow status

akm workflow status <run-id>
akm workflow status workflows/ship-release
akm workflow status <run-id> --units    # also list per-unit rows from the run journal
akm workflow status workflows/ship-release --all-scopes  # resolve across every scope, not just the current one

Accepts a run id, a unique 8+ character run-id prefix, or a workflow ref. When given a workflow ref, resolves to the most-recently-updated run for that ref in the current working scope, unless --all-scopes is passed (only relevant when the target does not resolve to a run id; a run id is always scope-agnostic, --all-scopes or not, #942). --units adds per-unit rows (unit id, status, failure reason, and any result/error diagnostic text) from the run journal — diagnostics only; step evidence stays deterministic and is unaffected.

workflow plan

akm workflow plan workflows/release
akm workflow plan workflows/release --format json

Evolving (see STABILITY.md). Compiles, resolves, and freezes the named workflow exactly as akm workflow run would when starting a new run — the same two calls, loadWorkflowAsset + compileResolveFreezeWorkflowV4 — and stops. It publishes no run row, takes no lease, appends no event, and writes to no other table: zero durable writes, verified by row count before and after, not merely assumed. Use it to preview what a run would freeze — the canonical step graph, which steps expand through a task or a composed child workflow, and any freeze-time lowering notices or compile warnings — before committing to a run, or to inspect a workflow's shape without side effects.

Two output modes:

Text-mode example:

workflow: team//workflows/release (markdown)
source:   workflows/release.md
plan:     irVersion 5, hash 4f2ba91c3d0e… (not published)
limits:   maxConcurrency 4; budget max_units 50, max_tokens 100000
params:   channel, version
outputs:  report <- steps.summarize.output
steps:
  1. notify   [command]        direct
  2. build    [script]         via tasks/plan-v4-task
  3. dispatch [child-workflow] -> workflows/release-checklist (plan 91acbe20f5d1…)
       with: channel="stable" (literal), files <- steps.build.output.files (reference)
       exports: report, changed_count
       3.1 verify [command] direct
  4. summarize [command]       direct
read set:
  workflows/release.md
  commands/notify.md
  workflows/release-checklist.md

Secret-free by construction. Neither mode ever prints a resolved reference value (references resolve at pre-attempt, not here), request content (request.command.content, request.persona, request.conversation, request.runtime.environment), a script's bytesBase64, or any credential — only binding shapes (inputBindings[].name/.kind, a literal's .value, a reference's .from), environment binding names (environment[].kind/ .name, an env-ref's .ref/.keys/.secretNames), and engine names (gate.judgeEngine).

workflow resume

akm workflow resume <run-id>

Flips a blocked or failed run back to active. Completed runs cannot be resumed. Use workflow list to find runs by status.

Workflow markdown contract:

See Workflows for the complete authoring contract.

How bundle add works

akm bundle add infers what to do from the input:

Input What happens
akm bundle add ~/.claude/skills Registers a local directory as a filesystem source
akm bundle add github:owner/repo Clones the repo into akm's cache as a git source
akm bundle add @scope/pkg Installs the npm package as an npm source
akm bundle add https://docs.example.com Crawls and caches a website as a website source
akm registry add <url> Adds a discovery registry (separate concept)

HTTP(S) URLs on known Git hosts, and URLs ending in .git, are treated as git sources. Other HTTP(S) URLs are crawled as website sources.

bundle add

Add a source — a local directory, npm package, GitHub repo, git URL, or website. akm detects the bundle's format automatically (its own native workspace, a Claude Code or OpenCode tool directory, an OKF or LLM-wiki knowledge base, …) — see Bundle Types for how detection works and what each format gives you.

akm bundle add ~/.claude/skills              # Local directory
akm bundle add @scope/pkg                    # npm package
akm bundle add npm:@scope/pkg@latest         # npm with version
akm bundle add github:owner/repo#v1.2.3     # GitHub with tag
akm bundle add https://github.com/owner/repo
akm bundle add git+https://gitlab.com/org/bundle
akm bundle add ./path/to/local/bundle
akm bundle add github:andrewyng/context-hub --name context-hub  # context-hub as a git bundle
akm bundle add https://docs.example.com --name docs
akm bundle add https://docs.example.com --max-pages 100 --max-depth 5
Flag Description
--name The bundle key. A contract, not a hint: it must be a legal bundle slug (no : . # / or whitespace) and not already taken by a different bundle, or the add fails before any write. Re-adding an already-installed source under a different --name than it already carries also fails — use akm bundle rename <old> <new> instead. Omit it and akm derives a name (falling back to a -<hash> suffix on a collision).
--provider Explicit provider for declarative source configuration; normally inferred from the input
--writable Mark a git source as writable so akm sync also pushes (default: false)
--options Provider options as JSON (e.g. '{"ref":"main"}')
--allow-insecure-transport Allow a plain-HTTP source URL after explicitly accepting transport substitution risk
--allow-dangerous-env-keys Allow reviewed process-hijacking env keys in the installed bundle; does not permit plain HTTP
--max-pages Maximum pages to crawl for website sources (default: 50)
--max-depth Maximum crawl depth for website sources (default: 3)

Dangerous env key audit

When akm bundle add installs a bundle that contains env files, it recursively scans every .env-suffixed file under env/ (the same "real env file" test used everywhere else — a bare .env or any name ending .env, at any depth) for environment variable names that can be used for process-execution hijacking. A non-.env file under env/ (e.g. env/notes.txt) is never scanned — such a file is never sourced as environment variables by any akm codepath, so a dangerous key sitting in its contents cannot hijack anything. The flagged key set is 41 literal names plus 2 regex pattern families (src/commands/lint/env-key-rules.ts): LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, LD_DEBUG, LD_BIND_NOW, LD_PROFILE, LD_ASSUME_KERNEL, LD_TRACE_LOADED_OBJECTS, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH, PATH, BASH_ENV, ENV, PROMPT_COMMAND, PS1, PS2, IFS, ZDOTDIR, NODE_OPTIONS, NODE_PATH, NODE_TLS_REJECT_UNAUTHORIZED, PYTHONSTARTUP, PYTHONPATH, PYTHONINSPECT, PYTHONHOME, PYTHONNOUSERSITE, RUBYLIB, RUBYOPT, PERL5LIB, PERL5OPT, JAVA_TOOL_OPTIONS, JDK_JAVA_OPTIONS, _JAVA_OPTIONS, GIT_SSH_COMMAND, GIT_EXTERNAL_DIFF, GIT_PAGER, GIT_EDITOR, EDITOR, VISUAL, and PAGER (41 literals), plus any key matching ^BASH_FUNC_ (Shellshock-class injection) or ^GIT_CONFIG_ (git config override injection).

When dangerous keys are found, akm bundle add pauses and prompts for confirmation (default: No). In non-interactive mode (CI, scripts) the install fails with exit 1 unless --allow-dangerous-env-keys is passed, and the freshly-installed bundle is rolled back before the process exits.

# Interactive: prompts before continuing
akm bundle add github:owner/repo-with-sensitive-env

# Non-interactive: fails unless bypassed
akm bundle add github:owner/repo-with-sensitive-env --allow-dangerous-env-keys

Bundle publishers: see the Author Bundles guide for guidance on env files that legitimately need these keys.

Website sources

An HTTP(S) URL outside known Git hosts is treated as a website source. akm crawls the site breadth-first from the given URL, converts each page to markdown, and stores the results as knowledge assets with the URL path hierarchy preserved.

akm bundle add https://www.agentic-patterns.com/ --name agent-patterns
akm bundle add https://docs.example.com/guide --name guide --max-pages 200

Pages are cached locally and refreshed every 12 hours. The crawl stays within the same origin (hostname) and skips static assets (images, CSS, JS, etc.).

Use --max-pages and --max-depth to control how many pages are fetched and how many link levels deep the crawler goes. These values are persisted in your config so subsequent re-indexes use the same limits.

See registry.md for the full install flow for managed sources.

Note: there is no akm bundle add context-hub convenience alias or akm enable/disable context-hub command — add it explicitly as a git bundle: akm bundle add github:andrewyng/context-hub --name context-hub. A bundle type string of "context-hub" in an existing config still normalizes to "git" at load time, so you don't need to edit your config files.

bundle list

Show all sources — local directories, managed packages, and remote providers.

akm bundle list                            # All sources
akm bundle list --kind filesystem          # Only plain filesystem/local directory sources
akm bundle list --kind git                 # Only git sources
akm bundle list --kind npm                 # Only npm-managed sources
akm bundle list --kind website             # Only crawled website sources
akm bundle list --kind filesystem,git      # Multiple kinds (comma-separated)
Flag Description
--kind Filter by source provider: filesystem, git, npm, website (comma-separated). Any other value is a usage error (exit 2) — there is no local/managed/remote grouping.

bundle remove

Remove a source by id, ref, path, URL, or name and reindex.

akm bundle remove npm:@scope/pkg           # Managed source by id
akm bundle remove owner/repo               # Managed source by ref
akm bundle remove ~/.claude/skills         # Local source by path
akm bundle remove my-provider              # Any source by name
akm bundle remove my-provider --yes        # Skip the confirmation prompt
Flag Description
-y, --yes Skip the confirmation prompt

bundle update

Update one bundle, or refresh every configured bundle with --all. Git, npm, and website candidates are staged and audited before they replace the active generation. Filesystem bundles require no hydration; update reconciles their current files into the index immediately.

akm bundle update npm:@scope/pkg
akm bundle update --all
akm bundle update --all --force   # Force fresh download even if version is unchanged
akm bundle update --all --yes     # Skip confirmation when an update needs to delete a moved install dir
akm bundle update npm:@scope/pkg --allow-dangerous-env-keys  # Explicitly approve reviewed dangerous env keys
Flag Description
--all Update all managed sources
--force Delete cached extraction before re-downloading
--allow-dangerous-env-keys Permit a staged update containing dangerous environment keys after warning. Without it, an interactive terminal prompts with a default of No; non-interactive use fails closed. This is independent of --yes.
-y, --yes Skip the confirmation prompt for the rare branch where the resolved content location moved and the previous install directory must be deleted. No effect on a normal refresh, which deletes nothing.

The audit examines key names in .env-suffixed files under the staged component root. Publisher lint suppressions do not bypass it. Rejection or an audit/publication/index failure preserves the prior active bytes, lock/config generation, and searchable index for that bundle.

Writable Git updates resolve configured roots to their physical checkout, reject component symlinks that escape it, and re-audit the exact materialized worktree before activation. AKM holds its source/index writer lease through that check and commit, and rechecks after the index pass at the final database commit boundary. All AKM writers cooperate with this lease. A non-cooperating local process can still edit ordinary files because POSIX/Windows filesystems provide no mandatory recursive directory lock: writes observed by either generation check make the update fail and restore the pre-update checkout, but a write racing after the final filesystem read cannot be guaranteed detectable and a write during compensation can be overwritten. Do not edit a writable checkout from another process while its update is running.

The index and its update-owned state maintenance share one deferred SQLite transaction. WAL readers continue to see the last committed generation while a full update index pass runs, and competing writers wait for that pass to commit or roll back. The semantic-status JSON file is a recomputable advisory: failure to refresh it after the database commit warns but does not undo a committed bundle update.

This boundary guarantees rollback for handled process faults (throws and SQLite commit failures); it is not an abrupt-termination or cross-database power-loss guarantee. Both index.db and state.db remain in WAL mode, where SQLite does not guarantee an atomic commit across attached database files after SIGKILL, abrupt power loss, or storage failure. The durable outcome may therefore combine approved old/new source bytes and lock state with adjacent index/state generations. akm health reports index-state-generation when a durable usage link disagrees with the searchable index, but that advisory cannot identify every theoretical split. Stop concurrent writers, rerun the targeted bundle update if the checkout/lock is not the intended approved revision, then run akm index --full to rebuild the search index and relink durable usage state.

Reports per-entry change flags: changed.version, changed.revision, and changed.any. With --all, each bundle is isolated: successful entries appear in processed/plainSynced; rejected entries report status: "blocked" and a security code; provider or transaction errors report status: "failed". The command continues with later bundles without half-publishing a blocked one.

bundle rename

Rename a configured bundle's key everywhere akm itself persists it — the one command allowed to change it (renaming by hand-editing config.json's bundles key strands every durable ref the tool minted under the old id; see akm health / the startup warning that names this).

akm bundle rename old-name new-name
akm bundle rename old-name new-name --dry-run   # Show the plan; write nothing
Flag Description
--dry-run Report what would change (index/state row counts, scheduler refs, content files that still mention the old name) without writing anything

<new> must be a legal, unused bundle slug (the same --name contract akm bundle add enforces) or the rename fails before any write. Rewritten: the config bundles key; defaultBundle/defaultWriteTarget when they name the old id; every scheduler.enabled[].ref with the old <old>// prefix; the lockfile entry id; every indexed entry's bundle_id/ref; and this tool's own state rows that name the old bundle (proposals.ref, a pending proposal's write target, and workflow task_history.target_ref). Reported, never rewritten: refs inside the bundle's own CONTENT (cross-references, uses: in a task, supersededBy) — the result's contentRefs lists the indexed files that still spell the old <old>// prefix so you can fix them by hand. A real run also re-syncs native scheduler bindings under the new name (taskSync in the result reports the outcome, never thrown, since config/index/state are already renamed by then). taskSync.ok is false both when the sync call itself fails and when it comes back reporting one or more taskSync.result.failures — a binding that failed to prepare has already lost its old native row and stays unscheduled until you re-run akm task sync; --dry-run lists the installed native rows that still name the old bundle (nativeSchedulerRows) so you can see what that sync will replace.

upgrade

Upgrade akm itself to the latest release. Standalone binaries are downloaded, checksummed, and staged before replacement; npm, Bun, and pnpm global installs use their package manager.

Upgrade replaces the installed program when a newer release exists, then runs akm-migrate apply — the migrator that shipped with whatever is now installed — and rebuilds the derived index. The migration step runs on every akm upgrade, install or no install, and its plan is reported under migration; an upgrade whose migration is blocked or could not run exits 1. That makes akm upgrade safe as a container entrypoint: on a current installation it is a no-op. An akm installed as a dependency of another package (installMethod: "package-local") is never reinstalled — the parent package owns that copy — but its migrations still run. Standalone downloads use a temporary rollback copy only during atomic executable replacement.

Standalone downloads are streamed directly to the staged file while SHA-256 is computed, with a 256 MiB binary limit. Release/checksum metadata is capped at 1 MiB; an oversized response is cancelled and the staged file is removed.

akm upgrade              # Install a newer release if there is one, then run every pending migration
akm upgrade --check      # Check for updates without installing (no migration step)
akm upgrade --force      # Force the install even if already on latest
Flag Description
--check Check for updates without installing
--force Force upgrade even if on latest version
--skip-post-upgrade Skip the post-upgrade index rebuild

Offline, or to migrate without a release check, run akm migrate apply directly: it is the same step.

Checksum verification is not optional and has no flag. If a release's checksums.txt is genuinely unreachable, the recovery hatch is the AKM_UPGRADE_SKIP_CHECKSUM=1 environment variable (Internal — deliberately not a discoverable, tab-completable flag). See STABILITY.md.

Shipping akm inside your own product (a Docker image, a plugin's own node_modules)? See Bundling akm for the full boot contract, JSON shapes, and exit codes.

akm upgrade replaces the binary in place for its own install method, but a scheduler binding recorded by an earlier akm task sync under a different install method is not repointed automatically — the scheduler runs the binary path recorded at sync time, not whichever akm upgrade just installed. Run akm task sync after switching installers so scheduled runs pick up the new binary; see task sync and akm health's scheduler-binary advisory.

clone

Copy an asset from any source into a managed writable bundle or an unmanaged custom destination for editing.

akm clone scripts/deploy.sh
akm clone "npm:@scope/pkg//scripts/deploy.sh"
akm clone scripts/deploy.sh --name my-deploy.sh
akm clone scripts/deploy.sh --force
akm clone scripts/deploy.sh --bundle team-bundle
akm clone scripts/deploy.sh --dest ./project/.claude
akm clone "npm:@scope/pkg//scripts/deploy.sh" --dest /tmp/preview
Flag Description
--name New name for the cloned asset
--force Overwrite if the asset already exists at the destination
--bundle <name> Managed destination bundle. When omitted, clone falls back to defaultWriteTarget, then the working bundle
--dest <path> Unmanaged destination directory. Bypasses managed target resolution and cannot be combined with --bundle; the type subdirectory is appended automatically

Skills (directories) are copied recursively. Other types copy a single file.

Remote clone: When the origin in the ref points to a package that is not installed locally (e.g. an npm package or local path not in your bundle sources), akm fetches it to the cache automatically and extracts the requested asset. The package is not registered as a managed source -- use akm bundle add for that.

# Clone a single script from a remote package without installing the full bundle
akm clone "npm:@scope/pkg//scripts/deploy.sh"

# Clone from a local directory that isn't configured as a search path
akm clone "/path/to/bundle//skills/code-review" --dest ./project/.claude

Without --dest, clone uses normal write-target resolution: explicit --bundle -> defaultWriteTarget -> working bundle. Managed clones use the destination bundle's canonical ref and are indexed immediately. When --dest is provided, no managed write target is required, which keeps clone usable in CI or fresh environments without running akm setup first.

sync

Stage and commit local changes in a git-backed bundle. If the bundle has a remote configured and is marked writable: true, the commit is also pushed.

Note: there is no akm save command — use akm sync.

akm sync                            # Sync primary bundle (auto timestamp message)
akm sync -m "Add deploy skill"     # Sync with custom message
akm sync --no-push                  # Commit only; never push even when writable
akm sync --format json             # Explicit format (both --format json and --format=json work)
akm sync my-skills                  # Sync a named writable git bundle
akm sync team/core -m "Update"    # Slash-containing source names are valid selectors
akm sync my-skills -m "Update"     # Sync named bundle with message
Argument / Flag Description
[name] Optional git-backed bundle selector. Matches the configured source name exactly and also accepts canonical GitHub aliases such as owner/repo, github:owner/repo, and branch-ref forms like github:owner/repo#branch. Forward slashes are allowed. Defaults to the primary bundle
-m, --message Commit message. Defaults to akm save <timestamp>
--no-push Commit only; never push even when the bundle is writable with a remote configured
--format Output format (any of the six global values). Both --format json and --format=json are accepted

If no positional selector is provided, akm sync --format json still targets the primary bundle. If a positional selector is provided, it wins even when the value also looks like a format token.

Behaviour by repo state:

State Result
Not a git repo Exit 0, skipped: true in JSON output — no error
Git repo, no remote Stage and commit only
Git repo, has remote, not writable Stage and commit only
Git repo, has remote, writable: true Stage, commit, and push
Writable with a remote, but no upstream branch, or behind or diverged from it Stage and commit; the push is skipped and reason says not pushed: ...
Writable with a remote and ahead of its upstream Stage, commit, and push, unpushed commits included
Any writable repo with --no-push Stage and commit only (push suppressed)

Primary bundle writable config:

To make the primary bundle push on sync, set writable: true on its bundles entry in your config file (~/.config/akm/config.json or the path shown by akm config path):

{
  "bundles": { "primary": { "path": "~/akm", "writable": true } },
  "defaultBundle": "primary"
}

When writable: true is set and the primary bundle has a git remote configured, akm sync will stage, commit, and push.

When akm setup successfully initializes the default bundle as a local git repo (requires git to be installed), akm sync will commit there safely without pushing. If git is unavailable, the bundle will not be a git repo and sync will return a skipped result.

To make a named remote git bundle writable, pass --writable when adding it:

akm bundle add git@github.com:org/skills.git --provider git --name my-skills --writable

remember

Record a memory. This writes a markdown file into memories/ in the configured write target and returns the resulting ref.

Write target resolution: the destination is the working bundle (defaultBundle) unless defaultWriteTarget is set in config, which overrides it to a named source. An explicit --bundle <name> flag overrides both. The full order is --bundle → defaultWriteTarget → working bundle → ConfigError. See Configuration for details.

A bundle-qualified mutation ref implies that bundle. In particular, a qualified --supersedes team//memories/old routes the correction and demotion to team; a different explicit --bundle is a usage error. Qualified --xref values only identify the cited copy and do not select the write target.

akm remember "Deployment needs VPN access"
akm remember --name release-retro < notes.md
akm remember "Pair with ops before rotating prod secrets" --name ops/prod-secrets

# With structured frontmatter:
akm remember "VPN required for staging deploys" \
  --tag ops --tag networking \
  --expires 90d \
  --source "skills/deploy"

# Opt-in heuristic tagging — derives `code`, `source`, `observed_at`, `subjective`:
akm remember "Found this snippet: \`curl -fsSL ... | bash\`" --tag ops --auto

# Opt-in LLM enrichment (requires configured LLM endpoint; fails soft):
akm remember "Long meeting notes..." --enrich

# Multi-tenant / multi-agent scope:
akm remember "Use staging cluster for blue-green" \
  --user alice --agent claude --run run-42 --channel "#ops"

# Cite provenance / related assets in frontmatter `xrefs:` (validated at write time):
akm remember "The token rotation quirk applies to staging too" \
  --xref knowledge/auth/vendor-x-token-api \
  --xref memories/projectA/token-quirk

# Correct an existing memory: write the fix AND demote the stale incumbent
# (beliefState: superseded + supersededBy on the old asset, in one step):
akm remember "Staging now uses the new gateway endpoint" \
  --name new-endpoint --supersedes memories/projectA/old-endpoint

# Route the write to a specific writable bundle:
akm remember "Deployment needs VPN access" --bundle team-bundle
Flag Description
--name Optional memory name. Defaults to a slug derived from the content
--force Overwrite an existing memory with the same name
--description <text> Short description written to frontmatter (persisted as the memory's description field). Honoured by both the zero-flag form and the tagged form.
--tag <v> Tag to attach to the memory. Repeatable: --tag foo --tag bar
--expires <dur> Expiry shorthand (30d, 12h, 6m). Resolved to an ISO date
--source <s> Free-form source reference — URL, asset ref, file path, or any string
--xref <ref> Cross-reference ref recorded in the memory's xrefs: frontmatter list. Repeatable: --xref knowledge/auth-flow --xref memories/vpn-note. Each ref must resolve in the write target or a configured source (read-only sources count); an unresolvable ref fails with exit 2 before anything is written. More than 5 refs warns (soft cap) but still writes. Does not trigger the tags-required check.
--supersedes <ref> Ref of an existing asset this memory corrects. Repeatable. Writes the correction with the old ref folded into its xrefs: (correction provenance) AND demotes the old asset — beliefState: superseded + supersededBy: [<new ref>], a metadata-only frontmatter edit that preserves every other key and the body — then reindexes it so --belief current hides the stale version immediately. An unresolvable ref fails with exit 2 before anything is written or demoted; so does a ref naming the asset being written itself (a correction cannot supersede itself, e.g. --force overwriting the same name). A ref that resolves only outside the write target and the working bundle still writes the correction but skips the demotion: stderr warns and the JSON output reports superseded: [{ref, applied: false, reason}] — the reason names the --bundle remedy when the old asset lives in a configured writable source. An old asset whose existing frontmatter is not parseable YAML is skipped the same way (applied: false) instead of being rewritten lossily. Re-running the same correction is idempotent. On a git write target the correction and the demoted old asset land in the same single boundary commit.
--auto Apply heuristic tagging from the body (opt-in, zero-latency, pure TS)
--enrich Call the configured LLM for tag/description proposals (opt-in, 10s timeout, fails soft)
--user <id> Scope this memory to a user id. Persisted as the canonical scope_user frontmatter key.
--agent <id> Scope this memory to an agent id. Persisted as scope_agent.
--run <id> Scope this memory to a run id. Persisted as scope_run.
--channel <name> Scope this memory to a channel name. Persisted as scope_channel.
--bundle <name> Override the write destination. Accepts a source name from your config; falls back to defaultWriteTarget then the working bundle.

Pass the content as a quoted positional argument for short notes, or pipe markdown into stdin for longer memories.

Zero-flag form (akm remember "body") writes a bare memory with no frontmatter — existing agent scripts keep working unchanged. --tag / --expires / --source still trigger the required-field check: if tags cannot be derived, the command rejects before writing the file, so you never end up with an orphan. --auto and --enrich are fail-soft metadata helpers: if they derive nothing, the memory still writes successfully.

Scope flags (--user, --agent, --run, --channel) are independent of the tag-required check. They write the four canonical top-level frontmatter keys (scope_user, scope_agent, scope_run, scope_channel) and a memory with only scope flags is valid (no tags required). Scope is the multi-tenant / multi-agent contract; the same shape is read back by akm search --filter and akm show --filter.

Cross-references (--xref) implement the bundle back-linking conventions' provenance channel: the refs land in the memory's xrefs: frontmatter list, which the indexer folds into the asset's search hints, so the new memory is findable from searches for its source. Refs are validated before anything is written — against the write target plus every configured source, including read-only cross-bundle sources — so a typo'd ref fails fast (exit 2) instead of becoming permanent silent noise. When a write lands at the type root (no --path, flat name) in a bundle that carries convention facts, the JSON output includes an additive hint key pointing at the bundle's placement conventions.

Corrections (--supersedes) implement the conventions' two-write corrections pattern in one command: the new asset is written with an xref to what it corrects, and the old asset gets a metadata-only demotion (beliefState: superseded + supersededBy: [<new ref>]) that the write path reindexes immediately. A qualified superseded ref selects that bundle as the write target. Same-bundle frontmatter edges remain short; cross-bundle edges stay qualified. The old asset is demoted only when it lives in the write target or the working bundle — a match in any other configured source (read-only, or writable but not this write's target) is reported as applied: false (with a stderr warning) while the correction still writes; so is an old asset whose existing frontmatter is not parseable YAML, which a demotion rewrite would corrupt. Validation happens before any write, so a typo'd ref, or a ref naming the asset being written itself (exit 2), leaves both assets untouched — no partial correction.

import

Import a knowledge document. This writes a markdown file into knowledge/ in the configured write target and returns the resulting ref. The source may be a file path, a single HTTP/HTTPS URL, or - for stdin.

Write target resolution: the destination is the working bundle (defaultBundle) unless defaultWriteTarget is set in config, which overrides it to a named source. An explicit --target <name> flag overrides both. The full order is --target → defaultWriteTarget → working bundle → ConfigError. See Configuration for details.

akm import ./docs/auth-flow.md
akm import ./notes/release.txt --name release-checklist
akm import - --name scratch-notes < notes.md
akm import https://example.com/docs/auth

# Cite provenance in the document's frontmatter `xrefs:` (validated at write time):
akm import ./notes/oauth-quirks.md --xref knowledge/auth/vendor-x-token-api

# Import a corrected doc AND demote the one it replaces (in one step):
akm import ./notes/modern-guide.md --supersedes knowledge/legacy-guide

# Route the write to a specific writable bundle:
akm import ./docs/auth-flow.md --target team-bundle
Flag Description
--name Optional knowledge name. Defaults to the source filename, URL path, or a slug from stdin content
--force Overwrite an existing knowledge document with the same name
--target <name> Override the write destination. Accepts a source name from your config; falls back to defaultWriteTarget then the working bundle.
--xref <ref> Cross-reference ref merged into the document's xrefs: frontmatter list. Repeatable. A document without frontmatter gains a block; a document with valid frontmatter keeps every existing key and value and gets the refs dedupe-appended (never a nested second block). Each ref must resolve in the write target or a configured source; an unresolvable ref fails with exit 2 before anything is written. If the document's existing frontmatter is not a parseable YAML mapping, the import fails (exit 2) rather than rewriting the block lossily — fix the frontmatter or import without --xref, which preserves the file verbatim.
--supersedes <ref> Ref of an existing asset this document corrects. Repeatable. Imports the correction with the old ref merged into its xrefs: AND demotes the old asset (beliefState: superseded + supersededBy: [<new ref>], a metadata-only frontmatter edit), then reindexes it. Same validation (including the self-supersede rejection), skipped-demotion (applied: false), idempotence, and git-boundary-commit behaviour as on remember (see above).

URL imports fetch only the exact page you pass, convert it to markdown, and do not register a persistent website source. The default knowledge name comes from the URL path (for example, /docs/auth -> knowledge/docs/auth.md).

The source must be a readable file path, a reachable HTTP/HTTPS URL, or - to read the document from stdin.

--xref behaves as on remember (validated refs, soft ~5 cap, additive hint output key on type-root writes), with one import-specific rule: because imported documents may already carry frontmatter, the refs are merged — existing keys are preserved and the xrefs: list is dedupe-appended, so the result always has exactly one frontmatter block. The merge requires the existing block to parse as a YAML mapping; a malformed block aborts the import (exit 2, nothing written) instead of silently flattening the values the parser could not read. Importing the same document without --xref always preserves it byte-for-byte.

feedback

Record positive or negative feedback for any indexed bundle asset. Record --negative only when the asset's content is wrong or stale, and say what is wrong and what it should say; a note that simply did not fit your task is not negative feedback, so record nothing for it. akm feedback <ref> --negative --reason "<what is wrong and what should change>" flags the asset: it ranks lower right away, and the next improve run may repair its description, title or when_to_use from your reason. Improve does not rewrite an asset's text. Once you have verified the correct fact, attach the exact fix with --replace, --with and --source: akm checks that each --replace text appears exactly once and that the frontmatter still parses, records nothing if either check fails, and queues the edit as a feedback proposal for review. To mark the asset's history, with or without a text fix, add --superseded-by <ref> (another asset replaces it) or --outdated (it describes a past state and no single asset replaces it), with --reason and --source. The same single proposal sets the asset's beliefState (superseded, or deprecated) and, for --superseded-by, adds the successor's ref to its supersededBy list, by editing only those lines of the frontmatter. --positive records that an asset helped (it raises its ranking) and does not trigger a rewrite. Both signals update the asset's utility score right away, so highly-rated assets rank higher in search results.

akm feedback scripts/deploy.sh --positive
akm feedback agents/reviewer --negative
akm feedback memories/deployment-notes --positive
akm feedback env/prod --positive
akm feedback skills/code-review --positive --reason "Worked perfectly for PR reviews"
akm feedback skills/code-review --negative --reason "references a removed flag"
akm feedback skills/code-review --negative --reason "flaky" --tag slice:train --tag team:platform
akm feedback knowledge/opencode-server --negative --reason "the default port is 4096, not 8000" --replace "port 8000" --with "port 4096" --source "https://opencode.ai/docs/server/"
akm feedback knowledge/setup-v1 --negative --reason "the v2 guide replaces it" --superseded-by knowledge/setup-v2 --source "knowledge/setup-v2"
akm feedback knowledge/api-v1 --negative --reason "describes the retired v1 API" --outdated --source "https://example.com/changelog"
Flag Description
--positive Record that an asset helped: it raises its ranking and does not trigger a rewrite
--negative Flag the asset: it ranks lower right away, and the next improve run may repair its frontmatter from --reason
--reason What is wrong with the asset's content and what should change; not for akm command errors. Attached to the feedback event (required for negative feedback by default, and always with a fix: --replace, --superseded-by or --outdated)
--replace <text> Exact text to correct, copied verbatim from the asset file; it must appear exactly once. Repeatable, each paired in order with a --with. Negative feedback only
--with <text> The corrected text for the matching --replace. Use --with=<text> for a value that starts with -
--source <where> The URL, command or file that shows the correct fact. Required with --replace, --superseded-by and --outdated; shown to the reviewer with the proposal
--superseded-by <ref> The ref of the asset that replaces this one. The proposal sets beliefState: superseded and adds the ref, as its bundle//conceptId, to supersededBy; contradicted and archived stay, a ref already listed is not added again, and a scalar supersededBy becomes a list. The ref must be indexed and must not be the asset itself, or nothing is recorded; nor is anything when the asset already says all this (the fix changes nothing). Negative feedback only; may be combined with --replace/--with, not with --outdated; markdown assets only
--outdated The asset describes a past state and no single asset replaces it. The proposal sets beliefState: deprecated, unless the asset already says superseded, contradicted or archived. Negative feedback only; may be combined with --replace/--with, not with --superseded-by; markdown assets only
--tag Tag to attach to the feedback (repeatable, e.g. --tag slice:train --tag team:platform)
--applied-to <ref> Credit a lessons/<name> lesson that helped resolve this task. When combined with --positive, appends this feedback ref to the target lesson's lessonStrength[] frontmatter array (dedup, idempotent). A non-lesson target, or a missing --positive, produces a warning rather than silently doing nothing.

Specify exactly one of --positive or --negative. The ref must already be present in the current local index.

Only negative feedback with a specific reason gets an asset reviewed and fixed: akm improve plans a rewrite (reflect) only for an asset with fresh negative feedback, or for an explicit ref. A positive or note-only signal never plans one, and improve no longer rewrites assets on a proactive cadence.

The --applied-to flag records the lesson-strength signal: each credit is kept in the lesson's lessonStrength[] frontmatter. Search ranking does not use it.

log

Append-only realtime events stream (#204). Every mutating CLI verb appends an event row to <dataDir>/state.db; akm log reads it.

Note: there is no akm events command, and no akm history command — use akm log. There is no akm log tail either (0.9.0: dropped — a foreground polling daemon in a one-shot CLI); poll --since '@offset:<id>' from a cooperating process instead.

akm log                                      # All events, oldest first
akm log --type feedback                      # Filter by event type
akm log --ref skills/deploy                   # Filter by asset ref
akm log --since 2026-04-01T00:00:00Z         # ISO timestamp
akm log --since '@offset:12345'              # Resume from a row-id cursor
akm log --limit 20                           # Only the 20 most recent events (unlimited by default)
akm log --run <run-id>                       # Only events for one workflow run
Flag Description
--since Lower bound. Accepts ISO 8601, epoch ms, or @offset:<id> for a durable row-id cursor that survives across processes.
--type Filter by event type. Common values include add, remove, update, remember, import, sync, feedback, promoted, rejected, propose_invoked, reflect_invoked, distill_invoked, select, and improve_skipped.
--ref Filter by asset ref ([bundle//]conceptId).
--run Filter to one workflow run's events (metadata.runId) — the replacement for the dropped akm workflow watch <run-id>. Poll with --since '@offset:<id>' for a live tail; there is no daemon.
--limit Return only the most recent N events matching every other filter. Default: unlimited.
--include-tags Only include events with ALL these tags (repeatable).
--exclude-tags Exclude events matching these tags (repeatable).

The envelope echoes a nextOffset row-id cursor — persist it and pass it back as --since '@offset:<nextOffset>' to resume from exactly where you stopped, with no duplicates and no losses, even across process boundaries (poll on an interval from a cooperating process if you need to follow the stream live).

Environment isolation

The events stream lives in <dataDir>/state.db, where <dataDir> is derived from XDG_DATA_HOME (or AKM_DATA_DIR) at the time of each call. Two processes with different inherited data-dir env values write to different databases; if the events stream is being used as a shared bus between cooperating processes, set those env vars consistently across them.

registry

Manage bundle registries. The registry command has three subcommands: list, add, and remove. Searching registries is akm search --from registry (0.9.0: registry search was dropped in favor of it — see search).

Building a registry index is maintainer tooling, not a CLI command — see bun scripts/build-registry-index.ts in the akm repository.

registry list

List all configured registries and their status.

akm registry list

registry add

Add a third-party registry by URL.

akm registry add https://example.com/registry/index.json
akm registry add https://example.com/registry/index.json --name my-team
akm registry add https://skills.sh --name skills.sh --provider skills-sh
Flag Description
--name Human-friendly label for the registry
--provider Provider type (e.g. static-index, skills-sh). Default: static-index
--options Provider-specific options as JSON (e.g. '{"apiKey":"key"}')
--allow-insecure-transport Allow a plain HTTP registry URL (rejected by default)

Duplicate URLs are rejected.

registry remove

Remove a registry by URL or name.

akm registry remove https://example.com/registry/index.json
akm registry remove my-team
akm registry remove my-team --yes    # Skip the confirmation prompt
Flag Description
-y, --yes Skip confirmation prompt

migrate

Inspect or apply every pending migration in one plan. akm migrate is a thin wrapper over the standalone akm-migrate executable (installed alongside akm, and embedded in the release binary), which owns every historical shape akm has ever written so the CLI proper reads only current schemas. The steps, in order:

  1. config.json rewritten in its current shape (configFile): retired and unknown keys dropped, legacy extraParams lifted onto first-class engine fields, the legacy stashDir/sources[]/installed layout converted to bundles/defaultBundle, configVersion bumped — the same pipeline every load already runs in memory, so this only persists it, under a backup;
  2. pending state.db migrations, historical-destructive ones included, with a verified sibling safety copy (stateMigrations) — the only path besides akm upgrade that admits released migration 018, which an ordinary command refuses;
  3. task files at version 2 or 3, and version 4 files still carrying the retired schedule[].enabled key, rewritten as task source v4 (taskFiles) under one backup directory per run, each emitted document re-parsed by the runtime v4 parser first; a file the planner cannot convert unambiguously is reported blocked and left alone;
  4. superseded residue removed (deadResidue): pre-0.9.0 .akm leftovers in the stash, and the transaction-journal, maintenance-barrier, lock-mutex and version-stamp files older releases kept under $DATA, $STATE and $CONFIG.
akm migrate status
akm migrate apply --dry-run
akm migrate apply
akm-migrate apply          # the same, without the akm wrapper

status and apply --dry-run are read-only. Apply skips blocked task sources rather than failing (and exits 1 while any remain), backs up each changed file immediately before replacement, and atomically publishes strict task source v4 YAML; it is idempotent, so a current installation is a no-op. akm upgrade runs apply after its install step. See Tasks: Migrating to task source v4 for the full blocked-reason table and worked examples, and Bundling akm for the plan JSON shape and how to drive this from a container/image boot step.

Each step above runs under its own catch: a step's own anomaly is always recorded in the plan's failedSteps: [{step, error}] instead of ending the whole run — the remaining steps still run in order. Under apply, a failed step's section falls back to its read-only preview; if that fails too, the fallback adds its own failedSteps entry, and the section is absent from the plan. Any failedSteps entry forces status: "blocked" and adds a matching line to blockers, so akm migrate status|apply reports the plan and exits 1 (not the internal-error 70) the same way it does for any other blocked plan.

config

Read and write configuration. Bare akm config (no subcommand) is a usage error (exit 2), the canonical bare-group behavior — name a subcommand.

akm config list                     # List current config
akm config get output.format        # Read one key
akm config get output.format --show-source  # Read one key, with where it came from
akm config set output.detail full   # Set one key
akm config set output.detail full --silent  # Set without the post-write config dump on stdout
akm config unset llm                # Remove an optional key
akm config path                     # Print path to config file
akm config path --all               # Print all config-related paths
akm config diff other-host/config.json  # Effective-config differences, secrets redacted

Subcommands:

Subcommand Description
get <key> Read one config key (the effective, post-extends value). --show-source wraps it as { value, source }, where source is local, extends:<ref>, or default.
list List current configuration
set <key> <value> Set one config key; prints the resulting config with ok: true
unset <key> Unset an optional key, or a whole embedding/engine section; prints the resulting config with ok: true
path Show paths to config, bundle, cache, and index. --all prints every path; without it, just the config path. Load-bearing: config path is the one subcommand the CLI still allows to run when the on-disk config itself fails to load, so you always have a way to locate a broken config.
diff <path|bundle//path> Compare this instance's effective config (its own extends already applied) against another config file or bundle-relative file (loaded through the same loader — its extends honoured too); prints sorted { path, local, other } rows for every differing leaf, secrets redacted on both sides.

set and unset accept --silent to suppress the post-write config dump entirely — nothing is printed on stdout, and the exit code is the status (the write still happens and errors still print) — use it from hooks and CI scripts.

See configuration.md's "Sharing configuration across installs" for the extends config key that diff and get --show-source work with.

Removed in 0.9.0: akm config enable/akm config disable. Use akm registry add|remove to toggle a registry, the general mechanism. akm config show (an alias of list) and akm config validate (load-time schema checks already reject an invalid config) were also removed.

See configuration.md for details.

models

Manage the installed and operator-owned model intent map. Bare akm models is a usage error; use list to inspect the effective table or copy-defaults when you want an editable full map.

akm models list
akm models copy-defaults
akm models copy-defaults --overwrite

list shows the fully resolved alias table — one row per (alias, column) pair with its model, optional inference, source (default: unchanged from the installed file; user: touched by the user overlay), and via (literal: a model string; engine: borrowed from a configured engines.<name> connection, in which case the row also names that engine). Read-only; it never writes models.json.

copy-defaults validates the packaged version-1 models.json, then stages and syncs it beside the normal AKM configuration target. Creation uses an atomic no-replace publish and fails safely on filesystems that cannot provide it. --overwrite performs an atomic pathname replacement after a best-effort regular-file identity recheck; it never dereferences a symlink, but portable filesystems do not offer a conditional rename that locks the previously observed inode. Symlinks and other non-regular targets observed during checks are refused. See Model-map files for schema, overlay, and resolution semantics — including the engine field (0.9.15) that lets a column borrow its model from a configured engine instead of a literal string.

help

Print the sectioned command overview, detailed help for any command, agent usage instructions, or a release's migration guidance.

akm help                       # Sectioned command overview (same as `akm --help`)
akm help bundle                # Detailed options and subcommands for `bundle`
akm help env                   # Detailed options and subcommands for `env`
akm help agents                # Agent-facing usage instructions
akm help migrate 0.6.0         # Notes for a specific release
akm help migrate v0.6.0        # v-prefix accepted
akm help migrate v0.6.0-rc1    # Prereleases normalize to the stable note
akm help migrate latest        # Resolve against the most recent CHANGELOG entry

akm help <command> is equivalent to akm <command> --help. Bare akm help prints the same sectioned overview as akm --help and exits 0 — this is the one group where a bare invocation is a complete request, not the canonical bare-group usage error.

Migration notes live as one markdown file per release in docs/migration/release-notes/. Adding notes for a future version is a one-file drop — no code edit required. Requesting an unknown version prints the list of bundled notes so you can pick one that exists. See CONTRIBUTING.md for the per-release workflow.

help agents

Print agent-facing instructions for using akm. Add this output to your AGENTS.md, CLAUDE.md, or system prompt so your agent knows how to use the CLI. Prints the short guide by default; pass --full for the complete one.

akm help agents

hints

Print the agent-facing CLI guide directly. The complete guide is the default; use --detail brief for the compact version. akm help agents remains the short-first form and accepts --full.

akm hints
akm hints --detail brief

env vs secret — which do I use?

Both protect their values identically (values never reach akm's stdout, the index, or akm show). They differ in purpose, not in how well they hide data:

env secret
Purpose configuration — a group of related settings for an app/service authentication — one sensitive value used on its own
Holds a .env file of many KEY=value pairs (URLs, flags, and any credentials it needs) one value per file (an API token, PEM key, cert, service-account JSON)
Sensitivity values may or may not be sensitive — all are protected anyway the value is always a credential
Injects many env vars at once (env run) one env var (secret run <ref> <VAR>)
Discoverable key names (not values) name only (the whole file is the value)

env is primarily for configuration — a group of related values you load together, protected whether or not any are sensitive. secret is primarily for a single sensitive value used for authentication. Reach for env to load a service's config; reach for secret when one value is an auth credential.

Note: there is no akm vault command — use env or secret.

env

Manage .env-backed environment files — a group of related configuration for an app or service (URLs, feature flags, and any credentials it needs), loaded together. Each env asset is an entire .env file stored under env/ in your bundle (mode 0600). Values may or may not be sensitive; akm protects them all the same — key names are discoverable; values and comment text never appear in structured output (comments routinely contain commented-out credentials, so they are treated like values). akm does not manage individual entries — you edit the .env with your own editor (or ingest one with --from-file) and akm loads it wholesale. list and show surface key names only; run and export are the supported value-use paths.

akm env list
akm env create prod                          # creates env/prod.env (mode 0600)
akm env create prod --from-file ./.env        # ingest an existing .env
akm env create prod --path staging            # creates env/staging/prod.env
$EDITOR "$(akm env path env/prod --quiet)"    # edit the file directly
akm env run env/prod -- npm test              # run a command with the whole file injected
akm env run env/prod -- $SHELL                # interactive shell with the env loaded
akm env run env/prod --only DATABASE_URL -- ./migrate   # inject just one var
akm env remove env/prod --yes                 # remove the whole env file

akm does not manage individual keys — edit the .env file directly ($EDITOR "$(akm env path <ref>)"). env remove <ref> removes the whole file.

Env mutations (create, remove) pick their write destination the same way every other write command does: an explicit --target <source> wins, else defaultWriteTarget, else the working bundle. The chosen source must be writable — a non-writable --target/defaultWriteTarget fails with a ConfigError before anything is written — and on a git-backed writable target the mutation lands in a single boundary commit (filesystem targets are committed by akm sync; env/ stays out of git when your bundle .gitignore excludes it). Reads (list, path, run, export) still span all configured sources and are unchanged.

Subcommands:

Subcommand Description
list List all env files across all bundles with key names only
run <ref> -- <command> Run a command with the env injected. --only / --except filter which keys are injected; --clean starts from a minimal inherited environment
create <name> Create an env file. Empty by default; seed with --from-file <path> or --from-stdin
path <ref> Print the absolute env file path (Docker _FILE / --env-file / direct editing). --quiet suppresses the warning
export <ref> --out <file> Write a safe sourceable export script to a file (never to stdout)
remove <ref> Delete an env file (and its .sensitive marker)

Removed in 0.9.0: akm env set/akm env unset. akm does not manage individual keys — edit the .env file directly.

env run — the primary value path

akm env run env/prod -- <command>
akm env run env/prod -- $SHELL          # interactive: a shell with the env loaded
akm env run env/prod --only A,B -- cmd  # inject only A and B
akm env run env/prod --except DEBUG -- cmd
akm env run env/prod --clean -- cmd
akm env run env/prod --clean --inherit SSH_AUTH_SOCK -- cmd
akm env run third-party//env/prod --allow-dangerous-env-keys -- cmd

Runs the command with the env file's values injected directly into the child process — never through a shell, and never into akm's own structured output. However, the child process controls its own stdout/stderr: if it prints its environment, those values will appear in your terminal or agent transcript. --only / --except (comma-separated key names, mutually exclusive) restrict which env-file keys are injected. --clean starts from a minimal inherited environment (PATH/HOME/locale/terminal basics) instead of inheriting the full parent environment; use --inherit KEY1,KEY2 to pass specific parent vars through in clean mode. Before spawning, the injected key names are scanned for known process-hijacking variables (LD_PRELOAD, PATH, GIT_CONFIG_*, ...): a first-party bundle warns and proceeds; a third-party-sourced bundle is refused unless the reviewed run explicitly passes --allow-dangerous-env-keys.

The single-key run <ref>/KEY form was removed. To inject one value, store it as a secret and use akm secret run secrets/<name> <VAR> -- …, or use akm env run <ref> --only <KEY> -- ….

Values injected via env run live in the child process environment for its entire lifetime and are visible to all subprocesses it spawns. Avoid env run for long-lived daemon or server processes, and do not use commands like env, printenv, shell tracing, or verbose diagnostics in agent contexts unless you explicitly intend to expose the child environment.

env create

akm env create prod                       # empty
akm env create prod --from-file ./.env    # seed from an existing .env (byte-for-byte)
printf 'A=1\nB=2\n' | akm env create prod --from-stdin
akm env create prod --path staging        # creates env/staging/prod.env
akm env create prod --sensitive           # hidden from `env list` and the search index
akm env create prod --target team         # write to the `team` source
Flag Description
--path <dir> Relative subdirectory under env/ to place the file in. The filename comes from <name>.
--from-file <path> Seed the env file from an existing .env at this path
--from-stdin Seed the env file from stdin
--sensitive Exclude this env file from env list output and the search index
--target <source> Override the write destination (falls back to defaultWriteTarget then the working bundle)

Creates env/prod.env with mode 0600. Empty create is a no-op if the file exists; --from-file/--from-stdin refuse to clobber an existing env (remove it first). --sensitive hides the file from env list and the search index.

env list

akm env list

One entry per env file across all configured bundles. The structured shape is envs: [{ ref, keys }] — values are never included and the absolute path is omitted from JSON output. Text output uses Markdown sections:

## env/prod

- DATABASE_URL
- API_KEY

env path

akm env path env/prod            # warns: don't source the raw file
akm env path env/prod --quiet    # for `_FILE` / `--env-file` use

Prints the absolute path to the env file — for the Docker _FILE convention (MY_VAR_FILE=$(akm env path env/prod --quiet)), docker run --env-file, or editing the file directly. By default a stderr warning steers you away from source-ing the raw file (its shell substitutions would execute); --quiet suppresses it for the legitimate file-path uses. Format-exempt (src/output/format-exempt.ts) — this command's stdout is always the bare path, never a result envelope; passing --format warns rather than doing anything.

env export

akm env export env/prod --out /tmp/prod.sh && source /tmp/prod.sh && rm -f /tmp/prod.sh

Writes a safe, sourceable export KEY='value' script to --out <file> (mode 0600). Values are re-serialised single-quoted, so a raw .env containing shell substitutions (e.g. X=$(rm -rf ~)) becomes a literal string — sourcing the generated file can never execute it. export never prints values to stdout (that would leak them into a captured/agent context) and so requires --out.

For most uses prefer akm env run (no file, no cleanup). export exists for the case where a tool must source a file or you need a generated env script.

secret

Manage secrets — a single sensitive value used on its own for authentication: an API token, a PEM private key, a TLS cert, a service-account JSON. Where an env file holds a group of related configuration and exposes key names, a secret is one value and its entire file is the value, so only the secret's name is ever surfaced. Each secret is a mode-0600 file under secrets/ in your bundle.

This mirrors Docker's secret model (one value per file, mounted at /run/secrets/<name>, read at runtime, never baked into the image or env at build time). The key security property: secret values never appear in structured output — not in the index, akm search, akm curate, or akm show. The supported value-use path is secret run (inject into a child env var).

akm secret list
printf '%s' "$TOKEN" | akm secret set secrets/deploy-token
akm secret set secrets/deploy-key --from-file ~/.ssh/id_ed25519   # byte-exact
AKM_VALUE="$TOKEN" akm secret set secrets/api --from-env AKM_VALUE
akm secret run secrets/deploy-token GITHUB_TOKEN -- gh release create v1.0.0

Subcommands:

Subcommand Description
list List all secrets across all bundles by name (contents never shown)
set <ref> Create/overwrite a secret — value from stdin (default), --from-file, or --from-env
run <ref> <VAR> -- <command> Run a command with the secret value injected into $VAR in the child only

Removed in 0.9.0: secret path and secret remove. The two resolved a ref through different bundle-selection logic — path through the read-side, all-sources resolver and remove through the write-target resolver — so for a ref present in more than one bundle they could silently name different files: you could inspect one secret and delete another. Both now exit 2 with Unknown command. A ref's file lives at <bundle>/secrets/<name> (run akm bundle list for bundle roots); locate or delete it there directly, or use akm secret run to consume the value without touching disk.

secret set

# Default: read the value from stdin (never crosses argv)
printf '%s' "$TOKEN" | akm secret set secrets/deploy-token

# Import an existing file byte-exact (multi-line PEM keys, certs, binary)
akm secret set secrets/deploy-key --from-file ~/.ssh/id_ed25519

# From an environment variable
AKM_VALUE="$TOKEN" akm secret set secrets/api --from-env AKM_VALUE

The value is never accepted via positional arguments. With stdin, a single trailing newline is stripped (so echo "$TOKEN" | akm secret set … stores the token without the shell-added newline); use --from-file for byte-exact storage of multi-line material. Writes are atomic (mode 0600) under an exclusive <secret>.lock. Maximum size is 5 MB.

secret set selects its write destination like every other write command: an explicit --target <source> wins, else defaultWriteTarget, else the working bundle. The chosen source must be writable (a non-writable target fails with a ConfigError), and on a git-backed writable target the mutation lands in a single boundary commit. Reads (list, run) still span all configured sources.

secret run

akm secret run secrets/deploy-token GITHUB_TOKEN -- gh release create v1.0.0
akm secret run secrets/deploy-token GITHUB_TOKEN --clean -- gh auth status

Runs one subprocess with the secret's value set as $VAR in the child's environment. The value never appears in akm's structured output — it is passed directly to the child process. The target variable name is validated and known process-hijacking names (LD_PRELOAD, PATH, etc.) are rejected. --clean starts from a minimal inherited environment instead of inheriting the full parent environment; use --inherit KEY1,KEY2 to pass specific parent vars through in clean mode.

Secrets injected via secret run live in the child process environment for its entire lifetime and are visible to all subprocesses it spawns. For long-lived daemons, point the process at the secret file directly (<bundle>/secrets/<name>) so the value never sits in an environment variable. Avoid commands that print the environment in agent contexts unless you explicitly intend to expose the child environment.

Sensitive marker

A sibling <name>.sensitive marker file excludes a secret from secret list and from indexing entirely (parallel to env files). The secret remains usable via secret run.

Wikis (no dedicated command)

An LLM wiki (the Karpathy pattern — schema.md rulebook, agent-authored pages/, immutable raw/ sources) is a bundle format, not a command family. There is no akm wiki verb; a bundle whose root holds schema.md plus pages/ is recognized automatically at install time, and its pages are indexed and addressed like any other asset:

akm bundle add github:team/research-wiki        # install a wiki bundle (or a local dir)
akm search "attention"                   # pages rank alongside all other assets
akm show research-wiki//pages/attention  # read a page by bundle//conceptId ref

Writing pages, ingesting raw sources, and maintaining index.md/log.md are the agent's job, using its native Read/Write/Edit tools guided by schema.md — akm's job is recognition, indexing, and search. See wikis.md for the full format.

completions

Generate or install a bash completion script for akm. The script is built dynamically from the command tree, so it always reflects the current set of subcommands and flags.

akm completions                # Print bash completion script to stdout
akm completions --install      # Install to the appropriate directory
Flag Description
--install Write the script to the XDG-compliant completions directory
--shell Shell type (currently only bash is supported)

Manual activation: pipe the output into your shell or source it from your profile:

source <(akm completions)

Install locations (checked in order):

  1. $XDG_DATA_HOME/bash-completion/completions/akm
  2. ~/.local/share/bash-completion/completions/akm
  3. ~/.bash_completion.d/akm

Improvement Flow

These commands define the self-improvement and agent-dispatch surface.

command run

Resolve a stored command through its owning bundle adapter and execute one fresh session through the common engine/model cascade:

akm command run <command-ref> [--arguments <exact-text>] [--agent <selector>] [--engine <name>] [--model <id-or-alias>] [--timeout-ms <ms>] [--cwd <path>] [--dry-run]

--dry-run does not dispatch and does not materialize credentials.

Argument / Flag Description
<command-ref> Local indexed command ref, optionally bundle-qualified (for example commands/review or team//commands/review)
--arguments <exact-text> Exact string substituted for every literal $ARGUMENTS; it is not trimmed, tokenized, quoted, or recursively expanded
--agent <selector> Portable agents/... ref or a native harness selector
--engine <name> Current-invocation engine override
--model <id-or-alias> Current-invocation exact model or operator model-map alias
--timeout-ms <ms> Current-invocation timeout override
--cwd <path> Current-invocation workspace override
--dry-run Resolve, authorize, and lower the command without dispatching or materializing credentials

Commands and portable personas are rendered by their bundle adapter. Native frontmatter is never sent as prompt text and native files are never rewritten. The only portable template token is literal $ARGUMENTS. Positional, named, expression, legacy {{...}}, and other native-only constructs fail before authorization or dispatch; invoke those templates through their native tool. Omitting --arguments and passing an explicit empty string both substitute empty text, but remain distinct in the resolved request.

akm command run ... --dry-run performs the real adapter read, cascade/model resolution, operator authorization or policy check, and engine lowering. It returns a successful JSON result with schemaVersion: 1, shape: "command-dry-run", ok: true, dryRun: true, the selected engine name, safe provenance, and safe lowering notices. It has no fake exit code, stdout, stderr, or duration.

Each provenance entry contains only field, layer, kind, and via. Each lowering notice contains code, severity, adapter, optional field, and fixed message. Dry-run does not dispatch and does not materialize credentials. It uses a read-only source lookup and records no usage, events, or accounting.

Diagnostics exclude resolved values. They never include prompt content. They never include command content. They never include environment values. They never include credential values. User-authored inference keys are collapsed to the safe wildcard field instead of being echoed.

For live execution, global --verbose emits the same safe provenance and notices to stderr before dispatch. The normal command result on stdout is preserved unchanged, so enabling verbose diagnostics does not corrupt scripts that consume stdout.

agent

Dispatch a configured agent engine, optionally selecting a bundle agent persona and model defaults. A nonempty tool request from that asset is not authorization: the current CLI rejects it at the execution boundary. Stored command assets execute only through akm command run; akm agent has no command compatibility alias.

akm agent [<agent-ref>] [--engine <name>] [--prompt <text>] [--model <model>] [--timeout-ms <ms>] [--cwd <path>]
Argument / Flag Description
<agent-ref> Optional agent asset ref (e.g. agents/code-reviewer). Resolves persona and model defaults; a nonempty tool request still requires separate operator authorization.
--engine <name> Agent engine to use; defaults to defaults.engine
--prompt <text> Task prompt to pass to the agent
--model <model> Model override. Accepts aliases (opus, sonnet, haiku) or exact platform model IDs. Overrides the model in the agent asset. Resolved per platform: opencode/claude-opus-4-7 for opencode, claude-opus-4-7 for claude.
--timeout-ms <ms> Override the agent CLI timeout in milliseconds
--cwd <path> Working directory for the spawned agent (defaults to the current directory)

When <agent-ref> is provided, akm resolves the bundle agent's persona, modelHint, and requested toolPolicy. The --model flag wins over any model specified in the asset. An alias resolves per the selected --engine's model-map column (see Model-map files), which — as of 0.9.15 — may itself be an engine-backed indirection, so --engine local-fast --model fast can resolve to local-fast's own engines.local-fast.model instead of a hardcoded per-platform literal. The requested tool policy never grants access by itself: authorization runs before lowering, credentials, or provider dispatch. The current CLI has no built-in allow-all authorizer, so a nonempty request is rejected rather than silently weakened. Selecting a persona or model without --prompt or --prompt-stdin is also rejected; akm never fabricates an empty command. The prompt-free interactive exemption applies only when no persona/model/tool/schema or inference payload was selected.

Platform-specific dispatch: akm uses a platform builder to construct the CLI argv for each engine's harness platform. platform: "opencode" engines emit: opencode run [--model opencode/claude-opus-4-7] "<prompt>". opencode run has no system-prompt option, so akm composes a persona into the prompt. platform: "claude" engines emit: claude [--system-prompt "..."] [--model claude-opus-4-7] --print -- "<prompt>". Agent engines may set bin, args, workspace, model, and timeoutMs in config. Semantic model aliases live only in the installed/user models.json files and resolve before dispatch.

Without any --prompt, <agent-ref>, or --model, the agent is launched interactively (no injected prompt, no platform-specific flags beyond the engine's base args).

Configure agent engines under engines.<name> with kind: "agent" and a registered harness platform (see Configuration). AKM lowers the selected engine to the spawn or embedded SDK runner with captured or interactive stdio, hard timeout, and structured failure reasons.

# Interactive launch:
akm agent --engine opencode

# Dispatch with a prompt only:
akm agent --engine claude --prompt "summarize recent changes"

# Embody a bundle agent asset:
akm agent agents/code-reviewer --engine opencode --prompt "review src/"

# Model override with alias:
akm agent agents/planner --engine claude --model sonnet --prompt "plan the sprint"

# Exact model ID override:
akm agent --engine opencode --model opencode/claude-opus-4-7 --prompt "audit the API"

Returns { ok, exitCode, stdout?, stderr?, durationMs, reason? }. On failure, reason is one of timeout | spawn_failed | non_zero_exit | parse_error. Captured dispatches render this final envelope using the selected akm format. Interactive child stdout/stderr remain inherited and raw. A failed dispatch exits 1; exitCode in the envelope retains the child's exact status when one exists.

lint

Scan bundle markdown files for structural issues: unquoted colons, missing updated field, orphaned stubs, placeholder stubs, missing name/type, stale paths, and broken refs — in body text and in refs/xrefs/supersededBy/contradictedBy frontmatter. A belief edge pointing at a memory that akm improve pruned resolves through the archive tombstone under .akm/memory-cleanup/archive/ and is not reported (#884). Also reports dangerous-env-key findings for env files (the same key set akm bundle add enforces — see Dangerous env key audit — but non-blocking here; lint only warns). --type workflows structurally parses and compiles peer Markdown and GitHub-shaped YAML workflows; errors surface as invalid-workflow-structure findings (0.9.0: this is the only structural-validation surface now that akm workflow validate is gone).

akm lint                        # Report findings; exits 0 regardless
akm lint --fix                  # Auto-fix Tier-1 issues in place
akm lint --type workflows       # Only lint one asset type
akm lint --dir ~/other-bundle    # Override the bundle root (default: from config)
akm lint --fail-on-flagged      # CI-friendly: exit non-zero when summary.flagged > 0
akm lint --prune-dangling-edges # Opt-in: drop belief edges whose target is gone
Flag Description
--fix (alias --auto-fix) Apply auto-fixes in place. Refused with a usage error when the target bundle is configured writable: false.
--dir Override the bundle root directory (default: from config)
--type Only lint assets of this type (e.g. workflows, tasks, memories). akm bundles only — every other adapter validates the whole bundle and warns on stderr that the flag had no effect.
--fail-on-flagged Exit non-zero when summary.flagged > 0. Default: exit 0 regardless of findings.
--prune-dangling-edges Opt-in repair (#884): drop supersededBy/contradictedBy entries whose target has neither a file nor a prune tombstone. Not implied by --fix — it edits well-formed files to delete a belief-graph claim, so review a plain akm lint report first. Clears the same writable: false gate --fix does.

Returns fixed[] and flagged[] arrays plus a summary: { fixed, flagged } count. Each entry carries file, issue, detail, and whether it was fixed.

Task files are checked for more than their fields: a tasks/*.yml whose YAML does not parse is reported as invalid-task-yaml (it used to fall through as an empty mapping and lint clean), and a tasks/*.yaml file — a spelling akm never indexes or schedules — is flagged for the extension rather than skipped.

--fix is transactional per file: a fix that cannot be written (read-only file, full disk) is reported as fixed: "failed" on that file and the sweep continues, so a mid-run write failure can no longer abort the command and hide the fixes that already landed.

improve

Improve existing assets and write the results to the proposal queue.

akm improve
akm improve memory
akm improve skills/code-review
akm improve workflows/release-checklist --task "reduce duplication"
akm improve --skip-if-locked           # for high-frequency scheduled runs: skip (exit 0) if a run is already in progress
akm improve --require-engines          # for scheduled runs: abort (exit 78) instead of degrading if an engine/credential is unavailable
akm improve --no-sync                  # skip the end-of-run git commit entirely (default: on for git-backed bundles)
akm improve --sync --no-push           # commit only, skip the push after it
akm improve --plan --strategy thorough # preview thorough's resolved engine/model routing; nothing is dispatched
akm improve lessons/my-lesson --show-prompt --format text # print the composed reflect prompt for one asset, unwrapped; no lock/index/engine call
akm improve report                     # LLM usage/routing report for the most recent real run
akm improve report --run <id>          # ...for one specific improve_runs id
akm improve report --since 7d          # ...aggregated over every real run started in the last 7 days
akm improve judge < revision.json      # reflect's quality judge on one revision; writes nothing
Flag Description
--run <id> report scope only (#944): show the usage report for one specific improve_runs row instead of the most recent real run. Mutually exclusive with --since. Rejected with any other scope, or no scope.
--since <window> report scope only (#944): aggregate the usage report over every real (non-dry-run) run started since <window> (a duration like 24h/7d, or an ISO timestamp) instead of one run. Mutually exclusive with --run. Rejected with any other scope, or no scope.
--task Optional extra guidance for this improvement pass
--dry-run Show the schema-v2 result on stdout without creating config, data, state, cache, bundle, log, or result artifacts. Dry-run results are never persisted, including on errors or signals.
--plan Alias for --dry-run (#947). Sets the exact same internal flag; no separate code path. Prefer this spelling when the goal is previewing plan.processes (resolved process -> engine -> model routing) rather than checking what would be written.
--bundle Select the bundle the run improves and writes to (default: defaultWriteTarget, else the working bundle); only that bundle's assets are planned. When the ref scope is bundle-qualified, it must name the same bundle
--limit <n> Cap the refs the run processes, highest salience first (refs routed to distill only come last). Overrides the strategy's processes.reflect.limit and limit
--timeout-ms <ms> Wall-clock budget for the run (default: 7200000 = 2 hours)
--require-feedback-signal Turn the fallback lanes (high salience, proactive maintenance) off for the run: they only select and score assets, and a rewrite needs negative feedback
--strategy <name> Override the active improve strategy (a built-in or entry under improve.strategies)
--json-to-stdout Also emit the full persisted JSON result on stdout for a live run. Without this flag, stdout stays empty. Dry-runs always emit their result and are never persisted.
--skip-if-locked If another improve run already holds the lock, skip gracefully (exit 0) instead of failing with "already running" (exit 75, TransientError, code IMPROVE_LOCK_HELD — field follow-up to #948: two legitimate improve invocations colliding on this lock is ordinary, retryable contention, not a broken config file). Use for high-frequency scheduled runs so they don't pile up failures while a longer run is in progress.
--require-engines Abort (exit 78, before any indexing, lock, or log side effect) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment, OR whose endpoint fails a bounded reachability probe — the same probe akm health's default-llm-engine/configured-engines checks run, once per distinct endpoint. An agent engine's check is that its binary is on PATH, and an opencode-sdk engine's is both its binary and, when it sets llmEngine, that LLM fallback's endpoint. Without this flag, improve degrades gracefully: it skips the affected processes and reports them in the result's skippedProcesses. Recommended alongside --skip-if-locked for scheduled runs, since the operator's own shell can pass config validation while a scheduler's stripped-down environment (see #953) cannot.
--show-prompt Print the composed reflect prompt (#952) for one asset and exit — before any lock, index write, or engine dispatch. Requires a fully-qualified asset ref as the scope (akm improve lessons/my-lesson --show-prompt); rejected with a type or whole-bundle scope. The default output format is JSON, which carries the prompt as a prompt field (escaped into one line) alongside the resolved engine/engineKind; pass --format text to print the prompt itself, unwrapped and readable by eye.
--sync / --no-sync Commit (and optionally push) the git-backed primary bundle when the run finishes. Default: on for git-backed bundles (per profile config).
--push / --no-push Push after the end-of-run sync commit when writable with a remote configured. --no-push commits only, skipping the push. Default: per profile config (true). sync.push stays outside the autonomy gate — this is a per-run opt-out, not a default change.

akm improve is the public entrypoint for whole-bundle, type-scoped, and ref-scoped improvement. It owns the memory-cleanup and lesson-distillation flow. A qualified scope such as team//skills/code-review selects that bundle; a different explicit --bundle is a usage error.

A run improves one bundle, the one it writes to, and plans only that bundle's assets: an asset that lives in another bundle is left alone even when that bundle is writable, and a bare ref scope (akm improve skills/x) resolves inside the write target only. To improve another bundle, name it (akm improve --bundle team, or akm improve team//skills/code-review). A scheduled akm improve therefore covers only its write target: schedule one akm improve --bundle <name> run per other bundle. --dry-run and --plan resolve the bundle the way a live run does (the working bundle starts from AKM_BUNDLE_DIR, then defaultBundle), so they preview the bundle a live run improves.

Every stage records what it did with each asset in the improve ledger (improve_ledger in state.db) and reads it before any model call: an asset whose proposal was rejected waits 14 days (reflect), 30 days (distill) or 7 days (other stages) before it is tried again; an expired proposal waits one day; an asset a stage looked at and left unchanged is revisited after 7 days, or as soon as new feedback (or, for consolidation, an edit) arrives.

Consolidation's promotion of a memory into knowledge/ is the exception to the 7-day rule: once a promotion is accepted or rejected, or the model judged the memory and proposed nothing, the memory is not offered to the model again until its body changes, however long that takes. The ledger records the body hash the promotion was decided against and compares it with the memory's current body (frontmatter edits do not count), the same content-driven rule the consolidate pair pass uses. A promotion decided by an older release, which recorded no hash, keeps the old windows. A memory whose body equals that of a consolidate promotion rejected on or after 2026-09-29 is held the same way, under whatever name it has. Consolidation also does not promote a memory that knowledge/ already covers: before it queues a promotion it compares the memory with the 20 knowledge/ docs in its bundle nearest to it by stored vector, and skips the memory when one of them holds at least 30% of its distinct 5-word shingles (skip reason dedup_covered_by_knowledge in the result's consolidation.skipReasons). A covering doc that ranks lower than the 20th nearest goes unseen. With no stored vector (semantic search off, or the memory not indexed yet) that check does nothing and the exact slug and whole-body checks still apply.

No built-in strategy turns the improve-stage extract process on, and only proactive-maintenance turns proactive maintenance on, which selects and scores due assets but plans no rewrite. Use that strategy or set the selected strategy's process enabled: true to opt in. The stage toggle does not disable a direct akm proposal extract --type <harness> or akm proposal extract --auto invocation.

The maintenance pass run by improve also expires stale proposals: any pending proposal older than the top-level archiveRetentionDays config key (default 90, not improve.archiveRetentionDays) is moved to the archive with the reason expired: no action within retention window and a proposal_expired event is emitted (a proposal put back by akm proposal reopen is counted from the reopen, not its original creation). Set archiveRetentionDays to 0 to disable expiration entirely. The total expired count surfaces in the improve result as proposalsExpired.

improve never promotes proposals on its own — there is no confidence gate. Every generated proposal lands in the queue with a pending status and is adjudicated later with akm proposal accept / akm proposal reject or the drain engine. Reflect still emits a confidence score (0..1) in its JSON response schema; it is recorded on the proposal for triage and ranking, but no threshold auto-accepts anything.

Selection plans a reflect (a proposed rewrite) only for refs with negative feedback in the last 30 days newer than the stage's last ledger attempt, or for an explicit ref scope. A positive or note-only signal never plans one, so improve does not rewrite an asset from a positive signal. Distill keeps its own trigger: a memory with feedback of any kind (a signal or a note) in that window, newer than distill's last attempt. It skips a memory flagged wrong and not edited since (a negative feedback in that window judged the body it still has, or, recorded without that body's hash, is newer than the file's last write), and a memory whose only feedback in that window is positive with no reason or note, unless the ref is explicit. Two fallback lanes pick refs with no such feedback: high salience (content-scored refs at or above improve.salience.salienceThreshold, default 0.75, that were never reflected, capped at 10% of the limit, at least one ref) and, in a strategy that enables proactiveMaintenance, refs due for a revisit. They only select and score refs (salience and outcome) and plan nothing, so improve does not rewrite on a proactive cadence; they pick only refs in the retrieval scope: returned by search, curate or show, or named by feedback, in the last 90 days, or new material no improve stage has processed. The planned refs are ranked by salience and cut to the limit; an explicit ref scope bypasses every gate. Use --require-feedback-signal to turn the fallback lanes off for the run.

When the active strategy enables a process (or the triage judgment engine) whose engine or credential cannot be resolved in this process's environment, the run does not silently do nothing for it: the process is skipped, and the result carries skippedProcesses — an array of {process, configKey, reason} entries (omitted entirely when nothing was skipped). When the process resolved a real engine whose credential just isn't reachable here (as opposed to never resolving an engine at all), the entry also carries the structurally resolved engine/model/contextLength it would have used — never the credential itself. ok and the exit code are unchanged either way, matching extract's skipReasons contract: consumers that need to know branch on skippedProcesses (or pass --require-engines to abort instead of degrading). reason names which engine and which credential reference (an env var, apiKeyFile path, or secret:// reference — never its value) is missing. A --dry-run/--plan preview never dispatches, so it never aborts on an unavailable credential either — even a strategy left with every process disabled this way still returns its plan, with the affected processes in skippedProcesses.

--timeout-ms is a run-wide wall-clock budget: when it expires, the run cooperatively aborts any in-flight engine request (the same AbortSignal every LLM call already honors) instead of waiting out the engine's own, much longer, per-call timeout — the run then finishes and reports normally rather than hanging past its budget. SIGTERM/SIGINT/SIGHUP end a live run the same way, within a short bounded grace period, and the process exits with a stable per-signal code (143/130/129) rather than needing a kill -9. If a live run has waited more than a few seconds without any engine response at all, one default-level line ("Still waiting for the first engine response...") is printed so a scheduled run's log is never silently empty while an engine is slow or dead.

For dry runs, plannedRefs is the effective post-limit work set, not every ref in the requested scope. The plan object preserves both views: raw scope size and per-gate removals, configured and effective caps, final ranked refs and their selection lanes, proactive and consolidation statistics, stage decisions, triage mode/caps, and snapshot.status/snapshot.reason for the read-side index boundary. limits.effective is the cap on the refs the run dispatches; the replay lane is retired, so limits.additiveReplayAllowance is always 0 and limits.totalCeiling equals the cap (omitted when the run is unbounded). A missing or incompatible index is an explicit empty snapshot and is not created or migrated. plan.mode is estimate and plan.dispatch is false; live JSON results use the same projection with mode: "execution". The dry result is a best-effort observation assembled during that invocation, not an atomic cross-store snapshot or a reservation. Live execution re-inspects mutable inputs, so a later run can differ after index, state, filesystem, clock, or session-log changes.

plan.processes (#947) is the resolved process -> engine -> model routing table: one row per improve process (reflect, distill, consolidate, memoryInference, extract, validation, triage, proactiveMaintenance), plus a triage.judgment row when the strategy configures a judgment engine. Each row carries enabled, the resolved engine, its model (when the engine has an LLM connection) and engineKind, this process's own lowering notices, and — for reflect/distill/consolidate only — eligibleRefs, the count of this run's effectiveRefs the process would act on (shouldSkipRef's allowedTypes/excludeRefPrefixes (reflect only)/ process-disabled check; a count, not a per-ref matrix, to keep the envelope bounded). A row that could not resolve an engine or credential carries unavailable: {configKey, reason} — the same data behind skippedProcesses above, reshaped per process. When the process resolved a real engine whose credential just isn't reachable here, the row still carries that engine's engine/model/engineKind alongside unavailable, rather than omitting them the way a never-configured process does — so a preview can show what would have run. This table is resolved before any dispatch on every invocation (dry or live), so akm improve --dry-run --strategy <name> (or --plan) previews an ad-hoc strategy override without changing config first; akm health's active-improve-strategy check performs the equivalent resolution but only for the configured default strategy (defaults.improveStrategy), and reports no model or per-process notices. Neither --dry-run nor --plan probes engine reachability over the network — pair with akm health --probe (or the default probe-on behavior) to check whether a named engine actually answers.

--show-prompt (#952) is the cheapest way to exercise reflect alone: it builds the exact prompt reflect would send for one asset — the same source resolution, runner selection, feedback/schema-hint/rejected-proposal gathering akm improve's live reflect step uses — and prints it without reading a credential, so it never calls an engine. An LLM engine receives the reply's JSON Schema as response_format, and an agent engine as an instruction that dispatch appends to this prompt. Add --format text (the default JSON/yaml envelope escapes the prompt into one line, which defeats a by-eye read) to confirm by eye that recent feedback is framed as a signal (never a fact to insert) and that the response contract asks only for confidence and a frontmatterPatch of description, when_to_use and title: akm keeps the body.

When reinforced facts need promotion, knowledge is the higher-authority destination than memory.

improve report

akm improve report (#944) answers "which engine did each model-calling process use this run, how much did it cost, and which enabled processes made zero calls (and why)" without hand-written SQLite against state.db. It is a scope value, not a subcommand — report is not, and will never be, a real asset type, so it is intercepted before any lock/log/index side effect (same precedent as the retired canary scope).

Every real (non-dry-run) akm improve invocation persists a usageReport field on the result (result_json in improve_runs, and in the --json-to-stdout / dry-run JSON): { byProcessEngineModel, noCalls }. byProcessEngineModel is a cross-tab of the LLM call records this run's own usage sink collects (#576) — one row per distinct (process, engine, model) triple, each with calls, failures, promptTokens, completionTokens, totalTokens, reasoningTokens, and totalDurationMs. noCalls lists every model-calling process (reflect, distill, consolidate, memoryInference, extract, validation — not triage/proactiveMaintenance, which never make an attributable LLM call themselves) the active strategy enabled but that ended the run with zero calls, each with a reason drawn from the existing skip-reason vocabulary: "engine_unavailable" (also in skippedProcesses), "autonomy_gated", "strategy_filtered_all_passes", a reflect/distill dominant skip reason (e.g. "no_change" for reflect, "no new signal since last proposal" for distill), or "no_signal" as the fallback — never a fabricated category. The field is omitted entirely when both would be empty. The same table is printed to stderr ([improve] usage report ...) after every real run, independent of --json-to-stdout.

akm improve report reads that field back: with no flags, the most recent real run; --run <id>, one specific run; --since <window>, summed across every real run in the window (byProcessEngineModel rows merged by (process, engine, model); noCalls lists a process only if it made zero calls across every included run). A run recorded before 0.9.15 has no persisted usageReport — the command recomputes byProcessEngineModel from that run's stored llm_usage events (summarizeLlmUsageCrossTab) instead of erroring, sets noCalls to [] (eligibility reasons are not reconstructable after the fact), and adds a notes entry saying so rather than fabricating precision the old row can't support.

improve judge

akm improve judge runs reflect's quality judge on one revision and prints its verdict, for testing a judge engine on revisions whose right answer you know. It reads {"source": "...", "candidate": "...", "feedback": "...", "ref": "..."} JSON from stdin (feedback and ref are optional; ref names the revised asset, which a judge on an agent engine may read), judges with the engine the strategy's processes.reflect.qualityGate.engine names (--strategy picks the strategy), and prints { engine, pass, score, reason, criteria } with the gate's prompt and pass rule. A score of -1 means the judge gave no verdict. It writes nothing.

proposal

Manage the proposal queue. The canonical grammar is akm proposal <verb>: extract, new, list, show, diff, accept, reject, reopen, revert, drain. Bare akm proposal is a usage error (exit 2) as of 0.9.0 — it used to behave as akm proposal list; name the verb. There are no flat-verb spellings (akm proposals, akm extract, akm propose, akm accept, akm reject, akm diff, akm revert) — use the akm proposal <verb> form.

list, show, diff, accept, reject, reopen, and revert (and bulk accept/reject) support --queue <source>. It selects the proposal queue stored for that configured writable source root; without it, commands use the primary queue. Queue selection is not a destination override. drain does not take --queue — it operates on the standing backlog via a policy, not a single queue.

New qualified proposals record their destination source name and materialized root. proposal diff, accept, and revert use that recorded target by default; an explicit --target must resolve to the same source and root or the command fails with exit 2. An unbound proposal in a selected non-primary queue uses that authenticated queue root. A short historical unbound proposal mutation requires either an explicit --target or a selected --queue that authenticates its root; it never falls back to an ambient write target.

proposal extract

Extract durable insights from native coding-agent session files (claude-code, codex, opencode) and queue them as proposals. This is the standalone entrypoint for session extraction — it replaces the legacy session-checkpoint hook and runs independently of the improve-stage extract toggle (see improve above).

akm proposal extract --type claude --session-id <id>
akm proposal extract --type claude --since 24h
akm proposal extract --type opencode --since 7d --dry-run
akm proposal extract --type codex --since 24h
akm proposal extract --auto                 # iterate every available harness
akm proposal extract --type claude --location /custom/path --session-id <id>
Flag Description
--type <harness> Harness name (claude, codex, opencode). Required unless --auto.
--session-id <id> Process only this session ID. When absent, discover sessions via --since.
--location <path> Override the harness's default session-discovery location.
--since <cutoff> Discovery cutoff. ISO timestamp or duration (24h, 7d, 30m). Default 24h.
--auto Iterate every available harness with the default --since. Mutually exclusive with --type.
--dry-run Show candidates without queuing proposals.
--force Re-process sessions even if they were already extracted and have no new events. Default: skip already-seen sessions.
--timeout-ms <ms> Per-session LLM timeout in ms (default 600000).
--engine <name> Named engine for this invocation: an LLM engine, or a claude, opencode or opencode-sdk agent engine. Mutually exclusive with --strategy.
--strategy <name> Improve strategy supplying extract behavior and engine. Mutually exclusive with --engine.

--type and --auto are mutually exclusive; one of them is required. --auto iterates getAvailableHarnesses() — every harness with a detectable session-log location on the current machine — and returns an aggregated extract-auto-result envelope (harnessesProcessed, totalProposals, per-harness results); the run exits non-zero only when every harness failed.

The codex harness reads Codex's rollout files under $CODEX_HOME/sessions (~/.codex/sessions by default; --location points at another rollout directory). It lists a person's sessions, codex exec runs included, and leaves out the rollouts Codex writes for subagents and its other internal agents: those are not sessions of their own.

There is no akm proposal extract --watch/--debounce-ms either (0.9.0: dropped — a foreground polling daemon in a one-shot CLI); the shipped core/extract.yml cron template (akm proposal extract --auto on a schedule) is the answer.

Requires an engine that can run unattended model work (an LLM engine, or a claude, opencode or opencode-sdk agent): pass --engine, select a --strategy whose processes.extract.engine is set, or configure defaults.llmEngine.

Output. ok means the command ran to completion — it is true even when every session was skipped (an unreachable LLM engine included); it does not mean anything was harvested. Consumers that need "did this run actually harvest" branch on skipReasons, warnings, or sessionsProcessed / sessionsSkipped instead. The envelope also reports:

Field Description
engine Resolved engine name for this run. Absent only when extract is disabled by the selected improve strategy (the run returns before an engine is resolved).
engineKind "llm", "sdk", or "agent" — the kind of runner engine resolved to. Same absence condition as engine.
skipReasons Per-skipReason count across sessions[] (e.g. { "llm_unavailable": 25 }). Present only when sessionsSkipped > 0.
warnings Includes one aggregate line per infrastructure skip reason that fired (llm_unavailable, read_failed, exception, locked_concurrent) — e.g. 25 of 25 sessions skipped: llm_unavailable (engine "default") — so an engine outage is visible without inspecting sessions[]. Session-content skips (already_extracted, too_short, triaged_out) are counted in skipReasons but never produce a warning line.
sessions[].warnings One line per candidate the model wrote that the output contract refuses, as <type>:<name> dropped: <reason> — a lesson without a when_to_use of 15 characters, a description under 20, a name that is not a kebab-case slug — and one per candidate held back by the improve ledger. A session whose every candidate was dropped has candidateCount: 0 and no rationaleIfEmpty, so this is where the loss shows.

proposal new

Generate a brand-new asset proposal from a description. Output is always a proposal — never a direct write.

akm proposal new <type> <name> --task "..."
akm proposal new <type> <name> --file ./prompt.md
akm proposal new skill code-review --task "PR-style review skill"
akm proposal new lesson docker-cleanup --file ./prompts/docker-cleanup.md
akm proposal new skill code-review --path team --task "PR-style review skill"  # writes under skills/team/
Flag Description
--path Relative subdirectory under the type dir to place the proposed asset in (e.g. release). The filename comes from <name>.
--task Inline task text
--file Read task text from a UTF-8 file
--engine Override the default execution engine. Any kind works: an LLM engine, an agent CLI or opencode-sdk
--timeout-ms Override the selected engine timeout for this call

Exactly one of --task or --file is required. Emits propose_invoked.

Every engine kind returns the proposal the same way: as one JSON object on stdout with the asset's ref, its full content and a self-rated confidence. akm sends the object's JSON Schema with the request, as response_format to an LLM engine and as an instruction at the end of the prompt to an agent engine (codex also gets it as --output-schema). akm captures the reply; an agent CLI runs headless, with no live terminal session. A harness's own JSON envelope, such as claude's --output-format json result, is unwrapped first. A reply that is not a valid proposal gets one corrective retry that says what was wrong. If that reply is not valid either, the command exits 1 with reason: "parse_error" and an error that names the engine, for example Engine "local" reply was not valid proposal JSON after 2 attempts: ….

Prompt-task timeoutMs: a version-2 prompt task may set timeoutMs to override its selected engine timeout. Set it to null to disable the timer, or to a positive integer (milliseconds) to apply a task-specific limit.

proposal list

List proposal queue entries.

akm proposal list
akm proposal list --queue team-bundle
akm proposal list --status pending|accepted|rejected|reverted
akm proposal list --ref skills/deploy
akm proposal list --generator consolidate-pair
Flag Description
--queue <source> Select the proposal queue by configured writable source name
--status Filter by pending, accepted, rejected, or reverted
--ref Filter by asset ref. A qualified ref preserves bundle identity; a short ref matches that concept in the selected queue
--type Reserved type filter
--generator <name> Filter by generator/source (e.g. reflect, distill, consolidate-pair) — the same value accept/reject --generator take

Each retire proposal's retirement.continuityRisk, when present, also shows in the default listing (⚠ continuity-risk inline) and in the text output of proposal show and proposal diff (the specific failing/unverified queries) — see Retirement continuity.

Each proposal record carries an optional confidence field (0..1) emitted by reflect/propose runs. It is recorded for triage and ranking only — there is no confidence gate or auto-promotion; proposals are adjudicated with akm proposal accept / reject. Once accepted, a proposal that overwrote an existing asset also carries a backup field pointing to the captured prior content, which akm proposal revert uses.

proposal show

Inspect a queued proposal and its validation findings.

akm proposal show <id>
akm proposal show <id> --queue team-bundle

proposal accept

Accept a proposal and promote it into its recorded destination. Accepts a full UUID, an 8-character UUID prefix, or an asset ref.

akm proposal accept <id>
akm proposal accept 7c115132                  # 8-char UUID prefix
akm proposal accept skills/akm-dream           # Asset ref
akm proposal accept <id> --queue team-bundle
akm proposal accept <id> --target team-bundle  # Must match a recorded target
akm proposal accept --generator reflect -y    # Bulk-accept by generator (requires -y)
akm proposal accept --generator reflect --max-diff-lines 50 -y    # ...only if <= 50 lines
akm proposal accept --generator reflect --older-than 7 --dry-run  # Preview a bulk accept
Flag Description
--queue <source> Select the proposal queue by configured writable source name
--target <name> Write destination; must match the proposal's recorded target
--generator <name> Bulk-accept all pending proposals from this generator (e.g. reflect, distill). Requires no positional id.
--max-diff-lines When bulk-accepting, only accept proposals whose content is <= this many lines. Larger proposals are skipped.
--older-than When bulk-accepting, only accept proposals created (or last reopened) more than this many days ago
--dry-run List proposals that would be bulk-accepted without accepting them
-y, --yes Skip confirmation (required in non-interactive mode for bulk accept)

Bulk-accept all pending proposals from one generator with --generator <name> (e.g. reflect, distill) and no positional id. Bulk accept requires -y/--yes in non-interactive shells.

When the destination bundle is a git repository (a .git directory, whatever its source kind), each accept is committed as it happens, with exactly the paths it wrote or removed and the subject akm accept: <generator> <proposal-id-8> <ref>. A retirement's archived copy and tombstone, and the source memory a consolidate promotion retires, are part of the same commit. The commit is local: akm sync, or the end-of-run sync of akm improve, pushes it. A commit that fails warns and the accept stands.

proposal reject

Reject a proposal and archive the reason. Accepts a full UUID, an 8-character UUID prefix, or an asset ref. akm proposal reopen undoes a rejection.

akm proposal reject <id> --reason "duplicates existing workflow"
akm proposal reject <id> --queue team-bundle --reason "duplicates existing workflow"
akm proposal reject 7c115132 --reason "not ready"      # 8-char UUID prefix
akm proposal reject skills/my-skill --reason "not ready" # Asset ref
akm proposal reject --generator reflect --reason "noisy" -y  # Bulk-reject by generator
akm proposal reject --generator reflect --reason "noisy" --max-diff-lines 50 -y
Flag Description
--reason Reason for rejection (required)
--queue <source> Select the proposal queue by configured writable source name
--generator <name> Bulk-reject all pending proposals from this generator (e.g. reflect, distill). Requires no positional id.
--max-diff-lines When bulk-rejecting, only reject proposals whose content is <= this many lines. Larger proposals are skipped.
--older-than When bulk-rejecting, only reject proposals created (or last reopened) more than this many days ago
--dry-run List proposals that would be bulk-rejected without rejecting them
-y, --yes Skip confirmation (required in non-interactive mode for bulk reject)

Bulk-reject all pending proposals from one generator with --generator <name> and no positional id. Bulk reject requires -y/--yes in non-interactive shells.

proposal reopen

Undo a rejection: move rejected proposals back to pending so they can be reviewed again (a proposal that retention expiry archived is a rejected one too). A rejection is otherwise final. accept refuses anything that is not pending, and a rejected consolidate pair-pass retire proposal also keeps the pair pass from proposing that retirement again while both documents are unchanged.

akm proposal reopen <id>
akm proposal reopen <id> --reason "the diff was misrendered (#997)"
akm proposal reopen <id> <id> <id>                 # several at once: all or none
akm proposal reopen <id> --queue team-bundle
# One id per call, and only the rejections whose reason says the diff was misread:
akm proposal list --status rejected --generator consolidate-pair \
  --detail normal --format json \
  | jq -r '.proposals[] | select(.review.reason // "" | test("blank line"))
      | .id' \
  | xargs -r -n 1 akm proposal reopen --reason "diff was misrendered"
Flag Description
--reason <text> Why the rejection is being undone. Kept in the proposal's review history and the proposal_reopened event, and in its ledger row's detail when it has a row
--queue <source> Select the proposal queue by configured writable source name

Takes full proposal ids: a UUID prefix only matches pending proposals, so it cannot name a rejected one (akm proposal list --status rejected prints the ids; add --generator consolidate-pair for the retire backlog). An asset ref also resolves, to the newest proposal for that ref, but only while none is pending, and it never reaches a retire proposal, which is named by its id.

Check a rejection's reason before reopening it. The default brief output of akm proposal list leaves it out; --detail normal --format json shows it as review.reason. Reopen only the rejections you want back, since a deliberate rejection would otherwise be undone with the rest. The pattern in the example, test("blank line"), matches the reason given in the 0.9.19 upgrade note (the diff read as a blank-line replacement); change it to yours. Pass one id per call (xargs -n 1) so a refusal skips only that proposal, and use xargs -r so GNU xargs does not run reopen with no id when nothing matches. A retire proposal refused because another pending retire proposal involves the same document can be reopened once that one is decided.

Reopening is refused, with the reason, when:

With several ids nothing is reopened unless every one can be: the error lists each refusal (exit 2).

A reopened proposal is pending again with its review cleared. The rejection (its review, and any gate verdict that came with it) is appended to the proposal's reviewHistory, which akm proposal show prints as one line per reopen: reopened: <when> (<reopen reason>), undoing rejected: <why> (<when>). The gate verdict is cleared so the drain treats the proposal as undecided, except a deferred one, the quality gate's hand-off to a person, which stays so the drain keeps leaving the proposal for that person.

A reopened retire proposal no longer counts as a settled pair for the pair pass, and while it is pending that pair is not proposed a second time. The proposal's improve_ledger row goes back to what the mint wrote (a retire proposal's mint writes none, so the row its rejection created is dropped). The age that retention expiry and --older-than see restarts at the reopen (retire proposals never expire), and a proposal_reopened event is appended. Accepting a reopened retire proposal archives the retired file exactly as for any retire proposal, and akm proposal revert restores it byte-exactly.

Output: for one id, the envelope reject returns (ok, id, ref, the proposal, and reason, here the reopen reason); for several ids, { reopened, results } with one such envelope per proposal.

proposal revert

Revert an accepted proposal by restoring the prior asset content from the backup captured at promotion time. Only works on proposals that overwrote an existing asset; new-asset proposals leave no backup. Sets the proposal's status to reverted and appends a proposal_reverted event to the audit log.

akm proposal revert <id>
akm proposal revert skills/akm-dream           # Asset ref
akm proposal revert <id> --queue team-bundle
akm proposal revert <id> --target team-bundle  # Must match a recorded target
Flag Description
--queue <source> Select the proposal queue by configured writable source name
--target <name> Select the destination for an unbound proposal, or confirm a recorded destination; a conflict with a recorded target is rejected

Accepts the full proposal UUID or the asset ref. UUID prefixes are not supported for reverting (archived proposals require the full identifier). Errors with exit code 2 if the proposal is not in accepted status, has no captured backup, or cannot be found.

proposal diff

Preview the proposed change against the live asset. Accepts a full UUID, an 8-character UUID prefix, or an asset ref directly.

akm proposal diff <id>
akm proposal diff skills/akm-dream             # Asset ref form
akm proposal diff 7c115132                    # 8-char UUID prefix
akm proposal diff <id> --queue team-bundle
akm proposal diff <id> --target team-bundle    # Must match a recorded target
Flag Description
--queue <source> Select the proposal queue by configured writable source name
--target <name> Select an unbound destination or confirm a recorded one for proposal accept, diff, or revert; a conflict with a recorded target is rejected

proposal accept runs full validation before promoting. proposal reject requires --reason.

A retire proposal (the consolidate pair pass's consolidate-pair retirements) writes no content: accepting it archives the retired file under .akm/memory-cleanup/archive/ (nothing is deleted), and akm proposal revert restores it byte-for-byte. Its diff shows just that, the retired file's lines as removals and nothing added, under a retire header. It does not present the file as replaced by a blank one, which is how earlier releases rendered it:

$ akm proposal diff <id>
# proposal <id> (retire: memories/old-note -> memories/new-note)
retire.label: duplicate (cosine=0.986)
retire.reason: Same durable facts, B adds nothing new.
note: Accepting archives the retired file under .akm/memory-cleanup/archive/ (nothing is deleted); `akm proposal revert` restores it byte-exactly.
--- stash//memories/old-note (existing)
+++ /dev/null (retired: archived; successor memories/new-note)
@@ 1,5 0,0 @@
----
-description: an old note
----
-The durable fact.
-A second line.

The JSON result carries three more fields for a retire proposal, and none of them on any other proposal: op ("delete"), retirement, and note (what accept and revert do to the file). retirement uses the keys proposal show reports the pair under: retiredRef, successorRef, judgeLabel, judgeReason (the judge's own text; the stored block's reason is the tombstone vocabulary and is not repeated), cosine, and continuityRisk when the retirement continuity check flagged the pair. The text output prints the same verdict lines show does, continuityRisk and its failing queries included, above the diff. isNew is always false for a retire proposal; when the retired file is already gone (retired, or removed by something else), the diff is only its two header lines, --- <ref> (missing) and the +++ line.

proposal drain

Drain the standing pending-proposal backlog instead of adjudicating proposals one at a time. One rule decides each proposal: a proposal whose quality judge passed on its current content is accepted (unless its target changed since it was minted — that one is auto-rejected as stale-target); an empty diff is rejected; a proposal that reflect or distill deferred for review is left for a person; everything else goes to the judgment tier when one is enabled, and is otherwise left for review. A reflect revision that changes the body, and every distill lesson or knowledge promotion, is deferred for review even when its judge passes it. Default mode stages decisions (queue mode); pass --promote to actually accept.

akm proposal drain --dry-run                        # Preview without writing
akm proposal drain --promote -y
akm proposal drain --max-accepts 10 --older-than 7 --promote -y
akm proposal drain --strategy default --promote -y  # Read the triage block from an improve strategy
Flag Description
--strategy Read the triage block (apply mode, ceilings, judgment) from this improve strategy instead
--promote Promote (accept) judge-passed proposals. Default is queue mode — stage only, no writes to assets.
--dry-run List what would be accepted/rejected/deferred, without writing
--max-accepts Hard per-run accept ceiling; accepts beyond this are reported as skippedByCap
--older-than Only consider proposals created (or last reopened) more than this many days ago
--judgment Explicitly enable the judgment tier for this standalone drain, including when the selected strategy says judgment.enabled: false; execution overrides still come from that strategy. Without this flag, strategy judgment config does not enable standalone drain judgment. A missing runner remains a no-op with a logged triage_deferred summary.
-y, --yes Skip the confirmation prompt (required in non-interactive mode for promotion)

feedback (--reason)

akm feedback accepts an optional --reason <text> flag whose value is forwarded into feedback metadata and consumed by improve/distill proposal prompts. Negative feedback requires a reason by default: say what is wrong and what should change.

Write the reason about the asset's content. Reflect treats it as an unverified report to investigate, not a fact to insert, and is told to leave the section unchanged when the reason asks for information the asset lacks. Distill's quality gate rejects a lesson that is off-subject for the asset it was distilled from and sends a borderline one to review. A command that failed (akm show erroring on the ref, say) says nothing about the asset, so it is not a reason to record against it.

task

akm task is the scheduling surface for workflows, agent prompts, and shell commands. It manages on-disk task definitions under <bundle>/tasks/<id>.yml and reconciles them with the OS-native scheduler (cron / launchd / schtasks). Task source v4 YAML (version: 4) is the only executable source contract this release accepts; akm task add writes v4 — see the canonical Tasks reference. The group is add | enable | disable | run | explain | validate | list | sync | doctor | history | prune — there is no show or remove; use akm show tasks/<id> to inspect one task. Use task enable / task disable for host-local activation; edit the file only to change the authored schedule or remove the task. task list is a delegating alias for akm search --type task — both spellings return the identical envelope.

akm task list                                # List tasks (cross-bundle) — alias for `search --type task`
akm show tasks/<id>                          # Inspect one task
akm task add <id> --schedule "@daily" \     # Register a new task and install it
  --command "akm improve --strategy default"
akm task add review --schedule "@daily" --prompt "Review recent changes" --engine reviewer
akm task add nightly --schedule "@daily" --command "akm improve" --disabled  # register but leave off
akm task add nightly --schedule "@daily" --command "akm improve" --force    # overwrite an existing task id
akm task enable team//tasks/nightly       # Add local activation and sync its bundle
akm task disable team//tasks/nightly      # Remove local activation and unschedule it
akm task run <id>                           # Execute now (what the scheduler calls)
akm task explain <ref>                      # Read-only: declared inputs, target, schedule — spawns nothing
akm task validate <path>                    # Read-only: parse one task file by path, report sync's diagnostic
akm task history [<id>] [--id <id>] [--limit <n>]  # Recent runs from state.db (positional id == --id)
akm task sync                               # Reconcile activated refs from all enabled configured bundles
akm task sync --dry-run                     # Preview the reconcile — zero scheduler writes
akm task sync --rebind                      # Also capture the current installed runtime
akm task doctor                             # Report scheduler backend + paths
akm task prune                              # Preview orphaned scheduler entries — zero writes
akm task prune --yes                        # Remove every currently-computed orphan
akm task prune --id ghost,stale --yes       # Remove only the named orphan ids

task add also accepts --disabled (write the task but leave its ref out of this host's scheduler activation), --force (overwrite an existing task with the same id), and --rebind (also point the bundle's installed scheduler rows at this akm invocation, as akm task sync --rebind does).

akm task list [<query>] [--limit <n>] [--from local|registry|all] is a pure alias for akm search --type task with the query, --limit, and --from flags passed through — same envelope, same results alias, no second implementation. 0.9.0 removed task list as a redundant implementation of task listing (see the 0.9.0 CHANGELOG entry); this reintroduces only the spelling, not the logic.

akm task explain <ref> [input flags] prints a task's declared inputs:, the values that would actually be supplied (with provenance), the resolved target, effective execution settings, and schedule bindings — read-only: it never spawns anything, writes history, or touches the scheduler. A secret-shaped value prints as <redacted>. See akm task explain.

akm task validate <path> parses ONE task file by filesystem path — not a concept ref or id, and the file need not live in any configured bundle — and reports the same diagnostic akm task sync would produce for it, INCLUDING sync's own cron-dialect check and its per-schedule-entry input-contract check (so a file sync would reject can never be reported valid here): {ok, path, sourceVersion, outcome, reason?, resolved?} where outcome is valid (parses as task source v4 directly and passes both sync checks), blocked (task v2/v3 that must first be rewritten by akm migrate apply), invalid (the YAML doesn't parse, or the document fails schema validation, or it parsed but fails one of the two sync checks), or not-a-task (the YAML parses but never declares a version: field — not shaped like a task source). resolved is the compiled task shape akm task sync itself would build a scheduler binding from — id, the compiled schema version, resolved uses/run target, declared inputs contract, and schedule bindings — present only on valid. Unlike akm task explain, it never runs execution lowering: a command-kind task validates the same whether or not the local config has an engine configured. Exits 0 for valid, 1 for blocked/invalid/not-a-task, 2 for a missing or unreadable path. Read-only: it never touches the scheduler and never requires the file to be indexed or wired into a bundle.

akm task run is what cron / launchd / schtasks invoke at the scheduled time. Each run is recorded as a row in the durable task_history table (state.db), surfaced by akm task history — not by akm log; there is no task_invoked/task_completed event type on the akm log stream.

Task source cannot enable itself. akm task enable <fully-qualified-ref> adds the ref to this host's scheduler.enabled list and syncs that bundle; akm task disable removes it and unschedules the task. Manual akm task run remains available. To remove a task, delete its file (<bundle>/tasks/<id>.yml) and run akm task sync — sync uninstalls the orphaned scheduler entry.

A config with no scheduler.enabled list at all (written before 0.9.17) means "keep what is installed": akm task sync takes the akm-written rows already in the scheduler as this host's choice and writes the list; an explicit list is never second-guessed. akm task sync --dry-run prints the planned adds/updates/removes (removals carry their owning bundle) without touching the scheduler — zero writes. Exits non-zero when removals are pending, so it can gate a CI/health check on "sync would change something."

sync's (and sync --dry-run's) result always carries failures: [{path, ref?, reason}] — one entry per item sync could not reconcile: a task/workflow source that failed to parse or prepare (its installed row is left as it is), two sources claiming the same scheduler id, a desired binding whose id is already scheduled from a different bundle or installation, a row whose install or removal failed, or — for an unscoped, multi-bundle sync — a whole bundle whose sources could not be read. Every one of these is a per-item failure: the item is left exactly as it was and reported here, while every OTHER item and bundle in the same sync still reconciles; with --bundle, that one bundle IS the whole sync, so a bundle that cannot be read raises instead of being reported here. failures is empty on a fully clean sync; a non-empty failures still exits non-zero, same as a pending removal. A crontab whose akm markers are malformed is refused unmodified, and another akm process holding the scheduler lock makes sync exit 75 (retry shortly).

akm task prune reclaims installed scheduler entries that sync can never clean up on its own: entries that no longer resolve to a live bundle (a row whose AKM_BUNDLE_DIR names a directory that is gone, or a row written before 0.9.17-alpha.7 whose --scheduler-context descriptor cannot be read). It never touches an entry that still resolves to a live bundle — that's sync's job. Like sync --dry-run, the default is a dry-run preview (zero scheduler writes) that exits non-zero when there are candidates to remove; --yes executes the printed plan, and --id <id1,id2,...> narrows a --yes run (or a preview) to specific binding ids — naming an id that isn't a current orphan candidate (not installed, or it still resolves to a live bundle) is refused with a usage error and removes nothing.

Scheduler activation is host-local config and captures the installed akm runtime. Ordinary task sync reconciles activated refs from all enabled configured bundles while preserving that runtime binding. Use task sync --rebind only after intentionally moving or replacing the installation, or to repair a stale runtime path, then verify the result with akm task doctor. Interactive akm setup reviews every embedded task template (both the core set and the improve-schedule set) and asks once before changing task files or scheduler state; non-interactive setup changes neither.

Because the scheduler runs the exact binary path recorded at the last task sync, upgrading akm through a different installer than the one active at that sync (npm-global to a standalone download, or vice versa) leaves scheduled runs invoking the old, now-stale binary — task sync re-resolves the current path and repoints them. akm health --probe's scheduler-binary advisory warns when the two diverge, naming both versions.

Setup reconfiguration preserves existing scheduler runtime bindings. Changing the AKM storage path or installed runtime path therefore requires an explicit akm task sync --rebind; setup does not silently migrate those entries.

Bundle targeting (--bundle <bundle>). By default read/write commands operate on the primary/default bundle, while an unscoped sync reconciles all enabled configured bundles. add, enable, disable, history, sync, run, and explain accept --bundle <bundle> to schedule, reconcile, or inspect tasks that live in another configured bundle (doctor reports scheduler-wide state and takes no --bundle; validate takes a bare filesystem path instead of a ref, so it has no bundle to target either):

akm task add nightly --schedule "@daily" --command "akm improve" --bundle team-bundle
akm task sync --bundle team-bundle             # reconcile only that bundle

A non-default bundle is recorded in the installed scheduler entry as a --bundle <bundle> token, so the scheduled akm task run resolves the task (and its relative asset refs) from that bundle. sync --bundle limits a run to one bundle; unscoped sync reconciles every configured bundle as one transaction. Scheduler ids are the bare task id and are never namespaced: registering a task whose id is already scheduled from a different bundle is a hard error.

task add accepts exactly one CLI target selector (--workflow <ref>, --prompt <text-or-ref>, or --command <shell>) and writes a task source v4 document (version: 4). In the file, exactly one of uses or run is allowed. uses accepts command, workflow, and script refs plus akm/command; agents and task refs are not executable. run accepts a shell string with the closed shell and contained working-directory contract. Task source v4 has no akm: options bag or on: trigger block — scheduling, resolver overrides, timeout, maxSteps, maxRetries, and redaction names are all top-level keys on the document itself. Normal execution rejects v2 and v3 and points to akm migrate apply --dry-run followed by akm migrate apply. See Tasks for the complete grammar and fail-closed migration behavior.

Task-log redaction and redact:. A task's persisted output — the run .log file and its logs.db rows — is scrubbed before it is written. Two passes run: credential shapes (Bearer …, sk-…, webhook URLs) are matched by pattern, and exact secret values are matched by value. akm knows a value is secret when the config declares it (engines.<name>.apiKey, embedding.apiKey, and the AKM_ENGINE_<NAME>_API_KEY / AKM_LLM_API_KEY / AKM_EMBED_API_KEY recipes), and it infers others from the variable name (*_TOKEN, *_SECRET, *_API_KEY, *_PASSWORD, …) provided the value is at least 8 characters — a short one is far more likely to be a flag than a credential, and redaction replaces substrings, so guessing wrong mangles the log.

Any task kind may add redact: for a secret exported under a name none of those rules recognise:

version: 4
run: ./deploy.sh
schedule: "0 3 * * *"
redact: [ACME_DEPLOY_TOKEN]   # NAMES, never values

akm looks each name up in the environment the run is given; a name that is unset contributes nothing. Names only. A literal secret in a task file would leak far more widely than the redaction closes: task files are indexed into the search database, can be sent to an embedding provider, are printed verbatim by akm show, and ship inside bundles over git and npm. This is the same rule exec units' pass_env: follows.

A workflow-target task executes the same native orchestration as akm workflow run; it does not stop after creating a run. Completion maps to task completed, while workflow failure or verifier rejection maps to task failed. A workflow-target task's declared inputs: (with their default: values) remain the non-CLI way a scheduled definition supplies its new-run parameter snapshot.

Workflow-task run bounds. Top-level timeout, maxSteps, and maxRetries correspond to akm workflow run --timeout, --max-steps, and --max-retries. Unlike the interactive command, a scheduled workflow task gets a default whole-run timeout of 6 hours (DEFAULT_WORKFLOW_TASK_TIMEOUT_MS): nobody is at the terminal to Ctrl-C an unattended run, so without one a single wedged unit hangs the task forever. An explicit timeout always wins, and timeout: null opts out entirely. On expiry the runner aborts the run's signal, which the engine treats as a graceful break at the next step boundary — the journal is kept and the run stays resumable with akm workflow resume <run-id> (the run id is in the task run's detail.error and log). The attempt itself is recorded as failed, so the OS scheduler sees a non-zero exit.

version: 4
uses: workflows/nightly-report
inputs:
  region:
    type: string
    default: us-east-1
schedule: "@daily"
timeout: 3600000   # 1h whole-run bound (omit for the 6h default, null for none)
maxSteps: 20       # optional
maxRetries: 1      # optional