akm docs

Migrating from akm 0.7.x to 0.8.0

0.8.0 is a storage reorganization release. The pre-0.8 self-improvement flow based on reflect / distill has been consolidated into the improve + proposal flow (improve, propose, proposal list, proposal accept, proposal reject). This guide focuses on what changed in storage/runtime state and which legacy write paths were removed.

Most users need to do exactly one thing: run the migration script. The guide below explains what the script does and what you need to verify afterward.

Table of contents

Installing 0.8.0

Pick whichever method you used for 0.7.x:

# npm
npm install -g akm-cli@0.8.0

# bun
bun install -g akm-cli@0.8.0

# From source (contributors)
git fetch origin && git checkout v0.8.0 && bun install

Do not run any akm commands yet. Run the migration script first (see below). The 0.8.0 binary reads from the new locations; data that has not been migrated will appear missing.

What changed in 0.8.0

0.8.0 reorganizes on-disk storage around four XDG-compliant directories and replaces two remaining JSONL write paths with SQLite tables:

Area 0.7.x 0.8.0
Durable databases $CONFIG $DATA (~/.local/share/akm)
akm.lock location $CONFIG/akm.lock $DATA/akm.lock
Event log $CACHE/events.jsonl events table in $DATA/state.db
Registry index cache $CACHE/registry-index/<slug>.json registry_index_cache table in $DATA/index.db
Task run history $STATE/tasks/history/<id>.jsonl task_history table in $DATA/state.db
Task definition files <stash>/tasks/<id>.md (Markdown + YAML frontmatter) <stash>/tasks/<id>.yml (pure YAML)
--target flag inconsistent across commands uniform on remember, import, wiki stash
JSONL fallbacks present for backward compat removed; state.db only

No asset files under $STASH/ are touched. Your memories, skills, lessons, workflows, and other content are unaffected.

Behavior change: --auto-accept is now OFF by default

Action required if you relied on the 0.8.0-RC auto-accept default.

The --auto-accept flag on akm improve has been redefined as a confidence threshold. Earlier 0.8.0 RCs shipped this flag on by default at threshold 90, which surprised operators who expected Phase B operations to require confirmation. The 0.8.0 release flips that default: auto-accept is OFF unless you explicitly pass a threshold value.

If you were already on 0.7.x and never touched the flag, this is a no-op for you — both 0.7.x and 0.8.0 default to interactive review. The only audience that must update scripts is users who were tracking a 0.8.0 RC where the default was ON.

New semantics

Form Effect
(flag absent) Auto-accept OFF (default)
--auto-accept (bare) Auto-accept OFF (treated as flag absent)
--auto-accept=<N> Auto-accept ON at integer threshold N (0-100)
--auto-accept=safe Permanent alias for --auto-accept=90
--auto-accept=false Auto-accept OFF (explicit form, matches the default)
any other value Error

What you need to do

# 0.8.0 default (interactive review of proposals):
akm improve

# Opt in to legacy whole-batch auto-accept:
akm improve --auto-accept=safe        # or --auto-accept=90 — doclint:ignore (0.8.0-only flag, removed in 0.9.0)

# Opt in to a stricter threshold (no-op until confidence scoring lands):
akm improve --auto-accept=95          # doclint:ignore (0.8.0-only flag, removed in 0.9.0)

# Explicit OFF (matches the default; useful for self-documenting scripts):
akm improve --auto-accept=false       # doclint:ignore (0.8.0-only flag, removed in 0.9.0)

Review any scheduled akm improve tasks, agent prompts, or CI invocations that were authored against a 0.8.0 RC with the previous default and update them before upgrading.

Running the migration script

The migration script is the canonical upgrade path. It is idempotent — safe to run more than once.

The akm-migrate binary ships alongside akm in the global install (both npm and prebuilt-binary distributions). Developers working from a source clone can run bun scripts/akm-migrate.ts storage instead. The npm launcher requires Bun >= 1.0; the prebuilt migration binary is runtime-free.

Step 1 — preview:

akm-migrate storage --dry-run

The dry run prints every file move and database import it would perform without touching disk. Review the output. If any paths look unexpected, check your AKM_CONFIG_DIR, AKM_CACHE_DIR, AKM_DATA_DIR, and AKM_STATE_DIR environment variables (see New XDG directory layout).

Step 2 — apply:

akm-migrate storage --yes

The script performs these steps in order:

  1. Creates $DATA and $STATE directories if they do not exist.
  2. Moves $CONFIG/index.db → $DATA/index.db.
  3. Moves $CONFIG/workflow.db → $DATA/workflow.db.
  4. Creates $DATA/state.db with the initial schema.
  5. Copies $CONFIG/akm.lock → $DATA/akm.lock (leaves the original in place until you confirm everything works).
  6. Imports $CACHE/events.jsonl into the events table in state.db. Duplicate rows (same ts + eventType + ref) are skipped.
  7. Imports per-task JSONL files from $STATE/tasks/history/ (or $CACHE/tasks/history/ for older installations) into the task_history table in state.db.
  8. Prints a summary of rows imported and files moved.

After step 2 completes successfully you can delete the legacy files:

