# 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](#installing-080)
- [What changed in 0.8.0](#what-changed-in-080)
- [Behavior change: `--auto-accept` is now OFF by default](#behavior-change---auto-accept-is-now-off-by-default)
- [Running the migration script](#running-the-migration-script)
- [New XDG directory layout](#new-xdg-directory-layout)
- [Event log: events.jsonl → state.db](#event-log-eventsjsonl--statedb)
- [Registry index cache: files → index.db](#registry-index-cache-files--indexdb)
- [Task history: JSONL files → state.db](#task-history-jsonl-files--statedb)
- [Task definition files: .md+frontmatter → .yml](#task-definition-files-mdfrontmatter--yml)
- [akm.lock moved to \$DATA](#akmlock-moved-to-data)
- [Graph extraction will re-run after upgrade](#graph-extraction-will-re-run-after-upgrade)
- [Removed fallbacks and aliases](#removed-fallbacks-and-aliases)
- [`akm enable context-hub` / `akm disable context-hub` removed](#akm-enable-context-hub--akm-disable-context-hub-removed)
- [`akm improve` no longer accepts `--format`](#akm-improve-no-longer-accepts---format)
- [`akm index --enrich` / `--re-enrich` removed](#akm-index---enrich----re-enrich-removed)
- [Memory inference and graph extraction moved out of `index`](#memory-inference-and-graph-extraction-moved-out-of-index)
- [`akm wiki ingest` now dispatches an agent](#akm-wiki-ingest-now-dispatches-an-agent)
- [`config.agent.processes["task"]` and `improve.schedule` removed](#configagentprocessestask-and-improveschedule-removed)
- [Bootstrap-from-file: `akm setup --from <file>`](#bootstrap-from-file-akm-setup---from-file)
- [Manual actions required](#manual-actions-required)
- [Verifying the upgrade](#verifying-the-upgrade)
- [Troubleshooting](#troubleshooting)
- [Rolling back](#rolling-back)
- [Config 0.8.0 migration (unified profiles)](#config-080-migration-unified-profiles)
- [Config layer rewrite (late-0.8.x)](#config-layer-rewrite-late-08x)
- [End-of-run auto-sync for git-backed stashes](#end-of-run-auto-sync-for-git-backed-stashes)

## Installing 0.8.0

Pick whichever method you used for 0.7.x:

```sh
# 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 to `akm improve`
  and to any scheduled task YAML, agent prompt, or CI invocation that
  expects un-prompted Phase B operations.
- The literal string `safe` is a permanent alias for `90` and is **not**
  deprecated.
- **Confidence scoring is not yet implemented.** Proposals do not currently
  carry per-item confidence scores, so any non-`undefined` value of
  `--auto-accept` behaves identically to the legacy `safe` mode (whole-batch
  auto-accept). Once confidence scores ship, the threshold will gate
  individual proposals.

```sh
# 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:**

```sh
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](#new-xdg-directory-layout)).

**Step 2 — apply:**

```sh
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:

```sh
# 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.jsonl` directly must be updated. Use
  `akm events` to inspect the event stream from the CLI, or query `state.db`
  directly 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 the `events` table.
  If you have scripts that persist a byte-offset cursor to resume event
  polling, reset the cursor to `0` after migration. The new `readEvents()`
  API accepts `sinceOffset` as a row `id`.

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:

```sh
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:

```sh
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:

```markdown
---
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:

```markdown
---
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:

```yaml
# <stash>/tasks/daily-backup.yml
schedule: "0 9 * * 1-5"
workflow: workflow:daily-backup
params:
  region: us-east-1
enabled: true
```

```yaml
# <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:

```yaml
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 `.yml` files. Old `.md` files 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 `.md` suffix (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 `.yml` file exists the command
  returns a "task not found" error, not a parse error.
- `akm tasks add` — always writes a new `.yml` file. It will not overwrite or
  convert an existing `.md` file.

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:**

```sh
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`:

```markdown
---
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`:

```yaml
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:

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

**Step 3 — validate the new file:**

```sh
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:**

```sh
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:

```sh
rm ~/akm/tasks/*.md
```

### Verifying task migration

```sh
# 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 the `entries_vec` table all regenerate from
  disk via `akm 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 next `akm improve` cycle. 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:**

```sh
# 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 `candidatePaths` filter so it only
  visits assets that have actually changed, dramatically reducing repeat work
  on a cold cache.
- `graphExtractionBatchSize` defaults to 4 (was 1), auto-tuned against
  `llm.contextLength`, so each LLM round-trip processes more assets.
- `replaceStoredGraph` is now incremental — unchanged entries skip, changed
  entries swap their child rows in place, and removed entries cascade out.
- `listRelatedPathsForFile` is 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.

```sh
# 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:

   ```sh
   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

```sh
# 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

```sh
# 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 index` continues to own **fast metadata enhancement** when LLM
  metadata enrichment is enabled (controlled by `index.metadataEnhance` in
  the unified config tree).
- Slow LLM maintenance work — memory inference, graph extraction, lesson
  distillation, consolidation — runs from `akm improve`, not from `akm index`.

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

```sh
# 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.

```sh
# 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 `--execute` flag 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`.

```sh
# 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 its `mode` and `profile`
  (and optional `timeoutMs`) 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 wrap `akm improve` calls (via the `command:` 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:

```yaml
# <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:

```yaml
# <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.json` using 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:

```sh
# 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`:

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

```sh
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:

```sh
# 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:

```sh
# 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](#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](#task-definition-files-mdfrontmatter--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](#graph-extraction-will-re-run-after-upgrade).

## Verifying the upgrade

After running the migration script, run:

```sh
akm info
```

Look for:
- `version` reports `0.8.0` or higher.
- No `WARNING: legacy events.jsonl found` message.

Then run the post-upgrade checklist:

```sh
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:

```sh
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:

```sh
akm-migrate storage --dry-run
```

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

```sh
sqlite3 ~/.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:

```sh
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:

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

If `.md` files exist, convert them to `.yml` as described in the
[task migration section](#task-definition-files-mdfrontmatter--yml) 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:

```sh
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:

   ```sh
   sqlite3 ~/.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:

   ```sh
   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 required
  `platform: "opencode" | "claude" | "opencode-sdk"` field.
- **`profiles.improve.<name>`** — named improve-pipeline profiles. Each lists
  per-process bindings under `processes.{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 legacy `features.index.*` entries.
- **`search.curateRerank`** — first-class feature section replacing the legacy
  `features.search.curate_rerank` entry.

### Keys removed (not migrated)

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

- `sdkMode: true` on agent profiles (replaced by `platform: "opencode-sdk"`)
- `config.agent.processes["task"]` (tasks now declare `mode` + `profile` in
  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:

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:**

```sh
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):

```sh
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:

```sh
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:

```sh
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:

```sh
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

```sh
# 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:

   ```text
   [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:

   ```sh
   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.

- **Project-level configs (deprecated):** move every setting that lives in a
  `.akm/config.json` somewhere 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.
- **`apiKey` in 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 for
  `export AKM_LLM_API_KEY=...` before the `akm` command.

## 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:

```json
{ "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:

```jsonc
{
  "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.
