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
- What changed in 0.8.0
- Behavior change:
--auto-acceptis now OFF by default - Running the migration script
- New XDG directory layout
- Event log: events.jsonl → state.db
- Registry index cache: files → index.db
- Task history: JSONL files → state.db
- Task definition files: .md+frontmatter → .yml
- akm.lock moved to $DATA
- Graph extraction will re-run after upgrade
- Removed fallbacks and aliases
akm enable context-hub/akm disable context-hubremovedakm improveno longer accepts--formatakm index --enrich/--re-enrichremoved- Memory inference and graph extraction moved out of
index akm wiki ingestnow dispatches an agentconfig.agent.processes["task"]andimprove.scheduleremoved- Bootstrap-from-file:
akm setup --from <file> - Manual actions required
- Verifying the upgrade
- Troubleshooting
- Rolling back
- Config 0.8.0 migration (unified profiles)
- Config layer rewrite (late-0.8.x)
- End-of-run auto-sync for git-backed stashes
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
- To get the legacy "always auto-accept" behavior, pass
--auto-accept=safe(or--auto-accept=90) explicitly toakm improveand to any scheduled task YAML, agent prompt, or CI invocation that expects un-prompted Phase B operations. - The literal string
safeis a permanent alias for90and is not deprecated. - Confidence scoring is not yet implemented. Proposals do not currently
carry per-item confidence scores, so any non-
undefinedvalue of--auto-acceptbehaves identically to the legacysafemode (whole-batch auto-accept). Once confidence scores ship, the threshold will gate individual proposals.
# 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:
- Creates
$DATAand$STATEdirectories if they do not exist. - Moves
$CONFIG/index.db→$DATA/index.db. - Moves
$CONFIG/workflow.db→$DATA/workflow.db. - Creates
$DATA/state.dbwith the initial schema. - Copies
$CONFIG/akm.lock→$DATA/akm.lock(leaves the original in place until you confirm everything works). - Imports
$CACHE/events.jsonlinto theeventstable instate.db. Duplicate rows (samets+eventType+ref) are skipped. - Imports per-task JSONL files from
$STATE/tasks/history/(or$CACHE/tasks/history/for older installations) into thetask_historytable instate.db. - 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 migration script imports all existing JSONL events into
state.db. After a successful import, the JSONL file can be deleted. - Any tooling that read
events.jsonldirectly must be updated. Useakm eventsto inspect the event stream from the CLI, or querystate.dbdirectly with any SQLite client. - Cursor reset: The old cursor was a byte offset into the JSONL file. The
new cursor is the integer
id(autoincrement rowid) in theeventstable. If you have scripts that persist a byte-offset cursor to resume event polling, reset the cursor to0after migration. The newreadEvents()API acceptssinceOffsetas a rowid.
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
akm tasks list— enumerates only.ymlfiles. Old.mdfiles are silently invisible; they will not appear in the list or be registered with the OS scheduler.akm tasks show <id>/akm tasks run <id>— if you pass an id that includes the.mdsuffix (e.g.akm tasks show daily-backup.md), the suffix is stripped before the lookup, so the command resolves to<stash>/tasks/daily-backup.yml. If no.ymlfile exists the command returns a "task not found" error, not a parse error.akm tasks add— always writes a new.ymlfile. It will not overwrite or convert an existing.mdfile.
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):
entries,embeddings, FTS, and theentries_vectable all regenerate from disk viaakm index. On most stashes this completes in a few seconds.
What requires LLM calls (the only user-visible cost):
- The graph tables (
graph_files,graph_file_entities,graph_file_relations,graph_meta) are dropped during the upgrade and repopulated by graph extraction on the nextakm improvecycle. Graph extraction makes LLM calls to identify entities and relations in each asset, so the first improve cycle after upgrade will be slower than steady state.
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:
- Graph extraction now uses an incremental
candidatePathsfilter so it only visits assets that have actually changed, dramatically reducing repeat work on a cold cache. graphExtractionBatchSizedefaults to 4 (was 1), auto-tuned againstllm.contextLength, so each LLM round-trip processes more assets.replaceStoredGraphis now incremental — unchanged entries skip, changed entries swap their child rows in place, and removed entries cascade out.listRelatedPathsForFileis now a scoped SQL self-join instead of a full in-memory scan, cutting cold-call latency from tens of milliseconds to single-digit milliseconds.
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:
-
Remove the toggle calls. They never re-enable a "context-hub provider" — that provider type was deleted in 0.6.0.
-
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) -
Verify with
akm list. The output should showcontext-hubas 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:
- Replace
--format jsonwith--json-to-stdout. The envelope shape is unchanged — only the flag name moved. - 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.jsoninstead. 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:
- Plain
akm indexcontinues to own fast metadata enhancement when LLM metadata enrichment is enabled (controlled byindex.metadataEnhancein the unified config tree). - Slow LLM maintenance work — memory inference, graph extraction, lesson
distillation, consolidation — runs from
akm improve, not fromakm index.
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:
- The
--executeflag is removed. Dispatch is the only mode. - Without an accessible agent profile, the command fails with a clear error
pointing at
profiles.agent. - Output is the agent's run envelope, not the workflow text.
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:
-
config.agent.processes["task"]— task dispatch parameters now live in the task's own stash YAML file. Each task declares itsmodeandprofile(and optionaltimeoutMs) directly. This removes the global "all tasks use this dispatch config" coupling that the legacy key encoded. -
config.improve.schedule/improve.schedule— global improve scheduling is no longer a config key. Scheduling is owned by stash task YAMLs that wrapakm improvecalls (via thecommand:task target).
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:
- Move an existing config to a new machine without typing the same answers into the interactive wizard.
- Recover from a clobbered
~/.config/akm/config.jsonusing the timestamped snapshots that akm writes to~/.cache/akm/config-backups/before every destructive save. - Replay a known-good config in CI without piping a large JSON literal on the command line.
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:
- The file does not exist:
ConfigError(INVALID_CONFIG_FILE): Config file not found: <path>. - The file cannot be parsed:
ConfigError(INVALID_CONFIG_FILE): Failed to parse JSON|YAML config file <path>: <parser error>. - The top-level payload is not an object (e.g. a JSON array or string).
- Both
--from <file>and--config <json>are passed:UsageError.
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:
versionreports0.8.0or higher.- No
WARNING: legacy events.jsonl foundmessage.
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:
-
The migration script copies (not moves)
akm.lockand imports (not removes) JSONL events. Your original files are intact. -
Events written to
state.dbby 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 -
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
profiles.llm.<name>— each named LLM endpoint declared once (endpoint, model, apiKey, temperature, supportsJsonSchema).profiles.agent.<name>— each named agent runtime, with a requiredplatform: "opencode" | "claude" | "opencode-sdk"field.profiles.improve.<name>— named improve-pipeline profiles. Each lists per-process bindings underprocesses.{reflect,distill,consolidate,memoryInference,graphExtraction,feedbackDistillation,validation}.defaults.llm/defaults.agent/defaults.improve— fallback profile names.index.metadataEnhance/index.stalenessDetection— first-class feature sections replacing the legacyfeatures.index.*entries.search.curateRerank— first-class feature section replacing the legacyfeatures.search.curate_rerankentry.
Keys removed (not migrated)
The following keys are dropped during migration — they are not transformed to a new location:
sdkMode: trueon agent profiles (replaced byplatform: "opencode-sdk")config.agent.processes["task"](tasks now declaremode+profilein their own YAML)config.improve.schedule/improve.schedule(scheduling lives in stash task YAMLs)
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:
- Prints a one-time notice describing the migration.
- Writes a timestamped backup to
$DATA/config-backups/before touching the file. - Rewrites the config file in place with the v2 shape.
- 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
-
akm config set llm.apiKeyand friends now throw. Persisting API keys in the config file leaked them through backups and version control. The new behavior is to throwUsageErrorpointing at the environment variable instead:llm.apiKey→AKM_LLM_API_KEYembedding.apiKey→AKM_EMBED_API_KEYprofiles.llm.<name>.apiKey→AKM_PROFILE_<NAME>_API_KEY(with-normalized to_and uppercased)
If you had
apiKeyin your config before, it was silently stripped on the first save; that behavior is unchanged for already-loaded configs. The throw only fires on newakm config setinvocations. -
Malformed config JSON now throws. Before the rewrite, if your
config.jsonhad a syntax error, AKM silently fell back toDEFAULT_CONFIGand your settings appeared to vanish. Now you get a clearConfigError("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. -
Project-level
.akm/config.jsonfiles 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.
-
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 editingconfig.jsondirectly, you can now useakm config setinstead. -
Unknown keys in nested objects are now rejected. Before, an unknown key inside
registries[],sources[], orprofiles.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. -
Config backups are bounded to 5. Each
akm config setkeeps 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.jsonis preserved separately and continues to hold the most recent pre-write copy. -
akm config validateandakm config migrateare 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`) -
defaultWriteTargetvalidation tightened. It must name a configured source insources[]. 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.
- Project-level configs (deprecated): move every setting that lives in a
.akm/config.jsonsomewhere under your project tree into your~/.config/akm/config.json. After 0.9.0 the project-level discovery will be removed entirely. - Unknown fields you added by hand: the strict-mode parser will now reject them. Either move them to a documented key, or accept that they were never doing anything anyway.
apiKeyin your config file: export the appropriate env var instead and remove the field from the JSON. AKM reads the env var on every connection.- CI scripts that captured
akm config set llm.apiKey: swap forexport AKM_LLM_API_KEY=...before theakmcommand.
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.