# Optional cleanup — only after confirming akm works
rm -f ~/.config/akm/index.db ~/.config/akm/workflow.db
rm -f ~/.cache/akm/events.jsonl
rm -rf ~/.cache/akm/registry-index/

The old $CONFIG/akm.lock can be left in place; 0.8.0 reads only from $DATA/akm.lock and ignores the old location.

New XDG directory layout

akm 0.8.0 uses four XDG base directories:

Variable Default (Linux/macOS) Default (Windows) Override env var
$CONFIG ~/.config/akm %APPDATA%\akm AKM_CONFIG_DIR
$CACHE ~/.cache/akm %LOCALAPPDATA%\akm AKM_CACHE_DIR
$DATA ~/.local/share/akm %LOCALAPPDATA%\akm\data AKM_DATA_DIR
$STATE ~/.local/state/akm %LOCALAPPDATA%\akm\state AKM_STATE_DIR
$STASH ~/akm %USERPROFILE%\Documents\akm AKM_STASH_DIR

What lives where after migration:

Path Contents
$CONFIG/config.json User configuration (unchanged location)
$DATA/index.db Main search index, embeddings, registry cache
$DATA/workflow.db Workflow run state
$DATA/state.db Events, proposals, task history
$DATA/akm.lock Installed stash lockfile
$DATA/config-backups/ Pre-save config snapshots
$CACHE/ Regenerable data: registry downloads, binary cache
$STATE/tasks/logs/ Per-run stdout/stderr logs (unchanged)

If you set AKM_CONFIG_DIR or AKM_DATA_DIR in CI or agent environments, update those environment variables to reflect the new split. The AKM_CONFIG_DIR-as-data-directory fallback (which previously caused data files to land in $CONFIG if AKM_DATA_DIR was unset) is removed in 0.8.0.

Event log: events.jsonl → state.db

The file $CACHE/events.jsonl (one JSON object per line) is replaced by the events table in $DATA/state.db.

What this means for you:

The event schema is unchanged on the wire — the same eventType, ts, ref, and metadata fields are present. New event types added in 0.8.0:

Event type Emitted by Purpose
select akm show (after search within 60s) MemRL selection signal
improve_skipped akm improve (cooldown guards) Observability for skip distribution
promoted / rejected akm proposal accept / akm proposal reject Proposal lifecycle decisions

The search event now carries mode: "semantic" | "keyword" in its metadata field.

Registry index cache: files → index.db

Per-registry JSON files at $CACHE/registry-index/<slug>.json are replaced by the registry_index_cache table in $DATA/index.db.

No manual action is required. The cache is regenerable data: akm fetches and stores a fresh copy the next time it queries each registry. The migration script does not import the old JSON files — they are simply superseded.

After confirming akm works normally, you can delete the directory:

rm -rf ~/.cache/akm/registry-index/

Task history: JSONL files → state.db

Per-task JSONL execution logs at $STATE/tasks/history/<task-id>.jsonl (or $CACHE/tasks/history/ for older installations) are replaced by the task_history table in $DATA/state.db.

The migration script imports all discovered JSONL history files into task_history. Existing entries are not overwritten.

After confirming task history is visible via akm tasks history, you can delete the old files:

rm -rf ~/.local/state/akm/tasks/history/
# or, for older installations:
rm -rf ~/.cache/akm/tasks/history/

Note: per-run stdout/stderr log files at $CACHE/tasks/logs/<task-id>/ are not moved. They remain under $CACHE and are unaffected by this migration.

Task definition files: .md+frontmatter → .yml

In 0.7.x and earlier, scheduled task definitions lived at <stash>/tasks/<id>.md — a Markdown file where the schedule, target, and options were specified as YAML frontmatter and, for inline prompts, the Markdown body served as the prompt text:

---
schedule: "0 9 * * 1-5"
workflow: workflow:daily-backup
params:
  region: us-east-1
enabled: true
---

For inline prompts the old format used prompt: inline as a sentinel and the Markdown body held the text:

---
schedule: "@daily"
prompt: inline
profile: opencode
enabled: true
---

Do the thing.
And the other thing.

In 0.8.0 task definitions are pure YAML files at <stash>/tasks/<id>.yml. The frontmatter wrapper and the Markdown body are gone. Inline prompts are written as a YAML block scalar:

# <stash>/tasks/daily-backup.yml
schedule: "0 9 * * 1-5"
workflow: workflow:daily-backup
params:
  region: us-east-1
enabled: true
# <stash>/tasks/morning-prompt.yml
schedule: "@daily"
prompt: |
  Do the thing.
  And the other thing.
profile: opencode
enabled: true

The 0.8.0 release also adds optional display fields that were not present in the old format:

schedule: "@weekly"
command: akm improve --auto-accept=90 --limit 25  # doclint:ignore (0.8.0-only flag, removed in 0.9.0)
enabled: true
name: Weekly improve cycle
description: Runs the improve pipeline against the full stash.
when_to_use: Run manually or let the scheduler trigger it every Monday.
tags: [scheduled, improve]

What 0.8.0 does and does not do with old .md files

There is no automatic migration of task definition files. Each .md file must be converted manually (see below).

Step-by-step migration

Step 1 — list your existing task files:

ls ~/akm/tasks/*.md 2>/dev/null
# or, if AKM_STASH_DIR is set:
ls "$AKM_STASH_DIR/tasks"/*.md 2>/dev/null

Step 2 — for each task, create the equivalent .yml file.

Extract the frontmatter keys from the old .md file and write them as a top-level YAML document. If the old file used prompt: inline, replace it with prompt: | followed by the indented body text.

Old file ~/akm/tasks/weekly-review.md:

---
schedule: "0 8 * * 1"
prompt: inline
profile: opencode
enabled: true
---

Review the week's completed tasks and summarize action items.

New file ~/akm/tasks/weekly-review.yml:

schedule: "0 8 * * 1"
prompt: |
  Review the week's completed tasks and summarize action items.
profile: opencode
enabled: true

If the old file used prompt: <asset-ref> or prompt: ./path/to/file.md, carry the value through unchanged:

schedule: "0 8 * * 1"
prompt: agent:weekly-review-agent
enabled: true

Step 3 — validate the new file:

akm tasks show weekly-review  # doclint:ignore (0.8.0-era subcommand; inspection moved to `akm search`/`akm show` by 0.9.0)

This command parses and prints the task. Any YAML errors or missing required fields are reported here before the task is registered with the scheduler.

Step 4 — register with the OS scheduler:

akm tasks sync  # doclint:ignore (`tasks` was renamed to singular `task` in 0.9.0)

akm tasks sync scans all .yml files, installs tasks that are not yet registered, and removes scheduler entries whose backing .yml file no longer exists. Run this once after converting all tasks.

Step 5 — remove the old .md files:

Once akm tasks list shows all your tasks correctly and akm tasks sync reports them as installed, delete the old Markdown files:

rm ~/akm/tasks/*.md

Verifying task migration

# All tasks appear with expected schedules
akm tasks list  # doclint:ignore (0.8.0-era subcommand; inspection moved to `akm search`/`akm show` by 0.9.0)

# Each task resolves without errors
akm tasks show <id>  # doclint:ignore (0.8.0-era subcommand; inspection moved to `akm search`/`akm show` by 0.9.0)

# Scheduler entries are in sync
akm tasks sync  # doclint:ignore (`tasks` was renamed to singular `task` in 0.9.0)

If a task that was registered on 0.7.x is now missing from the scheduler, the most likely cause is that its .md file has not been converted yet. Convert the file, run akm tasks sync, and confirm with akm tasks list.

akm.lock moved to $DATA

akm.lock (the installed stash lockfile) moves from $CONFIG/akm.lock to $DATA/akm.lock.

The migration script copies the file to its new location. The original at $CONFIG/akm.lock is left in place and can be deleted once you confirm akm list shows your stashes correctly.

0.8.0 reads only from $DATA/akm.lock. The old location is silently ignored.

Graph extraction will re-run after upgrade

0.8.0 includes a graph schema redesign and ships DB_VERSION = 17 (the v0.7.x line shipped at version 10; intermediate bumps land in this release). On first launch after upgrade, akm detects the version mismatch and rebuilds the affected index.db tables in place. A warning is logged during the upgrade so this is visible in akm output.

What rebuilds automatically (free, fast):

What requires LLM calls (the only user-visible cost):

Recommendation:

# Run after the storage migration to repopulate graph data.
akm improve

Subsequent improve cycles return to their normal cadence; only the first run pays the full re-extraction cost.

Why this is faster than it would have been on 0.7.x:

No manual SQL or repair commands are required — the DB_VERSION bump (10 → 17) and the DROP+rebuild path handle everything. If you query the graph tables directly from external tooling, note that graph_files is now keyed on entry_id INTEGER PRIMARY KEY REFERENCES entries(id) ON DELETE CASCADE and the child tables (graph_file_entities, graph_file_relations) are re-keyed on entry_id rather than (stash_root, file_path). body_hash is now NOT NULL. New columns: extraction_run_id (on graph_files and graph_meta) and extractor_id (on graph_meta).

Removed fallbacks and aliases

The following backward-compatibility shims that shipped in 0.7.x are removed in 0.8.0. Update scripts, agent prompts, and config before upgrading.

JSONL write fallbacks

akm remember, akm import, and akm wiki stash no longer fall back to JSONL-based event writing. All writes go through state.db. If you were relying on the JSONL file for event streaming, switch to akm events or akm events --tail.

Deprecated filePath alias

The filePath field on task history entries (deprecated in 0.7.x in favour of log) is removed. Tools that read task history records must use the log field.

--target flag standardisation

The write-target flag is now uniformly --target <stash-name> on akm remember, akm import, and akm wiki stash. Previous inconsistent flag names are removed with no compatibility aliases.

# Before (0.7.x inconsistent forms — removed in 0.8.0)
akm remember "note" --stash team-stash  # doclint:ignore (pre-0.8.0 flag spelling)
akm wiki stash my-wiki ./page.md --source team-stash  # doclint:ignore (pre-0.8.0 flag spelling; `wiki` also removed in 0.9.0)

# After (0.8.0)
akm remember "note" --target team-stash  # doclint:ignore (0.8.0 flag spelling; renamed --bundle in 0.9.0)
akm import ./page.md --target team-stash
akm wiki stash my-wiki ./page.md --target team-stash  # doclint:ignore (`wiki` command family removed in 0.9.0)

Config-dir fallback removed

In 0.7.x and earlier, setting AKM_CONFIG_DIR without setting AKM_DATA_DIR would cause data files (index.db, workflow.db) to land in $CONFIG. This fallback is removed. Set AKM_DATA_DIR explicitly if you override paths.

akm enable context-hub / akm disable context-hub removed

context-hub is no longer a recognised target for akm enable / akm disable in 0.8.0. The toggle was originally retired in 0.6.0 (context-hub became a regular git stash); 0.8.0 tightens the parser so invoking either form now errors with a usage message:

Unsupported target "context-hub". Supported targets: skills.sh

The only valid targets for akm enable / akm disable in 0.8.0 are skills.sh (and its provider-id alias skills-sh). Every other component that used to be toggled — context-hub, builtin stash sources, registry defaults — is configured via the regular config file or akm registry commands rather than via enable / disable.

What to do

If you still have automation or muscle-memory invocations of akm enable context-hub:

  1. Remove the toggle calls. They never re-enable a "context-hub provider" — that provider type was deleted in 0.6.0.

  2. Add context-hub as a regular git stash if you want its content in your stash list. This is the same shape used by any other git source:

    akm add github:andrewyng/context-hub --name context-hub  # doclint:ignore (historical — pre-0.9.0 command spelling)
    
  3. Verify with akm list. The output should show context-hub as a git-type stash, not as a special component.

If you only ever ran akm disable context-hub to suppress a phantom provider entry, no replacement action is needed — there is nothing to disable in 0.8.0.

Before / after

# Before (0.7.x and earlier — silently no-op after 0.6.0)
akm enable context-hub  # doclint:ignore (retired toggle, pre-0.6.0 command)
akm disable context-hub  # doclint:ignore (retired toggle, pre-0.6.0 command)

# After (0.8.0 — explicit error; replace with regular stash management)
akm add github:andrewyng/context-hub --name context-hub  # doclint:ignore (historical — pre-0.9.0 command spelling)
akm remove context-hub          # if you previously disabled it  # doclint:ignore (historical — pre-0.9.0 command spelling)

akm improve no longer accepts --format

In 0.7.x akm improve --format json printed a JSON envelope to stdout. In 0.8.0 the flag is rejected with INVALID_FLAG_VALUE to remove the implicit "stdout swallow" failure mode that masked errors when the improve pipeline emitted progress lines.

New behaviour

Form Effect in 0.8.0
akm improve (no --format) Human-readable output to stderr; full JSON result written to .akm/runs/<run-id>/improve-result.json
akm improve --format json Error — INVALID_FLAG_VALUE: --format is no longer supported on akm improve
akm improve --json-to-stdout Same as 0.7.x --format json — JSON envelope on stdout for capture by pipelines

The on-disk JSON file at .akm/runs/<run-id>/improve-result.json is always written regardless of stdout mode, so post-hoc inspection of an akm improve run no longer depends on having captured stdout at the time.

What to do

If you have CI jobs, agent prompts, or shell scripts that pipe akm improve --format json into jq or another consumer:

  1. Replace --format json with --json-to-stdout. The envelope shape is unchanged — only the flag name moved.
  2. Or read the run artifact. If your pipeline can tolerate reading from disk after the fact, drop the stdout capture entirely and read .akm/runs/<run-id>/improve-result.json instead. The run id is printed to stderr at the start of the run.

Before / after

# Before (0.7.x)
akm improve --format json | jq '.proposals[] | select(.confidence > 80)'

# After (0.8.0)
akm improve --json-to-stdout | jq '.proposals[] | select(.confidence > 80)'

# After (0.8.0 — disk-based, no stdout capture required)
akm improve
jq '.proposals[] | select(.confidence > 80)' \
  "$(ls -1t .akm/runs/*/improve-result.json | head -n1)"

If you see this error after upgrading:

INVALID_FLAG_VALUE: --format is no longer supported on akm improve.

it is always a script-side fix — add --json-to-stdout (or switch to the file-based path) and re-run.

akm index --enrich / --re-enrich removed

The --enrich and --re-enrich flags on akm index are gone. In 0.7.x they forced a slow LLM-enrichment pass alongside the regular index rebuild; in 0.8.0 the responsibilities are split:

If you have cron jobs or scripts that call akm index --enrich (or --re-enrich) on a schedule:

# Before (0.7.x)
akm index --enrich  # doclint:ignore (pre-0.8.0 flag, removed)

# After (0.8.0)
akm improve         # slow LLM maintenance work
akm index           # fast metadata enrichment is already included

Pure index rebuilds without any LLM work continue to call akm index with no flag changes required.

Memory inference and graph extraction moved out of index

Closely related to the --enrich removal above: the LLM-driven memory inference and graph-extraction passes that previously ran from akm index --enrich now run exclusively from the akm improve maintenance phase, after consolidation and (when relevant) the post-consolidation reindex.

If any automation relied on akm index to refresh derived memories or graph data, switch it to call akm improve on the desired cadence. The improve pipeline reindexes when memory inference writes new derived memories and refreshes graph extraction against the post-improve corpus state, so a single akm improve run settles both surfaces.

# Before (0.7.x)
akm index --enrich              # also refreshed memories + graph — doclint:ignore (pre-0.8.0 flag, removed)

# After (0.8.0)
akm improve                     # refreshes memories + graph as part of maintenance

Read-only callers (akm graph entities, akm graph relations, etc.) are unaffected — they continue to query whichever graph data is already in index.db.

akm wiki ingest now dispatches an agent

akm wiki ingest <name> no longer prints the ingest workflow to stdout for a human to copy/paste or pipe elsewhere. It now resolves an agent profile (from --profile or config.defaults.agent) and dispatches that agent directly with the workflow as its prompt. The agent does the work end-to-end.

Behavior changes:

New flags:

Flag Description
--profile <name> Override the agent profile resolved from config.defaults.agent
--model <model> Override the agent's model alias or platform ID
--timeout-ms <ms> Override the agent CLI timeout in milliseconds

Migration:

Scripts that piped the previous print-only output into another tool will break. If you still need the raw workflow text (e.g. for an external orchestrator), capture it from the wiki source files directly rather than from akm wiki ingest.

# Before (0.7.x — print-only or --execute)
akm wiki ingest my-wiki > workflow.md  # doclint:ignore (`wiki` command family removed in 0.9.0)
akm wiki ingest my-wiki --execute  # doclint:ignore (`wiki` command family removed in 0.9.0)

# After (0.8.0 — agent dispatch is the only mode)
akm wiki ingest my-wiki                          # uses defaults.agent — doclint:ignore (`wiki` command family removed in 0.9.0)
akm wiki ingest my-wiki --profile opencode-cli   # explicit profile — doclint:ignore (`wiki` command family removed in 0.9.0)
akm wiki ingest my-wiki --model opus --timeout-ms 600000  # doclint:ignore (`wiki` command family removed in 0.9.0)

config.agent.processes["task"] and improve.schedule removed

Two keys are dropped during config auto-migration and not transformed to a new location:

Migration:

If your 0.7.x config had config.agent.processes.task, rewrite each task file to set mode / profile / timeoutMs at the top level:

# <stash>/tasks/nightly-improve.yml
schedule: "@daily"
command: akm improve --auto-accept=90 --limit 50  # doclint:ignore (0.8.0-only flag, removed in 0.9.0)
profile: opencode-cli   # was config.agent.processes.task.profile
timeoutMs: 7200000      # was config.agent.processes.task.timeoutMs
enabled: true

If your 0.7.x config had improve.schedule, create an equivalent task file:

# <stash>/tasks/improve-cycle.yml
schedule: "0 3 * * *"   # the cron you used to put in improve.schedule
command: akm improve
enabled: true

Run akm tasks sync after creating or editing task files so the OS scheduler picks them up. Auto-migration strips the old keys from config.json; the backup it writes before doing so retains the original schedule string if you need to recover it.

Bootstrap-from-file: akm setup --from <file>

0.8.0 adds an --from <file> flag to akm setup that bootstraps the config from a JSON or YAML file on disk. This is the recommended way to:

The flag accepts both JSON and YAML — detection is by file extension (.yml / .yaml → YAML; anything else, including .json, parses as JSON). A leading ~ in the path is expanded against $HOME, and relative paths resolve against the current directory.

Recovering a clobbered config

If the post-incident audit (docs/technical/incidents/2026-05-23-setup-clobbers-user-config.md) ever fires for you and you lose your ~/.config/akm/config.json, restore from the most recent backup:

# 1. Locate the backup written immediately before the destructive save.
ls -1t ~/.cache/akm/config-backups/ | head

# 2. Replay it through setup. The wizard will skip prompts for keys
#    present in the backup and only prompt (or accept defaults) for
#    anything missing.
akm setup --from ~/.cache/akm/config-backups/config-2026-05-22T18-44-31.json

Bootstrapping from a YAML profile

YAML is easier to hand-edit and version-control than JSON. The flag accepts the same top-level keys as --config <json> — stashDir, llm, embedding, agent, semanticSearchMode, output, profiles, defaults:

# ~/akm-bootstrap.yml
stashDir: /home/dev/akm
llm:
  endpoint: http://localhost:11434/v1
  model: gpt-oss-20b
semanticSearchMode: off
akm setup --from ~/akm-bootstrap.yml

Validation

The CLI rejects with a friendly error when:

Partial files are allowed — the wizard simply prompts (or in --yes mode, accepts defaults) for any required keys that the file omitted.

Manual actions required

Most users need only run the migration script. The following are exceptions:

1. Update event stream consumers

If any script, cron job, or agent prompt reads $CACHE/events.jsonl directly, update it to use akm events [--tail] or query state.db directly:

# Before
tail -f ~/.cache/akm/events.jsonl | jq .

# After
akm events --tail  # doclint:ignore (0.8.0-era spelling; renamed `akm log` in 0.9.0)
# or for scripted consumption with cursor:
akm events --since-offset <last-row-id> --format json  # doclint:ignore (0.8.0-era spelling; renamed `akm log` in 0.9.0)

2. Reset byte-offset event cursors

If you maintain a persistent cursor (file, env var, database) that stores a byte offset into events.jsonl for incremental polling, reset it to 0 after migration. The new cursor is a row id integer, not a byte offset.

3. Update environment variable splits in CI

If your CI sets AKM_CONFIG_DIR and relies on data files landing there:

# Before (implicit; worked because of config-dir fallback)
export AKM_CONFIG_DIR=/ci/akm-config

# After (explicit split required)
export AKM_CONFIG_DIR=/ci/akm-config
export AKM_DATA_DIR=/ci/akm-data

4. Update write-target flags in scripts

See Removed fallbacks and aliases above. Replace any --stash <name> on remember / import / wiki stash with --target <name>.

5. Convert task definition files from .md to .yml

If you have existing task files at <stash>/tasks/*.md, they will not appear in akm tasks list or be run by the scheduler. Convert each one to a .yml file as described in Task definition files: .md+frontmatter → .yml, then run akm tasks sync to register them with the OS scheduler.

6. Run akm improve once to repopulate graph data

The DB_VERSION bump from 10 to 17 drops and rebuilds the graph tables. The non-graph tables regenerate automatically the first time akm opens index.db, but graph extraction needs LLM calls and only runs from akm improve. See Graph extraction will re-run after upgrade.

Verifying the upgrade

After running the migration script, run:

akm info

Look for:

Then run the post-upgrade checklist:

akm info --format text                          # 1. version 0.8.x
akm config list --format json | head -40        # 2. stashes[] populated
akm list                                        # 3. your sources resolve  # doclint:ignore (historical — pre-0.9.0 command spelling)
akm events --limit 5 --format json              # 4. event log readable from state.db — doclint:ignore (0.8.0-era spelling; renamed `akm log` in 0.9.0)
akm workflow list --format json | head -20      # 5. active workflow runs intact
akm search "<a query you know works>"           # 6. indexed content still matches

If step 4 returns zero events after a known-active installation, the migration script may not have imported the JSONL file. Re-run:

akm-migrate storage --yes

The import step is idempotent: each (event_type, ts, ref, metadata_json) tuple is pre-checked against the events table and skipped if already present. Re-running will only insert rows that are missing — it will not double-import the rows already in state.db.

Troubleshooting

akm search returns no results after upgrading. The search index (index.db) may not have been found at its new location. Run akm info and check the dataDir field. If it points to $CONFIG instead of $DATA, AKM_DATA_DIR may be unset but AKM_CONFIG_DIR was previously relied upon as a fallback. Set AKM_DATA_DIR explicitly, re-run the migration script, or move index.db manually.

akm events shows 0 results but I had thousands. The migration script imports events in append order. If the JSONL file was corrupted or truncated, some rows may have been skipped. Run:

akm-migrate storage --dry-run

and look for import warnings. You can also inspect state.db directly:

sqlite3 -readonly ~/.local/share/akm/state.db 'SELECT COUNT(*) FROM events;'

akm list shows empty stashes. Check that $DATA/akm.lock exists. If only $CONFIG/akm.lock exists, the migration script may not have run. Run:

akm-migrate storage --yes

Tasks show no history. akm tasks history <id> reads from the task_history table in state.db. Run the migration script to import existing JSONL files. If you see records from the migration but are missing recent ones, confirm your akm binary is 0.8.0 (akm info | grep version).

akm tasks list shows fewer tasks than expected (or none). Task definitions are now read exclusively from .yml files. Any .md task files from 0.7.x are silently skipped. Run:

ls "$AKM_STASH_DIR/tasks"/*.md 2>/dev/null

If .md files exist, convert them to .yml as described in the task migration section and re-run akm tasks sync.

akm tasks show says "task not found" for a task that used to exist. The task's .md file has not been converted to .yml yet. The lookup resolves to <id>.yml regardless of whether you pass .md or no suffix in the command. Convert the file and run akm tasks sync.

--stash flag not recognized on remember. The --stash flag was removed in 0.8.0. Use --target instead:

akm remember "note" --target team-stash  # doclint:ignore (0.8.0 flag spelling; renamed --bundle in 0.9.0)

Rolling back

If you need to roll back to 0.7.x:

  1. The migration script copies (not moves) akm.lock and imports (not removes) JSONL events. Your original files are intact.

  2. Events written to state.db by 0.8.0 will not be visible to 0.7.x. If you wrote new events on 0.8.0 and need them in 0.7.x, export them:

    sqlite3 -readonly ~/.local/share/akm/state.db \
      "SELECT json_object('schemaVersion', schema_version, 'ts', ts, 'eventType', event_type, 'ref', ref, 'metadata', json(metadata)) FROM events ORDER BY id;" \
      >> ~/.cache/akm/events.jsonl
    
  3. Reinstall 0.7.x:

    npm install -g akm-cli@0.7
    # or
    bun install -g akm-cli@0.7
    

Asset files under $STASH/ are unchanged — stash content is fully forward/backward compatible across 0.7.x and 0.8.x.

If you find a regression in 0.8.0, file an issue at https://github.com/itlackey/akm/issues with the output of akm info and a redacted copy of your config.json.


Config 0.8.0 migration (unified profiles)

0.8.0 ships a new config shape (configVersion: "0.8.0") that removes the legacy llm, agent, and features top-level blocks and reorganizes LLM and agent configuration into a unified profiles + defaults tree alongside first-class index.* and search.* feature sections.

What changed

Keys removed (not migrated)

The following keys are dropped during migration — they are not transformed to a new location:

Old key → new location mapping

Old key New location
config.llm (endpoint/model/apiKey/temperature) profiles.llm.default + defaults.llm = "default"
config.llm.judgeModel preserved on the migrated profiles.llm.default entry
config.agent.profiles.<n> profiles.agent.<n>
config.agent.default defaults.agent
config.agent.processes.<name> profiles.improve.default.processes.<name> (mode: "agent" + profile: <agent profile name>)
config.improve.reflectCooldownByType profiles.improve.default.processes.reflect.cooldownByType
config.improve.limit profiles.improve.default.limit
config.llm.features.memory_inference profiles.improve.default.processes.memoryInference.enabled
config.llm.features.graph_extraction profiles.improve.default.processes.graphExtraction.enabled
config.llm.features.metadata_enhance index.metadataEnhance.enabled
config.llm.features.memory_consolidation profiles.improve.default.processes.consolidate.enabled
config.llm.features.feedback_distillation profiles.improve.default.processes.feedbackDistillation.enabled
config.llm.features.curate_rerank search.curateRerank.enabled
config.llm.features.lesson_quality_gate profiles.improve.default.processes.distill.qualityGate.enabled
config.llm.features.proposal_quality_gate profiles.improve.default.processes.reflect.qualityGate.enabled
config.llm.features.memory_contradiction_detection profiles.improve.default.processes.consolidate.contradictionDetection.enabled
config.features.improve.* identical migration to the matching profiles.improve.default.processes.* entry
config.features.index.staleness_detection.options.thresholdDays index.stalenessDetection.thresholdDays

Auto-migration at first run

When akm loads a config that does not have configVersion: "0.8.0" (or that contains old keys), it:

  1. Prints a one-time notice describing the migration.
  2. Writes a timestamped backup to $DATA/config-backups/ before touching the file.
  3. Rewrites the config file in place with the v2 shape.
  4. Sets configVersion: "0.8.0" so subsequent launches skip migration.

If you run multiple config layers (user + project), each layer is visited and rewritten independently. Read-only layers print the migrated content for manual apply instead of writing.

Suppress auto-migration:

export AKM_NO_AUTO_MIGRATE=1

Useful on read-only CI mounts. With this flag set, akm still loads and uses the v1 config for the current run, but does not rewrite the file.

Auto-migration banner: Starting in 0.8.0, when akm auto-migrates your config it prints a loud banner to both stderr and stdout so you can see it regardless of which stream your shell or pipeline captures. The banner includes the resolved config file path, the backup directory path, the opt-out instruction, and the preview command:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  akm: auto-migrated config → 0.8.0 format
  file:   /home/user/.config/akm/config.json
  backup: /home/user/.cache/akm/config-backups/config-<timestamp>.json
  to opt out of future auto-migration: AKM_NO_AUTO_MIGRATE=1
  to preview a dry-run diff:            akm config migrate --dry-run --print-diff
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Manual migration

To preview the migration without writing (shows what would change):

akm config migrate --dry-run  # doclint:ignore (0.8.0-only config-schema-rewrite subcommand, not present in 0.9.0's `akm migrate`)

To preview with a full unified diff of old vs new config:

akm config migrate --dry-run --print-diff  # doclint:ignore (0.8.0-only config-schema-rewrite subcommand, not present in 0.9.0's `akm migrate`)

To apply:

akm config migrate  # doclint:ignore (0.8.0-only config-schema-rewrite subcommand, not present in 0.9.0's `akm migrate`)

To apply and see a unified diff of what changed:

akm config migrate --print-diff  # doclint:ignore (0.8.0-only config-schema-rewrite subcommand, not present in 0.9.0's `akm migrate`)

akm config migrate acquires a file lock (<config-dir>/.akm/migrate.lock) before writing, so concurrent akm commands launched while a migrate is in flight block cleanly rather than racing. Pass --no-wait to fail immediately instead of blocking.

CI/CD note: On read-only mounts (e.g. ephemeral containers), set AKM_NO_AUTO_MIGRATE=1 and run akm config migrate explicitly during the deploy pipeline — before any command that reads config — so the config file is rewritten once at deploy time and the container never tries to write at runtime.

Verifying the config migration

# Confirm configVersion is present
akm config get configVersion

# Confirm profiles resolved
akm config get profiles.llm

# Confirm profile tree populated
akm config get profiles.improve.default

# Smoke test: confirm improve resolves cleanly without invoking the runner
akm improve --dry-run

Config layer rewrite (late-0.8.x)

Late in the 0.8.x cycle the config layer was rewritten end-to-end around a single Zod schema in src/core/config-schema.ts. The on-disk shape is unchanged — ~/.config/akm/config.json from before the rewrite still loads. But several behaviors that used to be silent are now loud, and a handful of configurations that used to silently misbehave are now rejected.

Behavior changes you should know about

  1. akm config set llm.apiKey and friends now throw. Persisting API keys in the config file leaked them through backups and version control. The new behavior is to throw UsageError pointing at the environment variable instead:

    • llm.apiKey → AKM_LLM_API_KEY
    • embedding.apiKey → AKM_EMBED_API_KEY
    • profiles.llm.<name>.apiKey → AKM_PROFILE_<NAME>_API_KEY (with - normalized to _ and uppercased)

    If you had apiKey in your config before, it was silently stripped on the first save; that behavior is unchanged for already-loaded configs. The throw only fires on new akm config set invocations.

  2. Malformed config JSON now throws. Before the rewrite, if your config.json had a syntax error, AKM silently fell back to DEFAULT_CONFIG and your settings appeared to vanish. Now you get a clear ConfigError("Failed to parse config JSON") with the underlying JSON parse error. Fix the syntax error and AKM resumes normal operation. The file-not-existing case is still the legitimate cold-start path.

  3. Project-level .akm/config.json files are deprecated. AKM still merges them into the loaded config in 0.8.x (giving you one release of grace), but you'll see a one-time warning on first discovery:

    [akm] DEPRECATED: project-level config file found at <path>.
    Project-level config files will be ignored in 0.9.0+.
    Move your settings to ~/.config/akm/config.json.
    

    If you relied on this, plan to migrate your project-level settings to the user config before 0.9.0 ships.

  4. Every schema-leaf key is now reachable via akm config set. Previously the CLI maintained a hand-listed whitelist of settable keys, and several common ones (defaults.agent, search.minScore, improve.eventRetentionDays, embedding.provider, llm.temperature, profiles.llm.<name>.*, profiles.agent.<name>.*, improve.utilityDecay.halfLifeDays, feedback.requireReason, index.metadataEnhance.enabled, etc.) were missing. The schema is now the source of truth — if it's in the schema, you can set it. If you have a script that worked around this by editing config.json directly, you can now use akm config set instead.

  5. Unknown keys in nested objects are now rejected. Before, an unknown key inside registries[], sources[], or profiles.llm.* was silently accepted. The schema now uses .strict() on those objects — unknown keys throw with a path-pointing error (e.g. registries.0.somethingTypo: Unrecognized key(s)). Fix the typo or remove the key.

  6. Config backups are bounded to 5. Each akm config set keeps a timestamped backup of the previous config under ~/.cache/akm/config-backups/. Before the rewrite this was unbounded and could accumulate thousands of entries; now it's pruned to the 5 most recent on each save. config.latest.json is preserved separately and continues to hold the most recent pre-write copy.

  7. akm config validate and akm config migrate are real subcommands. Before, both lived in the codebase but weren't wired into the CLI surface — error messages that referenced them were misleading. Now:

    akm config validate         # check the on-disk file against the schema — doclint:ignore (removed in 0.9.0; load-time schema checks already reject an invalid config)
    akm config migrate           # migrate legacy shape to 0.8.0 in place — doclint:ignore (0.8.0-only subcommand, not present in 0.9.0's `akm migrate`)
    akm config migrate --dry-run # preview the migration without writing — doclint:ignore (0.8.0-only subcommand, not present in 0.9.0's `akm migrate`)
    
  8. defaultWriteTarget validation tightened. It must name a configured source in sources[]. With no sources configured, save-time validation now errors rather than silently accepting an unresolvable name.

Manual migration

If you have a custom config beyond the documented surface — especially project-level .akm/config.json files, env-var literals stored in non-apiKey fields, or manually-edited unknown fields — read this section carefully.

End-of-run auto-sync for git-backed stashes

0.8.0 adds an end-of-run batch commit to akm improve. After a non-dry-run pass, if the primary stash has a .git directory, improve automatically calls saveGitStash — the same path as akm sync. No remote is required; detection is based purely on the presence of .git.

Behavior by profile

Profile Sync default Push default
default enabled true
thorough enabled true
quick disabled —
memory-focus disabled —

Lightweight and limited passes (quick, memory-focus) opt out of auto-sync to avoid committing a partial stash state when the user did not ask for a full improve. The --sync / --no-sync CLI flags override the profile default for a single run.

New CLI flags on akm improve

Flag Effect
--sync Force sync even on profiles that disable it
--no-sync Skip end-of-run commit for this run
--push Push after commit (default: true when sync enabled)
--no-push Commit only; skip push for this run

Result envelope

The AkmImproveResult now includes a sync field when sync was attempted:

{ "committed": true, "pushed": false, "skipped": false }

skipped: true means the run was a dry-run, the stash is not git-backed, or sync was disabled. A reason string is included when skipped due to an error. A stash_synced audit event is emitted to state.db.

Configuring a custom commit message

Set profiles.improve.<name>.sync.message in your config:

{
  "profiles": {
    "improve": {
      "default": {
        "sync": {
          "enabled": true,
          "push": true,
          "message": "akm improve {scope} — {refs} refs, {accepted} accepted ({date})"
        }
      }
    }
  }
}

Supported tokens: {timestamp}, {date}, {time}, {scope}, {refs}, {accepted}. Unknown tokens pass through verbatim.

What to do if you do not want auto-sync

Add --no-sync to any akm improve invocations in scripts or automation, or set sync: { enabled: false } in your custom improve profile.