akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
8,901 lines • 579 kB
Markdown
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
## [0.9.32] - 2026-10-13
A patch release: `akm feedback` no longer reports success for feedback it could not store.
### Fixed
- **`akm feedback` fails when it cannot record the feedback.** When `state.db` was read-only to the process, as it is
inside an agent sandbox that only allows writes in the workspace, the feedback row was silently dropped and the
command still printed `"ok": true`. It now fails with a `DATA_DIR_NOT_WRITABLE` config error (exit 78) and records
nothing, so an agent knows its feedback was lost. Add akm's data directory to the sandbox's writable paths.
## [0.9.31] - 2026-10-09
A patch release: concurrent config writes no longer lose a change, OpenAI's reasoning models work as `llm`
engines, `akm improve --require-engines` probes send API keys, and the npm package's docs links resolve.
### Fixed
- **`akm improve --require-engines` sends each engine's API key with its probe.** The probe used the engine's
connection as resolved, where the key is still a symbolic reference that a dispatch reads only when it sends a
request, so every keyed endpoint (OpenAI, a gateway that enforces auth) answered 401 and the run stopped with
"engine completion path is not reachable". The probe now builds the connection as a dispatch does; a required key
that is missing is reported as that engine's failure.
- **OpenAI's reasoning models (gpt-5.x) work as `llm` engines.** They answer 400 to any `temperature` but the
default and to `max_tokens`, so every akm model call failed on them. Like the existing fallback for the
thinking-control fields, a 400 that names `temperature` is retried once without it, and one that asks for
`max_completion_tokens` is retried with the cap under that name; the connection keeps the working shape for
the rest of the process.
- **Every relative link in the npm package's docs resolves again.** #1118 made `docs/README.md`,
`docs/migration/README.md` and `docs/reference/cli.md` link by relative path to guides and pages the package did
not ship. npm always ships a folder's `README.md`, so those links were dead for npm users. The package now ships
the user-facing `docs/` folders (`agents`, `architecture`, `guides`, `integration`, `maintainers`, `migration`,
`posts`, `reference`), `docs/product-surface.md`, the one plan they link to, and `ROADMAP.md`. `docs/plans` stays
out. The link check skips link syntax inside code, which is an example, not a link.
- **Concurrent `akm config set` runs no longer lose each other's changes.** A lock file was created empty and its
owner's PID written a moment later; a process that looked in that gap read no PID, took the live lock for an
abandoned one and removed it, so two writers ran at once and one change was lost. A lock now appears with its
owner's PID already in it. Every akm lock file (config, index writer, scheduler, `akm.lock`, run, extract, secret)
is created this way.
## [0.9.30] - 2026-10-09
A patch release: `akm upgrade` acts only on the `akm-opencode` spec OpenCode's config names.
### Fixed
- **`akm upgrade` leaves alone an OpenCode whose config pins another `akm-opencode` spec (#1117).** A config naming
an exact version (seen: `akm-opencode@0.8.0`) loads that version's cache, but the plugin step refreshed `@latest`:
the cache went to the trash, OpenCode did not re-create it, and the run exited 1. The step now acts only on the
spec the config names: bare or `@latest` on a plain run, `@next` under `--next`; any other spec is skipped with a
note, no cache is touched and the CLI is not held to a pin OpenCode does not load. This generalizes #1115.
## [0.9.29] - 2026-10-09
A patch release on the 0.9.28 line: `akm upgrade` is Stable, and a plain upgrade no longer breaks OpenCode's
plugin on a host that follows `akm-opencode@next`.
### Changed
- **`akm upgrade` is Stable**, including its plugin step and `--next`, after real-host, empty-container and
failure-path runs on 0.9.28 (#1099).
### Fixed
- **A plain `akm upgrade` no longer breaks OpenCode's plugin on a host whose OpenCode config follows
`akm-opencode@next` (#1115).** It refreshed the `@latest` cache, which such a host never loads: the cache went to
the trash, OpenCode did not re-create it, and the run exited 1. The OpenCode entry is now skipped with a note to
run `akm upgrade --next` (or switch the config to the bare `akm-opencode`), and neither cache is touched.
## [0.9.28] - 2026-10-08
The stable release of the 0.9.28 line: 0.9.28-alpha.1 to alpha.11.
When upgrading from 0.9.27:
- **`akm upgrade` updates each installed harness's akm plugin** (Claude Code,
Codex, OpenCode) after the CLI, and `--next` installs the `@next` prerelease
of akm and its OpenCode plugin. An OpenCode refresh waits while a host
OpenCode runs; servers inside containers no longer hold it up (alpha.4–alpha.11).
- **OpenCode runs through the SDK (`opencode-sdk`) reliably:** the server gets
`OPENCODE_API_KEY` and `OPENCODE_CONFIG`, sessions are kept, permission
requests are rejected instead of hanging the run, a provider usage limit ends
the run with its reason instead of a silent stall, and an engine can name a
default `agent` (alpha.7–alpha.10, #1108).
- **Knowledge is never retired in favour of a memory** by consolidate's pair
pass, and the drain's judge sees promotions it accepted earlier in the same
drain; `akm proposal drain --strategy` honours that strategy's judgment
setting (alpha.7, alpha.11).
- **Indexing:** a skill's reference file with a shell `$1` or a documented
`$ARGUMENTS` stays a skill resource, and existing indexes re-file such entries
on the next `akm index` (alpha.4, alpha.6, alpha.11).
- **Added (experimental):** `akm metrics` reports what akm has recorded, in every
format including a self-contained HTML dashboard; LLM usage is recorded for
every command (alpha.2).
- **Changed:** stability tiers for 0.10's stabilization work; workflows are
Experimental (alpha.7). Distill skips deprecated or superseded memories and
keeps a rejected lesson's text; the pair judge's reasons no longer swap A and
B (alpha.1, alpha.3).
### Fixed
- **An `opencode-sdk` dispatch now stops promptly when a provider usage limit schedules a retry past its deadline.**
The retry, provider and reset time are recorded in the dispatch `stderr`, and account limits fail as provider limits
rather than appearing to hang until a timeout (#1108).
## [0.9.28-alpha.11] - 2026-10-08
### Fixed
- **Skill resources that document `$ARGUMENTS` command templates no longer index as commands.** The command
placeholder matcher now respects a skill folder as declared context, and the indexer re-files existing entries
on the next incremental index (#1084).
- **A proposal drain judge now sees overlapping promotions accepted earlier in the same drain (#1085).**
- **`akm proposal drain --strategy <name>` now honors enabled triage judgment.** A named strategy with
`processes.triage.judgment.enabled: true` runs its configured judge without `--judgment`; the flag still
forces judgment for disabled strategies, and drains without `--strategy` keep their existing behavior (#1086).
- **`akm upgrade` no longer defers the OpenCode plugin refresh for OpenCode servers running in containers
(#1099).** The "is OpenCode running" check counted an `opencode serve` inside a Docker container, which
keeps its own cache, so the host's refresh was deferred for as long as the container ran. On Linux a matching
process now counts only when it shares akm's mount namespace (or, where that is unreadable, when its cgroup
is not a docker/containerd/podman/lxc scope); a process that cannot be inspected still defers.
- **Five test suites no longer leak their temp folders on a direct `bun test` (#1088).** The outcome-loop,
outcome-invariance, memory-improve-archive, plan-flag-cli and require-engines-cli suites created `akm-*`
directories under the OS temp dir and never removed them; `scripts/sweep-test-tmp.ts` swept them only for
`test-unit.sh`/`test-integration.sh` runs, so running a file straight with `bun test` left a folder per test
behind. Each suite now registers its temp dirs with the shared test sandbox helper (`makeSandboxDir`, drained
by an `afterEach`), the pattern the other suites already use.
## [0.9.28-alpha.10] - 2026-10-08
### Fixed
- **An `opencode-sdk` dispatch no longer hangs when OpenCode asks for a permission (#1104).** The runner never
answered permission requests, so a tool call that needed one (OpenCode's default `external_directory: ask`,
e.g. reading the plugin's `/tmp/akm-opencode/curated/<session>.md`) stayed `running` until the dispatch
timed out. The runner now rejects each request of its session, and of the sub-sessions it spawns, exactly as
a non-interactive `opencode run` does ("auto-rejecting"), so the model gets a refusal it can work around;
each request is recorded in the dispatch's `stderr`, and the event subscription is closed when the dispatch
ends. Grant access in the agent's `permission` config to allow it instead.
## [0.9.28-alpha.9] - 2026-10-08
### Fixed
- **The `opencode-sdk` server receives `OPENCODE_API_KEY` and `OPENCODE_CONFIG`.** It was started with only the
common baseline and the XDG names, so a provider that reads its key from the environment (OpenCode Go) failed
with `UnknownError: Unexpected server error` where `opencode run` worked. The `opencode` CLI profile and the
SDK server now share one list of OpenCode names; a different key value starts another server.
- **`akm upgrade --next` no longer fails OpenCode as "older than `@latest`" (#1089).** akm-plugins' stable
versions concatenate a timestamp into the patch (`0.9.27202610072331`), so by semver every prerelease build
(`0.9.28-alpha.8.202610081938`) sorted below stable and `akm-opencode@next` was always rejected. `@next` is
now judged by the `akm-cli` it pins against the one `@latest` pins; plugin versions are only compared for
equality (is the cache already this build?).
## [0.9.28-alpha.8] - 2026-10-08
### Fixed
- **`opencode-sdk` runs keep their OpenCode session (#1100).** The SDK runner deleted the session after every
dispatch, so a workflow, task or `akm agent` run left no transcript in OpenCode's history to debug or to learn
from, unlike `opencode run`. The session is now kept; a dispatch akm gives up on (timeout or abort) is still
aborted on the server so it stops calling the model. The OpenCode session id is on the `akm agent` / command
result as `sessionId` and in the task log line, as workflow steps already carried it.
## [0.9.28-alpha.7] - 2026-10-08
### Added
- **An agent engine can name a default `agent`.** `engines.<name>.agent` (`opencode`, `opencode-sdk`, `claude`)
is the native agent the engine runs when a request names none, so an `opencode-sdk` engine, which takes no
`args`, can run e.g. `akm-workflow` for workflows and tasks. A request's own agent wins, a persona is kept,
and model work keeps its confined agent. See [Configuration](docs/reference/configuration.md) (#1049).
### Changed
- **Stability tiers for 0.10's stabilization work.** Experimental: the built-in
improve strategies other than `default` and `consolidate` (`quick`,
`reflect-distill`, `thorough`, `catchup`), the proactive-maintenance and
high-salience lanes, and session extraction (`akm proposal extract`).
Evolving and now listed: `akm command run` and `akm models`; every agent
harness (the `opencode` CLI path, `copilot`, `pi`, `gemini`, `aider`,
`amazonq`, `openhands` included) and the `okf`, `llm-wiki` and
`generic-files` adapters are named Evolving. `akm upgrade`'s plugin step and
`--next` are targeted to become Stable before the 0.9 series ends.
- **Workflows are Experimental.** The `akm workflow` commands (`run`, `status`,
`list`, `create`, `resume`, `abandon`, `plan`), the workflow asset format and
the engine move to Experimental in STABILITY.md and the help text, for the rest
of 0.9 and all of 0.10, which stabilizes the feature and then promotes it or
removes it. Nothing changes in how they run.
### Fixed
- **Consolidate's pair pass no longer retires a knowledge note in favour of a memory (#1092).** Knowledge is the
reviewed form and a memory is raw capture, often the very memory the note was promoted from, yet a `duplicate`,
`supersedes` or `subsumed` verdict could retire `knowledge/...` for `memories/...` (a `.derived` memory counts as a
memory). When the verdict would do that and the judge lists no claim held only by the memory, the memory is retired
in favour of the note instead, through the same guards and staging as any memory retirement; when the memory holds
a claim of its own, nothing is minted and the pair is counted as a no-action verdict. A memory is still retired in
favour of a knowledge note, and knowledge against knowledge is unchanged.
## [0.9.28-alpha.6] - 2026-10-08
### Fixed
- **The #1063 fix now reaches files that have not changed (#1063).** An incremental `akm index` skips a
directory whose files are unchanged, so a reference file already filed as a command stayed
`commands/skills/<name>/...` until `akm index --full`. The `akm` adapter's version, which is part of each
directory's freshness, is bumped, so the next `akm index` after upgrading re-files those rows once (about 18
entries in a 18,000-entry library) and drops their old refs. Reading every directory once makes that run slower
(about 27 s against 2 s on that library) and re-embeds the entries whose embedding hash was written by an older
release (about 8,000 of them); later runs cost what they did before.
## [0.9.28-alpha.5] - 2026-10-08
### Added
- **`akm upgrade --next` installs the `@next` prerelease of akm and its OpenCode plugin.** The CLI target is
the `next` dist-tag of `akm-cli` when it is newer than the latest stable release, else the stable release
(never a downgrade); npm/Bun/pnpm installs name that exact version and binary installs take its GitHub
release. The OpenCode lockstep and cache refresh follow `akm-opencode@next` when the OpenCode config's
`plugin` list names it (akm never edits that config); with a bare `"akm-opencode"` the entry is `skipped`
with a message saying to set `"plugin": ["akm-opencode@next"]`, and lockstep stays on the `@latest` pin.
Missing, older-than-`@latest` or unpinned `@next` fails closed like the stable lockstep. Claude Code and
Codex are unchanged (no prerelease channel). The result gains `channel`. Works with `--check`, `--force`
and `-q`. See [`akm upgrade`](docs/reference/cli.md#prereleases-next).
## [0.9.28-alpha.4] - 2026-10-08
### Added
- **`akm upgrade` also updates the akm plugin of each installed harness (#1007).** After the CLI step
it refreshes Claude Code (`claude plugin marketplace update`, then `claude plugin update`) and
Codex (`codex plugin marketplace upgrade`) when the `akm-plugins` marketplace is configured and the
plugin is installed, and replaces a stale cached `akm-opencode` (moved to the trash, then re-fetched
by `opencode debug config`) unless OpenCode is running, in which case it is `deferred`. It only
updates: a missing plugin is never installed. The result gains `plugins` (one `outcome` per harness:
`updated`, `current`, `skipped`, `deferred`, `failed`; under `--check`, which changes nothing,
`pending` for OpenCode and `unknown` for Claude Code and Codex, whose check needs a fetch) and a failed plugin exits 1. With the OpenCode plugin present the CLI moves to the akm-cli
that `akm-opencode@latest` pins instead of the newest release, reported under `lockstep`; if the pin
cannot be read the CLI is held where it is rather than moved ahead of the plugin. The
Containers entrypoint and the Codex hook trust entries are documented under
[`akm upgrade`](docs/reference/cli.md#upgrade).
### Fixed
- **A skill's reference file with a shell `$1` is no longer indexed as a command (#1063).** A file under
`skills/**` other than `SKILL.md` that showed `local var="$1"` in a code block was retyped to
`commands/skills/<name>/...`. A skill's folder now counts as a declared context, as `memories/` and the other typed
directories already did, so the file stays a skill resource (`knowledge/skills/<name>/...`). A `$1` under
`commands/` or in a loose file is still a command.
## [0.9.28-alpha.3] - 2026-10-08
### Changed
- A consolidate pair-pass retire proposal now records the judge's claim lists in its `retirement` metadata as `onlyInRetired` and `onlyInSuccessor`, named by role rather than by the judge's A/B, so what the judge found only on each side survives past the run. Proposals minted before this lack both fields; no row is added for a pair akm keeps.
- A distill lesson the quality gate rejects or sends to review now keeps its text: the `distill_invoked` event and the distill result carry `rejectedContent`, cut to 2000 characters (the judge prompt's cap). Before, a rejected lesson left only its score and reason. It stays local in `state.db`.
## [0.9.28-alpha.2] - 2026-10-08
### Added
- **`akm metrics` (experimental) reports what akm has recorded, in every `--format`.** One
read-only command covers asset usage (searches, shows, curates, selects, the
queries that returned nothing), feedback with its reasons and tags, utility
and outcome scores, LLM tokens, latency, task runs,
proposals and workflow token spend. `--since` (default `30d`) sets the window start; the window ends now, counts
`user`-source usage and keeps the top 20 of every ranked list. The
window rows (usage rows as recorded, LLM calls summed per day, engine, model, process and stage) ride along with
`--format html` and `--detail full`. A window
longer than a store's retention says so in `notes`.
- **`akm metrics` renders as text and Markdown.** `--format text` prints aligned
Usage, Feedback, Utility, LLM, Index, Tasks, Proposals and Workflows sections
with top-N tables (cut to 5 at `--detail brief`); `--format md` prints one
heading per section with GFM tables.
- **`akm metrics --format html` writes a self-contained dashboard.** The page
carries the window's raw rows, so you can filter by date, bundle, source and
event type, sort the tables, open an asset to see its timeline, queries and
feedback, and download any table as CSV, all in the browser. Charts load
ECharts from the same CDN tag as `akm health --report`. A page keeps the most
recent 50,000 usage rows and says so when it cuts older ones.
### Changed
- **LLM usage is recorded for every command.** `akm index`, curate, workflow,
agent dispatch and `akm command run` now persist their `llm_usage` events
like `improve` and `proposal drain` already did; before, a call made outside
those two was dropped. An improve run still keeps its own sink and the
process-wide one resumes when it ends.
- Search latency is recorded in the search summary usage row (`totalMs`, plus `rankMs` and `embedMs` when present), and every `akm index` run appends an `index_completed` event with its phase timings.
### Fixed
- **Usage-event retention no longer deletes a day early.** The purge on `akm index`
compared the space-separated `created_at` against an ISO cutoff, so every row on
the cutoff's date was removed up to 24 hours before its 90 days were up. Both
sides are now normalized before the comparison.
## [0.9.28-alpha.1] - 2026-10-08
### Fixed
- **Distill skips a memory marked `beliefState: deprecated` or `superseded`.** Such a note is no longer true and gave
no lesson (26 of about 900 memories carry the state; distill ran on 17 of them). The improve loop records a
`distill-skipped` action and an `improve_skipped` event (`distill_deprecated_or_superseded`), and the attempt goes
in the ledger as `unchanged`; an explicit `--scope` ref still runs. `contradicted` is still distilled.
- The distill judge scores a lesson 1–2 on non-redundancy when a listed asset
states most of what it says, not only when it states the same rule, and is
told to compare only with the listed assets, never with the source memory.
Before, a lesson that restated a skill the library holds scored 3 ("largely
redundant") and went to a reviewer. Judge-only replay of the lessons the
writer produced (local qwen3.8-27b, 3 repeats): of 4 public lessons for
memories that deserve none, 12 verdicts passed 3 before and 1 after, and of
12 such own lessons 33 of 36 passed before and 27 after, while the 31
lesson-worthy own lessons lost no verdict (2 of 93 rejected before, 0 after).
- **The pair judge's reason no longer swaps A and B.** In a replay of 120 recorded pairs, 18 of 90 retirement
reasons said the opposite of what the judge's claim lists decided (for example "B contains all claims from A" for a
pair where B was retired). The lists were right, so no retirement changed, but the reason a reviewer reads was
wrong. The prompt now asks the reason to name the asset that can be deleted; two replays gave 0 and 1 of about 85.
## [0.9.27] - 2026-10-07
The stable release of the 0.9.27 line: 0.9.27-alpha.1, alpha.2 and alpha.3.
When upgrading from 0.9.26:
- **The nightly queue stops refilling with duplicates.** Consolidate no longer
re-offers a memory whose body a reviewer rejected, and the drain's judge sees
the five knowledge notes nearest a promotion and rejects one they already
cover. A confident `subsumed` or `supersedes` retirement that the second look
confirms resolves unattended, like a duplicate (alpha.2).
- **The judge rejects a promotion that is not durable:** a status, rollout,
version or test count, a plan not yet carried out, or something retired or
replaced. On 65 labelled promotions it accepted 13 of 61 bad ones before and
3 to 6 after, keeping 3 of the 4 good ones (alpha.3).
- **Distill writes a lesson only when its memory holds one.** The writer may
answer that there is none; its judge rejects a lesson that only records what
was done, repeats an asset the library holds or claims what the memory does
not say; both see the related lessons, knowledge notes and skills (alpha.2).
- **Reflect skips feedback an accepted exact fix already resolved**, and no
longer writes a second copy of a skill's reference files (alpha.1, alpha.2).
- **Fixed:** an old (0.9.1-era) index is recreated instead of crashing every
`akm index`; an updated index scores like a fresh one; strict APIs that reject
`chat_template_kwargs` get one retry without it, and every response schema is
strict-valid; extract reports a lesson it refuses; a codex dispatch with a
schema leaves no temp folder (alpha.1).
- **Removed:** `scripts/akm-eval`, now in itlackey/akm-eval (alpha.2).
## [0.9.27-alpha.3] - 2026-10-07
### Fixed
- **The drain's judge rejects a promotion that is a status snapshot, a plan or
something retired, not only a duplicate.** Its rubric for a promotion (a memory
proposed as a new knowledge note) said to reject a note that is wrong, a
duplicate or contradicts the live asset, and to accept a correct, valuable one,
so it judged only whether the note was new: the reasons it gave for accepting a
note pinned to commits and versions, a rollout status or a retired host were
"distinct from the existing notes". The reject line now also names a note that
reports the state of something that changes (a status, rollout, branch, commit,
version, test count, "as of <date>"), a plan not yet carried out, or something
already retired, replaced or superseded, and says a lesson drawn from an
incident is durable. On the owner's 65 labelled promotions (chat/qwen3.8-27b,
two runs each) the judge accepted 7 and 8 of the 14 stale ones and 4 and 5 of
the 7 ephemeral ones before, and 1 and 3, 0 and 1 after. Other proposals keep
the old rubric. No new settings.
## [0.9.27-alpha.2] - 2026-10-07
### Removed
- **The `scripts/akm-eval` toolkit has moved out of this repository.** The
read-only measurement toolkit (the case runner and its suites, the twin
experiment, the real-query verdict for the proactive lane, the state
analyzers and the curate benchmark) is retired; every live eval is in
[itlackey/akm-eval](https://github.com/itlackey/akm-eval). Its code is kept
there, to read and not to run, in `retired/akm-scripts-akm-eval/`, copied from
commit `f57a7fd44b37`. It imports akm's `src/` by relative path, so it runs
only in a checkout at that commit. Removed here with it: `scripts/akm-eval/`,
its tests (`tests/integration/akm-eval/`, `tests/akm-eval-*.test.ts`,
`tests/curate-metrics.test.ts`) and fixtures (`tests/fixtures/akm-eval/`, and
the `curate-golden` stash, which only the curate benchmark read), the
`akm-eval determinism` CI job, and `getMeasurementVerdictsDir`, whose only
caller was the verdict runner. akm no longer names
`$STATE/improve/measurement/verdicts/<stash>/`; a file already there is inert.
`docs/maintainers/eval.md` is now a pointer to the new home.
### Fixed
- **The drain's judge no longer sees a note with a code block as truncated.**
The judgment prompt fenced the proposed content (and the live asset, sibling
proposals and neighbour excerpts) in three backticks, so a note holding its
own code block closed the fence early and read as cut off; real rejections said
"ends in an empty code block" or "truncated". Each block now uses a fence longer
than any backtick run inside it. The judge's reason is also kept on accepts,
staged accepts and defers (as the gate decision's `judgeReason`, until now
rejections only), and a judge reply that is not a verdict is stamped
`judgment-parse-failure`, and a runner failure `judgment-error`, instead of
looking like a defer.
- **Distill writes a lesson only when its memory holds one, says only what the
memory says, and its judge rejects what a reviewer would.** 2 of the 22 distill
proposals since 0.9.26 began were accepted, and 17 of the 19 queued on
2026-10-05 were bad (they restated their memory, filed a dated status as a
lesson, claimed what the memory does not say, or repeated an asset the library
holds). Four causes, found in the code and the rejected proposals, and fixed:
(1) the prompt and schema forced a lesson from every memory, and 18 of the 19
were records of what was done; the writer now says why a memory holds a
lesson or none (`reason`, then `decision: lesson|none`, or the word `NONE`),
defined as a cause and what to do about it, or a rule with its reason, and
writes only what the memory and its feedback state, in the scope they have; a
`NONE` is a `skipped` distill (`skipReason: nothing_reusable`, the writer's
reason in the message) with no proposal and no judge call, and the loop keeps
its ledger row `unchanged`. (2) The judge asked for "information not already
present in the source", so an invented claim scored as novel and a faithful
lesson of a lesson-worthy memory as a restatement, and it passed anything that
"goes beyond the source" because it "may draw on feedback you are not shown".
The rubric is now reusable (a rule with its reason, not a record of what was
done), non-redundancy and grounding (every cause, step, number and limit is
in the source or its feedback), and the judge is shown the feedback the writer
saw. (3) A mean hid a decisive score (4 and 1 average 2.5, a review), and a
reviewer read everything the judge did not reject; any criterion at 2 or
below, grounding included, is now `quality_rejected`, and the reason names it
(`grounding 2/5: …`). The "borderline grounding" routing is gone. (4) Neither
the writer nor the judge could see a knowledge note or a skill that already
states the rule (the judge saw the 3 lexically nearest lessons, none of them
related); both now see the lessons, knowledge notes and skills nearest the
memory, which is the existing `processes.distill.cls` context turned on by
default (`enabled: false` turns it off). Judge scores are keyed `reusable`
where they were `novelty`. Measured on the local qwen3.8-27b with akm-eval's
`evals/distill` (30 fictional memories, 5 runs each side): good lessons 7/14 on
average (5 to 9) against 3.7/14 (3 to 5), lessons queued for memories that
deserve none 0.4/16 against 3.3/16. On 37 real memories with their feedback
(34 reviewed bad, 3 good; 3 runs against 2): a lesson was queued for 11% of the
bad ones against 44%, and for 6 of 9 good ones against 4 of 6; of the memories
that pass 0.9.26's skip of bare positive feedback, 20% of the bad against 50%.
No new settings.
- **Reflect no longer plans an asset whose negative feedback is already acted
on.** A negative `akm feedback` that came with an exact fix (`--replace` and
`--with`, `--outdated` or `--superseded-by`) makes a `feedback` proposal, and
once that proposal is accepted the feedback has done its work. Reflect still
took the ref as having fresh negative feedback, and on 2026-10-07 44 of its 50
refs were of that kind: the judge refused or the model changed nothing for
most of them. A negative event with a fix is now left out of the reflect
cursor when an accepted `feedback` proposal for the ref was created at or
after it. A negative with no fix, one given after the proposal, and one whose
proposal is still pending or was rejected plan a reflect as before.
- **Consolidate stops re-offering memories a reviewer already turned down, and
the nightly judge sees what a promotion may duplicate.** About 53 promotions a
night reached review at ~5% precision, 64-70% of them a memory body already
proposed or rejected. Four causes, four changes: a memory whose body equals
that of a consolidate promotion rejected on or after 2026-09-29 is held until
its body changes, under any name (earlier rejections, the bulk audits of
2026-08, do not count); a memory the model judged and left alone is held by its
body hash instead of a 7-day clock, so an unchanged memory is no longer judged
every week (a row recorded without a hash keeps the 7 days); the coverage gate
skips a memory when 30% of its text, not 50%, is in a neighbouring knowledge
doc, which catches paraphrases; and the drain's judgment tier, which judged a
promotion seeing only the proposal and never `knowledge/`, is now shown the 5
nearest knowledge notes (ref, description, excerpt) and told to reject a
promotion they already cover. No new settings.
- **A confident `subsumed` or `supersedes` retirement resolves unattended, as a
`duplicate` already did.** The pair pass staged a retire proposal for the
triage drain only when the judge's label was `duplicate`; every other retirement
waited for a person. It now stages any of the three retire labels when the
second look (what does the retired note hold that the kept one lacks?) comes
back empty and there is no continuity risk; the retired side's claim list is
already empty for any proposal, and a `duplicate` must still have an empty list
on the kept side too. The staged gate reason is the judge's label, and the drain records it. Replay over 360
judged pairs: 336 safe (0.93): `duplicate` 0.98, `subsumed` 0.92, `supersedes`
0.875. In production, unstaged `subsumed` retirements were accepted 45 of 54
times by hand, and in the latest run 41 of 47 pair proposals would have resolved
without a person. `docs/architecture/internals/improve-workflow.md` said triage
never auto-accepts a retire proposal, which stopped being true in 0.9.26; it,
and the matching lines in `improvement.md`, now describe the staging rule.
## [0.9.27-alpha.1] - 2026-10-06
### Fixed
- **A codex dispatch with an output schema no longer leaves a temp folder
behind.** Every build of the codex command for a request with a schema made a
new `akm-codex-schema-*` folder in the OS temp dir for `--output-schema` and
nothing ever removed it (the builder has no post-run hook, and the file is read
after it returns), so a machine whose `/tmp` is tmpfs held a folder in RAM per
dispatch until reboot. The schema is now written once to akm's cache dir, in a
file named by its hash: concurrent units dispatching the same schema share it,
a rewrite is an atomic rename of identical bytes, and the only residue is one
small file per distinct schema.
- **An index that has been updated ranks like a fresh index of the same files,
and `akm index --full` no longer doubles the full-text totals.** `entries_fts`
is contentless, and FTS5 cannot take a deleted row out of a contentless
table's BM25 totals (its row count, and the token counts the average document
length comes from). Every replaced or removed row left them one row too high:
40 notes read 40, then 41 after one edit, then 81 after `--full`, and a delete
never lowered them, so scores drifted away from what a fresh index gives.
SQLite has no command that recomputes them (`delete` and `rebuild` are refused
on a contentless table), so a delete that removes a row now stamps
`index_meta.ftsTotalsStale` in its own transaction, whichever process made it,
and the next `akm index` rebuilds the table from `entries` before it finishes:
about a second at 25,000 entries, and only when rows have left the table. A
row the write-path index replaced after an accepted proposal is settled the
same way, and an index that has already drifted is corrected by the first run
that replaces or removes a row.
- **`akm improve` runs against an API that rejects `chat_template_kwargs`,
OpenAI's among them.** Improve's reflect, consolidate and judge calls always
ask for thinking off, and the client sends that as
`chat_template_kwargs.enable_thinking` and a top-level `enable_thinking`. A
strict API answers 400 `Unknown parameter: 'chat_template_kwargs'`, the retry
without the response schema sent both fields again, and `akm improve judge`
reported `judge timeout/error — routed to review`. No engine setting could
stop it: the call sites override the engine's `enableThinking`, and
`extraParams` can only add fields. A 4xx that names either field is now
answered by one retry without both (which may in turn fall back without the
schema), and akm stops sending them to that endpoint and model for the rest
of the process, as it already does for `response_format`.
- **A lesson the model wrote without a `when_to_use` is no longer thrown away
silently by `akm proposal extract`.** The extract schema left `when_to_use`
optional while the parser dropped a lesson without one (or with one under 15
characters) and said nothing, so a model that followed the schema could
write a sound lesson that akm discarded and the session reported no
candidates: on akm-eval's extract eval qwen3.8-27b kept 3 of 11 expected
insights against 9 for gpt-oss-120b. Every property of the schema is now
required, `when_to_use` and `rationale_if_empty` included, with an empty
string for none (a memory or knowledge candidate needs no trigger, a
non-empty answer no rationale), which is also what a strict structured-output
provider needs; the prompt's output contract says the same. Any candidate the
contract still refuses, for this reason or another, is named in its session's
`warnings` as `<type>:<name> dropped: <reason>`.
- **Distill's response schemas are valid for a strict structured-output
provider.** The client sends a response schema `strict: true`, and OpenAI
refuses one whose objects leave a property out of `required`: `400 Invalid
schema for response_format 'akm_response': ... Missing 'tags'`. The lesson
schema left out `tags`, and the knowledge schema `tags` and `sources`, so
the first distill request on such a provider always failed. After a 4xx the
client retries once without the schema, but a gateway that answers the same
rejection with a 502 is not retried, and every distill call through it
failed; the workaround, `supportsJsonSchema: false`, loses the guidance that
keeps a model from leaving out `when_to_use`. Every property is now
required, and an empty array stands for none (distill already dropped an
empty `tags` or `sources`).
- **`akm improve` no longer reflects on an asset whose file a proposal would not
write, so accepting a reflect proposal no longer adds a second file.** A
skill's `references/a.md` is indexed as `knowledge/skills/<name>/references/a`,
but a proposal writes the path derived from that ref,
`knowledge/skills/<name>/references/a.md`. Nothing is there, so the proposal
was a `create`, and accepting it wrote a copy beside the skill's own file;
later proposals then revised the copy while the skill's file drifted. Reflect
now refuses before it calls the model, naming the file and the path a
proposal would write, and the loop records it as a skip (`unsupported_type`,
`file_outside_layout` in the `reflect_completed` event). This is the rule
0.9.26 added to `akm feedback --replace`. An asset that another bundle owns is
still refused by `createProposal` (#1000).
- **An index that 0.9.1 wrote no longer crashes akm.** Every `akm index` on it
exited 70 with `null is not an object (evaluating 'doc.xrefs')`, `akm migrate
apply` and `akm index --full` did not help, and `akm search` failed with
`null is not an object (evaluating 'item.entry.quality')`; the only way out
was moving `index.db` aside. That layout (20) keeps the transitional
`entry_key`, `dir_path`, `stash_dir`, `entry_json` and `entry_type` columns
that layout 21 removed, each NOT NULL, beside the current columns, and leaves
`document_json` NULL on every row. The table had every column akm checks for,
so it was taken for a current one: the links migration read the NULL
documents, and no insert could ever have succeeded (`NOT NULL constraint
failed: entries.entry_key`). akm now treats a table that still has a retired
column as older than layout 21, as the compat notes already said: the
writable opener recreates its entries-keyed tables (the LLM enrichment cache
is kept) and the next `akm index` re-walks every source, and a read rebuilds
inline. An index that a failed open already half-migrated (layout stamp still
20, `search_text` dropped, `asset_links` created) recovers the same way.
- **Two files that claim one ref no longer trade places in the index, and `akm
index` says so.** A skill's `references/a.md` and a note at
`knowledge/skills/x/references/a.md` are both the ref
`knowledge/skills/x/references/a`, and the index holds one row for it. The
first file a run persisted held it, so a full build followed the filesystem's
listing order (one bundle indexed on tmpfs and on ext4 held different files),
and the first incremental run after a full build handed the row to the other
file, because it drains only the directory that lost: with no file touched,
the row, its search entry and the text its vector is embedded from changed,
and the vector was dropped and recomputed. When a smaller-path file was added
later and then deleted, the ref also left the index until `--full`, although
its other file was still on disk. The file with the smaller path (code-point
order, as `akm show`'s refusal lists them) now holds the ref however the
directories are drained and the walk is ordered, a directory that gives a ref
up is drained again so the ref passes back when its holder goes, and each
pair is reported in the `warnings` of `akm index`, naming the file indexed
and the one skipped.
- **Consolidate's plan schema and the session summary schema are valid for a
strict structured-output provider, and a new response schema can no longer
skip the rule.** The client sends a response schema `strict: true`, and
OpenAI refuses one whose objects leave a property out of `required`. The
consolidate plan left out `description` and `confidence`, and the session
summary `tags`, so the first request of every plan and every summary to such
a provider was refused. The client then retries without the schema and
remembers that per connection (endpoint and model), not per schema, so one
invalid schema also switched the response schema off for every valid one
that followed on that connection. Every property of both is now required: an
empty `description` keeps the memory's own, a null `confidence` records none
and an empty `tags` array is no tags, which is how all three were already
read. A contract test runs every schema akm sends through the rule, so a
property added later without being required fails CI.
## [0.9.26] - 2026-10-05
The stable release of the 0.9.26 line: 0.9.26-alpha.1 and alpha.2, and the
changes in this section. When upgrading from 0.9.25:
- **Consolidate retires far fewer notes that hold something the kept note
lacks.** Its pair judge lists what each note alone holds and akm never
retires a note with anything listed; on 385 held-out pair proposals, 91% of
its retirements are safe, against 61% before. A duplicate that a second look
confirms is accepted by the triage drain (alpha.1).
- **Negative feedback can carry the fix:** `akm feedback --negative --replace
"<exact text>" --with "<corrected>" --source "<evidence>"` (alpha.2), and
`--outdated` or `--superseded-by <ref>` to mark a note's history. Either
becomes one `feedback` proposal for review.
- **Negative feedback is for wrong or stale content.** The hints and docs say
so; a note that did not fit the task records nothing. Each feedback event now
records the hash of the text it judged, and reflect marks feedback given on
an earlier version.
- **Distill no longer accepts a lesson unattended.** A lesson that passes its
judge waits for review instead of the drain, and distill skips a memory
flagged wrong since its last edit, one whose only feedback is a positive
without a reason, and a lesson that already exists.
- **Fixed:** a proposal no longer rewraps a note's frontmatter to add its
`type`, and accepting one stamps its provenance as lines of its own instead
of writing the frontmatter out again; a feedback fix for an asset outside the
bundle's layout is refused instead of queued at the wrong path.
- **Removed:** `akm feedback --failure-mode` and `feedback.allowedFailureModes`
(an old config still loads, naming the key once).
### Added
- **`akm feedback --negative --superseded-by <ref>` and `--outdated` mark an
asset's history, in the same single `feedback` proposal as any `--replace`
edits.** `--superseded-by` (another asset replaces this one) sets
`beliefState: superseded` and adds the ref to `supersededBy`; akm resolves it
through the index first, and it must be indexed and not the asset itself, or
nothing is recorded. The rules are those of `akm remember --supersedes`: an
asset that already says so is left as it is, `contradicted` and `archived`
stay, and a scalar `supersededBy` becomes a list. `--outdated` (the asset
describes a past state and nothing replaces it) sets `beliefState:
deprecated`, unless the asset already says superseded, contradicted or
archived. Like every fix, both are for negative feedback and need `--reason`
and `--source`; they apply to markdown assets and are not used together. akm
edits only the `beliefState` and `supersededBy` lines of the text, so
comments, quoting, key order and line endings stay (it writes the frontmatter
out again only when a line edit cannot follow how a key is spelled), and `fix`
in the command's output and in the feedback event says what was set.
### Changed
- **Each feedback event records the text it judged, and reflect marks feedback
given on an earlier version of it.** `akm feedback` adds `contentHash`, the
sha256 of the asset's body without its frontmatter as it stood when the
feedback was given, to the event and its usage row, for positive and negative
feedback. It is left out for an env or secret file, whose bytes akm never
reads, and when the file cannot be read. When reflect gathers an asset's
recent feedback, a line whose `contentHash` differs from the asset's current
body ends with ` (given on an earlier version of the text)`, so the model
knows the text changed since. A line without a `contentHash` (all feedback
recorded before this change) or with a matching one reads as before, and the
rest of the prompt is unchanged.
- **The shipped hints and docs say what negative feedback is for.** Record
`akm feedback --negative` only when an asset's content is wrong or stale, and
say what is wrong and what it should say. A note that simply did not fit the
task is not negative feedback: record nothing for it. Once the correct fact is
verified, attach the exact fix with `--replace`/`--with`/`--source`. Of the 195
negative reasons recorded in the last 30 days, 100 named no error in the note,
and 89 of those only said it did not fit the agent's task, which still lowered
the note's ranking and sent it to improve. The hints and the guides no longer
list "unhelpful" among the reasons to flag a note, and their example reasons
name a wrong fact instead of "wrong framework" or "incomplete-edge-cases". The
hints also say that an outdated note can be marked with `--outdated`, or
`--superseded-by <ref>` when another note replaces it.
- **Distill skips a memory that was flagged wrong and not edited since.** A
memory with negative feedback in the last 30 days on the text it still has is
no source for a lesson, so the improve loop skips distill for it: a
`distill-skipped` action with the reason "flagged wrong since its last edit"
and an `improve_skipped` event (`distill_flagged_wrong`). The attempt goes in
the improve ledger as `unchanged`, so the memory waits for newer feedback, and
reflect still plans it. Feedback records the hash of the body it judged
(`contentHash`), so a write that leaves the body alone, such as an inference
stamp or an accepted frontmatter repair, does not lift the flag; changing the
body does. Feedback recorded without a hash keeps the earlier test: it flags
the memory while it is newer than the file's last write (its modification
time, as in the retrieval scope's new-material test), so any later write ends
it. An explicit `akm improve <ref>` still distills it.
- **A distill proposal that passes the quality judge goes to review, not to the
drain.** The gate used to stamp a passing lesson (or knowledge promotion)
`staged`, and the triage drain accepted it on the next run with no one
looking. It is now minted `deferred` for a person: gate decision
`deferred`/`quality-gate` with the reason `distill-review`, carrying the
judge's per-criterion `scores` and `judgeReason`, so neither the drain nor its
judgment tier accepts it, and the improve ledger records `review_needed`. The
`distill_invoked` outcome is still `queued`. On 2026-10-05 the gate had
staged 12 lessons and 10 were bad (they restated the memory, claimed what it
does not say, or filed a dated status as a lesson); no judge score separated
them from the two good ones. A failed judgment behaves as before, and with
`processes.distill.qualityGate` off nothing is judged, so the proposal is
minted unstamped for the drain to decide.
- **Distill skips a memory whose only recent feedback is positive and says
nothing.** A bare `akm feedback --positive` records that a note helped, which
gives the writer nothing to distil, so it restated the memory: 10 of the 11
lessons made from a memory with only that kind of feedback were rejected (the
11th was good). When every feedback event in the last 30 days that counts as a
signal is positive with no reason and no note, the improve loop skips distill
for the memory: a `distill-skipped` action with the reason "only positive
feedback, without a reason" and an `improve_skipped` event
(`distill_positive_without_reason`). The attempt goes in the improve ledger as
`unchanged`, so the memory waits for newer feedback. A reason, a note or a
negative signal anywhere in the window lets it through, and an explicit
`akm improve <ref>` still distills it. Record `--reason` with a positive
signal to say what helped.
- **Distill no longer regenerates a lesson that already exists.** A lesson's ref
comes from its memory's name (`memories/deploy` gives
`lessons/memory-deploy-lesson`), so a second distill of the same memory
proposed the same ref, and accepting it replaced the lesson. All 5 such
overwrites recorded by the 2026-10-05 review were rejected. When the stash
the proposal is filed in already holds a file at the lesson ref, distill now
returns `skipped` with the reason `lesson_exists` (in the result and the
`distill_invoked` event) before any model call, mints no proposal and leaves
the memory untouched, and the improve loop records it in the ledger as
`unchanged`. A lesson of that name in another bundle does not count, since
the proposal would not replace it. To change a lesson, edit it.
### Removed
- **`akm feedback --failure-mode` and the `feedback.allowedFailureModes` config
key.** The flag labelled negative feedback `incorrect`, `outdated`,
`dangerous`, `incomplete` or `redundant`. None of the 195 negative feedback
events of the last 30 days set it, and nothing read it back. It now fails as an
unknown flag, and `akm feedback`'s output and the `improve_review_needed`
event no longer carry `failureMode`. A config that still sets
`feedback.allowedFailureModes` loads, names the key once as unknown, and
`akm migrate apply` drops it. Events recorded earlier keep their `failureMode`.
### Fixed
- **`akm feedback --replace` no longer queues a fix that accepting would turn
into a duplicate file.** A proposal writes the path computed from the ref's
type and name under the bundle's root, which is not where an asset indexed
outside that layout lives (a git bundle's `tasks/README.md` is
`knowledge/tasks/README`; a skill's `references/symptom-map.md` is
`knowledge/skills/<name>/references/symptom-map`). Accepting such a proposal
created a second file and left the real one unfixed. akm now refuses before
recording anything, naming the file and the path the proposal would write, and
says to edit the file directly. Plain negative feedback on these assets is
unaffected.
- **A proposal no longer rewraps, reorders or strips a note's frontmatter to
add or correct its `type`.** When a proposal's content had no `type:` or a
different one, akm re-serialized the whole frontmatter block, so the reviewer
of a one-line correction also saw long values rewrapped, keys reordered and
YAML comments dropped. In practice 40 of 70 one-line correction proposals did
this. akm now adds a missing `type:` (and `updated:`) as a line before the
closing `---`, or replaces the existing `type:` line where it stands, and
leaves every other byte as written, line endings and body included. It
re-serializes only when it cannot do that safely, such as a `type` value that
spans several lines. The same write path serves `akm remember`, `akm import`
and `akm workflow create`, so a note akm writes without a `type` now carries
it, and `updated`, as its last frontmatter lines instead of its first.
Accepting a proposal also stamps its provenance (`generated`, `verified`,
`provenance`) as lines of its own, the last of the frontmatter, instead of
writing the whole frontmatter out again; a staging run found the old stamp
rewrapping 3 of 16 accepted edits. Accept still turns CRLF into LF, and still
writes the frontmatter out again when it carries bookkeeping keys such as
`inferenceProcessed` from the live note into a proposal that lacks them.
- **`akm lint --fix` keeps a CRLF note's line endings** when it adds a missing
`updated:`. It used to rewrite the whole file to LF, so one added line showed
as every line changed.
## [0.9.26-alpha.2] - 2026-10-05
### Added
- **`akm feedback --negative` can carry an exact fix of the asset's text:**
`--replace "<exact current text>" --with "<corrected text>" --source "<URL,
command or file>"`, repeatable for several edits. akm checks at once that each
`--replace` text appears exactly once and that the frontmatter still parses,
records nothing if a check fails (so the caller can correct it and retry), and
queues the edit as a `feedback` proposal that shows the reason and source to
the reviewer. Improve has edited only an asset's frontmatter since 0.9.25, so
this is how a wrong fact in a note's text gets corrected: by the agent that
found it, with its evidence, instead of by a nightly rewrite.
### Changed
- **`akm feedback` help, the shipped hints and the docs no longer promise that
improve proposes a fix of the text from a negative reason;** they say it may
repair the frontmatter, and point to the exact-fix flags.
## [0.9.26-alpha.1] - 2026-10-05
### Changed
- **Consolidate's pair judge lists what each note alone holds before it
classifies the pair, and akm never retires a note the judge listed anything
for.** The judge also reads 12,000 characters of each note instead of 2,500.
On 650 reviewed pairs from the owner's library, 64% of the old judge's
retirements lost nothing; with the lists, 92% do, and it picks the wrong
note to keep far less often.
- **A confirmed duplicate retires unattended.** When the judge finds nothing
unique on either side, one more call asks only what the retired note holds
that the kept one lacks; an empty answer stages the proposal, and `triage`
`applyMode: "promote"` accepts it like a judged revision (it counts against
`maxAcceptsPerRun`, and a continuity risk keeps it for review). 109 of the
111 duplicates that passed in the reviewed pairs were safe to retire, and an
accepted retirement can be undone with `akm proposal revert`. Every other
retirement still waits for `akm proposal accept`.
## [0.9.25] - 2026-10-04
The stable release of the 0.9.25 line: 0.9.25-alpha.1 to alpha.4, unchanged. Their
sections below list every change. When upgrading from 0.9.24:
- **Reflect edits only an asset's `description`, `when_to_use` and title, and
keeps the body byte for byte;** it may answer "nothing to change" (alpha.3).
Its quality judge has a rubric for these revisions and tells one answering
negative feedback from maintenance (alpha.4). A proposal deferred as
`body-edit` by 0.9.24 stays deferred.
- **Reflect refuses defective revisions before the judge runs** (placeholder
text, talk about its own edit, frontmatter copied into the body), with the
wording lists under `processes.reflect.defectFilter` (alpha.2).
- **`akm improve judge`** runs reflect's quality judge on one revision, for
testing a judge engine (alpha.2).
- **akm learns from Codex sessions:** `akm proposal extract --type codex`,
`--auto` and `akm improve` read `$CODEX_HOME/sessions` (alpha.3).
- **Engines are interchangeable for model work:** one tool policy, one config
rule for which engines model work may use, a 600-second bound on every engine
kind, and `reasoningEffort` as the one effort word (alpha.1 and alpha.2).
- **Removed:** the `--track-usage` / `--no-track-usage` flag on `akm search`,
`akm curate` and `akm show` (either spelling now fails as an unknown flag),
and opencode model work's step limit (alpha.2).
## [0.9.25-alpha.4] - 2026-10-04
### Changed
- **The reflect quality judge's rubric is written for the frontmatter-only
revisions reflect makes, and says what drove each one.** A revision answering
negative feedback (any `[negative]` line) is judged only on the fields it
changes, since it cannot change the body and need not resolve feedback about
it. A field change is justified when the field is missing or broken, claims
more than the body covers, or, for a note the feedback calls stale, needs to
name the version or date the body records. A maintenance revision needs a
missing or broken field, and rewording a sound one is churn. New values are
checked as claims against the body. On 113 frontmatter-only proposals
reviewed by Claude Opus, the production model passed 78 of the 95 good ones
and 3 of the 18 bad ones, against 53 and 7 under the previous rubric, which
was tuned on body rewrites. The agent judge's tool paragraph drops its
feedback-points check, which contradicted that rule.
## [0.9.25-alpha.3] - 2026-10-04
### Added
- **akm learns from Codex sessions.** `akm proposal extract --type codex` reads
the rollout files Codex writes under `$CODEX_HOME/sessions` (`~/.codex/sessions`
by default), as it reads Claude Code's and opencode's session files. `--auto`
and `akm improve`'s session extraction include Codex on a machine that has
them, and a session is extracted once, as for the other harnesses. The model
sees what the person and Codex said and the tool calls and results between
them, without Codex's own instructions or injected context (AGENTS.md, the
environment, an invoked skill). The reader lists a person's sessions,
`codex exec` runs included. It leaves out the rollouts Codex writes for
subagents and its other internal agents, which are not sessions of their own, and
unlike a Claude Code subagent transcript it does not fold them into their
parent.
### Changed
- **Reflect changes only an asset's `description`, `when_to_use` and title;
akm keeps the body byte for byte.** On 396 labelled reflect edits, those that
fixed a frontmatter defect and left the body alone were good 24 times in 26;
those that also rewrote the body were bad 175 times in 224. The reply is now
`confidence` and a `frontmatterPatch` of `description`, `when_to_use` and
`title` (each a non-empty single-line string, or `null` for no change), plus
`ref` when no target was given, and no body: the JSON Schema, both output
contracts and the repair prompt say so, and the framed reply for an endpoint
that rejects JSON Schema has no content markers. akm applies the patch to the
asset it read by rewriting only the changed keys' frontmatter lines, so every
other line, including a list beside a description the YAML parser cannot read,
stays as it was; a source whose closing `---` is fused onto a value gets no
proposal. A non-null `title` becomes a
`# <title>` heading and one blank line at the top of the body, only when the
body has no level-1 heading; otherwise it is ignored. A patch that changes
nothing, whether every field is `null` or equal to the source's, creates no
proposal (`no_change`); an asset that requires a `description` and has none
still gets one derived from its own text, as before (#636).
- **Reflect may answer "nothing to change."** Its prompt forbade it: "your
proposal must correct or add something the source lacks", "must meaningfully
differ" from a rejected proposal, "do not return the same content
unchanged", and "you MUST generate" a `when_to_use`. On the same edits, those
that fixed no defect were bad 86 times in 93, and those driven by negative
feedback 50 times in 53; most of that feedback says the asset did not help
with an unrelated task. One goal sentence now covers every asset type: check
the three fields against the body and the feedback, and return `null` for
each that needs no fix. The feedback caveat says that feedback about a task
the asset never claims to cover needs no change, and with no feedback the
prompt says to fix only a missing or broken field. A rejected proposal is not
to be proposed again, and the engine returns `null` when no other change is
justified.
- **Reflect's prompt names the frontmatter problems akm can see** (a
description split by a stray period or carrying an escaped quote, no
`when_to_use`, no title) and asks for a repair that keeps the description's
wording, names, numbers and paths rather than a rewrite. Without the list the
model fixed 51 of 122 broken descriptions and 93 of 193 missing titles; with
it, 120 and 189. When the feedback calls a note stale or historical, a new
`when_to_use` names the version or date the body records, or stays as it is.
On an 80-case sample of the labelled set, Claude Opus reviewers judged 64 of
the new reflect's 78 proposals good; the old reflect's edits in the set are
good 81 times in 396.
- **`akm proposal accept` warns about an echoed "Avoid These Patterns"
section instead of refusing.** Reflect keeps a body as it is, so a proposal
for an asset that already carries the section (a leftover of the run-only
prompt text) would never be accepted, and the person accepting cannot edit
the proposal.
### Removed
- **Everything reflect needed to rewrite a body.** The prompt's "Content
preservation rules" and the size bounds computed for them; the related
distilled lessons section and the companion-doc
(`knowledge/skills/<skill>/references/<topic>`) option, with the gathering
behind them and the `derived_from_reflect` marker it read; the size guard and
the truncation-marker check on reflect's output, and their review reasons
`reflect-size-ratio` and `reflect-truncation-leak`; the stripping of an
appended frontmatter block and of an echoed "Avoid These Patterns" section
from a body; and the restore of identity fields, which a patch cannot name.
The accept-time size advisory and truncation-marker block stay.
- **The `body-edit` review reason.** A reflect revision no longer changes the
body, so one the judge passed is stamped `staged` and the triage drain
accepts it, as before 0.9.24; with `processes.reflect.qualityGate` off it is
minted unstamped for the drain to decide. A proposal already deferred as
`body-edit` stays deferred.
## [0.9.25-alpha.2] - 2026-10-03
### Added
- **`akm improve judge`** runs reflect's quality judge on one revision, read as
`{"source", "candidate", "feedback", "ref"}` JSON from stdin, with the engine
`processes.reflect.qualityGate.engine` names, and prints the verdict. It
writes nothing: it tests a judge engine on revisions whose right answer you
know.
### Changed
- **Reflect refuses three kinds of defective revision before the judge runs,**
with the quality gate on or off: one that adds placeholder text ("please
confirm", "to be confirmed"; not `TODO`, `TBD` or `FIXME`), one that talks about
its own edit ("the feedback says", "this revision", "the source asset", a
quoted gate rejection), and one that copies frontmatter into its body (key
lines such as `sources:` or `updated:` outside code, or a `sources`,
`xrefs` or `contradictedBy` value). Each rule counts only what the revision
adds to its source. A hit is a `quality_rejected` refusal with no proposal
and no judge call, and the event names the rule (`reflectDefect`). Each
rule's wording is a list that `processes.reflect.defectFilter` can replace:
`placeholders` and `metaCommentary` are plain phrases, matched as whole words
in any case, and `frontmatterKeys` are exact key names. A list left out keeps
its default, and `[]` turns its rule off. On 396 labelled reflect edits the
default lists hit 22 of 313 bad edits and none of 83 good ones.
- **The reflect quality judge's rubric names what it kept missing.** Tuned on
the labelled judge-gate set (plain judge, qwen3.8-27b): a description with a
sentence split by a stray period or an unbalanced quote is broken text; a
missing title, description or `when_to_use` is a concrete problem whether or
not the feedback mentions it, while a bare `type:` or a provenance stamp is
not; PRESERVATION and QUALITY are judged line by line in the changed region,
so a fix elsewhere no longer excuses a dropped fact, an invented or
strengthened claim, a hedge or a placeholder. On a stratified 100-case sample
the gate passed 31 of 40 good edits instead of 18 and 3 of 60 bad instead of 2.
- **A reflect quality judge on an agent engine reads only to verify what a
revision adds.** Its prompt names the revised asset's ref and adds one
paragraph: read that asset, or one the changed region names, with `akm_show`
only to check a fact the revision adds or alters, at most twice, and never
search; before scoring, find each added statement in the asset or the
feedback (a step or cause that merely seems to follow does not count), each
source fact in the revision, and each feedback point in a change to the text
it is about. The plain judge's prompt is unchanged. On the same 100 cases
(qwen3.8-27b, thinking on) the agent judge passed 38 of 40 good edits and 14
of 60 bad, against 34 and 12 for the plain judge on that model. Give the
engine's `llmEngine` `enableThinking: true`: without thinking the model
looped on tool calls.
- **A failed reflect reply reads the same on every engine kind:** the parser's
own message. 0.9.25-alpha.1 named the engine on an agent engine.
- **Inference reaches opencode only where akm writes opencode's config:** an
improve process's `llm` overlay on model work's agent, and an `opencode-sdk`
engine's `llmEngine` fallback model. Set the rest in your opencode config; a
task's or workflow's `inference` is an `untranslated-field` notice, as before
0.9.25-alpha.1. `claude` takes no `--effort`.
- **An `opencode-sdk` session the dispatch times out on or aborts is aborted on
the server for every dispatch,** not only model work.
- **An improve stage retries a reply only when the stage cannot read it,** not
when it misses the JSON Schema. A reply it can read costs no second call.
- **opencode model work can read the stash and search it.** The `akm-model-work`
agent reads, greps and globs in the stash and its working directory, edits
only in the working directory, and has `akm_search` and `akm_show` from the
akm-opencode plugin (0.9.21 or later, in your own opencode config). The
plugin's curation, learning and write gate are off for these dispatches, and
its state goes to akm's state directory, not `~/.local/state/akm-opencode`.
### Removed
- **The agent-engine inference fields,** which 0.9.25-alpha.1 accepted: they
fail to load again.
- **The `opencode-sdk` step watcher and the git-repository refusal for model
work's scratch directory.**
- **opencode model work's step limit.** At the limit opencode sends a "maximum
steps" message as a trailing assistant message, which a qwen chat template
(LM Studio, llama-server) renders as the start of the model's reply: LM
Studio returned nothing, llama-server returned that message as the answer,
and the dispatch failed. The dispatch timeout bounds a run.
- **A prompt builder that nothing called** (`buildSchemaRepairPrompt`).
- **The `--track-usage` / `--no-track-usage` flag on `akm search`, `akm curate`
and `akm show`.** A successful read always records its usage event, stamped
with its source (`user`, `improve`, `task` or `audit`); only `user` events
feed ranking and eval, so machine reads never skew them. Either spelling now
fails as an unknown flag.
### Fixed
- **The reflect size guard no longer flags a body that does not grow.** The
expansion ceiling is capped at 25,000 characters, so a source body longer than
that was flagged `EXCESSIVE_EXPANSION` even when the proposed body was its own
length (ratio 1.00), and went to review instead of the judge. A body no longer
than its source is never expansion; one that grows past the cap still is, by
any amount. The reflect prompt agrees: it told the model its body could be at
most 25,000 characters even when the source was longer, and now gives such a
source's own length.
- **An asset's or a task's own `tools:` can no longer name the model-work
policy.** In 0.9.25-alpha.1 exactly `read`, `edit`, `akm search`, `akm show`,
in that order, skipped `execution.allowedTools`. It is ordinary tools now:
only akm's own model-work callers ask for the policy.
- **An agent engine that names its model only in `args` is named in the usage
report,** where it showed `unattributed`.
- **`opencode` and `opencode-sdk` engines receive the XDG base-directory
variables.** Under a custom `XDG_CONFIG_HOME` the spawned opencode missed its
provider config and failed every dispatch with `Unexpected server error`.
## [0.9.25-alpha.1] - 2026-10-02
### Changed
- **Unattended model work runs under one tool policy, on any engine that
confines it.** The model may read, edit inside a scratch directory akm removes
after the dispatch, and run `akm search` and `akm show`; the stash stays
read-only. An LLM engine has no tools, `claude` confines the policy, and
`opencode` and `opencode-sdk` run an injected `akm-model-work` agent with
read and edit only. Every other harness refuses it before the request starts.
- **One config rule names the engines model work may use.** Every key it reads
its engine from (`defaults.llmEngine`, `index.*.engine`, a strategy's or
process's `engine`, an enabled triage `judgment.engine`, a
`qualityGate.engine`) must name an LLM engine or a `claude`, `opencode` or
`opencode-sdk` agent engine, or the config fails to load. `--require-engines`
and `akm health` check agent engines too.
- **Model work is bounded at 600 seconds on every engine kind** whose engine
sets no `timeoutMs`; agent and `opencode-sdk` engines ran until they finished.
- **`akm proposal new` returns the proposal as JSON on every engine kind,**
with no draft file and no live session. A reply that is not a proposal gets
one retry.
- **Reflect asks every engine kind for the same JSON reply and repairs it
once.** An agent or `opencode-sdk` engine gets reflect's JSON Schema and a
repair turn, as an LLM engine did. An LLM engine's requests are unchanged.
- **An improve stage's reply that fails its JSON Schema gets one corrective
retry,** then the stage reads the last reply with its own parser.
- **One schema instruction for every agent engine,** appended by the shared
request lowering: `opencode` and `opencode-sdk` now receive a requested
schema, and a workflow unit on seven harnesses no longer carries it twice.
- **Agent and `opencode-sdk` dispatches leave a usage record.**
- **An `opencode-sdk` engine's LLM fallback comes only from its own
`llmEngine`; `defaults.llmEngine` no longer supplies one.** If you relied on
that, set `llmEngine` on the SDK engine. Without one, opencode uses its own
provider, model and auth.
- **Inference reaches `opencode`, `opencode-sdk` and `claude` engines.**
`temperature`, `maxTokens`, `contextLength`, `enableThinking` and
`reasoningEffort` from an engine, an improve process's `llm` overlay, an
asset, a workflow or a `models.json` alias were dropped on every agent
engine. An agent engine may set them, and `claude` takes `reasoningEffort` as
`--effort`.
- **Reasoning effort has one word, `reasoningEffort`.** `effort` in a
`models.json` alias or an asset is read as it, so an LLM engine now sends it
as `reasoning_effort`.
### Removed
- **Dead code; nothing you run changes:** each harness's unused `pattern` and
`structuredOutput` fields, `resolveLlmEngineUse`'s swap of an agent engine
for an LLM engine, the agent request's unread `effort` hint, and the agent
file-write contract (`DRAFT_WRITTEN`, reflect's `ref_mismatch` check).
### Fixed
- **An `opencode-sdk` engine with an LLM fallback reaches its endpoint, and a
failed dispatch is a failure (#1015).** An SDK error or provider rejection is
`ok: false` with opencode's message, and a reply's last text part is its answer.
- **An LLM engine reports a provider error sent with HTTP 200 as a failure**
(OpenRouter does this), not as an empty reply.
- **An `opencode` engine runs a persona** instead of failing on
`--system-prompt`, which opencode 1.18 rejects.
- **An LLM engine sends a requested schema** unless it sets
`supportsJsonSchema: false`; it sent one only when it set `true`.
- **A feature gate's timeout stops the call it bounds,** and a stage call
reports a timeout or abort as `timeout` or `aborted`, not `error`.
- **`akm proposal new` keeps the reply's `confidence`.**
- **Model work on an agent or `opencode-sdk` engine with no `timeoutMs` stops
after 600 seconds** as documented; it resolved to no timeout.
## [0.9.24] - 2026-10-02
### Changed
- **Reflect never auto-accepts a revision that changes the body; it waits for
review.** On 396 labelled reflect edits, the judge-passed edits that changed
the body were good 12 times in 37, and those that changed only the
frontmatter 13 times in 13. A revision whose body differs from the asset's,
ignoring whitespace, or whose source could not be read, is now created
pending and deferred for review with the reason `body-edit` (gate
`reflect`). When the quality judge passed it, the judge's scores and reason
stay on its gate decision. With `processes.reflect.qualityGate` off, no
judge ran, so the decision carries none. Before, the triage drain accepted a
judge-passed body edit, and its judgment tier could accept one made with the
gate off. These body edits now appear in the review queue
(`akm proposal list`), and `akm proposal show <id>` gives the reason and any
judge scores. A judge-passed revision that leaves the body unchanged is
still staged and accepted by the drain. A frontmatter-only revision made
with the gate off is still left to the drain, and a revision the judge fails
is still refused.
### Fixed
- **Reflect sends a revision to review when its judge fails, instead of
rejecting it.** A quality judge that timed out, errored or returned a reply
that could not be parsed gave no verdict, but reflect treated that as a
rejection. It created no proposal, recorded a `quality_rejected` attempt that
kept the asset from being reflected again for 14 days, and reported
`quality gate rejected: score=-1` with a reason saying the revision had been
routed to review. Reflect now creates the proposal and leaves it pending for
a person, deferred by the quality gate with the reason `judge-error`, as
distill does when its judge fails. The triage drain leaves it alone. It has
not been through the retrieval regression check, which runs only on a
revision the judge passes. A revision the judge scores too low is still
refused.
- **The triage drain leaves every proposal a stage deferred for review to a
person.** It skipped a deferred proposal only when the quality gate had
deferred it. Reflect defers with its own gate, `reflect`: a revision whose
size the size guard flagged, one that echoed the truncation notice, one made
with no judge configured, and now a body edit. With
`processes.triage.judgment` enabled, the drain's judgment tier decided those
proposals, and under `applyMode: promote` it could accept them before anyone
saw them. The drain now skips every deferral it did not make itself, and
judges again, as before, the ones it did.
## [0.9.23] - 2026-10-01
### Added
- **A quality gate can judge with an engine of its own (#1011).**
`processes.reflect.qualityGate` and `processes.distill.qualityGate` take
`engine`, `model`, `timeoutMs` and `llm`. These resolve over the process's own
settings, the way `processes.triage.judgment` resolves over triage's. Before,
a gate could not choose its judge. Config validation
rejects a gate `engine` that is missing or is not an LLM engine. A gate whose
settings resolve to no LLM engine fails before anything is generated, and
never falls back to another engine. The judge's LLM usage is recorded under
its own engine, so the improve report shows it on its own row.
### Changed
- **A judge whose engine sets `enableThinking: true` now thinks.** The reflect
and distill judges always turned thinking off. They still do, unless the
judge's engine turns it on, so a thinking judge can be configured. To keep a
thinking engine's judge from thinking, set
`qualityGate.llm.enableThinking: false`. In the maintainer's replay of
labelled reflect edits, a thinking judge passed more of the good edits, and
more of the bad ones too. It used about 20 times the tokens and took a median
of 30–67 s per judgment, against about 5 s, so it stays opt-in.
### Fixed
- **The reflect quality gate follows its own switch.** It ran when either
`processes.reflect.qualityGate.enabled` was true or distill's gate was on,
which it is by default. So `reflect.qualityGate.enabled: false` did nothing
unless distill's gate was off too, and turning distill's gate off also turned
off reflect's. Each gate now follows only its own `enabled`, which defaults to
on.
- **Memory inference uses the configured temperature.** It always sent 0.1,
ignoring the engine's `temperature` and
`processes.memoryInference.llm.temperature`. It now sends the configured
temperature, and 0.1 only when nothing sets one.
## [0.9.22] - 2026-10-01
### Fixed
- **Accepting a proposal no longer rewrites a long description at its first line
break.** `yaml.stringify`, which reflect and the other writers serialize
frontmatter with, wraps a description past about 80 columns over indented
lines. The truncation repair that `akm proposal accept` runs before it
promotes read only the first of those lines. When that line ended in a
connector word (`the`, `and`) or a comma it took the wrap for a truncation,
dropped the tail words, added a period and left the continuation lines
behind: `...the cooldowns that drive the` followed by `rest of the pipeline`
was written as `...the cooldowns that drive.` followed by `rest of the
pipeline`. Plain, single-quoted and double-quoted descriptions were all
damaged this way, and the repair has behaved so since it shipped in
0.9.0-beta.36 (#645). It now runs only on a single-line description and
leaves a wrapped one exactly as proposed. Assets already promoted with a
damaged description are not repaired by this change (the dropped words and the
stray period stay in them), so they need a separate repair.
- **The same repair no longer rewrites a body line or another key.** It looked
for `description:` in the whole file, and its `\s*` ran past the end of the
line. A file whose frontmatter had no description had a body line starting
with `description:` completed instead, and an empty `description:` had the
next key's line rewritten (`title: Notes about how we configure the` became
`title: Notes about how we configure.`). It now reads only the `description:`
line of the frontmatter block. Content already promoted that way is not
repaired by this change.
## [0.9.21] - 2026-10-01
### Changed
- **`akm improve` rewrites an asset only from negative feedback.** Reflect's
signal delta reads negative feedback only, so an asset whose recent feedback
is positive or a note is no longer planned for a rewrite; any signal planned
one before. `akm feedback <ref> --negative --reason "<what is wrong and what
should change>"` flags the asset for review, and the next improve run
proposes a fix based on the reason. `--positive` records that the asset
helped (it raises its ranking) and never triggers a rewrite. An explicit ref
(`akm improve skills/x`) still plans one, and distill still reads any recent
signal on a memory.
- **The fallback lanes score assets and no longer plan them.** High salience,
and proactive maintenance in a strategy that enables it (the shipped
`proactive-maintenance` strategy), still pick assets and score them
(salience and outcome), but a pick no longer enters the loop, so nothing is
reflected or distilled for it and improve no longer rewrites assets on a
proactive cadence. `eligibilitySource` is now `signal-delta` or `scope`;
`proactive` and `high-salience` stay valid on the rows an older release wrote.
`--require-feedback-signal` still turns the lanes off.
- **`distill.requirePlannedRefs` is `false` in the `default` and `thorough`
strategies** (the other presets inherit it from `default`). With reflect
planned only from negative feedback, `true` would have skipped distill on
every run where no ref has negative feedback; distill now keeps working
through memories with fresh feedback on each run. A strategy that sets it
`true` still skips distill when no ref is planned for reflect.
- **`akm feedback`'s help and errors, both CLI hints, the stash README and the
docs say what each signal does:** `--negative --reason` flags the asset for
review and improve proposes a fix from the reason, so the reason should say
what is wrong and what should change; `--positive` raises the ranking and
does not trigger a rewrite. They also say that improve no longer rewrites on
a proactive cadence or from positive signals. The `default` and
`proactive-maintenance` strategy descriptions match.
- **The reflect judge asks whether a rewrite is needed, and both judges pass
only when every criterion scores 4 or more.** Judging on the mean (3.5 or
more) let rewrites that only reworded a correct asset through. The reflect
judge's criteria are now **need** (does the revision fix a concrete problem
in the source: something the feedback reports, a factual error, or broken,
garbled, truncated or missing text, frontmatter fields included; rewording,
restating, reformatting or adding headings scores 1 or 2), **preservation**
and **quality**, replacing feedback alignment, preservation and quality, and
the "overlap with the source is expected" line is gone. Its JSON keys are
`need`, `preservation` and `quality`. The lesson judge passes only when
novelty and non-redundancy both score 4 or more. A verdict that does not
pass keeps its routing (a mean of 2.5 or more is a review, below that a
rejection) and the grounding rules are unchanged, grounding staying outside
the mean and the pass rule.
- **A quality-judge pass keeps its evidence.** The `staged` decision the
quality gate stamps on a proposal now carries the per-criterion `scores` and
the judge's `judgeReason` (`gateDecision.scores`, `gateDecision.judgeReason`
in `akm proposal show --format json`), and they stay on the proposal when the
drain accepts it, so a later audit can read why a rewrite passed.
- **Each accepted proposal is committed as it happens when its bundle is a git
repository.** Manual `akm proposal accept`, `akm proposal drain`, the improve
triage pre-pass and judgment accepts all commit exactly the paths the accept
wrote or removed, a retirement's archived copy and tombstone and the source
memory a consolidate promotion retires included, as one local commit
`akm accept: <generator> <proposal-id-8> <ref>`. Before, only a `kind: "git"`
bundle committed at accept; an accept into a filesystem bundle with a `.git`
directory (the working bundle `akm init` creates is one) stayed uncommitted
until a sync, and a standalone `akm proposal accept` never committed it. A
commit that fails warns and the accept stands. A non-git bundle is
unchanged.
- **The end-of-run sync and `akm sync` commit even when they cannot push.** A
branch with no upstream, or behind or diverged from it, used to fail before
committing, leaving the run's changes in the working tree. They are now
committed and only the push is skipped, with `not pushed: ...` as the result's
`reason`. A branch ahead of its upstream (an accept commits locally) is pushed
along with the sync commit, where it used to be refused.
### Fixed
- **A dotted token no longer splits a synthesized description.** `akm
remember` ended a sentence at every `.`, so `192.168.0.203` became `192. 168.
0. 203` and `0.9.12` became `0. 9. 12`. A `.`, `!` or `?` now ends a sentence
only when whitespace or the end of the text follows it.
- **The nightly commit message's `{accepted}` and the run metrics count the
proposals triage promoted.** `{accepted}`, `autoAcceptedCount` in
`improve_runs.metrics_json` and `improve.autoAccept.promoted` in `akm health`
read `gateAutoAcceptedCount`, which nothing has set since the confidence gate
was deleted, so they were always 0. They read the triage pre-pass's promoted
count, and the dead field is gone from the result type. A result row an older
akm wrote with the field still decodes in `akm health`: the decoder accepts
the key and ignores its value.
## [0.9.20] - 2026-09-30
### Fixed
- **A command killed by SIGTERM or SIGINT no longer leaves the `opencode serve` it
started running (#1005).** The SDK runner closes the servers it caches when a
command finishes, with a `process.once("exit")` hook as the backstop. A command
with no signal handler of its own dies on the signal without running `exit` hooks,
so a plain `kill <pid>` (or a supervisor's or scheduler's timeout) left the server
running, reparented to init, until someone killed it; `akm agent --prompt`
reproduced it. The runner now closes its servers on SIGINT and SIGTERM when the
command has no listener of its own, then re-raises the signal, so the command
still ends by it with the same status. The handlers go up before the first server
is spawned, so one that is still starting is covered too. `workflow run`,
`improve`, `index` and shell/script tasks keep handling the signals themselves.
## [0.9.19] - 2026-09-30
### Added
- **`akm proposal reopen <id...> [--reason <text>]` (#997).** A rejection was
final: nothing undid it, and a rejected `consolidate-pair` retire proposal
also kept the pair pass from ever proposing that retirement again while both
documents were unchanged. Reopen moves rejected proposals back to `pending`
(a proposal that retention expiry archived is a rejected one too), keeping
the rejection, and the gate verdict that came with it, in the proposal's new
`reviewHistory`, which `proposal show` prints. The verdict itself is cleared
so the drain sees the proposal as undecided, except a `deferred` one (the
quality gate's hand-off to a person), which stays. It takes a full id or an
asset ref (an id prefix only matches pending proposals) and is refused
unless the proposal is `rejected` and `accept` would not refuse it as stale: an update's target
unchanged, a create's target still absent, a retire proposal's successor
present and both documents' body hashes as recorded. A retire proposal is
also refused while another pending retire proposal involves either of its
documents. Several ids are all-or-nothing. The pair pass follows the status:
a reopened proposal is no longer a settled pair and, pending, is not minted
twice. Its `improve_ledger` row is reset (a retire proposal's rejection row
is dropped, any other goes back to `proposed`), the age that retention expiry
and `--older-than` (bulk accept/reject, `drain`) see restarts at the reopen,
so a scheduled sweep does not take a proposal a person just put back, and a
`proposal_reopened` event is appended. `akm proposal reject`'s confirmation
prompt no longer says a rejection cannot be undone.
### Changed
- **`akm improve` improves only the bundle it writes to (#1000).** Candidate
selection admitted assets from every writable bundle, but every proposal is
filed in the run's write target and reflect reads each asset from the bundle
that owns it. An asset owned by another bundle therefore came back as a
`create` fork in the write target, or as an `update` of the write target's
own copy built from the other bundle's copy. A run now plans only the bundle
it writes to (`--bundle`, else `defaultWriteTarget`, else the working bundle:
`AKM_BUNDLE_DIR` when set, otherwise `defaultBundle`), and a bare ref scope
(`akm improve skills/x`) resolves inside that bundle. Distill's
memory-to-knowledge promotion likewise merges only with a doc that already
exists in the write target. As a second line of defence, `createProposal`
refuses a rewrite whose `itemRef` names an asset owned by a different
configured bundle than the queue's. **Narrowed behaviour:** a run no longer
picks up assets from your other writable bundles (it used to read them and
queue the result in its own write target), so a scheduled `akm improve`, the
shipped improve task templates included, now covers only its write target:
add one `akm improve --bundle <name>` run per other bundle you want improved.
`akm improve <ref>` for an asset that lives only in another bundle now fails
with a not-found error whose hint names the remedy (`--bundle team`, or
`akm improve team//skills/x`). Proposals the old behaviour already queued
stay in the queue; review them with `akm proposal list`. `--bundle`'s help
text now says it selects the bundle a run improves and writes to.
- **Consolidation stops re-proposing memories that `knowledge/` already covers
(#998).** The promote pass copied a memory into a new `knowledge/` proposal
with no notion of what `knowledge/` already held: the model never sees it,
the mint-time checks only caught the same slug or an identical body, and an
accepted promotion's memory was eligible again at once (a rejected one after
7 days). On one bundle 88% of a run's proposals came from memories promoted
before, one of them 14 times, and 215 of 224 rejections said the memory
duplicated, was covered by, or overlapped an existing knowledge doc. Two
changes, no new setting: before queuing a promotion, consolidate now compares
the memory with the 20 `knowledge/` docs in its bundle nearest to it by
stored vector (the lookup the pair pass uses) and skips it, with skip reason
`dedup_covered_by_knowledge` and a warning naming the covering doc, when one
of them holds at least half of the memory's distinct 5-word shingles
(measured against every knowledge doc, that share was at least 0.5 for 122 of
the 224 rejected proposals and for none of the 103 accepted ones; a covering
doc past the 20 nearest goes unseen); and a memory whose promotion was
accepted or rejected is offered again only when its body (not its
frontmatter) changes, the same content-driven rule the pair pass uses,
instead of at once or after 7 days. A promotion decided by an older release
recorded no body hash and keeps its old windows. With no stored vector
(semantic search off) the coverage check does nothing.
- **Distill's quality judge also scores grounding: a 1 is vetoed, a 2 goes to
review (#999).** A lesson about a different subject than its source reads as
novel and non-redundant, so it used to pass or, in the review band, be minted
as a pending `review_needed` proposal. The judge, which gates
memory-to-knowledge promotions the same way, now also scores **grounding**,
whether the lesson is about what its source is about: 1 or 2 only for a
different subject, 3 for a lesson on the source's subject that goes beyond or
corrects it (distill folds feedback into the lesson, and the judge is not
shown that feedback), 4 or 5 when the source supports it. Grounding is left
out of the mean of novelty and non-redundancy. A grounding score of 1 is
`quality_rejected` whatever the mean is (an `improve_ledger` row and a
`distill_invoked` event, no proposal, reason
`Off-subject for its source (grounding 1/5): …`). A 2 is borderline and is
`review_needed`: a pending proposal for a person, with the reason
`Borderline on grounding (2/5), routed to review: …`, even when the mean
alone would pass it; a mean that alone rejects the lesson stays
`quality_rejected`. A 3 to 5 changes nothing. Only a 1 is a veto because the
judge is not repeatable enough to discard a 2 unseen: on a calibration on a
local llama.cpp model (34 cases, 5 passes), the only legitimate lesson it
scored 2 or lower (2 of 120 judgments) was an on-subject lesson that adds
advice beyond its source, and temperature 0 did not make the scores
repeatable there: they moved by up to a point between passes. The judge also
reads the same slice of the source the lesson was generated from (its body
without frontmatter, first 3000 characters) instead of the raw file's first
2000. A lesson that contradicts its source still reaches a human through the
optional fidelity check (`processes.distill.fidelityCheck.enabled`, off by
default), and every other `review_needed` reason is unchanged.
### Fixed
- **`akm proposal diff` shows a retire proposal as a retirement (#997).** It
rendered the retired file as replaced by one blank line (`----`, a lone `+`,
then every other line as a removal, under an `(update: <ref>)` header) and
said nothing about the retirement, so one reviewer rejected all 65 of a
bundle's `consolidate-pair` proposals as "would destroy content". The diff
now lists only the removed lines, under a
`(retire: <retired> -> <successor>)` header and
`+++ /dev/null (retired: archived; successor <ref>)`, and its JSON result
gains `op: "delete"`, a `retirement` block under the keys `proposal show`
uses (`retiredRef`, `successorRef`, `judgeLabel`, `judgeReason`, `cosine`,
and `continuityRisk` when the pair was flagged) and a `note` that accepting
archives the file under `.akm/memory-cleanup/archive/` and
`akm proposal revert` restores it byte-exactly; the text output prints the
same verdict and note above the removed lines. The new fields are additive
and appear on retire proposals only. `akm proposal show --detail full` also
stops ending a retire proposal with a bare `payload:` heading over nothing.
Rejections made over the old rendering can be taken back with
`akm proposal reopen`.
- **A tool failure recorded with `akm feedback` no longer becomes a lesson
about the error or a `TODO` placeholder in a memory (#999).** Agents recorded
`akm show` failing on a memory with a `.derived.md` child (fixed in 0.9.17)
as negative feedback, and distill and reflect read it as evidence about the
memory's content. On one bundle, 9 such events on 8 memories produced 4
distill lessons about "duplicate physical owners" for memories on unrelated
subjects (one auto-accepted and live), and a reflect proposal, also
auto-accepted, that added a `TODO: verify physical owner` section to a
memory, on which a fifth lesson was then built. The shipped hints had told
agents to record `--negative` "when it fails"; they, and the help for
`akm feedback --reason`, now say a failed akm command is not feedback on the
asset (if you pasted `akm help agents` into an AGENTS.md or a system prompt,
regenerate it). Reflect's feedback caveat no longer offers a `TODO: verify …`
placeholder: when feedback asks for information the asset lacks, it says only
to leave the section unchanged. Distill's judge now also checks that a lesson
is about its source (see Changed). `TODO:` lines already in a memory are not
removed.
- **`akm improve --dry-run`/`--plan` previews the bundle a live run improves.**
With no `--bundle` and no `defaultWriteTarget`, a live run starts from
`AKM_BUNDLE_DIR` before `defaultBundle`, but a dry run read `defaultBundle`
only, so the two could plan different bundles. The preview now resolves the
working bundle the same way.
- **Errors name the flag the command takes, not the retired `--target`
(`improve`, `remember`, `clone`, `task`, `proposal --queue`).** A `--bundle`
that names no configured bundle, names a read-only one, or differs from a
bundle-qualified ref (`akm improve team//skills/x --bundle stash`) failed
with "--target must reference a source name", "or pass --target to a
different source" or "conflicts with --target". `akm improve`, `remember`,
`clone` and every `task` verb reject `--target` (renamed `--bundle` in 0.9),
and `proposal --queue` takes `--queue`, so each message sent the user to a
flag that does not work. They now name the flag the command takes, and the
remedy in `akm remember --supersedes` says "re-run with --bundle" instead of
"--target". A bundle that came from a ref inside a task
(`ghost//workflows/x`) is described as such rather than blamed on a flag. The
commands that really take `--target` (`import`, `env`, `secret`, and
`proposal accept`/`diff`/`revert`) are unchanged.
- **Stale help and warning text corrected.** The help for
`akm improve --skip-if-locked` said a lock collision without the flag exits
78. It exits 75 (`IMPROVE_LOCK_HELD`), as the CLI reference says. The
`--limit` help says "highest salience first" (it said utility), the
`task add --command` help example uses `--strategy reflect-distill` (the
`frequent` strategy no longer exists), and improve's warning about missing
`show` events now speaks of the retrieval scope (it named a zero-feedback
fallback).
## [0.9.18] - 2026-09-29
### Fixed
- **A running `akm improve` no longer loses its SQLite file locks, which let
an older `sqlite3` delete `state.db`'s WAL.** Copying a database file
inside a process that also holds it open drops every POSIX lock the process
holds on it, and `akm improve` did exactly that (its read snapshots copy
`state.db`, hundreds of times a run). A peer on SQLite older than 3.51 (the
system `sqlite3` command, Python's `sqlite3` module) that then opened
`state.db` read-write, even just for `.backup`, saw no reader and deleted
`state.db-wal`/`-shm` at close, leaving colliding rowids, stale index
entries and lost rows. Affected: every release since 0.9.2 through the
snapshot, and 0.9.2 through 0.9.16 also on every `openStateDatabase` (fixed
in 0.9.17-alpha.4). The snapshot now copies in a child `cp` (Windows keeps
the in-process copy; its locks belong to the handle), and `improve` reads
proposals through its own connection instead of snapshotting per asset.
`akm health`'s `state-db-integrity` check now runs `PRAGMA integrity_check`,
since `quick_check` does not compare indexes with their tables and reported
this damage as `ok`. On earlier releases, open a live database only with
`sqlite3 -readonly`.
## [0.9.17] - 2026-09-29
### Added
- **Declared links (#935).** `akm index` now records the relations a bundle
already declares — `xrefs:`, `supersededBy:`, `contradictedBy:`,
`currentBeliefRefs:`, a `.derived` memory's parent, a wiki's `sources:`,
resolved wiki/OKF page links, and a workflow step's or task's target — as
typed links, read from what it already parses, with no model call. `akm
show` lists an asset's links grouped `outgoing`/`incoming`/`unresolved` (up
to 10 per kind, with a total), and `akm info` reports link counts and how
many are unresolved. Curate's support refs (at most two per item) now come
from declared links instead of the LLM entity graph's `related` list (see
Removed): on a 23,979-entry snapshot, `related` supplied support refs for
5.4% of curate items against 29.6% for declared links, and judged
usefulness rose from about 7 to about 24 useful support refs per 100 items.
- **Consolidate pair pass and retire proposals.** A second consolidation pass
compares each new-or-changed memory, flat `knowledge/` file, or lesson
against its nearest neighbours (the best 5 of 20 candidates, same bundle,
memory tier only) and has an LLM judge label each pair `duplicate`,
`subsumed`, `supersedes`, `contradicts`, `overlap`, or `unrelated` (judged
at cosine ≥ 0.93, or ≥ 0.95 for an asset the pass has never attempted —
unless git added it within the last 7 days, which still judges at 0.93).
The first three labels mint a reviewed `retire` proposal for the losing side
(owner-calibrated precision 20/22 = 0.91 [0.72, 0.97]); `contradicts` is
counted but left to a human; at most 300 pairs are judged a night. Retire
proposals mint under their own generator, `consolidate-pair` — kept apart
from the promote pass's `consolidate` proposals in bulk accept/reject — and
are reviewed like any other proposal (`akm proposal list --generator
consolidate-pair`, `show`, `diff`, `accept`, `reject`). Accepting one
archives the losing asset (and its `.derived` twin) instead of deleting it;
`akm proposal revert` restores the archived bytes exactly, including a file
with no trailing newline. Triage never auto-accepts a retire proposal,
whatever `applyMode` says.
- **Retirement continuity check (rule R3).** Before the pair pass mints a
retire proposal, it replays up to five of the retired asset's own past
`search`/`curate` queries and requires the successor to rank in the top 10
everywhere the retired asset did. A failing or unverifiable query
(including one where the embedding endpoint is unreachable) flags the
proposal's `retirement.continuityRisk`, which excludes it from every bulk
accept path — but not bulk reject, and a person can still accept it by id.
`akm proposal list`/`show` surface the flag and the failing queries;
`akm proposal accept --generator ... --dry-run` reports how many
otherwise-matching proposals were skipped for this reason. `akm proposal
list` also gained a `--generator <name>` filter.
- **A promotion now retires its source memory (O1).** When `akm proposal
accept` promotes a consolidate `promote` proposal — by a person or by
triage auto-promotion — it archives the source memory (and its `.derived`
twin) through the same retire-archive path, provided the source is
unchanged since the promotion was minted. A promotion no longer leaves a
memory/knowledge duplicate behind. Best-effort: a failure to archive only
warns.
- **Archive purge sweep.** At the very start of every `akm improve` run, a
deterministic pass with no LLM call deletes an archived retirement's files
(never its `cleanup.md` tombstone) once it is more than 30 days old and
every file under its archive directory is git-tracked, clean, and
verifiable; anything less than that — or a bundle with no git history at
all — is left in place for a later sweep. A new `akm health` advisory,
`memory-cleanup-archive`, reports how many archived files and bytes are
not yet purgeable — untracked, modified, or unverifiable for a git-backed
bundle; everything, forever, for one with no git history at all.
- **`akm bundle rename <old> <new>`.** Properly re-keys a bundle instead of
requiring a hand-edit of `config.json`: it rewrites `bundles`,
`defaultBundle`/`defaultWriteTarget`, and every scheduler ref that name the
old id, re-keys the indexed entries and enrichment cache in `index.db`,
rewrites this tool's own state rows that name the old bundle, and re-syncs
native scheduler rows under the new name. Refs inside asset content
(cross-references, a task's `uses:`) are reported, never silently
rewritten. `--dry-run` shows the full plan without writing anything.
### Changed
- **Search and curate are rebuilt on measured evidence.** Search now ranks by
reciprocal rank fusion (RRF) of two 100-candidate lists — whole-document
BM25 and nearest document vectors — replacing the previous require-every-
word keyword ladder plus a dozen additional ranking-signal boosts (exact-
name, type, belief-state, tag, graph, utility, and more): on a 221-query
LLM-judged retrieval suite, plain whole-document BM25 alone beat that whole
boosted pipeline by 0.156 nDCG@10. A hit's `score` is now its fused RRF
value (at most 2/61 ≈ 0.033, not comparable to an old score), keyword
matching no longer does prefix matching (a search for `dock` no longer
matches `docker`), and a slow embedding endpoint falls back to keyword-only
ranking after `embedding.queryTimeoutMs` (default 3000 ms) instead of
blocking the search. `akm curate` is now the top hits of that same fused
search, enriched with a preview and up to two support refs, rather than its
own layer of second-guessing fallback searches and nudges. Query embedding
now uses the template a model actually expects (Qwen3's retrieval
instruction, `search_query:`/`search_document:` for nomic,
`query:`/`passage:` for E5, and others — overridable with
`embedding.queryTemplate`/`documentTemplate`) instead of one generic
prefix; a nomic-embed or E5 configuration re-embeds every entry on the next
`akm index` because the new document template changes what gets embedded
(set `embedding.documentTemplate: ""` to keep the old vectors instead).
Measured on the suite: search nDCG@10 rises from 0.346 to 0.556 and curate
precision@5 from 0.350 to 0.551; search p50 latency falls from about 787 ms
to about 400 ms, and a freshly built index shrinks from 560 MB to 340 MB.
Content indexed under two names (a memory also promoted verbatim to
knowledge, say) now returns only the higher-ranked copy instead of both,
and `akm curate` returns an empty, explained result for input that is not
a real query (a harness/tool envelope, a bare stash README) instead of
`--limit` unrelated items.
- **Index layout 26.** Vectors are stored once — the sqlite-vec mirror, the
fragment full-text table, and the stored embedding-input text are dropped
in favor of a derived hash — and every relation `akm index` already parses
is stored as a typed link (see Added). An index an older 0.9.17 prerelease
or 0.9.16 wrote migrates to layout 26 in place on the first writable open —
a 0.9.16 index also gets a one-time full-text rebuild along the way, a few
seconds for a 24k-entry index — and the run ends with a VACUUM that
reclaims the space the migration frees (in one measurement, a 601 MB index
built one layer short of layout 26 dropped to 377 MB; actual savings vary
with how much of an existing index was already free space). An index this
release cannot read (written by a newer akm) is refused, naming the
upgrade, instead of being silently reinterpreted.
- **A scheduled task is just a command and a schedule.** Each native
crontab/launchd/Task Scheduler row now carries its own `AKM_BUNDLE_DIR`
(plus any `AKM_CONFIG_DIR`/`AKM_DATA_DIR`/`AKM_CACHE_DIR`/`AKM_STATE_DIR`
the syncing shell set explicitly) inline, instead of pointing at a
`--scheduler-context <file>` descriptor; a crontab now carries its PATH the
same way, in a `# akm:env` block akm writes and rewrites next to its task
rows, instead of inside each row's own descriptor file. The first `akm task
sync` after upgrading rewrites every akm-managed row once, in place — same
launcher, same schedule, nothing added or removed — and once `akm task
doctor` lists no binding still pointing at a descriptor, the old
`$DATA/tasks/context/` files it leaves behind can be deleted. Rows written
by 0.9.0 through 0.9.16 keep firing until that sync.
- **`akm improve` reworks only what retrieval actually returned, or what's
new.** The proactive-maintenance and high-salience lanes, and the memory
consolidation judge, are now scoped to assets a real `search`, `curate`,
`show`, or `feedback` touched in the last 90 days, plus material no
improve stage has processed yet — not the whole stash.
Measured on a 19,870-asset copy of the maintainer's bundle: the fallback
lanes' candidate pool drops from 15,686 to 6,450 assets, and in July the
proactive lane had rewritten 3,069 assets, 3,059 of which had never been
retrieved since usage logging began. Left-out assets are reported under a
new `retrieval` gate (`akm improve --dry-run`, and health's `not_retrieved`
skip reason).
- **Reflect refuses a rewrite that grades worse on the asset's own searches
(#722).** Before proposing a rewrite of existing content, reflect grades
the old and new versions on up to five queries that actually retrieved the
asset, using the same relevance judge the retrieval eval uses (kappa 0.83
against human grades); a rewrite that grades lower on average is refused as
`quality_rejected`. Of 60 accepted rewrites reviewed this way after the
fact, 23% [14-35%] graded lower than the content they replaced.
- **Config is fully tolerant of what an older or newer release wrote.** No
config object is strict any more: an unknown key at any depth — retired,
misspelled, or written by a newer release — is kept in memory, named once,
round-trips through ordinary writes, and is dropped only by `akm migrate
apply`. `akm migrate apply` itself collapses to one config step that reads
`config.json` through the normal load pipeline and writes the current
shape back under a backup.
- **Locking and writes are simpler.** A lock file is now one `O_EXCL`
create. A write to a git-backed bundle writes the file directly and
commits exactly that path. `akm proposal accept`/`revert` write the asset
file, then record the proposal and its event in one `state.db`
transaction, so a crash in between leaves a re-acceptable pending proposal
rather than something corrupt.
- **Scheduling is one list.** `scheduler.enabled` in `config.json` holds the
fully-qualified refs a host schedules; it is still written and read in the
`{kind, ref, sourceId}` shape 0.9.16 used. A config with no list at all —
0.9.15 and earlier — is read on the first sync after upgrading as "keep
what's already installed," and the list is written from there.
- **A frozen workflow plan carries `irVersion` 6.** Every workflow — Markdown
and the GitHub-shaped YAML subset alike — compiles to the one plan type
that was previously irVersion 4/5's target; a stored irVersion 4 or 5 plan
is still read and run tolerantly (an unrecognized key in it is ignored
instead of abandoning the run), and only a plan a *newer* akm froze is
refused. An explicit `engine: null` on a task, workflow, or command layer
now means "no preference here" and falls through to `defaults.engine`,
instead of forcing the `opencode-sdk` fallback.
- **The quality judge that gates reflect and distill scores each criterion
separately** instead of one blended float, no longer scores an
ACTIONABILITY criterion that measured no better than chance (AUC 0.46), and
now runs at a pinned temperature of 0 — at the previous effective default
of 0.3, 10 of 16 identical inputs had flipped verdict. Consolidate's own
prompt and schema now ask only for `promote`; `merge`/`delete`/`contradict`
were advisory-only and had not actually executed since July regardless.
- **A pasted credential in a search or curate query is redacted before it's
stored.** A password, bearer token, JWT, `ghp_…` token, PEM key, and
similar patterns are replaced with `[REDACTED]` in `state.db`'s usage and
event logs; a scan of mined queries had found 22 stored verbatim. Existing
rows are not rewritten.
- **One bad item no longer aborts a whole `akm task sync` or `akm migrate`
run.** A binding or bundle that fails to reconcile, or a migration step
that throws, is now reported individually (`failures`/`failedSteps`) while
every other task, bundle, or step still completes.
- **A one-file change in a large directory no longer costs `akm index` tens
of minutes.** Both full-text tables' per-entry deletes were unindexed table
scans; on a 23.9k-entry index, one touched file in a 13.7k-entry directory
took 26-31 minutes before this release and well under a minute after.
- **Output shapes changed along with the features above.** A search hit
drops `selectedRef`, `parentRef`, `fragmentOrdinal`, `fragmentCount`, its
fragment line/size fields, `matchStage`, and `graph` (fragments no longer
compete as search candidates; `akm show <ref>#<fragment>` still resolves a
section). `akm index`/`akm info` drop `vecAvailable`, and
`semanticStatus` no longer reports `ready-vec`. `akm migrate status`'s
separate `taskV3Migration`/`taskV4Migration` sections are now one
`taskFiles` section. `akm workflow plan` drops its `sourceReadSet` block.
- **`akm health` gets a new hard `state-db-integrity` check** (a read-only
SQLite `PRAGMA quick_check` against `state.db`, plus a freelist-ratio
warning above 50%), and drops eight checks nothing acted on: the
`task-log-backing` hard check, the `pool-saturation` advisory, and six
research advisories (`outcome-proxy-adequacy`, `outcome-proxy-dead`,
`salience-uniformity-collapse`, `enrichment-lane-minting`,
`improve-churn-ratio`, `collapse-churn-detector`). `processes.reflect` also
gains `excludeRefPrefixes: string[]` to skip a ref prefix (a raw
wiki-ingest snapshot tree, say) that a type-only `allowedTypes` filter
can't carve out on its own.
### Removed
- **The LLM entity-graph extraction pass.** `akm improve`'s per-file
entity/relation extraction, its tables, and `akm show`'s `related` list are
gone: on the navigation eval, vector kNN beat `related` by 0.157 P@5
[0.051, 0.260], and its only other consumer, curate's support refs, moved
to declared links (see Added). The tables are dropped unconditionally on
an index's next writable open. The `graph-refresh` improve strategy and its
`akm-graph-refresh-weekly` task template are retired with it — naming
`graph-refresh` (via `--strategy` or a task) now fails outright, naming the
retirement. `akm health` drops every graph metric (KPI card, summary rows,
per-run duration/entity/relation columns).
- **LLM metadata enrichment (`index.metadataEnhance`).** On a 49-query,
1,968-entry measurement it moved search nDCG@10 by -0.0092 [-0.0324,
+0.0165] and long-prompt curate P@5 by -0.054 [-0.093, -0.012] — no
measurable benefit for a full pass costing about 27 B70-hours. It was
already off by default.
- **The per-run forgetting-safety lane.** A one-time cutover guard from a
June 2026 ranking-formula change that had kept running on every improve run
since. Thirty days of events showed no marginal pick over the signal-delta
lane once the new retrieval scope (above) applied: 4 of its last 5 flagged
refs were also picked by signal-delta, and the 5th was independently
planned under signal-delta the same run.
- **Removed flags and drain policies.** `akm search --no-project-context` and
`akm proposal drain --policy`/`--max-diff-lines` now fail as unknown flags
(exit 2), and the `personal-stash`/`conservative`/`manual` drain policies
are gone. Drain, and improve's triage pre-pass, now accept only a proposal
the quality judge passed on its exact content and reject an empty diff;
everything else goes to `processes.triage.judgment` or waits for review.
Extract and consolidate proposals, which `personal-stash` auto-accepted on
size alone, are no longer auto-accepted.
- **Retired config keys** — kept and tolerated, dropped only by `akm migrate
apply`: `index.graph.*`, `index.metadataEnhance`, `search.minScore`,
`search.graphBoost.*`, `improve.utilityDecay.*`, `improve.collapseDetector`,
`improve.salience.replayBudget`, `processes.triage.policy`,
`processes.triage.maxDiffLines`, `processes.consolidate.contradictionDetection`,
the retired `processes.consolidate.antiCollapse` merge guards
(`maxGeneration`, `lexicalDiversityCheck`, `mergeInformationFloor`,
`minSpecificityRetention` — `antiCollapse` itself, and its
`randomClusterFraction` mixing, are unaffected), and every improve
strategy's `processes.graphExtraction.*` and
`processes.consolidate.incrementalSince`/`.neighborsPerChanged` (the pair
pass replaces incremental-window candidate selection with the improve
ledger). A leftover `improve.strategies["graph-refresh"]` override block is
also dropped this way; `defaults.improveStrategy: "graph-refresh"` is not —
change that one by hand.
- **Also removed, superseded by the simpler mechanisms above:** the
scheduler source-grant layer and its fire-time re-check, the filesystem
transaction journals used by proposal accept/revert, the maintenance
barrier and its per-process activity registry, the SQLite lock-operation
mutex, the strict per-key config schemas and the retired-key registry, and
the health advisories tied to all of the above. `akm migrate apply` deletes
the files they left behind: `$DATA/txn/`, `$DATA/txn-quarantine/`,
`maintenance.barrier.lock`, and the `maintenance-activities/` registry,
which leaked an entry per process (one host had 229,943 entries, 927 MB).
### Fixed
- **`akm info` now always reports and exits 0, like a help command.** An
invalid `config.json`, an unreadable or missing bundle directory, and a
locked, newer, or corrupt `index.db` are each named in the report instead
of failing the command or showing unexplained zeros.
- **`akm health` no longer silently drops every improve run recorded before
#947** (2026-09-09): a `plan` object with no `processes` key failed to
decode entirely, excluding those runs from `--window-compare`, `--group-by
run`, and the HTML/MD reports. On a real 30-day window this had dropped 11
of 55 runs.
- **`akm show` works again for a memory with a `.derived.md` child** (835 of
them on one real bundle) — broken since 0.9.7.
- **`akm bundle add --name` is a contract on every add path, not a hint.**
An explicit `--name` that is not a legal bundle slug, or is already taken,
now fails before any write instead of silently falling back to a derived
name (`akm bundle add --provider ... --name` had its own gap in this same
check, now closed). A registry add with no `--name` is keyed by its
package or repo name instead of the fixed name `extracted` every registry
bundle after the first used to collide on.
- **`akm improve --require-engines` no longer aborts a scheduled run because
a local LLM endpoint was merely busy.** Its reachability probe now waits up
to the engine's own timeout (at most two minutes) instead of a flat 3
seconds.
- **A scheduled row's value survives a `$` or a backslash**, and a cron
command too long for one line is recognized by `akm task sync` again
instead of being rewritten on every sync.
## [0.9.16] - 2026-09-22
### Fixed
- **Result documents larger than 64 KiB are no longer truncated on a piped
stdout.** `akm show`/`search`/`config get … | python3 -c …` (or `| head`, or
any other pipe) returned exactly 65,536 bytes — the Linux pipe-buffer size —
producing unparseable JSON, while the same command with `--output <file>`
wrote the complete document. The two stdout writers (`deliverRendered` for
json/yaml/text/md/html, `outputJsonl` for jsonl) used `console.log`, and on
Bun `console.log` issues a single `write(2)` against a non-blocking fd 1 and
silently discards whatever the kernel did not accept; a pipe accepts at most
one buffer's worth. Both now go through `writeStdout`
(`src/output/stdout.ts`), which uses `process.stdout.write` — that handles
the short write correctly, and the queued remainder keeps the process alive
until it drains. Byte-for-byte output is unchanged on every format; only the
transport moved.
### Added
- **Scheduled execution is now granted by host-local config and bound to the
approved source installation.** `scheduler.enabled` records
`{kind, ref, sourceId}` entries written by `akm task enable`; authored task
frontmatter cannot enable itself. `akm migrate apply` performs the explicit,
backed-up conversion of older grants, and runtime config points unmigrated
installations to that command instead of silently inventing authority.
- **Executable assets now run beneath a host-owned tool ceiling.** Local
`execution.allowedTools` config caps asset tool requests. Workspace,
environment, and opaque runtime selection are no longer accepted from asset
frontmatter, and a transport that cannot enforce a non-empty resolved tool
policy fails before dispatch.
### Changed
- **Unscoped `akm task sync` reconciles every enabled bundle.** It refreshes
the native scheduler from the latest locally enabled task/workflow sources
and removes attributable bindings for bundles that have been disabled.
Removing a bundle also revokes its scheduler grants.
- **Bundle activation and identity are consistent across the CLI.** A disabled
bundle is inert for reads, writes, indexing, execution, and scheduling;
explicit lifecycle updates remain available. Physical-root identity rejects
duplicate or symlink-aliased bundle registrations and prevents an
`AKM_BUNDLE_DIR` alias from reactivating disabled content.
- **Shared config inheritance now separates portable policy from host
authority.** Source/default ownership, registries, scheduler and execution
grants, credentials, executable arguments/workspace, setup/experimental
state, reranker connections, and publication hooks stay local. Every
bundle-relative `extends` hop is checked by real path containment.
- **Unsafe overrides now name one risk each.** Use
`--allow-insecure-transport` for reviewed plain HTTP and
`--allow-dangerous-env-keys` for reviewed process-hijacking environment
keys; the former combined `--allow-insecure` switch is removed.
- **Bundle/source resolution carries explicit default and priority state.**
Search, show, write targeting, registry installation, and configuration
mutation no longer infer ownership or trust from array position.
### Fixed
- Protected generated improve content from credential echoes, redacted bodies,
and run-only scaffolding, while exercising the real bounded engine probe.
- Repaired degraded sqlite-vec mirrors, preserved scheduler intent across
synchronization, kept explicit setup choices and rollback serialization
stable, and tolerated valid sharded startup contention.
- Made git, website, npm, and filesystem bundle add/update/remove workflows
converge on the same lifecycle and dangerous-environment audit behavior.
## [0.9.15] - 2026-09-10
### Added
- **`akm workflow list`/`status --all-scopes` (#942).** Workflow runs are
partitioned by `scope_key`, a hash of the working directory a run was started
from, so a scheduled task and a human shell working in different directories
can each believe a ref has no active run and start one, while `akm workflow
list` from either scope shows only its own. `--all-scopes` searches every scope
instead of only the current one, and both commands' JSON envelope now carries a
top-level `scopeKey` naming the scope that was searched (`null` under
`--all-scopes`), so an empty result is no longer indistinguishable from
"nothing anywhere." Default output is unchanged.
- **`akm health`'s `task-fail-rate` advisory names the dominant command-task
failure reason (#943).** Evidence now always includes
`agentFailureReasonCounts`, a breakdown of `detail.reason` values (`timeout`,
`non_zero_exit`, `spawn_failed`, …) across command-task failures in the window,
and when the check is already `warn` and one reason covers at least half of
those failures the message names it, e.g. `(timeout-dominant: 9/12 command-task
failures)`. Investigating the reported issue found the underlying propagation
was already correct on this branch — a command dispatch that times out, is
killed, or returns a non-zero/`ok:false` result was already recorded `failed`
with its reason, and `akm task run` already exited non-zero for it — so that
contract is now pinned by a regression test instead of left implicit.
- **Every real `improve` run persists a `usageReport` field summarizing LLM usage
by process, engine, and model (#944).** `usageReport: {byProcessEngineModel,
noCalls}` folds a new process x engine x model cross-tab of the run's
`llm_usage` events together with the resolved process routing table (#947); it
is omitted from the result when both halves would be empty, matching the
envelope's existing convention. `noCalls` lists every LLM-backed process the
active strategy enabled but that made zero calls, each with a `reason` drawn
from the existing skip-reason vocabulary (`"engine_unavailable"`,
`"autonomy_gated"`, `"strategy_filtered_all_passes"`, a reflect/distill
dominant skip reason, or `"no_signal"`) — never a fabricated category. The same
table is printed to stderr after every real run.
- **`akm improve report [--run <id>] [--since <window>]` reads a run's LLM usage
back without hand-written SQLite against `state.db` (#944).** With no flags it
shows the most recent real run; `--run <id>` targets one specific
`improve_runs` row; `--since <window>` sums `usageReport` across every real run
started in the window. `"report"` is a reserved `improve` scope value matching
zero real assets, so it dispatches to the report before any lock, log, or index
side effect. A run recorded before 0.9.15 (or one whose result cannot be
decoded) has no persisted `usageReport`: the command recomputes the cross-tab
from that run's own `llm_usage` events and adds a `notes` entry explaining the
degradation instead of erroring or fabricating eligibility reasons.
`--run`/`--since` only mean anything with this scope, so passing either with
any other scope (or none) is now rejected instead of being silently ignored.
- **The health `llm_usage` aggregate (`akm health --since <window>`'s `llmUsage`
field) now tracks a `failures` count alongside calls and token totals (#944).**
Every existing breakdown (`byStage`, `byProcess`, `byEngine`, and the window
total) gains this count for free from the same aggregator; a call whose
`llm_usage` event records an error outcome increments it.
- **A config file can inherit a shared base via `extends` (#945).** Setting
`extends: <path|bundle//path>` deep-merges a base config underneath the local
file — local keys always win. The base resolves without the search index: a
filesystem path relative to the declaring file (`~` expanding), or an
already-synced bundle-relative file (`bundle//<path>`, resolved against that
bundle's content root, needing no asset type). It runs through its own version
and legacy-shape shims independently and may itself set `extends` (chained); a
cycle is a `ConfigError` naming the ref. A fleet of instances can now keep one
shared `engines`/`improve.strategies` block instead of hand-syncing it across
hosts.
- **`akm config get --show-source` and `akm config diff` inspect an effective,
`extends`-merged config (#945).** `config get <key> --show-source` wraps the
value as `{ value, source }`, reporting whether it came from `local`,
`extends:<ref>`, or `default`. `config diff <path|bundle//path>` prints sorted
`{ path, local, other }` rows for every leaf that differs between this
instance's effective config and another config file or bundle-relative file
(loaded through the same loader, so its own `extends` is honoured too),
redacting secrets on both sides first. Both are additive: `config get`'s
default bare-value shape is unchanged, and `config set`/`unset` still edit only
the local file.
- **A model-map column can borrow its model from a configured engine (#946).** A
`models.json` profile may now set `"engine": "<name>"` instead of a literal
`model` string, e.g. `"fast": { "opencode": { "engine": "local-fast" } }`, to
reuse `engines.local-fast`'s own `model` (and, for an `llm`-kind engine, its
`temperature`/`maxTokens`/`enableThinking`/etc. as `inference` defaults) rather
than hand-typing the value a second time. `model` and `engine` are mutually
exclusive on one profile; a reference to a missing engine, or one with no
usable model, fails with the same "a usable model is required after overlay"
error, now checked at `akm health` as well as at dispatch. Engine selection
itself is unchanged: `engine` is only an indirection for the model/inference
*value* of one column, never an override of which engine
`--engine`/`defaults.engine` actually dispatches to.
- **`akm models list` shows the effective model-alias table (#946).** It prints
every resolved (alias, column) pair with its `model`, `source` (`default` vs
`user`), and `via` (`literal` vs `engine`, naming the engine when applicable),
so an operator can see at a glance which mappings a local `models.json`
overrides and which fall through to AKM's installed defaults.
- **`akm improve --dry-run` reports the resolved process -> engine -> model
routing before anything runs, in a new `plan.processes` field (#947).** One row
per improve process (`reflect`, `distill`, `consolidate`, `memoryInference`,
`graphExtraction`, `extract`, `validation`, `triage`, `proactiveMaintenance`),
plus a `triage.judgment` row when configured: `enabled`, the resolved
`engine`/`model` for llm-backed processes, this process's own lowering
`notices`, an `unavailable: {configKey, reason}` when the engine or its
credential could not be resolved, and — for reflect/distill/consolidate —
`eligibleRefs`, the count of this run's effective refs the process would act
on. None of this is new resolution: the plan already computes it,
dispatch-free, before every invocation (dry or live); this only projects it
into the result. `--strategy` overrides are honored for free, unlike `akm
health`'s `active-improve-strategy` check, which still reads the configured
default strategy (`defaults.improveStrategy`) and reports no model.
- **`--plan` is a new alias for `--dry-run`, for previewing `plan.processes`
(#947).** It sets the exact same internal flag — no separate code path, no
additional network reachability probe; pair with `akm health --probe` to check
whether a named engine actually answers.
- **`akm workflow run --skip-if-locked` (#948).** Extends `improve
--skip-if-locked`'s skip-gracefully-instead-of-failing semantics to `workflow
run`: when another engine invocation already holds the run's lease
(`RUN_LEASE_HELD`) or `state.db` is contended (`STATE_DB_CONTENDED`), the flag
turns the failure into one warn line plus `{ ok: true, skipped: { reason:
"lock-held" | "state-db-contended" } }` at exit 0 instead of exit 75. Every
other failure — a bad flag, an unresolvable target — still fails loudly
regardless of the flag. Use it on high-frequency scheduled workflow runs so
they don't pile up failures while a longer-running invocation is in progress.
Not extended to `task run`.
- **`akm health` reports engines whose thinking-off control was ignored (#949).**
A new advisory, `thinking-control`, warns per configured LLM engine with
`enableThinking: false` when the report window's recorded usage shows reasoning
tokens anyway. It is passive — it re-reads the existing `llm_usage` window
aggregate rather than issuing a completion of its own, so a cold local model is
never woken just to run `akm health`. `unknown` when no engine sets
`enableThinking: false`, or when a configured one made no calls in the window.
- **`akm health` reports version drift, idle-but-bound engines, and which env
asset supplies a missing credential (#950).** Three new checks close a
fleet-awareness gap where `akm health` could pass on a stale host running
behind its peers, or on an engine that is configured and reachable but has not
actually been invoked in weeks. The new `cli-version` advisory compares the
installed akm-cli version against the latest GitHub release (the same source
`akm upgrade` already trusts) and warns when a newer release exists; it is
gated behind `--probe`/`--no-probe` like the engine-reachability checks, and
degrades to `unknown` (never a false warn) when offline or rate-limited. The
new `engine-last-used` advisory folds `llm_usage` events over a fixed 30-day
lookback — independent of `--since` — against the active improve strategy's
process-to-engine bindings, and warns when a bound engine has no recorded use
in that window; it stays `unknown` rather than warning until at least one
improve run has been recorded (started) in the same window, so a fresh install
is not noisy. Finally, when `default-llm-engine` or `configured-engines`
reports a required credential missing from the shell, the warn now names the
env asset (never the variable name) that supplies the same key when one exists
— for example `env asset env/lab supplies it — run under it (akm env run
env/lab -- ...)` — instead of a bare "unavailable" that hides the real remedy
for an operator whose normal workflow is `akm env run env/lab -- akm improve`.
- **`akm task list` is an alias for `akm search --type task` (#951).** 0.9.0
removed the `task list` command as a redundant second implementation of task
listing; this reintroduces only the spelling, not the logic. It delegates to
the same `akmSearch`/`parseSearchSource` path `akm search --type task` uses,
passing the query, `--limit`, and `--from` flags through unchanged and
returning the exact same envelope, including the `results` alias. `task show`
and `task remove` stay retired.
- **`akm info` exposes `dataDir`, `configDir`, `cacheDir`, and `stateDir`
(#951).** These are akm's resolved data/config/cache/state directories, so a
script can read `akm info --format json | jq -r .dataDir` instead of hardcoding
a path that differs between a host install and a container.
- **A real `curate_rerank` implementation: `search.curateRerank`, a cross-
encoder rerank pass over `akm curate`'s already-selected candidates (#951).**
`curate_rerank` was a dead `llm.features.*` key removed in 0.8.0 with no wire
call ever implemented under it. `search.curateRerank.{enabled, endpoint,
model, apiKey, timeoutMs, topN}` configures a standalone `/rerank`-style
endpoint (`src/llm/rerank-client.ts`, `POST {model, query, documents}` →
`{results: [{index, relevance_score}]}`); when enabled, curate sends its top
`topN` (default 8) already-ranked candidates and reorders them by the
endpoint's scores. Disabled by default, and best-effort like every other
bounded LLM feature: a misconfigured endpoint, network failure, timeout, or
malformed response falls back to curate's own ranking unchanged rather than
failing the command. Deliberately its own config arm rather than a third
`engines` kind alongside `"llm"`/`"agent"` — that union's kinds are
load-bearing through execution-lowering, runner dispatch, and the harness
model map, none of which a reranker touches.
- **Per-run task log files are purged past the retention window (#951).**
`tasks/logs/<taskId>/<timestamp>.log` (the per-run human-readable tail;
`logs.db` is the durable, already-purged record) had file separation but no
cleanup, so old run files accumulated forever. `akm improve`'s existing
retention pass now also deletes `.log` files under `getTaskLogDir()` older
than the same `improve.eventRetentionDays` window (default 90d, `0` disables
it) it already uses for `task_logs`/events/`improve_runs` — bounded to that
one directory, one level of `<taskId>` subdirectories, `.log` files only.
Path-agnostic bundled scripts and the `akm show env/<name>`/`akm task list`
items from the same review were previously confirmed shipped and are
unchanged here.
- **`akm index --reembed` forces a full re-embed (#955).** Bypasses the
compatibility check above entirely and purges + regenerates every stored
embedding, for the rare case where the check's verdict should not be trusted. A
targeted post-write embedding pass (after `akm remember`, `akm improve`, etc.)
never forces a rebuild on its own.
- **`akm index --skip-if-locked` lets a scheduled or opportunistic run step aside
instead of contending with one already in progress (#956).** Every explicit
`akm index` run now acquires an opt-in, PID-liveness-only rebuild lock (no
age-based stale reclaim — the same #872 lesson `akm improve`'s lock already
applies) and releases it on exit. This is **not** the blocking index-rebuild
lock #872 removed: a plain `akm index` with no flag is never gated by it — a
held lock only warns and the run proceeds unlocked, exactly as before.
`--skip-if-locked` mirrors `akm improve --skip-if-locked`: when the lock is
already held by a live process it skips gracefully (exit 0, `{ ok: true,
skipped: { reason: "lock-held", pid, launcherPid, startedAt } }` —
`launcherPid` is the holder's launcher pid when known, `null` otherwise,
#956) instead of piling up behind the other run. The shipped
`index-refresh` scheduled task now passes it.
- **`akm improve` reports which processes it skipped for an unavailable engine,
instead of dispatching with a doomed credential (#957).** A process whose
engine was configured but whose credential could not be resolved in this
process's own environment (a scheduler that strips `env/user.env` from the task
environment, for example) used to keep its runner and proceed to an
unauthenticated dispatch, with no run-level signal beyond a stderr warning. The
plan builder now folds a resolved-but-uncredentialed engine (including the
triage judgment engine) into the same `engineUnavailable` handling as "no
engine selected," and the result carries a new `skippedProcesses: [{process,
configKey, reason}]` field (omitted when nothing was skipped) naming which
engine and which credential reference — never its value — is missing. `ok` and
the exit code are unchanged, matching the `extract`/`skipReasons` contract
(#912): a scheduler must not start failing because one LLM process's credential
is temporarily missing while others still run. `--dry-run`/`--plan` never
dispatches, so a preview no longer aborts when every enabled process is
credential-unavailable either: it reports the affected processes in
`skippedProcesses` and shows them as unavailable (with their structurally
resolved engine and model) in `plan.processes`, the same as a run that does
dispatch.
- **`--require-engines` opts a run out of that degrade-and-continue behavior, and
is now set on all six shipped scheduled improve task templates (#957).** It
aborts (exit 78) right after the plan resolves, before any lock, log, or index
side effect, listing every unavailable process and its unresolved credential
reference — recommended alongside `--skip-if-locked` for scheduled runs, since
the operator's own shell can pass config validation while a scheduler's
stripped-down environment cannot.
- **`embedding.timeoutMs` configures the per-request embedding timeout
(#954).** The prior fixed 30s timeout cut off a slow local model server on
a large token-budget-bounded batch mid-response. Default 120s, used by both
the single-text and batch embedding request paths; it scales down for a
smaller-than-budget request (`clamp(timeoutMs × requestTokens /
tokenBudget, 30s, timeoutMs)`), so a dead endpoint is detected in seconds
on the common case of small documents. A request timeout no longer drops
its batch immediately: field confirmation showed the endpoint keeps
computing an abandoned request regardless, so akm now backs off (5s,
doubling, capped at 60s) and retries the SAME request once before ever
splitting or skipping it; a second timeout splits the batch in half (like
a context-size rejection) and retries each half the same way, down to
individual documents, and a single document that times out twice is
finally skipped.
- **`embedding.concurrency` overrides the fixed in-flight embedding request
window (#954).** Bounded 1-16; unset behavior is unchanged (1 for a
loopback endpoint, 2 for a remote one). 0.9.15-beta.1 shipped with no
config override for this window; the final release adds one after field
evidence that a multi-slot local server (llama.cpp `--parallel N`, vLLM)
sat idle behind the fixed default. Request size — `embedding.batchSize`
and `embedding.maxTokens` — remains the first throughput
lever; set this only for an endpoint that genuinely serves parallel
requests.
- **The embedding phase stops after 3 consecutive transport failures instead
of grinding through every remaining batch (#954).** A dead or hung
provider used to burn hours on a large stash, one request timeout at a
time, with no signal until a single aggregate warning at the end and
`ok: true`, exit 0. The pass now stops dispatching further requests and
reports failure after 3 consecutive failures at single-document size
(timeout or network error — a multi-document timeout is retried and split
smaller before it can ever count, so it is not by itself evidence the
endpoint is dead) or 3 consecutive network errors at any size (never
retried, so trusted immediately); a `context-window-exceeded` skip never
counts and resets both streaks. The failure message names how many
embeddings were stored before it gave up. Batches already committed are
kept.
- **`embedding.maxInputTokens` caps a single document's embedded text
instead of letting it fail a whole batch (#956).** llama.cpp rejects a
single sequence longer than its physical batch (`--ubatch-size`, default
512) with HTTP 500 "input is too large to process," and the only
per-entry cap before this was 1,000,000 characters. `akm index` now
truncates a document's embedded text to `embedding.maxInputTokens`
(default 512, head only, unicode-safe) before batching rather than
skipping it; a document is skipped only when its truncated head is empty.
`embedding.contextLength` is Ollama's `num_ctx` only now — it used to also
silently set the per-request token budget (`embedding.maxTokens`), so
setting it for the server's context window changed request batching too.
The request budget is `embedding.maxTokens` (default 6000, see #954
below), so a request carries about 11 documents alongside the new
per-document cap by default.
- **`akm index` reports where its embedding credential came from, before the
first provider request (#953).** A field report suspected a gateway was
receiving unauthenticated embedding requests despite `embedding.apiKey`
being set to a `secret://` reference. Auditing and reproducing every path
that reaches `RemoteEmbedder` — plain `akm index`, the CLI as a real child
process, an `extends`-inherited config with adapter detection persisting
mid-run (#945), `akm bundle update`'s post-commit embedding pass, and the
`akm remember` write path's targeted re-embed — found every one already
resolves `secret://` through the same store lookup, now pinned by
integration and contract tests so a future config-flow change cannot drop
`apiKey` unnoticed. `akm index` now prints one default-level line before
its first provider request naming the endpoint, model, and credential
SOURCE — `secret://lab-api-key (store)`, `$LAB_API_KEY (env)`, `literal
apiKey`, or `none configured` — never the credential's value, so a field
run can compare it directly against what the gateway actually logged.
`--verbose` also names the config file the run loaded.
- **`akm health` gains a `scheduler-binary` advisory for scheduler binary
drift (#953).** A field report found `akm task sync`'s recorded absolute
akm path can go stale after upgrading through a different installer (npm
global to a standalone download, or vice versa), leaving a scheduled run
invoking the old binary indefinitely with nothing surfacing it. The new
`--probe`-gated advisory reads the scheduler's recorded akm invocation —
the same binding `task sync`/`task doctor` already read, no crontab text
parsing — runs it with `--version`, and `warn`s naming both versions when
it differs from the running CLI, pointing at `akm task sync` as the
remedy. `unknown` when not probed, no task is installed, or the recorded
binary cannot be executed.
- **`akm improve <ref> --show-prompt` prints the composed reflect prompt for
one asset and exits (#952).** A beta.3 field round confirmed the #952 prompt
fix by reading source, but no live `akm improve` completed across three
attempts, leaving no cheap way to see the prompt in practice. `--show-prompt`
reuses every read-only step `akm improve`'s live reflect step already
performs (source resolution, runner selection, feedback/schema-hint/
related-lesson/rejected-proposal gathering) and stops before the dispatch
lease reflect would otherwise acquire — no lock, index write, or engine call,
same as `--dry-run`. Requires a fully-qualified asset ref as the scope; JSON/
yaml output carries the prompt as a `prompt` field, text output prints it
directly so the #952 framing (feedback shown as an unverified report, and
the instruction never to emit the truncation marker or out-of-asset content)
can be checked by eye.
### Changed
- **BREAKING: `RUN_LEASE_HELD` now exits 75, not 2 (#948).** A held workflow
run-lease refusal — `akm workflow run` or `akm workflow complete` racing
another engine invocation on the same run — was a `UsageError` (exit 2), which
schedulers and cron wrappers read as "fix the command line" rather than "try
again shortly." It is now a `TransientError` (exit 75, sysexits `EX_TEMPFAIL`);
the message, hint, and `RUN_LEASE_HELD` code are unchanged. A script or
scheduler that special-cased exit 2 to detect a held lease must switch to exit
75, or check the JSON envelope's `code` field instead.
- **Thinking-control wire forms no longer depend on `provider` (#949).**
`chat_template_kwargs.enable_thinking` was previously sent only when `provider:
"vllm"` was set; every other provider (including none) got a bare top-level
`enable_thinking` that nothing was observed to honor. AKM now sends both wire
forms whenever `enableThinking` resolves to a value, regardless of `provider`.
An engine relying on `provider: "vllm"` purely for this side effect keeps
working identically. An engine configured with `provider: "openai"`, another
provider name, or no `provider` at all now also receives
`chat_template_kwargs`, which it did not before — this is what lets the same
engine block turn thinking off consistently behind a direct vhost, freellmapi,
or Bifrost, without a gateway silently dropping the one wire form it happened
to send.
- **`akm health --no-probe` now also skips the version-drift check (#950).**
`--probe`/`--no-probe` previously gated only LLM engine reachability; it now
also gates the `cli-version` advisory's GitHub release lookup, so an offline or
air-gapped host's existing `--no-probe` habit suppresses both network calls.
This is the second deliberate network exception in `akm health`, alongside the
pre-existing `plugin-version` advisory's `git ls-remote`.
- **The direct-LLM reflect path sizes its asset-content budget from the target
engine's `contextLength` instead of a flat 12,000-character cap (#952).** The
flat cap only exists to keep the prompt under OS `ARG_MAX` when passed as CLI
argv to an agent or SDK runner; the direct-LLM HTTP path never touches argv, so
agent and SDK runners are unaffected. The budget reserves half of the usable
window for the model's response, since a reflect rewrite returns a body roughly
the size of the input. Configure `engines.<name>.contextLength` to raise the
budget for a given engine; unconfigured engines keep today's effective
~12,000-character behavior.
- **`akm index` commits embeddings per provider batch instead of buffering the
whole run for one final write (#954).** Earlier releases wrote every generated
vector in a single `db.transaction()` after the entire embedding pass finished,
so an interruption partway through (a competing indexer collision, a killed
process, any thrown provider error) discarded every embedding already computed,
not just the ones still in flight. Each request batch now commits inside its
own short transaction as it lands. This holds on every path that embeds: plain
`akm index`, the implicit reindex, the write path (`akm remember`/`import`/
`proposal accept`/`source clone`), and `akm bundle update` (see below).
- **A batch rejected for exceeding the endpoint's context window is split and
retried instead of skipped outright (#954).** `akm index`'s embedding pass now
recognizes HTTP 413 and known context-size error bodies and halves the failing
batch, retrying each half recursively down to individual documents. Only a
single document that still fails this way is skipped, as
`context-window-exceeded`; every other failure (network error, 5xx, malformed
response) keeps the prior skip-the-whole-batch behavior.
- **The default per-request token budget is lower, and adapts mid-run after a
context-size rejection (#954, field report on beta.1).**
`embedding.maxTokens`'s default dropped from 8000 to 6000: the 4-chars-per-
token estimator undercounts dense technical text by 7-55%, so 8000 regularly
overshot a real 8192-token endpoint. On an `akm index` run's first
context-size rejection, the effective request budget additionally shrinks to
three quarters of its current value (floored at twice
`embedding.maxInputTokens`) for every request not yet sent, and one
default-level line reports the new value; it never shrinks a second time in
the same run. Users who set `embedding.maxTokens` explicitly keep it as the
starting point but still benefit from this same-run recovery.
- **Embedding requests are dispatched through a small in-flight window instead of
strictly sequentially (#954).** The window defaults to 1 request at a time for
a loopback endpoint and 2 for a remote one; the actual throughput knob is
request size, via the existing `embedding.batchSize` (document cap) and
`embedding.maxTokens` (token budget), since a larger batch
takes about the same wall time as a single one. `embedding.concurrency`
(see Added, above) overrides this default for a server that genuinely serves
parallel requests.
- **`akm index` reports embedding progress and throughput in more detail as it
runs (#954).** A default-level line reports each provider batch as it
completes — document count, token count, elapsed time, and outcome
(`stored`/`failed: <reason>`/`retrying after <n> s`) — and a final line
reports total throughput (`entries/s`, `tokens/s`) plus every outcome: how
many embeddings were stored (and reused from a prior generation, when
salvage applied — see #955 below), oversized-skipped, timed out, and
failed, with the affected refs listed (first 20 by default, all of them
under `--verbose`).
- **A rename of `embedding.model` no longer forces a full re-embed by itself
(#955).** `akm index` used to purge and rebuild the entire vector index on any
change to the fingerprint it derives from `embedding.model`, including a pure
config rename that still resolves to the same underlying model (for example a
gateway prefixing `provider/model` onto an unchanged server). On a mismatch,
`akm index` now re-embeds a small sample of already-stored entries and keeps
the index when either the endpoint's reported model identity matches what it
reported last time, or the median cosine similarity between stored and freshly
re-embedded vectors is at least 0.999; otherwise it purges and rebuilds as
before, logging why. A genuine dimension change bypasses this check entirely
and still rebuilds unconditionally.
- **A canary that cannot reach the embedding endpoint leaves the index untouched
instead of purging it (#955).** When the fingerprint-rename canary's re-embed
attempt fails outright, `akm index` keeps the existing vectors and the old
fingerprint and reports the run as `unverifiable`, rather than destroying a
working index because the server happened to be down. The next `akm index` run
retries the canary once the endpoint is reachable again.
- **A purge now writes the new fingerprint before any embedding request, so an
interrupted rebuild resumes instead of restarting from zero (#955).**
Previously the fingerprint was only written at the very end of a successful
embedding pass, so an interruption partway through a fingerprint-triggered
rebuild left the old fingerprint in place — the next `akm index` saw the same
mismatch and purged again, discarding whatever had already been re-embedded.
The new fingerprint (and the observed identity) are now written in the same
transaction as the purge, before any vectors are requested; a restart then sees
a matching fingerprint and only re-embeds the entries still missing a vector.
- **`akm index --full` and an index-generation bump no longer re-embed
unchanged content (#955).** A full rebuild deleted every embedding
unconditionally and re-inserted entries under new ids, and the v22→v23
generation bump did the same on first open under a new binary — both
forced a full re-embed of the whole corpus even when nothing changed, the
likely cause of the multi-hour post-upgrade run reported against 0.9.14.
Vectors about to be discarded are now copied into a transient
`embedding_salvage` table (keyed by a hash of `search_text` plus the
fingerprint they were generated under) in the same transaction as the
discard — read back in bounded chunks rather than loaded wholesale, so a
large corpus does not spike memory, and a run with nothing to reuse costs
a single indexed lookup — and handed back to unchanged entries at the
start of the next embedding pass with zero provider calls — a progress
line reports the split (`Reused N embeddings from the previous
generation; embedding M new.`). Content that changed by even one byte, or
a fingerprint that no longer matches, still goes through the provider
normally. `akm index --reembed` and a canary "rebuild" verdict purge the
salvage table along with the stored embeddings; a canary "keep" verdict (a
fingerprint-string rename resolving to the same model) relabels it instead
so it stays reusable. An interrupted pass leaves the table intact for the
next attempt.
- **A write-path index update (`akm remember`, `akm import`, `akm proposal
accept`, `akm source clone`, extract session assets) never contends with a full
rebuild in progress; it skips and lets the rebuild heal the entry instead
(#956).** These commands make a just-written asset searchable immediately via a
targeted index upsert that opens `index.db` under a 5-second busy timeout — far
shorter than a full rebuild's single transaction. It now checks the new rebuild
lock first: a live holder means the upsert and embedding are skipped outright
(one log line naming the pid and the file that will be indexed by the next
pass), and every caller — including `akm proposal accept`, which used to report
a spurious "index finalization failed" — treats the skip as success, since the
file write itself already completed. A rebuild lock left by a dead process is
not treated as held here — reclaiming it stays `akm index`'s job.
- **A blocked `akm remember` (and other synchronous asset-mutation-lease writers)
now prints a wait notice instead of hanging silently for up to ten minutes
(#956).** The sync lease boundary (`withAssetMutationLeaseSync`) had no
progress feedback at all; a contended lease looked identical to a hang until it
either acquired or timed out. It now logs `waiting for <holder purpose> (pid N,
started T) — Ns` every 15 seconds, naming who actually holds the lease — the
same cadence the async path's `onWait` hook already had, but wired to an actual
warning since nothing called that hook. The 10-minute wait bound is unchanged;
this only makes an existing wait visible.
- **`akm health`'s `active-improve-strategy` check fails, not warns, when the
active strategy's LLM-backed work would be a total no-op (#957).** Previously
this check stayed `warn` regardless of how many of the strategy's enabled
processes were unavailable, so a nightly `improve` job that could not run a
single LLM-backed process still reported the same severity as one with a single
missing credential and several working ones. It now escalates to `fail`
specifically when every enabled `capability: "llm"` process in the strategy is
unavailable; a partial failure with at least one working process stays `warn`.
The check is also a direct projection of the same credential-aware plan
`improve` itself now builds (see Added, above), rather than a separate
re-derivation that could disagree with what a real run in the same environment
would do.
- **A failed embedding batch and `akm index`'s progress are visible without
`--verbose` (#954).** A failed provider batch used to log only under
`--verbose`; it now logs at the default `warn` level, naming the batch size
and reason. `akm index`'s `Embedded N/M entries.` line now fires after every
committed batch instead of every 500 stored entries, and the heartbeat names
the failed count too. In non-verbose JSON/yaml output mode, phase-start
messages and the heartbeat now reach stderr (via `info()`); text mode keeps
its spinner instead, and `--verbose` is unchanged. A silently grinding,
hours-long `akm index` run against a dead provider — with no output until
one aggregate warning at the very end — was the field report this fixes.
Source-cache hydration (which runs before `index.db` is even opened) now
reports its own progress the same way: `Hydrating source i/n: <name>` per
source, plus a 15s heartbeat while a sync is in flight.
- **`embedding.chunkSize` is retired (#954).** Nothing under `src/` ever read
it; it was declared in the config schema but had no effect. It is removed
from `EmbeddingConnectionConfigSchema` and `schemas/akm-config.json`. The
`embedding` object stays `.passthrough()`, so a config that still sets
`embedding.chunkSize` keeps loading exactly as before — the key is simply
ignored, not rejected or warned about.
### Fixed
- **An alias-level `engine` binding in `models.json` is honoured on every
platform column (#946).** The per-platform nested form
(`"fast": {"opencode": {"engine": "local-fast"}}`) already worked, but the
flat shorthand from this issue's own acceptance criteria
(`"fast": {"engine": "local-fast"}`) did not: `parseModelMapLayer` treated
every key under an alias as a platform-column name, so `engine` was parsed
as a fourth platform holding the model string, the three real columns kept
their hardcoded cloud defaults, and `akm agent --model fast` silently
dispatched to a cloud model instead of the configured local engine. The
flat `model`/`inference`/`engine` keys are now a wildcard default merged
onto every column, a same-alias per-platform entry still overrides it, and
an alias naming an unknown engine fails with a `ConfigError` rather than
falling back.
- **Two `improve` runs colliding on the lock is transient, not a broken
config (#948).** The lock-held path threw a `ConfigError`, surfacing as
exit 78 (`INVALID_CONFIG_FILE`) and telling a supervisor to stop retrying
an ordinary collision. It is now a `TransientError`/`IMPROVE_LOCK_HELD` at
exit 75, matching the `MAINTENANCE_BARRIER_BUSY`/`INDEX_DB_CONTENDED`
treatment the index rebuild lock already had.
- **The embedding path no longer leaks a raw `database is locked` (#956).**
`reclassifyIndexDbContention` moved out of `indexer.ts` into a shared
module and `materialize-embeddings.ts`'s embedding-generation catch now
routes through it, so contention produces the same "index database is
busy" wording as every other path instead of the raw driver string.
Control flow is unchanged: this was always non-fatal.
- **Starting a workflow ref that already has an active run in a different scope
now warns instead of silently duplicating it (#942).** `akm workflow run
<ref>`'s per-scope concurrency guard is unchanged by design — two unrelated
projects sharing one `state.db` can still run the same-named workflow
independently — but before starting a new run it now also checks for an active
run of the same ref in any OTHER scope and, if one exists, warns with that
run's id, scope, and start time, plus the same `akm workflow run <id>` / `akm
workflow abandon <id>` remedy. The existing "already active" guard errors now
also name the blocking run's scope, not just its id. `akm workflow
status`/`resume`/`abandon <run-id>` already worked from any scope (#919) and
are unchanged.
- **`akm workflow status <ref>` names the scope it searched when nothing is found
there (#942).** When the ref lookup finds no runs in the current scope and
`--all-scopes` was not passed, the not-found error now names the scope that was
searched and suggests retrying with `--all-scopes`, instead of a bare "no runs
found."
- **Concurrent akm commands writing `state.db` no longer crash with a bare
"database is locked" (#948).** An unrelated `akm improve` run, `akm workflow
run`, or scheduled task writing `state.db` at the same time used to exhaust the
write retry window and throw the raw SQLite driver error, surfacing as
`{"ok":false,"error":"database is locked"}` at exit 70 (internal/unclassified)
instead of a retryable failure. On exhaustion the error is now reclassified
into a `TransientError` with a dedicated `STATE_DB_CONTENDED` code (exit 75),
modelled on `RUN_LEASE_HELD`'s precedent (#924), with the original driver text
preserved as `cause`; a genuinely unrelated error (real corruption, a
body-thrown failure) is never reclassified and still surfaces as itself. The
improve run's own `improve_runs` ledger writes, previously bare
single-statement writes with no retry, now go through the same retry path.
- **`akm show env/<name>` lists key names in plain-text output (#951).** The
plain-text `show` renderer never read the response's `keys` field, so the
default (non-`--format json`) output for an env asset gave no way to audit
which keys a script depends on; `--format json` already carried them correctly.
- **The cron scheduler backend truncates its raw per-task log instead of
appending forever (#951).** The crontab entry installed for each task
redirected stdout/stderr with `>>` (append), so the log file grew without
bound; it is only a bootstrap safety net, since akm's own per-run log already
separates runs and keeps history in `logs.db`, so the redirect now truncates
(`>`) and the file holds exactly the latest run's raw output. An
already-installed `>>` row still parses correctly. `launchd`'s equivalent log
path is OS-managed append-only with no truncate mode, so it is left unchanged —
a wrapper-script rewrite is not justified by evidence that was Linux-only.
- **Removed the false `akm curate --rerank` / "curate reranks by intent" claims
from the docs and the `curate` command's own description (#951).** `akm curate`
never implemented reranking; a rerank engine kind is deferred to its own issue,
and the referenced Discord health-report embed script is outside this
repository.
- **Reflect no longer treats feedback lines as verified facts to insert into the
rewrite (#952).** A harness run against the reflect prompt on two model quants
showed the model inventing whole new sections — fabricated incident dates,
ports, disk layouts — whenever a feedback line asserted a claim the source
asset never made. Feedback is now framed as a signal to investigate, not a fact
to insert, and missing information gets a `TODO: verify …` placeholder instead
of an invented answer.
- **A leaked content-truncation marker can no longer end up in a proposal body
(#952).** Asset content over the active budget is capped with a marker the
model is told never to echo back; when a model echoes it anyway, the proposal
is now deferred for human review (`reflect-truncation-leak`) instead of
shipping silently. As a second layer, `proposal accept` — including drain
promotion — now rejects any reflect-sourced proposal whose body still contains
the marker, since a truncated body silently replacing a full asset is data
loss; re-run reflect on the ref to clear it.
- **`secret://<name>` engine credentials now resolve on the `akm improve` /
agent-dispatch and health-probe paths (#953).** 0.9.13's #917 CHANGELOG entry
claimed engine credentials could resolve from the secret store, but that was
only ever true for direct LLM calls (`llm/client.ts`) and, after a same-week
follow-up fix, embedding calls — `engine-resolution.ts`, the sole path `akm
improve`, workflow LLM steps, and `akm health`'s engine probes use, still threw
`Engine "<name>" has an invalid symbolic apiKey reference.` for any `secret://`
value and aborted the run. `secret://<name>` now resolves through the same
store lookup as those other call sites, deferred to actual dispatch so frozen
plans stay secret-free, and `akm health`'s credential check now probes the
store instead of reporting a `secret://`-backed engine as available
unconditionally.
- **An unset or empty `$VAR` referenced by an engine's `apiKey` now warns once,
naming the variable (#953).** Previously it silently sent an empty
`Authorization` header instead of surfacing the misconfiguration.
- **`akm bundle update` now commits its embedding pass durably instead of
nesting it inside its own transaction (#954).** Its coordinator called
`akm index` for its embedding phase too, INSIDE the same unified
`BEGIN IMMEDIATE` that covers content/lock/index/state — so every per-batch
commit (above) nested as an unobservable SAVEPOINT, and a SIGKILL mid-run
lost every embedding of the run rather than just the one in flight.
0.9.15-beta.1 shipped claiming per-batch commits held on this path; the
final release makes it true: `generateEmbeddingsForDb` now refuses to run
against a connection that already has a transaction open (an internal
contract error, not a user-facing one), and `akm bundle update` runs its
embedding phase on a fresh connection AFTER its own commit instead. A
failing post-commit pass (provider down) still leaves the update itself
successful — content, lock, and index generation are already durably
committed — with the response's `index.semanticStatus` (new field) the
only sign semantic search fell behind (`"blocked"`), exactly like a plain
`akm index` run today.
- **A batch rejected by llama.cpp for exceeding its physical batch size is now
recognized as a context-size rejection (#954).** llama.cpp reports this as
an HTTP 500 with a body like "input is too large to process. increase the
physical batch size", which the existing context-size pattern
(`exceed_context_size_error`, "context size", …) did not match, so the
whole batch was dropped instead of being split and retried like a 413.
- **The end-of-run throughput line now sums the capped text actually sent to
the embedding provider (#954).** `storedTokens` accumulated
`estimateTokenCount(entry.searchText)` — the entry's pre-cap search text —
while the request `embedBatch` received held the text `capEmbeddingText`
had already truncated to `embedding.maxInputTokens`, so the reported
`tokens/s` figure overstated throughput for every entry over the cap. The
final line now sums the estimate of the capped text the batching loop
already built, matching what the provider was actually asked to embed.
- **A `kill <launcher-pid>` no longer orphans the running `akm` process
(#956).** The published launcher (`scripts/node-runtime/akm`/
`akm-migrate`) now forwards SIGTERM/SIGINT/SIGHUP to its bun/node child
and exits alongside it, instead of leaving the child running — one field
report found 40 orphaned `bun …/dist/cli.js` processes in a single day,
some hours old, still hammering the embedding endpoint and holding the
rebuild lock. Every command also polls for reparenting (a launcher that
dies without delivering a signal — SIGKILL, an out-of-memory kill) and
re-raises SIGTERM on itself the moment it notices, reusing the same abort
path a real signal already takes. Lock messages ("another index run is
active...", "akm improve is already running...") and `akm index
--skip-if-locked`'s JSON result now name the launcher pid alongside the
pid that actually holds the lock — `pid 4242 (launcher 4240)` — since
every process listing and task log shows the launcher pid, not the
child's.
- **An index run interrupted before its first embedding pass ever completes
could force an unnecessary full re-embed on the next `akm index --full`
(#956).** A fingerprint-rename rebuild already wrote `embeddingFingerprint`
immediately, before any provider call, so an interruption right after that
decision still left a consistent record — but the common
first-pass/unchanged-fingerprint path deferred that write to a fully
successful run. A per-batch commit is durable the instant it lands
regardless, so an interrupted first-ever pass left real, already-embedded
vectors with no recorded fingerprint to tag them by, and a later full
rebuild's salvage-before-discard step (#955, above) treated the missing
fingerprint as "nothing was ever verified" and re-embedded everything
instead of reusing them. A plain `akm index` resume after an interruption
now embeds only the entries still missing a vector, with no purge and no
canary.
- **A concurrent `akm index` without `--skip-if-locked` now fails with a
retryable-shortly exit code instead of a raw driver error (#956).**
Contention with another writer touching index.db (a second `akm index`, a
source add/update's embedding pass, the per-command background reindex)
used to exhaust the SQLite driver's retry window and surface as
`{"ok":false,"error":"database is locked"}` at exit 70
(internal/unclassified). It is now reclassified into a `TransientError`
with a dedicated `INDEX_DB_CONTENDED` code (exit 75), naming the rebuild
lock's live holder pid when known, mirroring `STATE_DB_CONTENDED`'s
precedent (#948) for state.db; the original driver text survives as
`cause`. `--skip-if-locked` is unaffected — it already skips gracefully
before ever attempting the write.
- **A concurrent plain `akm index` (no `--skip-if-locked`) could still exit
78 instead of 75, a 2026-09-10 field re-test found (#956).** Two `akm
index` runs colliding on the short internal barrier that registers the
opt-in rebuild lock (shared with every other akm lock/lease) threw
`ConfigError("INVALID_CONFIG_FILE")` — a config-error exit that tells a
supervisor to stop retrying, when this is ordinary contention between two
legitimate runs. The barrier is meant to be held only milliseconds, so it
now retries briefly (a bounded, jittered backoff) before giving up, letting
an ordinary collision succeed instead of erroring at all; if it is still
busy after that, it raises `TransientError` with a dedicated
`MAINTENANCE_BARRIER_BUSY` code (exit 75) instead of the config error. The
rebuild lock itself is unaffected and still never blocks (#872).
- **The fingerprint-rename canary embeds the exact text the stored vector was
generated from (#955).** `sampleEmbeddedEntriesForCanary` handed the canary
the entry's raw `search_text`, while the main embedding pass caps it to
`embedding.maxInputTokens` before ever calling the provider — so for any
entry whose search text exceeded the cap, the canary's freshly re-embedded
vector came from a different input than the one that produced the stored
vector, and the median cosine similarity could fall below the compatibility
threshold for reasons unrelated to the model, triggering a needless full
purge and rebuild on a same-model rename. The canary now caps each sampled
entry's search text the same way, through the same `capEmbeddingText`
helper, before requesting its vector.
- **`akm improve --require-engines` now probes reachability instead of only
checking config/credentials, and a dead engine can no longer hang a run
past its timeout or a signal (#957).** Field, beta.3: engines pointed at a
dead endpoint, then `akm improve --require-engines` ran ~4 minutes with
zero output, ignoring an external `timeout 30` (SIGTERM) and akm's own
`--timeout-ms` — the documented exit-78 "required engines unavailable"
path could never be observed, because `--require-engines` only checked
that an engine was configured and credentialed, never whether it actually
answered. It now also runs the same bounded reachability probe `akm
health`'s `default-llm-engine`/`configured-engines` checks already use
(one `/models` request per distinct endpoint), before any lock, log, or
index side effect, and aborts at exit 78 naming the unreachable engine and
endpoint. Separately, a live run now prints one default-level line if it
has waited more than a few seconds on its first engine response, so a
scheduled run's log is never silently empty while an engine is slow or
dead — `--timeout-ms` and an engine's own configured timeout already
aborted the in-flight request correctly (confirmed by this investigation,
not changed), and SIGTERM/SIGINT already ended the process within its
documented grace period.
- **`akm improve --show-prompt` now includes `avoidPatterns` when a live
improve loop has set them (#952).** The preview built its own second copy
of reflect's prompt-source gathering and `ReflectPromptInput` assembly,
which had already drifted from the real dispatch path: it never read
`avoidPatterns` (recent-error context from earlier assets in the same
run), so the preview was not always the prompt a live iteration would
actually send. Both the preview and the real dispatch path (`akmReflect`,
`runReflectRefineIterations`) now gather sources and assemble the prompt
input through the same two shared helpers, so this cannot drift again.
## [0.9.14] - 2026-09-04
### Added
- **Long Markdown bodies can return the matching lexical fragment (#937).**
The derived index now keeps a separate, safe fragment population for lexical
retrieval. Search can return an addressable `#akm-fragment-…` ref, and `akm
show` resolves that ref to the exact indexed projection. Headingless and
oversized documents split at paragraph or word boundaries, so a fact in the
middle of a long body is no longer represented only by its parent document.
### Changed
- **Index generations are checked before use (#934).** A newer index is never
queried or rebuilt by an older binary; it reports that akm must be upgraded.
An older derived index is rebuilt from the materialized sources by the current
binary. A current-generation stamp is accepted only when the canonical entry,
parent FTS, and fragment surfaces all match, and the stamp is written only
after schema creation succeeds. This release advances the derived index from
v22 to v23 for fragment retrieval.
- **Lexical relevance remains stable through scoring and relaxed-query ties
(#933, #940).** Lexical scores use a fixed monotone calibration rather than a
result-set-relative scale, preserving score headroom. Relaxed matches retain
body relevance as tie evidence, including when a belief-state ceiling also
applies. This compound-safe implementation supersedes PR #941.
- **The frozen W0 lexical weight matrix remains the shipped policy (#930).**
The W1 and W2 alternatives were measured and rejected; no unvalidated weight
change is included in this release.
### Fixed
- **Fuzzy name matches require structural identity.** Short or opaque name
fragments no longer create a false identity match merely because their text
overlaps a stored name.
- **Full indexing honors canonical workflow-source ownership.** When peer `.md`
and `.yml` sources map to one workflow ref, the index now persists the same
deterministic `.md` winner used by lookup and execution instead of allowing
filesystem enumeration order to select the stored row.
## [0.9.13] - 2026-09-04
### Added
- **A stable `results` alias on every list-returning command (#922).** `search`,
`curate`, `proposal list`, `bundle list`, `env list`, `secret list`,
`registry search`, `registry list`, `workflow list`, `task history` and
`log list` each keep their existing semantic key (`hits`, `items`,
`proposals`, …) and now also expose the same array as `results`, in every
shape including `--shape agent`. It is the same array, not a copy. A caller
reading `hits` from a `curate` response previously got nothing back and could
reasonably read that as "no results" — there was no error and the `summary`
alongside it still reported that results were selected. Commands carrying
several heterogeneous collections (`health`, `task doctor`) are deliberately
excluded.
- **Engine and embedding credentials can resolve from the secret store
(#917).** `apiKey` accepts `secret://<name>` alongside `$VAR` / `${VAR}`,
resolved through the existing store-backed resolver. A detached SessionEnd
hook or a cron job — the two contexts least able to supply an environment
variable and most likely to need extraction — no longer requires editing the
login environment for a value akm already stores. Literal keys in
`config.json` are still refused, and an unresolvable reference fails loudly,
naming the reference and never the secret.
- **`akm proposal drain` reports what failed (#921).** The envelope carries a
`failed[]` naming each proposal and why it was refused (stale target,
validation), instead of reporting `failed: 0` while the same run printed five
failures to stderr. `--dry-run` now applies the same stale-target check the
real run does, so its prediction stops disagreeing with the outcome. The
refusals themselves are correct and unchanged: declining to overwrite a target
modified since the proposal was created is the desired behaviour. `akm
improve`'s triage pre-pass reports the same count.
- **Workflow step output is checked against its declared schema (#923).** A step
whose returned object does not match its `outputSchema` now says so, naming
the step and the specific problem (a missing required field, say). This is a
warning: the run continues and its status is unchanged, because a workflow is
a guide rather than a contract. Previously an author got neither enforcement
nor feedback — the only signal was a notice saying akm was not checking, which
is a different fact from whether the output matched.
### Changed
- **A held run lease no longer reports as database corruption (#924).** `akm
workflow run` surfaced raw SQLite text — `database is locked`, `database disk
image is malformed`, `disk I/O error` — for what was simply another
invocation holding the lease. `disk image is malformed` in particular reads as
data loss and sent one reporter through integrity checks and a WAL review
before finding the real cause. The lease message now appears at default
verbosity and carries a dedicated `RUN_LEASE_HELD` code, so a wrapper can tell
"retry shortly" from "you passed bad input". Genuine SQLite errors still
report as themselves.
- **`akm lint` stops flagging templated output paths (#927).** A documented
run-time filename such as `reports/review-<timestamp>.md` is no longer
reported as a `stale-path` broken reference. Angle-bracket and brace
placeholders, `${VAR}`, date-format runs like `YYYYMMDD`, and glob characters
are all recognised as parameterised rather than missing. Genuinely broken
literal paths are still reported.
- **The workflow level-2 heading rule is discoverable before you trip it
(#926).** The `workflow create` template states that `##` headings are step
ids and points at `###` for cross-cutting notes, and the compiler's rejection
now carries the remedy rather than only the diagnosis.
## [0.9.12] - 2026-09-03
### Added
- **`akm health` probes LLM engine reachability (#914).** `default-llm-engine`
and `configured-engines` now send one bounded `GET` to the endpoint's
`/models` route (3 s timeout, one probe per distinct endpoint per
invocation, no cross-run cache) instead of only checking that a credential is
present. Any HTTP response counts as reachable, so a cold local server is
never asked to load a model just to be checked. An unreachable default LLM engine is a hard `fail` naming the
connection error; an unreachable non-default engine is a `warn`. Pass
`--no-probe` on an offline or air-gapped host to keep the credential-only
verdict; the message then says reachability was not probed.
- **`akm proposal extract` reports the engine it resolved (#913).** The
envelope carries `engine` and `engineKind` (`llm`, `sdk`, or `agent`), every
`sessions[]` entry carries `engine`, and the `extract_sessions_seen` ledger
metadata records it, so "which engine did that run actually use" is one
field instead of config archaeology. `akm health`'s `active-improve-strategy`
check names the engine each improve process resolved to, which makes a
strategy-level `engine` pin that shadows `defaults.llmEngine` visible.
- **`akm workflow run <ref>` says when it resumes, and `--new` starts fresh
(#919).** Resolving a ref to an already-active run in the current scope is
unchanged, but the `workflow-run` envelope now carries `resumed: true` and
the text output leads with `resuming existing run <id> for <ref>; pass --new
to start a fresh run`. `--new` starts a second run and leaves the active one
untouched. `status`, `abandon`, `resume`, and `run <id>` all accept a unique
run-id prefix of eight or more characters.
### Changed
- **A run that skipped every session for an infrastructure reason is visible
in the extract envelope (#912).** `warnings[]` gains one aggregate line per
infrastructure skip reason (`llm_unavailable`, `read_failed`, `exception`,
`locked_concurrent`), for example `25 of 25 sessions skipped: llm_unavailable
(engine "default")`, and a `skipReasons` count map is present whenever
`sessionsSkipped > 0`. `ok` keeps meaning "the command ran" and the exit code
does not change; that meaning is now written down on the type.
- **`akm health`'s `session-extraction` check reads the extraction ledger
(#914).** It used to read only `improve_runs`, which the hook-driven
`akm proposal extract --session-id` never writes, so a plugin-driven machine
reported "not active" as `pass` forever. It now derives its verdict from the
last seven days of `extract_sessions_seen`: `unknown` when nothing was
recorded, `warn` naming the reason and engine when every session was skipped
or failed for an infrastructure reason (`llm_unavailable`, `read_failed`,
`exception`, `locked_concurrent`), otherwise `pass` with per-outcome counts.
- **Every passthrough success envelope carries `ok: true` (#918).**
`akm config set` and `akm config unset` printed the resulting config with no
`ok` field while their failure envelope had `ok: false`, so a caller
branching on `.ok` read success as failure. The shared passthrough stamp now
adds `ok: true` when a result has no `ok` of its own; commands whose exit
code grades the outcome (`task sync`, `task sync --dry-run`, `task prune`,
`workflow run`, `upgrade`, and the `migrate` subcommands) set `ok` and the
exit code from the same value, so the two cannot disagree. `--silent` still
prints nothing.
- **The unsupported-plan error names the real situation (#919).** A frozen
workflow plan with an `irVersion` above the current one no longer reports
"pre-irVersion-5 ... after the 0.9.2 upgrade"; it says the plan was probably
written by a newer akm. The reported `irVersion 111` did not reproduce
against this tree (the column is only ever written as `5` and no path
rewrites it on a source edit); a regression test pins that editing a
workflow source leaves its in-flight run executable.
### Fixed
- **`akm proposal accept` no longer rewrites your content.** Two "repairs" ran
before validation: one deleted every body line matching `description:` or
`when_to_use:`, the other deleted every `---` in a body that had
frontmatter. Both fired inside fenced code blocks, so any asset documenting
frontmatter — a note about akm, Claude Code skills, Jekyll, Hugo — was
silently gutted on accept, and the rewritten bytes were saved back over the
original in the proposals database. Nothing was printed. Both repairs are
gone; the truncated-description repair, which only ever rewrote a
frontmatter value, stays.
- **Prose-quality findings no longer block `proposal accept`.** A description
that read like a heading, an odd number of backticks, or a reflect revision
outside the size ratio refused the promotion and told the user to "fix the
proposal payload and try again" — but there is no `akm proposal edit` and
`accept` has no `--force`, so the only way out was hand-editing the
proposals database. These findings are now reported as warnings on a
command a human typed. Structural defects that genuinely cannot be written
(empty content, an unparseable ref, malformed frontmatter, a broken
workflow shape) still block.
- **akm no longer refuses to open a state database migrated by a newer akm.**
Two akm versions sharing one data directory is a supported deployment — a
bundled CLI beside a newer global install — and the old binary was bricked
for every command that touches `state.db`, not degraded. It protected
nothing: an older binary's entire migration registry is already applied, so
it has no pending migration to run. It now opens, warns once naming the
migrations it does not know, and reads and writes the tables it knows. A
ledger that genuinely diverges (a migration this akm has was never applied
and something else was applied in its place) is still refused.
- **`akm agent --prompt` no longer rejects prose that looks like a template.**
A prompt was validated as if it were a portable command template, so any
prompt containing `}}` from compact JSON, a `$VAR`, a `${...}`, a `$(...)`
shell snippet, an `@path`, or a `` !` `` was rejected with "unsupported
portable template construct" before the agent started. akm substitutes
nothing into a prompt, so it is now sent verbatim. The template language is
unchanged for stored command files, which are templates.
- **The 0.9.2 `claude-code` -> `claude` harness rename left the ledgers split
(#915).** State migration `027-extract-sessions-seen-harness-rename` moves
`extract_sessions_seen` and `workflow_runs.agent_harness` rows off the old
key, keeping a session already recorded under `claude` as the authoritative
row, and empties the old key space. `PERSISTED_HARNESS_IDS` in the harness
registry is now pinned by a test so the next rename cannot ship without a
migration. Scripts querying the ledger by `claude-code` will find it empty
after upgrade; see the 0.9.1 -> 0.9.2 migration guide.
### Changed — refusals that now degrade
A repo-wide audit reviewed every defensive refusal in the codebase against
three tests: has it demonstrably helped a real user, does its failure mode cost
less than the hazard it guards, and is the hazard already gated behind a
deliberate human command. Refusals that failed those tests were removed or
downgraded to a warning, in that order of preference. Machinery that prevents
data loss or corruption — atomic writes, backups, write-path validation, path
containment — was explicitly out of scope and is unchanged.
The user-visible effect is that akm stops aborting on conditions it can
survive. Highlights:
- **Config load no longer bricks every command over one bad key.** Retired
vocabulary (`profiles`, `llm`, `agent`, `features`, `stashes`,
`modelAliases`, `bindings`, top-level `writable`) warns and passes through,
and the pre-`bundles` `stashDir`/`sources[]`/`installed[]` shape folds into
the current shape in memory. An unknown `config set` key warns and stores.
- **A newer or unfamiliar state.db migration ledger no longer refuses to
open.** A ledger carrying migrations this binary does not know warns once and
degrades; only a genuinely inconsistent ledger still aborts. This is what let
a bundled older akm keep working against a newer host's data directory.
A `plan_ir_version` of NULL (a row predating the column) decodes normally.
- **Free text is no longer validated as code.** `akm agent` accepts prompts
containing `{{`/`}}`; inline workflow `akm/command` content and portable
command templates accept `$HOME`, `@file`, `${...}` and the rest as the prose
they are. Only `$ARGUMENTS[N]`, which merely looks like the one placeholder
akm expands, still warns.
- **Version skew stops being treated as corruption.** A newer index is left
alone rather than wiped, a stale indexed workflow identity falls back, and
frozen-plan spine drift from a formatting change between releases warns
instead of marking every in-flight run corrupt.
- **Deliberate commands stop being second-guessed.** `--since` bypasses
`extract`'s per-run cap the way `--force` already did; `workflow create
--force` no longer also demands `--reset`; scheduler writes, `setup --dir`
and `bundle create --dir` under transient paths warn instead of refusing.
- **Symlinks are followed on read paths** (task sources, `models.json`, stash
meta, `akm.include` entries) with realpath containment doing the actual
safety work, rather than being refused outright.
- **Blocks that had no escape hatch got one, or got removed.** `env run` gained
`--allow-insecure` for the third-party dangerous-key block that previously
had no bypass; `registry add` accepts a credentialed URL with a warning that
redacts the credential; promotion lint findings on `proposal accept` report
instead of blocking, since there is no `proposal edit` and no `accept
--force` to work around them.
- **Limits that bounded nothing real were deleted.** Workflow plan and embedded
child-plan byte caps, step-evidence tombstoning, search-text truncation, and
the JSON-schema node budget are gone; the akm.lock acquisition budget went
from 300 ms to ~30 s with backoff and a "waiting for another akm process"
notice.
- **A cron line too long for vixie-cron now spills into a wrapper script**
instead of refusing the install. The length limit stays, because a truncated
cron line would execute a partial command.
Guards that passed the three tests were kept and documented, including the
registry-URL credential inspection limit, the third-party dangerous-key block
itself, `MAX_SCHTASKS_TRIGGERS`, and every path-containment check.
## [0.9.11] - 2026-09-03
### Added
- **`akm task validate <path>` reports what `akm task sync` would say about one
task file (#907).** It parses a single file by filesystem path, without a
bundle, a concept ref, or a configured engine, and reports `valid`,
`converts` (a v2/v3 source the migrator converts deterministically),
`blocked` (needs a human decision), `invalid`, or `not-a-task`, with the
reason sync would give. It runs the same two gates sync runs before
installing a schedule, the defaults-applied schedule input contract and
the cron dialect check, which are now shared with `compileTaskSources` so
the two commands cannot drift. Exit 0 for valid/converts, 1 for the rest,
2 for a missing or unreadable path. `resolved` on success is the compiled
task shape (id, version, target, inputs, schedule), never an
execution-lowered plan.
- **`engines.<name>.apiKeyFile` supplies an LLM engine credential from a file
(#905).** A host that refuses secrets in the process environment can point
an engine at a path instead of a `$VAR` reference: `~` expands, one
trailing newline is trimmed, the file is read only at dispatch, and a
missing or empty file is a config error naming the engine and path but
never the value. Setting both `apiKey` and `apiKeyFile` is rejected;
setting neither still falls through to the implicit
`AKM_ENGINE_<NAME>_API_KEY` convention. The value is redacted from
dispatch output the same way an env-backed credential is, and `akm health`
checks the file is present and non-empty.
### Changed
- **`akm improve`'s machine-local writers moved out of `$STASH/.akm` into
`$STATE` and `$CACHE` (#890).** `distill-rejected/`, `eval-cases/`,
`measurement/verdicts/`, the synthetic `unresolved-sources/` placeholder,
and the improve-pipeline lock files never met the "must travel with the
content" rule; they now live under `$STATE/improve/…/<stash>/`,
`$CACHE/index/unresolved-sources/<stash>/`, and `$STATE/locks/<stash>/`,
namespaced by a short hash of the resolved stash path. `akm migrate
status` and `apply` gain a relocation step that covers the default stash
and every other filesystem-backed bundle (remote sources are skipped
without a network call), moves files by rename or copy-then-delete, and
deletes an old lock only when the same staleness check `akm improve` uses
says its holder is dead; a live lock is left in place and reported. The
step is idempotent and `--dry-run` moves nothing. Scripts that read the
old paths should switch to the new locations named in
`docs/architecture/internals/storage-locations.md`.
### Fixed
- **The dead-link check states its coverage and no longer lies about it
(#892).** `akm improve`'s post-loop check scanned at most ten knowledge
refs and fired every HEAD request at once with no timeout. It now scans
every actionable ref, checks at a bounded concurrency, bounds each request
at five seconds and reports a timeout as a dead entry, and counts a DNS or
connection failure as skipped rather than as dead or as fine. The result
carries `checked`, `total`, and `skipped`, threaded into the improve
result, the `improve_completed` event, and `akm health`'s improve summary.
The other constants the issue names (`MAX_URLS`, the per-entry slice,
`MAX_BODY_CHARS`, the duplicated `MAX_CONTENT_CHARS`) were already removed
in 0.9.8, and the curate score floors only decide whether a second search
pass runs; they never filter returned results.
## [0.9.10] - 2026-09-02
### Fixed
- **`akm task history <id>` now filters to that task instead of silently
ignoring the positional id (#911).** `task run <id>` and `task explain <ref>`
take the id positionally, so `task history <id>` was the natural thing to
write, and it answered with every task's newest rows while exiting 0. A
positional id now means the same as `--id`; passing both with different
values is a usage error (exit 2) rather than a silent pick.
## [0.9.9] - 2026-09-02
### Changed
- **`akm-migrate` is the one migration tool, and `akm upgrade` runs it**
(#895 follow-up, #901). The standalone `akm-migrate` executable now runs
every migration step in one plan — the legacy config `extraParams` lift,
pending `state.db` migrations (historical-destructive ones included, with
the verified safety copy), task v2 → v3, task v3 → task source v4, and the
residue sweeps — as `akm-migrate status` / `apply [--dry-run]`; its
per-generation verbs (`task-v4-status`, `task-v4-apply`) are gone.
`akm migrate status|apply` is now a thin wrapper over that executable, and
every historical shape (`scripts/akm-migrate/`) lives outside the CLI
proper. `akm upgrade` runs `akm-migrate apply` **after** its install step —
the migrator that shipped with whatever is now installed — on every run,
install or no install, and reports the plan under `migration`; a blocked or
failed migration exits 1. A package-manager install that fails (EACCES on a
root-owned global directory) still runs the migrator and says so. An akm
installed as a dependency of another package is now detected
(`installMethod: "package-local"`) and never reinstalled — an
`npm install -g` there "succeeded" while the parent kept executing its own
copy — but its migrations still run. `akm upgrade --state-only` and the
`stateUpgrade` response field are removed; `akm migrate apply` is the
offline path. An ordinary managed open still refuses a historical-
destructive migration, and its message names these two commands.
A container that ships akm can put `akm upgrade` (or `akm migrate apply`)
in its entrypoint: on a current installation it is a no-op.
### Fixed
- **`akm task sync` reports failed sources under `failures` on both the live
and `--dry-run` shapes** (#906). Live sync used to report them under
`failed` while `--dry-run` already used `failures`. No alias; both paths
now agree.
- **`akm task sync` no longer misdiagnoses a supercronic-managed container as
missing the `crontab` binary** (#910). `crontab -l` exiting non-zero with
no output, or with stderr saying "no crontab", is cron's own way of saying
"empty crontab" (BSD's `no crontab for <user>`, or a PATH shim like
OpenPalm's before any spool exists). Only a spawn that cannot find the
binary (ENOENT) is reported as missing; any other failure is reported as
what it said.
- **`akm task sync --rebind` no longer warns on every run of an image-baked
install** (#868 residue). The warning about binding scheduled tasks to a
mutable, unproven binary fires only when a rebind actually changes an
entry's bound invocation.
- **A mixed-layout bundle no longer auto-detects as a narrow tool-dir adapter
and silently drops the rest** (#908). A root that a tool-dir-shaped adapter
(`agent-skills`, `claude`, `opencode`) claims but that also carries ordinary
akm content detects as `akm`, the superset. `akm bundle list` now reports
each component's effective `adapter` and whether it was `detected`, and
indexing under an explicitly narrow adapter warns once with the count of
files and directories it skipped.
- **An unrecognised `components.*.adapter` is rejected** (#909) with
`INVALID_CONFIG_FILE` listing the accepted names instead of silently
falling back to `akm`. `akm bundle add --adapter <name>` overrides
auto-detection for a local directory; its `--help` lists the registry.
## [0.9.8] - 2026-09-02
A cleanup and stabilization release: deletion of machinery that policed the
codebase's shape rather than its behaviour, and — because auditing for that
machinery meant reading the code closely — a run of real defects it had been
sitting on top of. Two security holes, two search-correctness bugs, a
locale-dependent hash, a deletion shield that failed open, and sixteen places
that answered a failure with a confident wrong answer instead of an error.
Then a second round, from verifying the release against a real 23,865-entry
environment: a no-op incremental index costing ~21 CPU-minutes, legacy proposal
rows that could not be repaired, an npm probe spawning on every scheduler tick,
a blocked task migration that named no remedy, and a data directory that could
reach 74 GB with nothing reporting it.
> **Upgrading is one-way for `state.db`.** This release adds two migrations,
> `025-task-history-vocabulary-backfill` and
> `026-proposals-strip-legacy-fragment-refs`. Once any 0.9.8 command opens
> `state.db`, its ledger contains IDs that 0.9.7 does not know, and 0.9.7
> refuses to open it: `Refusing to open a database with a newer migration
> ledger: unknown migration ID 025-task-history-vocabulary-backfill`.
>
> The refusal is deliberate — an older binary must not write a database whose
> schema it cannot reason about — but the practical effect is that
> **downgrading to 0.9.7 requires restoring a `state.db` backup.** Commands
> that only read the derived index (`akm info`, `akm search`) keep working on
> 0.9.7; everything that touches `state.db` (`akm health`, `akm task`,
> `akm improve`, proposals) does not.
>
> Snapshot `state.db` before upgrading if you may need to go back:
>
> ```sh
> akm info --format json # confirm your data dir
> sqlite3 "$DATA_DIR/state.db" "VACUUM INTO 'state.db.pre-0.9.8.bak'"
> ```
> **`index.db` rescans once.** The per-directory freshness fingerprint changed
> shape, so the first `akm index` after upgrading re-reads every directory and
> then returns to the fast path. Nothing is lost; the index is derived.
### Added
- **`akm health` reports data-dir disk usage** (#896). A `data-dir-usage`
advisory sums the data directory with a stat-only walk and warns when it is
more than 3× the three live databases (state.db, index.db, logs.db) or when
one top-level subdirectory holds more than half of it, naming that
subdirectory with its size and share (for example `backups/ is 70G (94% of
data dir)`). The walk stops after 100,000 entries and says so. Silent when
nothing looks wrong.
### Changed
- **`akm workflow plan` returns JSON by default**, like every other command
(#903). It was the one verb whose unmarked default was a human summary. That
exception cost a bespoke branch which could not reliably distinguish "no
format named" from "`--format json` named globally before the subcommand" —
citty parses each command level against its own argv, so the leaf read
`undefined` in both cases. Working around that meant reading the invocation
singleton, folding in a persisted `output.format`, and finally leaving a
resolved `"json"` on the text branch because it was indistinguishable from
"nothing configured" — which meant an explicit `--format json` silently did
nothing for anyone whose config already resolved to json. Deleted, along with
~60 lines of comment justifying it. `--format text` still renders the same
summary through the same formatter; it is simply no longer the default.
- **Search no longer truncates long queries (#892).** `MAX_LEXICAL_QUERY_TOKENS
= 16` silently dropped every token past the sixteenth, and tokens are
collected in order, so the discarded half was the tail — for
natural-language input, usually where the discriminating words are. It also
fed ranking, so token-overlap scoring ran on the truncated set too. It was
unexplained in the code and in the commit that introduced it, and unreachable
from any flag, config key, or environment variable. Removed: the planner
handles 10,000 tokens in 9ms, so no performance cliff was being protected.
- **Content and memory bodies are no longer silently truncated.**
`MAX_CONTENT_CHARS` (100k, duplicated across 8 adapters) cut indexed content
so the tail of a long document was unsearchable; `MAX_BODY_CHARS` (4000) cut
the text sent for memory inference, so on a large-context engine the model
saw a fraction of the input while the derived memory looked complete. Both
removed.
- **GitHub Actions are pinned to commit SHAs (#768).** All 29 `uses:` steps
across every workflow, with the tag preserved in a trailing comment.
- **Gated CI runs on schedule, dispatch, and candidate tags only.** The
`detect-changes` job that selected suites by regex-matching a PR diff is
gone — its path patterns had gone stale and still named test files this
release moved or deleted, so it was silently under-selecting suites. Release
evidence is unchanged; the checklist always required an exact-SHA dispatch.
- **`akm-eval` in CI is now a determinism check only.** Its score gates are
removed. Measured before cutting: the baseline scored a perfect 1.0 against
a 0.75 gate, and seven of nine case types never ran — CI has no LLM and no
run history, so everything the eval exists to measure was skipped while the
job reported green. The harness itself is unchanged and remains a genuine
quality signal when run against a real bundle.
### Fixed
- **Historical state migrations are reachable where akm cannot reinstall
itself** (#895). A migration flagged `historical-destructive` is refused
during an ordinary open — it needs a verified safety copy, taken under the
migration writer lock, so an unattended `akm index` can never quietly drop
operator state. That guard is correct and unchanged. Its *remedy* was not:
the only code path that admitted the migration ran as a post-install step of
`akm upgrade`, behind an npm install. Where akm is installed globally by a
container image and the runtime user is unprivileged, that install fails
`EACCES` and throws long before the migration is reached, so `akm index
--full` was blocked with no supported way out — and the two obvious
workarounds are both wrong (upgrading as root installs a *newer* akm than the
image ships, which `config-version-shim` then fails closed against; deleting
`state.db` destroys task history, proposals, and lessons metadata, none of
which is derived). New `akm upgrade --state-only` applies pending state
migrations and installs nothing. Nothing about the migration needed the
network, root, or a new binary; coupling it to one was the bug. The safety
copy is still taken — this changes who may request the migration, not what it
does.
- **Two security holes closed.** A failed `git ls-remote` made
`verifyClonedRevision` a no-op, silently skipping the R-011 post-clone
revision-integrity check — the guard against a compromised mirror — so any
network blip disabled it without a word. And `scanExtractedFiles`, the
post-extraction TOCTOU path-traversal rescan, returned silently when a
directory could not be read: it passed clean in exactly the race it exists
to catch. Both now fail loudly.
- **`akm curate --type` ignored the type filter.** A set `--type` bypassed
`selectCuratedStashHits` entirely and did a raw slice of the hits as
received, which neither filtered by type nor ranked by score — so
`--type command` could return a skill, in arbitrary order. It now narrows
the candidate pool and runs the full curation pipeline over it. The test
that should have caught this passed by accident, because the off-type hit
happened to sit past the result limit.
- **Curate silently dropped relevant results.** An undocumented floor excluded
any hit scoring below `max(0.35, leader * 0.7)`. Measured against a real
3,265-memory bundle, one query went from one result to four once it was
removed — the suppressed hits were the ones actually matching. Ordering is
unchanged: results were already sorted before the floor ran, so it could
only ever hide the tail.
- **A bundle-audit hash depended on the machine's locale.** `canonicalJson`
in `installed-stashes.ts` sorted keys with `localeCompare` — ICU-dependent —
and fed the result to `sha256Hex`, so the same object could hash differently
on two machines. Now codepoint order, matching the other implementations.
- **A deletion shield failed open.** `isHotCapturedMemory` returned false when
it could not read or parse a memory, marking exactly the memories it failed
to inspect as fair game for consolidate to merge or delete. It now fails
closed.
- **Sixteen places returned a confident wrong answer instead of an error.**
The pattern (#791) recurred across search, graph, indexing, and scheduling:
a corrupt or locked index reported "no matches"; a failed inline rebuild
surfaced as "Index is empty. Run 'akm index'" at exit 0; `akm show` reported
"0 related files" for an unreadable index; a DB failure in
`listProposalsReadOnly` returned `[]`, so dedup could not tell "no prior
proposal" from "database unreadable" and could re-mint an already-rejected
one; a permission fault on the bundle directory told the user to run
`akm bundle create`; and `launchd`'s `list()` returned `[]` when the
LaunchAgents directory could not be read, so task reconciliation concluded
there was nothing to manage. In each case the failure now surfaces.
- **Drifted copies of shared helpers.** `asNonEmptyString` trimmed in two
places and not in four others, so a whitespace-only session id decoded to
`" "` in some subsystems and `undefined` in others. `isPlainObject` had two
incompatible definitions under one name; the loose form would accept a
`Date` or class instance as a plain record. Both consolidated, along with
`toPosix` (15 copies, whose "avoid an import cycle" justification was false
in every case), `isRecord` (11), `compareCodePoints` (8), and five more
families.
- **Pruning a memory no longer leaves dangling belief edges (#885).**
`writeContradictEdge` — the hardened, test-covered `contradictedBy` writer —
had no production caller; its docstring named three that did not exist. The
live pass used a private near-copy that had drifted on exactly the two
behaviours the original was hardened for: it read the key with
`Array.isArray` only, so a SCALAR edge (live data the indexer accepts and
lint never flags) read as "no edges" and was overwritten out of existence;
and it set `beliefState: "contradicted"` unconditionally, promoting an
`archived` memory back up the ranking. The tests for both behaviours were
guarding dead code. The copy is gone.
`persistBeliefStateTransition` is deliberately NOT routed through the shared
primitive: it is a state-transition writer that replaces the edge list
wholesale and can clear it, which an append-only, never-weaken primitive
cannot express.
- **`git worktree` operations no longer fail on a busy machine (#891).** The
module's internal `GIT_TIMEOUT_MS` was a flat 30s with no margin, so a
healthy `git worktree remove` on a loaded host returned
`{ removed: false, error: "timed out after 30000ms" }`. Anyone running a
workflow with `isolation: worktree` alongside other git activity could hit
spurious create failures, or have a clean worktree wrongly retained as
unremovable. Raised to 120s, matching the existing `GIT_PUSH_TIMEOUT_MS`
precedent. Note `scripts/test-integration.sh` had already raised *bun's*
per-test timeout for this reason, but that never touched the production
subprocess timeout, which fires first.
- **`akm health`'s dead-link check no longer reports an unearned all-clear
(#892).** `checkDeadUrls` capped at 20 URLs across the whole bundle, plus an
undocumented `slice(0, 3)` per entry, then the caller logged "URL check
complete (0 dead)". A bundle with thousands of links got a clean bill of
health after twenty were examined. Both caps removed; every URL is checked
and a failed request surfaces instead of being swallowed.
- **`akm lint --fix` no longer breaks on gitignored drafts (#887).** The doc
linter walked `docs/` with no gitignore awareness, so unpublished drafts
under `docs/.pending/` were held to the published-docs contract —
`bun run lint` failed locally while CI, which never has those files, stayed
green.
- **`akm task sync` no longer spawns `npm root --global` on every call** (#901).
The npm-global-root probe behind `resolveAkmInvocation` is memoized for the
process, so a `task sync --rebind` cycle spawns npm at most once instead of
twice, and an installation that loops it every minute stops accumulating an
npm debug log per spawn.
- **A blocked v2 task now says how to convert it** (#902, #899). The
`argv-array-has-no-portable-shell-string` blocker printed by `akm migrate`
and the `TASK_SCHEMA_VERSION_UNSUPPORTED` read error now state that manual
conversion is required and name the rewrite (`command:` argv array →
`run:` string plus `shell:`). The full v2 → v4 field mapping is documented in
`docs/migration/v0.9.1-to-v0.9.2.md`.
- **Legacy `#fragment` proposal rows are repaired instead of warned about
forever** (#898). State migration 026 strips the retired export-fragment
selector from `proposals.ref` in place so the rows parse again, and an
unparseable proposal row now warns once per process instead of once per
read (`akm health --report` read the table seven times).
- **A no-op incremental `akm index` no longer costs minutes of CPU** (#900).
Two causes: the per-directory freshness check ran two full scans of the
`entries` table for every directory (O(directories × entries)), and every
file was read, hashed, and parsed before the freshness check decided the
directory was unchanged. The directory lookup now uses the existing
`file_path` index, and a stat-based gate over each directory's walked file
set skips unchanged directories before any file is read. On a synthetic
800-directory, 4,000-entry corpus a no-op pass fell from ~37 s to under 1 s
of CPU with identical entries and search results. The persisted directory
fingerprint now covers every walked file and `index_dir_state` gains a
`row_count` column; an existing index.db drains each directory once more
after upgrading, then takes the fast path.
- **Task-migration snapshots are capped at the five most recent** (#897).
`akm migrate apply` writes one snapshot directory per run under
`backups/task-v3` and `backups/task-v4` and never pruned them; each apply
now keeps the five newest and removes the rest, the same policy config
backups already use. Nothing in the current code writes the legacy
`backups/migrations`, `manual`, `releases`, or `operations` directories,
so they are left alone; the new health advisory is what surfaces them.
- **The incremental index no longer misses an edit whose timestamp did not move
forward.** The per-directory freshness check summarised a directory as its
file-name set plus the single newest mtime, which lost two kinds of change.
An edit to any file other than the newest one landed below that maximum and
was invisible even though its own mtime changed — so a restore, checkout, or
archive extraction that stamped a plausible older date left stale content in
the index. And because mtime is writable by ordinary tooling (`touch -r`,
`rsync --times`, `cp -p`), an edit with a restored timestamp was invisible
outright. The directory is now digested per file over
`(basename, size, mtime, ctime)` at nanosecond resolution. It is the same one
`stat` call per file, so the incremental fast path costs what it did before.
Both gaps predate 0.9.8 and applied to every earlier release.
Trade-off worth knowing: `ctime` also moves on metadata-only changes such as
`chmod`, and after copying a tree, so those now cost one extra rescan. That
direction is deliberate — extra work, never stale content. Existing indexes
rescan once as the digest changes shape, then return to the fast path.
- **`akm migrate apply` can now clear a legacy `extraParams` config.** A config
still carrying a liftable key such as `extraParams.temperature` fails config
load closed, and that error names `akm migrate apply` as the fix — but the
migrate command resolved the stash directory and ran the task migrator, both
of which load config, so it died on the very error it exists to clear. An
operator hitting this had no reachable way forward. The config lift now runs
before anything that loads config, and `akm migrate status` reports the
pending lift as its blocker instead of re-raising the same error. A genuine
conflict, where an `extraParams` key and its first-class field disagree, still
hard-rejects and names both values rather than guessing.
- **`akm health` no longer warns about disk usage on a fresh install.** The
`data-dir-usage` advisory added earlier in 0.9.8 counted SQLite's `-wal` and
`-shm` sidecars toward the data directory's total but not toward the live
databases they belong to. On an untouched install the write-ahead log is most
of the directory, so the very first `akm health` reported a ~126x ratio and
exited `warn` with no user data present. Sidecars now count as part of their
database, and the advisory stays quiet below 1 GB, where a ratio says nothing
useful about disk pressure.
### Removed
- **`akm health --clean-dead-residue` and every other compensating shim.**
A special-purpose flag on `health` that deleted files existed because
migrations had not finished their own job. Removing a superseded layout IS
migration, so `akm migrate status` now reports it and `akm migrate apply`
removes it. Four more of the same class went the same way: the D8
`task_history` vocabulary is rewritten once by a state migration instead of
re-decided at three read sites; the legacy `extraParams` config lift happens
once in `migrate apply` instead of silently on every load forever (an
unmigrated config now fails closed, naming the command to run); retired
`type:slug` xrefs are rewritten by `lint --fix`; and stale transaction
journals are recovered by `migrate apply` rather than reported by an
advisory that pointed at documentation while the recovery function it needed
sat with zero callers.
- **Dead code with no consumers.** An empty `{}` type threaded through two
functions as a parameter neither read, with a `{}` placeholder at the call
site; unused helpers, imports, and parameters throughout.
- **14 architecture ratchets, 15 golden/snapshot suites, 12 characterization
suites.** Function-size and import-cycle ratchets that failed on refactors
harming nothing; byte-for-byte snapshots "fixed" by regenerating; tests that
pinned "what the code does today" by definition.
- **11 of 14 lint scripts (-3,988 lines).** `lint-tests-isolation` alone was
717 lines standing in front of `src/core/paths.ts`, which already throws
`TEST_ISOLATION_MISSING` at runtime. Also removed: the `gen-config-schema
--check` gate (`build` regenerates the schema anyway, so what ships is always
current), and `lint-devto-posts` (322 lines that were wired to nothing and
duplicated checks `devto-cli` performs itself). The three kept catch
user-visible problems: secrets must route through the resolver, no dead refs
in shipped assets, no docs teaching commands that do not exist.
- **Both `MIN_TESTS` floors** — arbitrary numbers whose only job was to fail
the suite when test count dropped.
- **`release-workflow-syntax.yml`** — a workflow that ran actionlint over the
other workflows. A broken workflow file already fails at GitHub.
- **Dead `$STASH/.akm` residue is now reportable (#889).** `akm health` gains a
read-only `stash-dead-residue` advisory naming each stale path and its size;
deletion is gated behind an explicit `akm health --clean-dead-residue`. On a
real bundle 82% of `.akm` (135 MB) was pre-0.9.0 leftovers no code reads.
### Testing
- Test isolation grandfather list drained 58 -> 2 (#785); the two retained
entries are the helper-definitions file and a meta-test of the guard itself.
- The test tree now has a stated unit/integration rule and ~250 files were
reclassified by evidence — real DB, network, or process spawn — rather than
by filename (#786).
- 31 status-only `not.toBe(0)` assertions replaced with exact exit codes and
machine-readable error codes, every value observed rather than inferred
(#787).
- Near-duplicate clusters drained (#788); real-server timing flakes removed or
made deterministic (#789).
- Kept deliberately: `registry-network-boundary.test.ts`, which enforces that
only `pinned-transport.ts` may import `node:http`/`node:https` — the control
that stops a raw request bypassing address-pinned fetching. No lint script
covers that boundary.
## [0.9.7] - 2026-08-31
### Added
- **`akm curate --pack <tokens>` (#746).** Packs the ranked stash hits' full
content into a single token-budgeted blob instead of returning refs the
caller has to follow up on with N separate `akm show` calls. Content is
resolved through the same path `akm show` uses, so a `ref#fragment` hit
packs just that section. Drops whole assets from the tail of the ranked
list first; only a single highest-rank hit that alone exceeds the budget
is truncated. Registry hits are never packed — that separation stays
opt-in and there is no flag to override it. Named `--pack` rather than
`--budget` because the workflow asset schema already uses `budget` for
run-cost caps.
- **`llms.txt`-aware website ingestion (#749).** `akm bundle add` against an
origin root now probes `<origin>/llms.txt` first and, when present, uses
that author-curated link list as the crawl frontier instead of discovering
links by parsing HTML. Each linked page still flows through the same
robots-compliant, host-guarded, content-extracting fetch as before, so
ingested pages stay individually addressable. Off-origin manifest entries
are dropped. The probe is restricted to origin-root URLs, so adding a
specific page still fetches that page. Sites without the file fall through
to the existing crawler unchanged. `llms-full.txt` is deliberately not
read: its `## <path>` separators are ambiguous against real page content.
### Changed
- **The improve-strategy presets were reviewed as a set (#878).** The
investigation first believed `thorough` silently ran without validation;
that was wrong — every strategy deep-merges onto `default` before
resolving, so the old asset behaved correctly. What was real: `thorough`
was the only preset of ten relying on that invisible inheritance instead
of an explicit process matrix, and its triage used `applyMode: "queue"`
while its description promised to "drain the backlog" — queue mode never
promotes, so nothing drained. `catchup` had the same inertness:
`maxAcceptsPerRun: 100` under queue mode, a key the promote loop never
reads.
Both now use the same judged-promotion pattern as `reflect-distill` and
`proactive-maintenance`: `applyMode: "promote"`, `judgment: true`, a
per-run accept cap (`thorough` 25, `catchup` 100), and `maxDiffLines:
200`. The autonomy gate demotes promote back to queue unless
`experimental.improveAutonomy` is enabled, so by default both still
review-only; with autonomy on, they actually drain. `thorough` now
carries default's full explicit matrix, and a regression test pins
"everything default enables, thorough enables identically, plus triage."
### Fixed
- **Pruning a memory no longer leaves dangling belief edges (#884).**
`analyzeMemoryCleanup` archives a pruned memory rather than deleting it —
the file moves under `.akm/memory-cleanup/archive/` beside a `cleanup.md`
audit record naming its `ref` and `originalPath`. Ref resolution only ever
looked at the memory's original location, so once #882 made
`contradictedBy`/`supersededBy` validatable, every memory pointing at a
pruned one began reporting `missing-ref`. `refExistsInAnyStash` now resolves
the archive tombstone, so a belief edge to an archived memory is satisfied
instead of dangling. Existence only: `resolveRefPathInStash` still ignores
the archive, because it returns a path callers mutate (`--supersedes`
demotion) and an archived file must never be written to. The contradiction
an edge records is preserved rather than erased, which a bare edge-scrub
would have destroyed.
A ref whose target has neither a file nor a tombstone was removed by
something other than prune — a hand `git rm`, or a pre-fix release — and is
genuinely dangling. Clearing those deletes user assertions, so it is opt-in
behind `akm lint --prune-dangling-edges` and is deliberately NOT folded into
`--fix`: every other auto-fix repairs a malformed file, whereas this one
edits well-formed files to drop a claim about the belief graph. The repair
is scoped to the `supersededBy`/`contradictedBy` channels — a stale `xrefs`
entry is an ordinary broken link that the author may want to re-target, not
delete — and preserves comments, key order, and surviving list entries. It
clears the same `writable: false` gate `--fix` does.
Known limitation: if a memory's only edge on a channel is dropped, a
`beliefState:` naming that channel is left without a supporting edge.
Choosing a demotion target is a belief-semantics decision this fix does not
make. No occurrences existed in the stash that surfaced #884.
- **A corrupt `index.db` now rebuilds instead of failing the command
(#865).** `src/core/state-db.ts` documented that "a corrupt index is
recovered by deleting it and re-running `akm index`" — that recovery was
never implemented. Real on-disk corruption surfaced a raw `SQLITE_CORRUPT`
and exited 70, and the existing inline-reindex fallback could not help
because it reopened the same corrupt file. `openIndexDatabase` now detects
corruption, removes the file and its `-wal`/`-shm` sidecars, and retries
the open once. Scoped to `index.db`, which is fully regenerable from the
stash; `state.db` is never touched.
- **`akm task sync` accepts an immutable package-local install (#868).** An
image-baked akm pinned at a path outside the npm global root was refused
outright, and `--rebind` then warned on every sync that the pinned binary
was "mutable, unproven" — the opposite of true, training operators to
ignore a recurring warning. The npm-global check was only ever a proxy for
"this binary won't change out from under the scheduler"; that property is
now tested directly, so a launcher this process cannot write to (a
read-only mount) is eligible without `--rebind` and without the warning.
Writable `npx`/project-`node_modules` installs remain ineligible, which is
the case the original check existed for. No new config knob.
- **`TASK_SCHEMA_VERSION_UNSUPPORTED` names the actual blocker (#869).** For
a v2/v3 file the message is only reached after the read-time shim has
already tried and failed to convert it — meaning a human decision is
required — yet it pointed at `akm migrate apply`, which would report the
identical block for that same file. The blocked `reason`/`detail` were
already computed and then discarded; they are now surfaced, so the error
names the specific reason (e.g.
`shell-command-resolution-changes-v2-literal-argv-semantics`) and says a
decision is needed rather than sending the operator to a command that
cannot help.
Also verified as already fixed and left alone: cross-bundle scoping (a
blocked file in one bundle does not prevent conversion in another —
resolved by #866) and `env`-prefixed command conversion (resolved by
#867).
- **`akm task sync` no longer dead-ends on pre-0.9.2 crontab rows (#881).**
Rows written by v0.9.0/v0.9.1 carry no `--scheduler-context` marker, so
`extractCronInvocation` could not parse them. Sync therefore saw the task
as absent, tried to install it, and collided with the row that was still
physically present — permanently blocking sync for every task on any
install upgrading past v0.9.2. Such a row is now recognized and
reconciled. The fallback fires only when the marker is entirely absent
and only inside akm's own `# akm:task … BEGIN/END` sentinels, so a row
carrying the marker that fails to parse for any other reason is never
reinterpreted, and no ownership or cardinality assertion changed.
- **`akm lint` validates `type:slug` xrefs instead of silently skipping
them (#882).** `typeNameFromConceptId` returns `undefined` for the colon
grammar, so `classifyConceptRef` treated those refs as "not a local ref"
and dropped them before validation — roughly half the xref channel on a
real corpus. Zero `missing-ref` findings read as "every reference is
healthy" when half had never been examined. akm still writes this
grammar today (`memory-contradiction-detect.ts` emits `contradictedBy:`
as `memory:<name>`), so it is a live channel, not legacy residue; it is
now accepted as a fallback in lint only, leaving the parser and write
paths on the single conceptId grammar.
- **Refs to derived memories resolve (#882).** `assetPathCandidatesForName`
only ever offered `<name>.md`, so any ref to a memory stored as
`<name>.derived.md` failed to resolve — affecting the conceptId grammar
equally, and invisible until the above fix made these refs validatable.
`.derived` is a provenance marker on the same identity, not part of the
name, so `<name>.derived.md` is now a secondary candidate with plain
`<name>.md` still taking precedence when both exist. Fixed at the
resolution primitive, so write-time `--xref`/`--supersedes` validation
benefits as well.
### Removed
- **The `frequent` and `memory-focus` improve strategies.** Zero recorded
invocations, and both were mid-points of other presets (`frequent` ≈
`reflect-distill` without distill or triage; `memory-focus` a subset of
`frequent`). The shipped hourly task template now uses `reflect-distill`
— matching what real deployments had already switched to by hand. Any
removed combination remains expressible via `improve.strategies` in
config. One caveat: a user-config strategy *named* `frequent` or
`memory-focus` previously merged over the built-in of the same name; it
now merges over `default` only, so a partial override that relied on the
built-in's values will resolve differently and should be made explicit.
### Testing
- **The previous-release corpus covers more than task sources (#880).**
Added fixtures for the synthesized `AKM_BUNDLE_DIR` duplicate-`stash`
bundle shape that caused #870, the retired 0.8 `stashDir`/`sources[]`/
`installed[]` config keys, and a downstream consumer's `config.json` plus
four task-source-v4 files. The 0.8 fixture deliberately asserts the
opposite of the others: those keys are hard-rejected by design, so it
guards that the rejection stays loud and actionable rather than silently
loading. Every fixture was proven to catch a regression by breaking the
guarantee, observing the failure, and reverting.
- Pinned current handling of truncated LLM responses (`finishReason:
"length"`) as a regression test (#865). No distinct classification or
retry was added: the underlying truncation was already resolved by
`reasoningEffort`, and threading the finish reason through four layers to
earn one diagnostic label was not justified.
### Not shipped
- **Orphan-asset detection for `akm health` (#750)** was implemented and
then backed out after measurement. Against a live 23,859-entry index it
flagged 16,609 of 16,615 in-scope assets (99.96%) and took 135 s. The
corpus is not densely cross-linked — 1,591 distinct xref tokens exist in
total — so the check has no signal to give. The broken-ref half of #750
had already shipped previously. Moved to Backlog; see #882 for the
resolver defect the investigation exposed.
## [0.9.6] - 2026-08-31
The deletion release: **net −2,100 lines**, almost all of it machinery that
gated, refused, verified, or cached a judgment. Nearly every guard removed
here had **zero confirmed firings** across 38,341 production telemetry events,
while several had already broken real installs.
The standard applied is now written down in `AGENTS.md` (`## Defensive Code`):
a guard survives only if it has demonstrably helped a real user, its failure
mode costs less than the hazard it prevents, and the operation is not already
gated behind a deliberate human command. "This hazard is conceivable" is not a
justification.
### Fixed
- **Embedding no longer discards an entire index because one batch was too
big (#874).** Remote embeddings batched by document count (100) against a
fixed 30s timeout; a single oversized batch failed the whole phase, leaving
`embeddings` at 0 rows on a real 23,857-entry bundle and silently disabling
semantic search. Batching is now bounded by a token budget, failures are
skipped-and-reported per batch, and an oversized single document is a named
skip rather than a phase failure.
- **`akm lint` no longer silently skips user directories named `.cache` or
`registry`.** Two name-based exclusion sites remained after the 0.9.5 fix, so
a bundle's own `knowledge/registry/` was never linted and reported clean.
Exclusion is now anchored to akm's resolved registry-cache path.
- **One directory can no longer register as two bundles (#870).** When
`AKM_BUNDLE_DIR` pointed at a directory already configured under another id,
akm minted a second bundle for it; `akm migrate` then enumerated every task
file twice and failed with `duplicate task migration file path` (exit 70) —
permanently, while health checks kept passing. Bundle identity is now the
resolved content root (`path.resolve(entry.path, component.root ?? ".")`) at
both registration sites, existing duplicates reconcile instead of throwing,
and a genuinely irreconcilable pair reports both bundle ids and the shared
path.
### Removed
- **The index writer lease (#872).** It guarded a *regenerable cache*, had zero
lease events in telemetry, and a live-but-wedged holder stranded all indexing
for 12 hours — `probeLock` only reclaims a dead PID. It blocked legitimate
work twice in a single day of real use. `withAssetMutationLease` is **kept**
(it guards authored, git-backed asset writes) but its identical 12h
age-based stale reclaim is gone; only a verifiably-dead holder is reclaimed.
- **`semantic-status.ts` and its cached `blocked` verdict (#873).** A failed
probe was persisted with a 24h TTL, and search consulted that verdict
*before attempting semantic search at all* — so one failure silently
disabled a working feature for a day. Semantic search now attempts per query
and falls back to FTS with a live warning. The remaining pre-flight check is
a real-time embedding count, not a stored judgment.
- **The improve-lock 4h stale reclaim.** Same hole: `--skip-if-locked`
silently no-oped nightly `improve` for up to four hours and reported success.
- **The persisted `supportsJsonSchema` capability cache**, which was never
invalidated by `akm config set`; a stale `true` sent `response_format:
json_schema` to an incompatible endpoint with no fallback (`isRetryable`
excludes 4xx). Replaced by attempt-then-fallback held in memory for the
process only. The config field survives as an explicit user override.
- **Workflow authoring resource caps** — steps, params, route branches,
inputs, outputs, gate loops, retries, JSON depth/node, composition depth,
and exec argv/env caps. None ever fired; several duplicated OS limits.
- **`LIFETIME_UNIT_CAP` (10,000)**, which hard-aborted workflows mid-run. The
maximum ever observed was 14 units.
- **`state.db` open-path identity re-verification**, which ran on *every*
command and threw a bare `Error`.
- **The task-source 1 MiB cap** at four sites, restating the `guarded-source`
cap already deleted in 0.9.5.
- **TOCTOU identity checks** — `assertGitPublicationIdentity` (git's
`--force-with-lease` already covers it), `assertFrozenDirectoryIdentity`
(replaced with a path-containment recheck: containment and resolved-path
identity stay, device/inode comparison goes, so a remount or container
rebuild no longer aborts a dispatch), `assertTaskSourceExpectation`'s stat
fields (its content hash stays), and redundant repeat HEAD-generation
compares in `write-source`.
- **`assertNotFlag`** on persona/system-prompt content; **`MAX_ENV_BYTES`**,
**`MAX_SECRET_BYTES`**, **`MAX_FEEDBACK_TAGS`**, **`MAX_CONFIG_FILE_BYTES`**,
model-map JSON budgets, memory-contradiction family caps, and two launchd /
schtasks re-checks that duplicated an existing fallback. `map.concurrency`
now clamps instead of rejecting a human-authored value.
- **17 dead symbols**, including several whose docstrings described behavior
nothing implemented. Three "canonical" constants that call sites were
ignoring in favour of hardcoded literals were **wired in** rather than
deleted, closing the drift instead of removing the evidence of it.
- **Speculative flexibility** — `AKM_ABLATE_CONTRIBUTORS` ablation plumbing
and the unexercised `graphBoost.confidenceMode` branches.
### Changed
- **Drifted duplicate implementations consolidated.** HTTP retry/backoff (the
generic copy's `Retry-After` parsing was unbounded and numeric-only; the
capped, date-aware one now applies everywhere), the child-process env
allowlist (the opencode-sdk copy was missing `AKM_EVENT_SOURCE` and the
Windows HOME equivalents), portable synchronous sleep (five call sites had
been silently taking the Node fallback instead of the Bun fast path), a
stacked LLM chunk retry, and a duplicate `isProcessAlive`.
### Kept, deliberately
Not everything unused is disposable. `isVecFastPathReady` stays: a partial
`entries_vec` table does not throw, it silently returns wrong neighbours, so
there is no error for a fallback to catch. `assertSupportedKind` stays: it has
a proven independent bypass path and is the real last-line check, not a
duplicate. The maintenance barrier and registry TTL cache stay. The four
improve strategies with zero recorded invocations stay — that measures one
install's cron schedule, not their worth.
## [0.9.5] - 2026-08-30
### Action required after upgrading
- **Run `akm index --full` once (#862).** Search ranking previously counted
every search *hit* as a small win for that entry's utility score, whether
or not you ever opened it — a plain impression, not a selection. Over
enough repeat searches this compounded into a real feedback loop: appear
in results -> score goes up -> rank higher next time -> appear again. On
the install this was diagnosed against, one entry had been searched 19
times, opened 0 times, and still carried a utility score of 0.83 — on par
with entries a user had actually picked every time; the single
highest-utility entry in the whole table had an 8% select rate. That bias
is baked into every existing install's stored scores and does not go away
on its own — the live bump has been removed (search impressions no longer
write utility scores at all; only an actual `akm show`/select or explicit
`akm feedback` feeds the offline recompute now), but the already-inflated
numbers stay in `utility_scores`/`utility_scores_scoped` until you
recompute them. Run `akm index --full` once after upgrading to rebuild
scores purely from selection rate and feedback. **Search result order will
change after that rebuild — that is intended**, not a regression.
- **The first `akm task sync` after upgrading may report a large number of
updates.** This is a backlog of reconciles that were being silently
refused, not new changes to your tasks. Two independent bugs combined to
make `sync` refuse work it should have done: (1) an installed scheduler
entry written by a pre-`--bundle` akm release couldn't prove ownership
under the newer, stricter check and was reported `(unproven owner)`,
which made `sync` refuse the *entire* run rather than reconcile everything
else; (2) separately, one task or workflow source that failed to compile
(e.g. a task still on schema v2 in a shape the shim can't convert) also
blocked every other, unrelated task from reconciling. Both are fixed —
ownership is now re-derived from the entry's own akm markers instead of
requiring a literal `--bundle` token, and a source that fails to compile
is now excluded and reported in `failed`/`failures` while every source
that DID compile still reconciles. On the install this was diagnosed
against, all 18 scheduled tasks showed up as updates on the first `sync`
after the fix — that is the backlog, not a sign your tasks changed.
### Added
- **`akm task prune` reclaims orphaned scheduler entries `sync` cannot reach
(#851).** `akm task sync` only ever reconciles entries that resolve to a
desired task/workflow definition in a live bundle; an entry whose own
`--scheduler-context` descriptor is corrupt/missing, or whose owning
bundle directory has since been deleted, was permanently invisible to it
and had to be removed by hand-editing the crontab/launchd plist/Task
Scheduler. `akm task prune` finds those and nothing else: it never
touches an entry that still resolves to a live bundle, and there is no
`--force`/"remove everything silently" mode. Defaults to a dry-run
preview that makes zero scheduler writes and exits non-zero when it finds
removal candidates (usable as a CI/health-check guard, mirroring `task
sync --dry-run`'s exit-code convention); `--yes` executes the printed
plan; `--id a,b` narrows a run to specific binding ids and refuses (with
no writes) any id that isn't a current orphan candidate — including a
live entry, which cannot be pruned even if you name it explicitly.
- **`configVersion` read shim (#863).** Config loading now tolerates a
known older `configVersion` by upgrading the parsed document in memory
(never rewriting the file), with a one-line stderr warning naming the old
and new versions. The upgrade is silenced the next time any command
writes the config (`akm config set`, etc.), since every config write
already stamps the current version. Anything else — unknown, newer, or
malformed `configVersion` — still fails closed with the same actionable
error as before. No current release needed this shim yet (akm has only
ever shipped `configVersion: "0.9.0"`); it's in place now so the next
real bump doesn't break every existing config on upgrade.
- **Truncated indexed content is no longer silent.** The two indexer caps
that truncate an entry's content before it's written to the search index
(a markdown-body cap and a search-text cap) previously truncated without
any signal. Indexed entries now carry a `contentTruncated: true` flag
when either cap fired, and truncation logs a `--verbose` diagnostic
naming the entry, so an unexpectedly-worse search match on a very large
file has a visible cause instead of a silent one. The cap sizes
themselves are unchanged.
### Fixed
- **`akm health --report` and `akm proposal list --status accepted` no
longer crash, and `improve`'s accepted-proposal counts are no longer
silently zero, on proposals written before every optional envelope field
existed (#859).** A prior fix already tolerated a missing `changes` key;
this closes the same gap for `proposedTarget`, which was still hard-required
at decode and — on the real archive this was checked against — absent
from 93% of accepted rows. Decoding a proposal now tolerates a genuinely
*absent* `proposedTarget` (accept-time resolution already had a
ref-derived fallback for this case, previously unreachable because decode
threw first); a *present but malformed* value still throws, so real
corruption is never silently accepted. Writing a new (`pending`) proposal
still requires the full envelope — this only widens what can be *read*,
not what akm will *write*.
- **`akm task sync` no longer refuses to reconcile an entry it can't fully
re-prove ownership of, and no longer lets one broken task/workflow source
block every other source (#867, plus the scheduler-ownership fix
described above).** The v2 task-source shim also no longer hard-fails a
command wrapped in `env NAME=value... cmd`, a shape that's common for
cron entries (e.g. `env AKM_BIN=/path/akm bash script.sh`) — the shim now
looks past a leading `env` and its assignments to the real command before
deciding whether the conversion to v3/v4 is safe, instead of checking
`env` itself.
- **`akm task sync --dry-run` no longer crashes on a real install with a
pre-`--bundle` cron entry.** The reconcile fix above made those entries
reachable for the first time, and the dry-run preview's frozen result
object was then mutated in place while stamping output metadata, throwing
"Attempting to define property on object that is not extensible" (exit
70). Output stamping now copies instead of mutating, so it tolerates a
frozen (or any) result uniformly.
- **`akm migrate apply` no longer aborts an entire batch because one file in
it is blocked (#866).** The task v2->v3 and v3->v4 migrators refused to
write *any* file — including files with no problem at all — the moment a
single file in the batch was classified `blocked`. Both migrators now
skip and report the blocked file(s) and still migrate everything else;
the run still exits non-zero whenever anything was skipped.
- Two flaky integration/unit tests fixed at the root cause rather than
quarantined (#864): a `state.db` byte-identity assertion that raced
SQLite's own WAL checkpoint timing (now forces a checkpoint before
comparing, so the assertion reflects real durable mutations only), and a
"resolveProjectContext when cwd is the home directory" test that did
real, un-mocked filesystem walks against the actual `$HOME` (now isolates
`os.homedir()` and the stash/XDG dirs like the rest of the suite).
### Changed
- **`test:integration` is green-by-default (#861).** Verified there is no
CI mechanism silently swallowing a failing test (no `continue-on-error`,
no lost exit codes, no allowlist of "expected" failures) — the fixes in
this release were the actual cause of the prior red baseline. Raised the
integration suite's minimum-test-count floor (`AKM_MIN_INTEGRATION_TESTS`,
5500 -> 5700) so a large silent test loss is still caught with headroom
for ordinary future deletions, and documented in `AGENTS.md` that `TMPDIR`
must be a real `/tmp`-family path for the suite to pass — some guards
(stash-path safety, `akm-eval`'s Docker twin) intentionally hardcode that
assumption rather than reading `TMPDIR`.
- Simplified several areas flagged in the #866 complexity review without
behavior changes: `improve_runs.result_json`'s `schemaVersion` decoding is
now an explicit, extensible table instead of one large inline
conditional, ready for a future schema bump to add a case rather than
restructure the function.
### Removed
- **Three defensive checks that only ever refused work a human explicitly
asked for, with no evidence (via telemetry) they ever caught a real
problem, were removed:**
- The 1 MiB hard cap on reading a bundle file (command/agent/script/task/
workflow source, or a frozen workflow secret/env source) into memory.
Reads remain exact and integrity-checked (hashed, CAS-verified, fail-
closed on read-time mutation) — there is simply no longer a size ceiling
that refuses to read a file you put in your own bundle.
- The task migrator's inode/hard-link-count/change-time identity
fencing on top of its existing lockfile + backup-before-write +
byte-for-byte drift check. The extra fencing modeled a multi-tenant
race that doesn't apply to a single-user CLI holding an exclusive lock,
and its failure mode (refusing to migrate a file that's byte-identical
to what was already previewed, or refusing to touch a file merely
because it has a hard link elsewhere) was worse than the risk it
guarded against. Symlink/path-escape rejection — a real hazard — is
unchanged.
- Dead anti-collapse "hard refusal" guard functions
(`checkGenerationGuard`, `checkMergeInformationFloor`) that had zero
production callers.
## [0.9.4] - 2026-08-30
### Changed
- **Node.js 22 support is restored for the npm package.** 0.9.3 raised the
npm bootstrap floor to Node >= 24 as a policy simplification; no code in
the package actually requires a Node-24-only API, and the pinned
better-sqlite3 12.11.1 ships a prebuilt binary for Node 22 (ABI 127), so
nothing compiles from source. The floor returns to Node >= 22 across the
preinstall check, the CLI bootstrap guard, and the pinned-registry
helper, and the CI node-smoke matrix again runs BOTH Node 22 and 24 so
the supported floor is tested on every run, not merely declared.
### Added
- **`akm task sync --dry-run` previews the reconcile without touching the
scheduler (#849).** Prints the planned adds/updates/removes — removals
now carry their owning bundle — makes zero scheduler writes, and exits
non-zero when removals are pending so scripts can gate on it. The plan
renderer is shared, ready for the planned `task prune` (#851) to reuse.
- **Search hits now report which stage of the progressive AND->OR lexical
ladder produced them (#856).** `akm search` already ran strict AND, then
prefix AND, then an OR/prefix-OR recovery, stopping at the first stage
that returned candidates — but callers had no way to tell a strict match
from a heavily relaxed one. Local bundle hits now carry an optional
`matchStage: "exact" | "prefix" | "relaxed"` field (omitted for hits with
no FTS component, e.g. a pure-semantic hybrid contribution), surfaced at
`--detail normal`, `--detail full`, and `--shape agent` across all output
formats. Purely additive; no `schemaVersion` bump.
- **Previous-release corpus test.** New
`tests/integration/previous-release-corpus.test.ts` holds fixtures of
data shapes prior releases actually wrote (task v2/v3 sources, pre-#858
proposal rows) and asserts the current CLI reads or auto-handles every
one. Policy: every schema bump must add the old shape here — this suite
failing means an upgrade break was about to ship.
### Fixed
- **`akm show`, `task run`/`explain`, `workflow run`/`plan`, and
`command run` no longer fail on bundles past 16,384 files (#857).**
Single-ref owner resolution walked the entire bundle tree on every
lookup, aborting with `file limit 16384 exceeded` once a bundle grew past
the cap — normal `improve` output accumulation was enough to get there.
Path-to-conceptId derivation is deterministic, so its inverse is now
computed in closed form: each adapter's `readCandidates` enumerates every
physical spelling that could own a conceptId (canonical placement, loose
off-canonical fallback, env `.env` duality) and the existing
verification/collision pipeline runs over that fixed set. The tree walk,
both scan caps, and `AdapterConceptScanError` are gone — no cap is needed
when nothing walks. Collision guarantees are unchanged (the pinned
loose-vs-canonical conformance case still throws), and symlinked
off-canonical files are now reachable where the old scan skipped them.
- **`akm health --report` and `akm proposal list --status accepted` no
longer crash on proposal rows written before the `changes` metadata
envelope existed (#858), and `improve` outcome-score salience no longer
silently computes from zero accepted-counts (#859).** ~89% of real
archived accepted/rejected rows predate the field and can never recover
it; `storedToChanges` now treats a fully-absent `changes` key as a
documented legacy gap (empty change list — the rows still count toward
accepted/rejected history), `listStateProposals` skips-and-warns on
genuinely corrupt rows instead of aborting the whole list, and the write
path still refuses to persist a new proposal without changes.
- **Task v2/v3 sources are auto-read as v4 instead of hard-failing with
`TASK_SCHEMA_VERSION_UNSUPPORTED`.** The v4 source gate would have broken
every pre-0.9.4 scheduled task headlessly on upgrade until the operator
manually ran `akm migrate apply`. `parseTaskSource` now runs the same
pure, deterministic migration planners the migrator uses (chained
v2->v3->v4) entirely in memory, emits a one-line stderr deprecation
warning, and never writes the file; the hard error survives only for
sources the deterministic conversion genuinely cannot translate.
`akm migrate apply` remains the way to rewrite files on disk and silence
the warning. The planner core moved from `scripts/akm-migrate/` into
`src/tasks/source/` so there is exactly one copy.
## [0.9.3] - 2026-08-29
### Fixed
- **A failing Windows `shell: powershell`/`pwsh` task could lose its exit
code's fidelity through `-Command` (#845).** `-Command` derives its own
process exit code from `$?`, so a genuinely failing command already
produced a nonzero exit and `status: "failed"` — but any native exit code
outside `{0, 1}` was collapsed to `1`, discarding the real value reported
in task history. The inner shell invocation built by `shellCommand()` now
appends a guard that reads `$?` first (reproducing `-Command`'s own
completed/failed determination, immune to a stale `$LASTEXITCODE` from an
earlier native call in the same command) and only then upgrades to the
precise native exit code when the failing last statement actually set
one — a bare `exit $LASTEXITCODE` was rejected because `$LASTEXITCODE`
stays `$null` for a pure-PowerShell command, and `exit $null` is exit
code `0`, which would have turned a failed cmdlet into a false
`"completed"`. Verified via unit tests asserting the exact argv `-Command`
string on `platform: "win32"`; the runtime exit-code behavior itself is
unverified on a real Windows host pending the owner's manual run (see the
PR body for the procedure) — Windows scheduler paths still have no
automated coverage (#770).
- **A valid 0.9.1 config hard-failed to load after upgrading to 0.9.2
(#852).** `reasoningEffort` became a first-class `engines.<name>` field in
0.9.2 (#815), so `extraParams.reasoning_effort` — the documented 0.9.1
workaround for LM Studio, where `enableThinking` is a no-op — started
tripping the extraParams protected-key check and hard-failing every
command that loads config, with no migration. Config load now lifts
`extraParams.{reasoning_effort,temperature,maxtokens,enablethinking}`
onto their first-class field automatically (in-memory only; the file is
not rewritten, and a warning names each lifted key) unless the
extraParams value and the first-class field disagree, in which case
config load still fails, now naming both values and the field to keep.
Any other protected `extraParams` key without a first-class equivalent
still hard-fails, but the error now names the fix instead of only the
rule.
## [0.9.2] - 2026-08-29
### Fixed
- **A blocked workflow run became permanently unresumable after `akm
workflow abandon` (#847).** Abandon correctly moved the run to `failed`
but left its current step `blocked`; the durable-spine validator then
rejected that honest abandoned shape as corruption before `resume` could
reopen the step. Failed-run validation now accepts the three legitimate
current-step states — `pending` for an abandoned active run, `blocked` for
an abandoned blocked run, and `failed` for an execution failure — and
`resume` normalizes each back to `pending` as promised by the CLI help.
- **`akm task sync` could compute another bundle's real scheduler entries as
drift and try to remove them (#846).** Removal was scoped by a bundle
*display name* (`bundleName`/`--bundle` target) — a value derived from a
directory basename that two unrelated bundles can legitimately share (an
unconfigured bundle's name is deduped only against its own config's
bundles, never against other bundles actually installed on the machine).
A primary/unconfigured-bundle sync now additionally confirms a
name-matching installed entry's *resolved bundle path*, recovered from
that entry's own scheduler-context descriptor, before treating it as
eligible for reconcile — and refuses (rather than assumes) when that path
can't be established. **Backward compatibility:** every entry installed by
this codebase already carries a scheduler-context descriptor (the
`--scheduler-context` file used to restore the scheduled process's
environment), so existing installations resolve correctly with no user
action required. An entry whose descriptor is missing, unreadable, or
owned by a different OS user is never assumed to belong to the invoking
bundle; such an entry is simply left untouched by sync (it will not be
auto-repaired or removed) until it is reinstalled or removed by hand.
- **Scheduled tasks on Windows ran but recorded no output.** A task fired by
Task Scheduler logged `exit_code=0` with an empty log: the command really
ran, but nothing it printed was captured. Captured runs asked for their own
process group, which is what lets a timeout reap the whole descendant tree
on macOS/Linux; on Windows that same flag instead means "start with no
console", and a console host started that way (`powershell.exe`, `cmd.exe`)
allocates its own console and thereby replaces the pipes it was handed, so
its output went nowhere. Windows got no reaping benefit in exchange — the
group kill it enables is a POSIX-only call — so captured Windows runs no
longer ask for it. A run whose output capture is incomplete for any other
reason now says so in the task log instead of leaving a silent gap.
- **A scheduled shell task could fail instantly with exit code 1 on Windows.**
Two defects on the default Windows task shell (`powershell`): a
scheduler-fired run restores the PATH captured at install time, which can be
minimal, and `powershell.exe` is not on it (it lives in a `WindowsPowerShell`
subdirectory, not `System32`), so the spawn failed outright; and rebinding a
bare leading `akm` produced a quoted path, which PowerShell parses as a
string rather than a command to run. Both shells are now resolved to
absolute paths on Windows, and a rebound invocation carries PowerShell's
call operator. `cmd`-shell tasks additionally pass their hand-quoted command
line through verbatim, since `cmd /s /c` does not read a standard argv.
- **`state.db` could fail to open on macOS** with "this platform has no
descriptor-backed path." The descriptor-alias optimization used to bind a
SQLite open to its exact held inode isn't reliably available everywhere —
Windows never has one, and macOS's `/dev/fd` is a small fixed-size table
that a process holding higher file-descriptor numbers (as a bundled
standalone binary routinely does) can miss. The surrounding identity checks
(dev/ino/uid, re-verified immediately before and after every open) are the
real protection; a missing alias now falls back to the plain path on any
platform instead of throwing, matching the fallback Windows already used.
- The Windows build shipped no embedded template assets, because the
build-time asset copy anchored its rewrites on forward slashes while the
glob yields platform-native separators.
## [0.9.2-alpha.5] - 2026-08-28
### Breaking changes & migration
- **Durable workflow plans bump to `irVersion` 5.** A stored run frozen
before this release (`irVersion` 4 or earlier) can no longer `resume`,
`next`, `complete`, or `run` — those fail closed with `UsageError` code
`WORKFLOW_IR_VERSION_UNSUPPORTED`, naming the run and pointing at
`akm workflow abandon`. `akm workflow status`, `akm workflow list`, and
`akm workflow abandon` keep working on those runs — no data is lost, and
their step spine is untouched by abandoning. **Before upgrading**, run
`akm workflow list --active` and either let in-flight runs finish or
abandon them; after upgrading, recover a blocked run with
`akm workflow abandon <id>` followed by `akm workflow run <ref>` to start
fresh from the current authored source. There is no second executor and no
compatibility replay layer for a pre-`irVersion`-5 plan. The unit and gate
input-hash prefixes bump alongside it, from `hashVersion` 5 to
`hashVersion` 7, so that a freshly frozen plan's units are never
content-addressed the same way an old, no-longer-executable plan's were.
(`hashVersion` 6 existed only inside this release's own development and
never shipped in any version — the durable step a released install sees is
5 → 7.) The unit preimage also gains one **conditional** field,
`taskInputs`: the *resolved* values of a task-composing step's input
bindings, present only for a unit whose frozen target carries
`inputBindings` — a binding-free unit's preimage keeps exactly the shape it
had. Hashing the resolved values, not just the frozen binding expression,
is what makes a resumed run whose upstream step output changed under a
`{from: "steps.<id>.output"}` binding fail loudly as a replay divergence
instead of silently reusing the completed unit's stale result.
See [Migrating from akm 0.9.1 to 0.9.2](docs/migration/v0.9.1-to-v0.9.2.md#workflow-cutover).
- A workflow step that passes `with:` to a `tasks/<ref>` target whose task
declares **no** `inputs:` (a `version: 4` task with no `inputs:` key at
all) is now **rejected** (`UsageError` code
`COMPOSITION_INVALID`, exit 2) instead of having the authored mapping
silently dropped at freeze — for any authored shape, including `with: {}`.
When the target's task source **does** declare `inputs:`, `with:` now
**binds** them instead: a literal value, or a `{from: "steps.<id>.output…"}`
reference resolved just before the unit dispatches (the reference grammar
also accepts `{from: "params.<name>"}`, but a composing step's own
document can never declare `params:`, so that form is not reachable in
this release). See [Task input bindings](docs/reference/tasks.md#typed-inputs-and-output)
for the full grammar. `with:` on `uses: akm/command` is unaffected — it is
still that builtin's own action-argument bag, never an input binding.
- **New rejection:** `with:` on a workflow step targeting `uses:
commands/<ref>` or `uses: scripts/<ref>` is now **rejected**
(`COMPOSITION_INVALID`, exit 2) instead of being silently discarded at
freeze — neither target is a binding surface. Remove the `with:` block from
any such step; a command/script-composing step never accepted its values
in the first place, so this closes a defect rather than a feature.
- Composing a task source v4 document from a workflow step's `uses:
tasks/<ref>`, where the task's own target is a workflow, is no longer
deferred: it now **freezes and dispatches** normally, the same as a
direct `uses: workflows/<ref>` step. The prior release's
`TASK_SOURCE_INVALID` "arrives in a later 0.9.x release" rejection for this
case is gone.
- Task-source validation errors raised through the shared `sourceError`
funnel (field- and semantic-level checks: missing/invalid fields, schedule
conflicts, and similar) now report code **`TASK_SOURCE_INVALID`** instead of
`INVALID_FLAG_VALUE`. YAML syntax, size, structure, and expansion failures
(malformed YAML, oversized source, unsupported YAML constructs, and
alias/tag/depth/node-count limits) are raised earlier, before that funnel is
reached — early in this release these still reported `INVALID_FLAG_VALUE`,
but by 0.9.2's release they report `TASK_SOURCE_INVALID` too (the terminal
diagnostics ratchet, see the Changed entry below): task-source failures no
longer split across two codes, so **scripts that were branching on both
`TASK_SOURCE_INVALID` and `INVALID_FLAG_VALUE` for a task-source error can
drop the `INVALID_FLAG_VALUE` arm**. Every such error's message prefix is
`Invalid task source at <path>[:<line>]: …` — not `Invalid task v3 source`,
since the label no longer names a specific schema generation (task source
v4 is the only version `src/` accepts by release; see the "Task v3 sources
no longer parse" entry below). The envelope's `error` message text and exit
code 2 are unchanged for every task-source error. The envelope's `hint`
field and the `detail` text `akm lint` and the akm-task adapter report for
the same failure change from `… Run \`akm <command> --help\` to see
accepted values.` to `… Fix the task source at the reported path and line,
then re-run.`
- **Task-history / JSON-output `target.kind` vocabulary changed.** A prepared
command (agent/LLM) run now reports `"command"` (formerly the confusingly
inverted `"prompt"`); the former shared `"command"` string for the native
arm splits into `"shell"` and `"script"`, now distinguishable in history;
`"workflow"` and `"unknown"` are unchanged. **Consumers branching on
`"prompt"` must handle `"command"`** — this affects `akm task run`'s and
`akm task history`'s JSON output (`result.target.kind` /
`rows[].target.kind`) and any code reading `task_history.target_kind`
directly. Rows written by earlier akm versions are read back **mapped** to
the new vocabulary (legacy `"prompt"` → `{kind:"command", engine}`, legacy
`"command"` → `{kind:"shell"}`), so `akm task history` output stays uniform
across vintages. New rows carry a `targetVocab: 2` marker inside their
`metadata_json`, which akm versions before this one reject as an unknown
metadata field — a mixed-version fleet must upgrade every `akm` that writes
task history before an older one reads it.
- **Task source v4 (`version: 4`) is the task source grammar.** By 0.9.2's
release, `version: 4` is the *only* version `src/` accepts — see the
"Task v3 sources no longer parse" entry below for the cutover, the
`TASK_SCHEMA_VERSION_UNSUPPORTED` rejection, and the migration path. What
follows describes the v4 grammar itself. Scheduling is **optional**: a
task source v4 document with no `schedule:` parses, is runnable with
`akm task run`, and is **skipped** by `akm task sync` (zero bindings, zero
failures) instead of being rejected for missing a trigger — this is now
the *only* scheduling grammar; the second syntax task v3 offered
(`akm.schedule` / a document's top-level `on:`) is retired along with v3
itself. Task source v4 removes the `akm:` options bag and the `on:`
trigger block outright;
every field they carried is a top-level key instead: `akm.description` →
`description`, `akm.when_to_use` → `when_to_use`, `akm.tags` → `tags`,
`akm.agent` → `agent`, `akm.engine` → `engine`, `akm.model` → `model`,
`akm.inference` → `inference`, `akm.outputSchema` → `output`,
`akm.tools` → `tools`, `akm.timeout` → `timeout`, `akm.redact` →
`redact`, `akm.maxSteps` → `maxSteps`, `akm.maxRetries` → `maxRetries`,
`akm.schedule` → top-level `schedule:`, and `akm.enabled` → each
`schedule:` entry's own `enabled` (v3's single document-level flag
becomes per-binding in task source v4, defaulting to `true`; the
document-level `enabled` skip that read it is gone with v3, since a v4
document has no document-level `enabled` key to read). The GitHub-action
`uses:` target (`owner/repo@ref`) is removed outright — see the "GitHub
Action locators are no longer recognized anywhere" entry below.
`with:` is legal in task source v4 only alongside
`uses: akm/command`; every other target uses the new typed `inputs:`
declarations instead of `with:`. A declared `inputs:` name may not collide
with a flag `akm task run` already declares for itself (`bundle`, `format`,
`detail`, `shape`, `output`, `scheduled`, `quiet`, `verbose`, `help`,
`no-quiet`, `no-verbose`) or with `target`, the spelling `akm task`
retired in 0.9 and still answers with a rename hint in every spelling —
such a document now fails `TASK_SOURCE_INVALID` at parse time, since the
colliding name would otherwise route a caller's value into `akm task run`'s
own flag, or into that rename hint, instead of the declared input. `akm
task run <id>` now accepts exact-name input flags for a task source v4
document's declared `inputs:` (an undeclared flag name fails
`UNKNOWN_FLAG`; a bad value or an unsatisfied `required: true` declaration
fails `INPUT_BINDING_INVALID`; both exit 2 with the usual JSON error
envelope).
Where those materialized values go depends on the task's own target: for
`uses: workflows/<ref>` they become the child run's params (the existing
`with:` → params path); for a `run:`, `scripts/<ref>`, or `commands/<ref>`
target they are validated and then **discarded** — `akm task run`'s own
flags never populate an `AKM_TASK_INPUTS` environment variable or a
`## Task inputs` prompt block. Those two surfaces are a separate delivery
path: they carry a **workflow step's** `with:` binding into a task it
composes via `uses: tasks/<ref>` (see the `with:`-binding bullet above),
not `akm task run`'s own CLI flags. `schedule[].inputs` on a `version: 4`
task source are compiled the same way `akm task run`'s flags are: `akm
task sync` builds them into the scheduler binding's own invocation tail
instead of only validating and discarding them, so a scheduled run is
subject to the identical workflow-target-only delivery rule. `akm task
add` now authors task source v4 (see the "Task v3 sources no longer
parse" entry below for the `--params` → typed `inputs:` change). A
`version: 4` task source is now a valid workflow-step target (see above).
The published [task schema](schemas/akm-task.json) now publishes only the
single `version: 4` shape — the `version: 3` arm and its `githubActionRef`
definition are removed; a `version: 2` or `version: 3` document validates
against nothing in this schema and is converted by `akm migrate apply`
instead. **Binding a task's inputs adds nothing to the hash preimage of a
step that binds none**: a unit whose frozen target carries no
`inputBindings` has exactly the preimage *shape* it had before this
feature — no `taskInputs` key at all. Its hash *value* still moves, because
every unit and gate hash in this release re-versions once (`hashVersion`
5 → 7, see the `irVersion` 5 entry above), and no pre-`irVersion`-5 plan can
execute here to be compared against.
- **A task's `output:` is legal only with a command target.** `output:`
alongside `run:`, `uses: scripts/<ref>`, or `uses: workflows/<ref>` now
fails `TASK_SOURCE_INVALID` (exit 2) at parse instead of being accepted and
never enforced: those runtimes decide success from the process exit code or
from a child run's own status and consume no task-level response schema, so
an authored contract there was silently unenforced. `uses: commands/<ref>`
and `uses: akm/command` — the targets that forward it as the model's
response schema — are unchanged. The published
[task schema](schemas/akm-task.json) enforces the same rule, so an editor
validating against it no longer green-lights a document `akm task run`
refuses to load. Migration handles this for you: `akm migrate apply` drops
an `akm.outputSchema` that sat on one of those three targets (it was inert
in v3 as well — nothing ever read it there) and reports the drop as a
notice on that file's plan entry rather than blocking the file.
- **A `schedule:` entry must be able to satisfy the task's declared
`inputs:`.** A scheduled firing supplies no input flags, so a task
declaring a `required: true` input — which may not also carry a
`default:` — and a `schedule:` entry that names no value for it could only
ever install a binding that fails at every firing. Such a document now
fails `TASK_SOURCE_INVALID` (exit 2) at parse, naming the unsatisfied
input, instead of syncing cleanly and failing once per fire. This covers
every entry shape: the `schedule: "<cron>"` string shorthand and a list
entry with no `inputs:` key are held to the same contract as one that
authors `inputs:`. Give the entry an `inputs:` value for each named input,
or declare a `default:` on the input instead. Manual-only tasks are
unaffected — a `required: true` input with no `schedule:` is still valid
and is supplied per run with `akm task run <id> --<name> <value>`.
- **`akm workflow create --json` renames its `stashDir` envelope field to
`bundleDir`.** The success envelope now reads
`{ok, ref, path, bundleDir}`; the value (the owning bundle's directory) is
unchanged. Scripts reading `stashDir` off `akm workflow create --json`
must read `bundleDir`. This was the last `stash`-vocabulary field on a
0.9.2 command envelope; the indexer's internal `IndexOptions.stashDir` is
not a CLI surface and is unchanged.
- **Task v3 sources no longer parse.** A task document with `version: 3`
(or `version: 2`) fails with `UsageError` code
`TASK_SCHEMA_VERSION_UNSUPPORTED` (exit 2) instead of executing — the v3
parser is gone from `src/`; it survives only vendored inside the
`akm-migrate` executable, which is how the migrator still reads what it
converts. Run `akm migrate apply --dry-run`, review every `changed` /
`skipped` / `blocked` result, then `akm migrate apply` — the command now
runs **both** generations in one pass: task-v2 → task-v3, then
task-v3 → task source v4, against the same tree. `akm task add` authors
task source v4 directly; a task's `--params` becomes typed `inputs:` with
defaults instead of a `with:` bag. A task's enabled state is now per
schedule binding (`schedule[].enabled`) rather than a document-level
`akm.enabled` flag — `akm task add --disabled` writes
`schedule: [{cron: …, enabled: false}]` instead of a document-level
`akm.enabled: false`. See
[Migrating task v3 to task source v4](docs/migration/v0.9.1-to-v0.9.2.md#migrating-task-v3-to-task-source-v4).
- **GitHub Action locators are no longer recognized anywhere.** A task's
`uses: owner/repo[/path]@rev` is now a source error at parse
(`TASK_SOURCE_INVALID`); a workflow step's `uses: owner/repo[/path]@rev`
now fails with reason `unsupported-uses-target` instead of
`remote-action-acquisition-out-of-scope`. Nothing acquired or executed a
remote action in any akm release — this deletes the *recognition* of the
shape, not a capability that ever worked. The migrator still names the
target explicitly when it blocks a file
(`github-action-target-removed`).
- **Multi-job YAML is rejected at the adapter boundary.** A GitHub-shaped
workflow document whose `jobs:` map does not contain exactly one job now
fails at the source adapter with reason `multi-job-unsupported`, surfaced
from `akm workflow run` (and `akm workflow plan`) as `UsageError` code
`COMPOSITION_INVALID` (exit 2). Previously such a document parsed and
ordered its jobs cleanly and was refused only later, in two different
places, with two different shapes (one thrown error, one `ok: false`
compile result). Split a multi-job document into separate single-job
workflows and compose them with a child-workflow step
(`uses: workflows/<ref>`). Every other workflow-source compile failure now
reports `UsageError` code `WORKFLOW_SOURCE_INVALID` rather than
`INVALID_FLAG_VALUE`.
- **The second task scheduling syntax is removed.** `akm.schedule` and a
task document's top-level `on:` are gone along with task v3 (see "Task v3
sources no longer parse" above); task source v4's optional top-level
`schedule:` is the one canonical scheduling form, and a task with no
`schedule:` is manual-only and fully composable as a workflow-step
target.
### Changed
- **`INVALID_FLAG_VALUE` is now rare in task or workflow domain failures,
with two named exceptions.** Every task-source, workflow-source,
target-classification, and composition failure now reports a
phase-specific code — `TASK_SOURCE_INVALID`, `TARGET_REF_INVALID`,
`COMPOSITION_INVALID`, `WORKFLOW_SOURCE_INVALID`, `INPUT_BINDING_INVALID`,
`TASK_SCHEMA_VERSION_UNSUPPORTED`, or `TASK_TARGET_UNSUPPORTED` — **except**
a task's workflow-target `env:` composition rejection (a `uses:
workflows/<ref>` task that also authors `env:`) and a workflow child-ref
asset-resolution failure (`Workflow source target <ref> was not found.`),
both deliberately preserved as `INVALID_FLAG_VALUE` so an existing pinned
test's code and message stay byte-unchanged. The remaining
`INVALID_FLAG_VALUE` sites in the task/workflow domains (38 total, across
`src/tasks/**` and `src/workflows/**`) are these two preserved exceptions
plus scalar CLI-argument parsing (a cron expression, a task id, a workflow
parameter flag) and one code-allowlist membership entry — genuine
flag-value validation or a pinned exception, not a re-codable
task/workflow source or composition failure. **Scripts branching on
`code` for a task/workflow domain error should switch on the specific
code above rather than assuming `INVALID_FLAG_VALUE` — except for the two
named exceptions, which still report `INVALID_FLAG_VALUE`.** Exit codes
are unchanged (2 for every one of these).
- **A typed task-input or workflow-param flag no longer echoes the supplied
value in a validation error.** `akm task run <ref> --<input> <value>`
against a `type:`-declared input used to report
`must be <types>; received "<value>"` on a coercion failure, and a value
that failed its declared `enum:`/`minimum:`/`maximum:` constraint reported
the value in that message too — both are closed now, since a typed flag
can carry a credential and this detail lands in stderr envelopes that get
pasted into CI logs and issue reports. The declared constraint (the
allowed list or the bound) is still named, since it comes from the
author's own schema rather than the caller's data. Error and exit codes
are unchanged.
### Added
- **Child workflows.** A workflow step can now compose another workflow —
directly (`uses: workflows/<ref>`) or through a task source v4 document
whose own target is a workflow (`uses: tasks/<ref>`) — instead of
failing to freeze. `with:` on the composing step binds the child's
declared `params:`. Composition is bounded: depth (8 levels), a
composition cycle, and aggregate embedded plan bytes (1 MiB total across
one root freeze) are all checked at **freeze**, before the parent run is
published, and fail with `UsageError` code `COMPOSITION_INVALID`. The
child workflow is compiled, validated, and frozen **completely** — its own
complete plan embedded inside the parent's — before the parent run exists,
so editing the child's source afterward cannot affect an already-frozen
parent, and the child's transitive sources join the parent's guarded
source read set. A composing step's own `env:` is rejected at freeze
(`UsageError` code `COMPOSITION_INVALID`) rather than silently dropped —
a child run carries its own frozen environment inside its own plan, so a
parent-level `env:` on the composing step has nothing to apply to.
Running a step that composes a child workflow now drives
the child to completion — see **Child workflows now execute** and
**Workflow `outputs:`** below. (Correction: an earlier development
increment of this same 0.9.2 release briefly made an unexecuted composing
step fail closed with `UsageError` code
`WORKFLOW_CHILD_EXECUTION_UNSUPPORTED`. That code never reached a release
and is gone from the shipped 0.9.2 — it is listed here only because a
0.9.2 pre-release snapshot may otherwise be the sole place it was seen.)
See
[Workflow Schema: Child workflows](docs/reference/workflow-schema.md#child-workflows).
- **Child workflows now execute.** Running a step whose target is a child
workflow drives that child inline, in the parent's own process, with the
same engine `akm workflow run` uses — publication is idempotent, so a
retried or resumed composing step reuses the same child rather than
starting a new one. The child's final status maps onto the composing
step and the parent run: `completed` promotes the child's exported result
as the step's output and the parent continues; `failed` fails the step
and the run; `blocked` blocks the step and the run, with recovery notes
naming the exact sequence — `akm workflow resume <childRunId>`, then
`akm workflow resume <parentRunId>` and `akm workflow run <parentRunId>`.
`akm workflow status` on a run that composes children now renders a
`children:` tree; `akm workflow list` excludes child runs by default
(`--children` includes them), and a child run id always works directly
with `status`/`resume`/`abandon`/`run`. See
[Workflow Schema: Child execution](docs/reference/workflow-schema.md#child-execution)
and [Running Workflows: Child runs](https://github.com/itlackey/akm/blob/main/docs/guides/run-workflows.md#child-runs).
- **Workflow `outputs:`.** A workflow may declare a run-level export in its
Markdown frontmatter — `outputs: {<name>: {from: steps.<id>.output(.<seg>)*,
schema?}}`, up to 64 entries — resolved once, from persisted step
evidence, at run completion. An unresolvable reference, a truncated
step artifact, or a schema violation rolls the completion back
(`UsageError` code `WORKFLOW_OUTPUT_INVALID`): the run stays `active` and
its final step stays `pending` rather than completing with missing
exports. A run with no `outputs:` declaration exports `{runId, status}`
instead. This is a Markdown-frontmatter-only key — a GitHub-shaped
workflow's closed root key set has no extension surface for it, the same
reason it cannot declare `params:` either. See
[Workflow Schema: Workflow outputs](docs/reference/workflow-schema.md#workflow-outputs).
- **`akm workflow plan <ref>`** (Evolving) — compiles, resolves, and freezes
a workflow exactly as starting a run would, then stops: zero durable
writes, no published run, no event, no lease. Prints the canonical step
graph, per-step frozen target kinds, task/child expansion, input
bindings, the source read set, and freeze-time lowering notices —
secret-free by construction (no resolved reference value, request
content, script bytes, or credential is ever printed). Defaults to a
human-readable summary; `--format json` returns the full envelope. See
[CLI reference: workflow plan](docs/reference/cli.md#workflow-plan).
- **`akm task explain <ref> [input flags]`** — read-only task introspection.
Prints the task's source path and version, its declared `inputs:` (with
defaults — a secret-shaped default prints as `<redacted>`), the supplied
values with provenance (`default` | `flag` | `schedule-binding`, likewise
redacted when secret-shaped), the resolved target kind/ref, effective
execution settings with field-level provenance, and schedule bindings.
Never spawns anything, writes history, or touches the scheduler; never
prints an `env:` value, a credential, a prompt body, a `run:` string, or
`with.content`. It accepts the task's own declared input flags and nothing
else: `--scheduled` — which `explain` neither declares nor implements —
fails `UNKNOWN_FLAG` (exit 2) rather than being silently discarded, in
every spelling (`--scheduled`, `--scheduled=false`, …). See
[CLI reference: task](docs/reference/cli.md#task).
- **`AKM_TASK_INPUTS`** — the exec-context environment variable a
task-composed step's shell/script target receives: canonical JSON of its
resolved, schema-validated `inputs:` bindings. Present only when the
bindings are non-empty; subject to the same per-platform size ceiling as
`AKM_INPUTS` / `AKM_PARAMS`. See
[Context reaching the command](docs/reference/workflow-schema.md#context-reaching-the-command).
- **A v3 → task source v4 migrator**: the separate `akm-migrate` executable
(installed alongside `akm`) gains `task-v4-status` / `task-v4-apply
[--dry-run]`, a second, independent generation of the same dry-run-first,
`changed | skipped | blocked` migration planner — options bag flattened to
top-level keys. A `with:` authored on any target other than
`uses: akm/command` is `blocked` for manual review, alongside a
github-action-targeted `uses:` (blocked reason
`github-action-target-removed`) and anything else ambiguous: the migrator
translates structure, never intent, so `inputs:` is never invented on a
file's behalf — declaring it is an authoring decision left to the person
editing the migrated file. Nothing is overwritten without a backup. A
`changed` file can carry an informational **notice** for a translation that
is faithful but not one-to-one — a manual-dispatch-only trigger that v4
expresses as "no `schedule:`", and an `akm.outputSchema` dropped because v4
accepts `output:` only with a command target — so read the notices on a
dry-run plan, not just the outcomes. A v3 document that was never valid in
the first place (an empty `on:`, or a `workflow_dispatch:` carrying
`inputs:`) is `blocked` as `invalid-v3-task` rather than being converted
into runnable v4 bytes. By
0.9.2's release this generation runs automatically as the second half of
`akm migrate status` / `akm migrate apply [--dry-run]` (see "Task v3
sources no longer parse" above) — `task-v4-status`/`task-v4-apply` remain
as the standalone, single-generation entry points the frozen migrator
always exposes. See the
[0.9.1 to 0.9.2 migration guide](docs/migration/v0.9.1-to-v0.9.2.md#migrating-task-v3-to-task-source-v4).
### Fixed
- **A flag value for a parameter or input declaring both `array` and a
scalar type is no longer forced into an array.** `akm workflow run <ref>
--<param> <value>` (and, new in this release, `akm task run <id>
--<input> <value>`) unconditionally grouped a supplied value into an array
whenever the declaration mentioned `array` at all, so
`type: ["array", "string"]` with `--x hello` delivered `["hello"]` instead
of the permitted string, and `type: ["array", "null"]` could never produce
`null` — silently, since the altered value still satisfied the array
branch. A single, non-bracketed value now tries the union's scalar
alternatives first. An `array`-only declaration, the JSON-array shorthand
(`--x '["a","b"]'`), and grouping a repeated flag are all unchanged.
- **`akm task <subcommand> --target=<value>` now answers with the 0.9 rename
hint instead of ignoring the flag.** The retired-spelling check compared
whole argv tokens, so it caught a bare `--target` but not `--target=team`;
because `target` is exempt from the generic unknown-flag gate on `task`
subcommands precisely so that check can answer, the `=`-spelling was
rejected by nothing at all and the bundle the caller named was silently
dropped. It now fails with `UsageError` code `INVALID_FLAG_VALUE` (exit 2)
naming `--bundle`, in every spelling.
- **The embedded `akm` hint sheet no longer teaches a task format this
release rejects.** Its "Scheduled Tasks" section still told readers to
author `version: 3` with `akm.enabled` and `akm.timeout`; it now describes
task source v4 (`version: 4`, per-entry `schedule[].enabled`, top-level
`timeout`, typed `inputs:`/`output:`) and points at `akm migrate apply`.
The `stash`-terminology doc lint now scans the shipped hint assets too, so
the embedded help cannot drift out of the active-docs vocabulary again.
- **The macOS native scheduler backend no longer refuses a real
`launchctl` inventory.** Its loaded-service reader enforced a narrow,
hand-written grammar over `launchctl print`'s full output and rejected
the entire read — surfacing as `INVALID_CONFIG_FILE` from every akm
scheduler command — the moment any line fell outside it, which real
`launchctl` output on a real Mac routinely does. It now scans for akm's
own `com.akm.task.*` labels and ignores everything else, which is what
every caller actually needed. Caught by the gated native-scheduler
suite's first run against macOS.
- **The Windows-built package no longer ships without its embedded
template assets.** The build's asset-copy step matched paths against a
forward-slash pattern, but path separators on Windows are backslashes,
so the match silently failed and `dist/assets/` was never populated —
the packaged npm tarball built on Windows carried no templates at all,
and any command rendering one (for example `akm health --format html`)
crashed with `ERR_MODULE_NOT_FOUND`. Also caught by the gated
native-scheduler suite's first run, this time against Windows.
## [0.9.2-alpha.4] - 2026-08-26
### Added
- **`akm health`: flag assets whose resolved type disagrees with their
directory** (#837). Adds a `type-directory-disagreement` advisory that
compares every indexed asset's resolved type against the type its
`DIR_TYPE_MAP` directory declares (`memories/`, `knowledge/`, `commands/`,
`agents/`, `workflows/`, `facts/`, `lessons/`, `sessions/`,
`instructions/`, `scripts/`, `env/`, `secrets/`, `tasks/`). This is the
diagnostic that would have caught #824 (three `memories/` files silently
indexed as commands) the day it was introduced. Since `knowledge/` +
`$ARGUMENTS` and `agents/` + `agent:` frontmatter are deliberate command
overrides, the check never hard-fails: every disagreement is reported as a
warning naming the winning classifier signal, with a `knownGoodOverride`
flag so a sanctioned override reads differently from an unexplained one.
- **`akm health`: report the Claude harness plugin's version and warn when
it's stale or out of range** (#838). Adds a `plugin-version` advisory that
reports each installed Claude Code `akm` plugin's version, warns when a
newer tag is published upstream (naming the update command), and warns
when the plugin's own declared `AKM_VERSION_RANGE` no longer admits the
running CLI — meaning the plugin has silently disabled itself. Makes an
outbound `git ls-remote` when network is available to check for a newer
tag; per owner decision, this is read-only and degrades to a benign pass
(no plugin, no marketplace clone, unreadable manifest, malformed range, or
a failed remote lookup) rather than crashing or blocking offline use.
### Changed
- **Extract: LLM prompt is now built from parent-origin events only —
"harvest-without-prompting hybrid" (#840).** #830 folds a session's
subagent transcripts into its event stream for hashing and inline-ref
harvesting; the prompt sent to the extraction LLM previously included that
folded subagent content too, competing with the parent's own transcript
for the 80,000-char pre-filter budget. #840's design-determination doc
(`docs/plans/subagent-extraction-design.md`) measured that this "fold"
approach evicts up to 28.6% of parent-origin content on real sessions to
make room for subagent noise that mostly gets evicted anyway, while a
"harvest-without-prompting hybrid" — keep folding for hashing/inline-ref
purposes, but filter the prompt down to `data.events` whose `filePath`
matches the session's own (`data.ref.filePath`) — matches or beats the
folded prompt's size with zero eviction on every session measured, and
recovers the exact same inline refs (`akm remember`/`akm feedback` calls
the agent made inside a subagent), because that harvesting already runs on
the raw stream independent of what reaches the prompt. Only
`runPreLlmSessionGates`'s call into `preFilterSession` changed; folding
(`session-log.ts`) and `buildExtractPrompt` are untouched.
- **No forced re-extraction wave.** `hashSessionContent` still hashes the
full folded `data` (parent + subagents), computed before the
parent-origin view is built — no previously-computed session hash
changes, so no session already extracted under the fold prompt shape is
automatically re-processed. Use `--force` to re-process a specific
session under the new, parent-only prompt shape.
- **`processes.extract.maxTotalChars` is unchanged in meaning and default**
— it still caps the single-call prompt built from parent-origin events;
it simply no longer has to compete against subagent-origin noise for
that budget.
- **`minContentChars`** (the raw-size skip gate, #595/#596) is still
measured on the FULL folded `data.events` (parent + subagents),
deliberately left unchanged: narrowing it to parent-origin chars would
newly skip delegation-heavy sessions with a thin parent transcript
before extraction runs at all, even though their subagent-origin work is
still fully harvested via inline refs. The full-stream measurement is
today's existing behavior; the worst case it preserves is an LLM call
over a small parent-only prompt, not a missed extraction.
- #839's task-notification dedupe (which stubs a parent's
`<task-notification>` only when the matching subagent's own event ALSO
survives into the same kept prompt set) composes safely with this
change without modification: subagent-origin events never reach
`preFilterSession` on this path, so the dedupe's own scoping check
naturally makes it a no-op — the parent's notification (the only
remaining trace of delegated work in the prompt) survives untouched.
### Fixed
- **`akm remember` synthesizes a description when the caller doesn't supply
one** (#835). Both the zero-flag hot path and the structured-args path
(e.g. `--tag`-only, with no `--description`/`--enrich`) previously wrote
memories with no `description:` and no `tags:`. akm's indexer covers only
synthesized frontmatter/headings, never body prose, so those memories were
retrievable only by whatever words survived into the auto-generated
filename — effectively write-only. Verified on a real stash: 272/3169
memories lacked a description, 100% of those written via `akm remember`.
The new `synthesizeMemoryDescription` (ported from akm-eval's
`firstSentencesCapped` rule, which independently arrived at the same fix)
is deterministic and makes no LLM call: it accumulates whole sentences
from the body up to `DESCRIPTION_MAX_CHARS`, skipping a leading markdown
heading so the description doesn't just repeat the title. Wired into both
write paths as a fallback only — a caller-supplied `--description` (or one
derived by `--enrich`) is never overwritten. Closes the write-only-memories
gap on 0.9.1 indexes.
- **Extract: deduped the doubled subagent conclusion in the extraction prompt**
(#839). After #830 folded a session's subagent transcripts into its event
stream, a completed subagent's final report could appear twice in the same
extraction prompt: once as the subagent's own folded final message, once as
the parent's `<task-notification>` record of that same call (#836 measured
~92-99% textual overlap on a real pair; reproduced here as a byte-identical
match after decoding the XML entities Claude Code escapes into `<result>`).
The parent's notification copy is now stubbed to `[subagent <agentId>
completed: <description>]` when its `<result>` is a near-duplicate
(Dice-bigram similarity ≥ 0.9) of a folded subagent transcript's own text;
the subagent's original is untouched, per #839's owner-decided direction
(the inverse — dropping the subagent's own terminal event — was evaluated
and rejected in #836 because some subagent transcripts consist only of
that one event). Matching is scoped by `<task-id>` to the one subagent
transcript it names and still requires content similarity, so an earlier
notification for a *resumed* agent (Claude Code re-notifies the same
task-id on each stop) that carries a genuinely different, intermediate
result is left alone.
**Scoped to the final, post-budget kept set — not the raw stream** (#840's
design-determination doc flagged this as a hazard while this PR was in
flight): the dedupe only fires when the subagent's own event ALSO survives
into the same kept set as the notification. #840 measured that today's
recency-biased 80k budget already evicts one side of nearly every raw
duplicate pair before dedupe would matter (0 of 89 raw pairs across four
real sessions had both sides survive); an unconditional raw-stream stub
would, under that same eviction pattern, sometimes delete a parent's
notification whose subagent copy never made the cut in the first place —
and would unconditionally delete the *only* surviving trace of delegated
work under #840's recommended future design (prompting from parent-origin
events only). Verified against the real session #836 and #839 both cite
(`4a0d9e9b…`): under the actual 80,000-char budget, 0 notifications are
stubbed today (consistent with #840's finding) because the cited pair's
subagent copy doesn't survive the budget; with the budget cap lifted,
1 of 10 raw duplicate pairs in that session both survive AND still exceed
the 0.9 similarity bar after the pre-filter's independent per-event
2000-char truncation (the other 9 exceed that per-event cap and truncate
down far enough to fall below the bar — a conservative miss, never a wrong
stub). The fix is real and correct for sessions/pairs small enough to avoid
both eviction and truncation, and is structurally inert wherever it would
be unsafe to fire.
Implemented in the pre-filter (`preFilterSession`), which runs AFTER
`hashSessionContent` — so **no `contentHash` moves and no re-extraction
wave is triggered** (unlike #830's own folding change, which changed the
raw event stream #602's hash covers).
- **Extract: regression-tested the no-double-extraction guarantee** (#839).
Discovery-mode extraction over a project with a parent + subagent
transcripts now has an explicit end-to-end test proving exactly one
session is processed, that `--session-id agent-<hash>` resolves to the
not-found result rather than an extraction, and that folded subagent
content is attributed only to the parent's session/contentHash. Pins
behavior already true since #830 (`listSessions()` excludes `subagents/`
dirs for both discovery and `--session-id` lookup); nothing tested it
end-to-end before.
### Documentation
- **Measured whether subagent-transcript folding (#830) duplicates the
parent's own summary, and disclosed the one-time re-extraction cost
(#833).** Using the actual reader/pre-filter/prompt-builder code against 3
real sessions on this machine — no LLM calls; `contentHash`,
`preFilterSession`, and `buildExtractPrompt` are deterministic:
- Raw event counts grow 2x-12x once subagent transcripts are folded in
(measured: 1209 -> 14224; 1583 -> 6738, the exact session cited in
#829/#833's "1583 -> 6738" figure; 155 -> 2408). `contentHash` is
computed over that stream, so every previously-extracted session's hash
changes and the next `--since` run re-extracts all of them once, each
with a larger prompt (+1.2% to +5.2% prompt chars across the 3 sessions,
since the 80,000-char pre-filter budget caps how much of the growth
actually reaches the LLM).
- The result is a genuine tradeoff, not a clean win or loss. **Benefit:**
inline `akm remember`/`akm feedback` calls made *by subagents* are
recovered regardless of the budget cap (inline-ref extraction runs on
the raw event stream, not the pre-filtered one) — up to 162 refs
recovered on the largest session measured (was 2 without folding),
fixing #829's "delegated work is never harvested" defect. **Cost:** on
sessions whose raw content is near or under the pre-filter's character
budget, folding evicts a large share of the parent's own kept content to
make room for subagent tool-call trace — parent-origin kept events
dropped 27% and 71% respectively on the two smaller sessions measured.
On the largest session the budget was already saturated by the parent's
own tail, so folding changed nothing there. Duplication is real, not
hypothetical: on the smallest session, one subagent's conclusion appears
twice in the same prompt sent to the extraction LLM — once via its own
folded final message, once via the parent's own record of that
delegated call's result, which independently already captured ~92% of
the same text verbatim.
- A narrowing that drops a subagent transcript's terminal event (its
apparent "final report") to avoid this specific duplication was
considered and rejected: the existing #830 regression fixture has a
subagent transcript whose *only* event is that terminal turn (a single
delegated `akm remember` call) — the same rule would drop the only
content in short single-step delegations, undoing the harvesting #830
added.
- **Decision: keep folding as shipped.** The data does not cleanly favor
removing or narrowing it, and the one narrowing considered would cost
more than it fixes. #829's phantom-session exclusion is unaffected
either way.
- Recorded the fold-vs-link subagent-extraction design determination in
`docs/plans/subagent-extraction-design.md` (#840). Measured four candidates
(fold+dedupe as shipped, link-only, a harvest-without-prompting hybrid, and
chunked map-reduce extraction) on the same real sessions #836 used plus one
added for scale. Headline: the hybrid recovers 100% of #830's inline-ref
harvesting (162/162, 38/38, 1/1, 8/8 across the four sessions) with zero
parent-content eviction (vs 27.5%/28.6% evicted under fold on two of the
four), and #839's dedupe was measured to have zero effect on the actual
LLM prompt on all four sessions (the flagged duplicate content is already
evicted by the recency-biased budget before dedupe would matter). Chunked
extraction was measured at 9x-229x more LLM calls per session on real
data and is not recommended. No behavior changes shipped in this PR.
## [0.9.2-alpha.3] - 2026-08-26
### Fixed
- **Currency in prose no longer retypes an asset as a command** (#824). The
smart-Markdown classifier matched `$1`/`$2`/`$3` with a trailing word
boundary, and that boundary sits between the `2` and the comma in `$2,000` —
so any note quoting a price was indexed as a `command`, its ref moved to
`commands/<dir>/<slug>`, and it left its own namespace. Measured on a real
corpus, 3 of 51 memory documents were affected, and those 3 were exactly the
3 whose bodies matched. `$ARGUMENTS` is unambiguous and keeps its existing
precedence over a directory hint; the numeric placeholders now exclude a
following digit (or a `.`/`,` followed by one), and where they still
disagree with a directory that declares a type, the declaration wins. The
defect is present identically in 0.9.1 — it only became visible once the
0.9.2-alpha.2 retrieval work let mistyped assets surface in results.
### Documentation
- Recorded the 0.9.2 retrieval measurement in
`docs/plans/benchmark-tuning-findings.md` §2e (#825). Retrieval-only probes
with no model in the loop, identical corpora, only the CLI version differing:
LoCoMo zero-hit 75.0% -> 0.0% and evidence recall@5 0.154 -> 0.590;
LongMemEval zero-hit 100% -> 0.0% and recall@5 0.000 -> 1.000.
## [0.9.2-alpha.2] - 2026-08-25
### Fixed
- **Retrieval:** relaxed zero-hit lexical queries centrally, stabilized
relaxed-retrieval quality, and preserved name quality through relaxed
ranking. This is the change measured in §2e above — it is what lifted the
retrieval ceiling that had floored memory-backed evaluation.
- **Indexing:** verify vec completeness before promotion; compare vec IDs as
exact sets; materialize vectors for targeted writes; make nested entry
mutation atomic; reconcile clean before final verification; restore static
embedding imports.
- **Markdown projection:** parse nested links safely, parse destination
phases, and project with stateful delimiters.
- **Sources:** reconcile local bundle updates and report incomplete filesystem
reconciliation.
- **Extract:** keep malformed model output retryable.
### Performance
- Keep targeted embedding selection narrow and preserve targeted vec
degradation.
## [0.9.2-alpha.1] - 2026-08-24
### Breaking changes & migration
- **Node.js 22 is no longer supported by the npm package.** `akm-cli` now
requires Node.js >= 24 to bootstrap its command. A working Bun >= 1.0 remains
optional and preferred for execution after that bootstrap; the standalone
binary remains runtime-free. Upgrade Node before installing or running the
npm package.
- The `inherit_env` option is a breaking removal: new durable-v4 workflow starts reject it.
Replace ambient whole-process inheritance with exact named environment bindings and
`exec.pass_env` names. Pre-v4 stored workflow plans are not executable in
0.9.2; start a new run from current source.
- `improve.strategies.*.processes.triage.judgment` now accepts `true`/`false`
and honors `enabled` on object values; existing `{}` and configured objects
remain enabled. Unknown object keys are now rejected instead of ignored, so
correct typos such as `egnine`, `judgement`, or other unrecognized fields
before upgrading. Use `judgment: false` to disable the tier explicitly.
### Added
- **WP0 — characterization and release contracts:** pinned direct-agent,
command, task, workflow-freeze, model-alias, and harness-lowering behavior;
added cross-entry-point equivalence fixtures and immutable release-surface
gates.
- **WP1 — shared execution contracts:** introduced adapter-rendered source
identity and one resolved-request representation carrying final content,
engine/model/inference selection, authorization, runtime settings, source
hashes, and structured lowering notices.
- **WP2 — layered model maps:** ship a versioned installed `models.json`, merge
an optional user overlay from the config directory, expand known aliases by
engine, pass unknown identifiers through exactly, and provide guarded model
map initialization.
- **WP3 — common cascade and authorization:** resolve far-to-near defaults once,
preserve explicit false/zero/empty values, authorize selected tools before
dispatch, and expose stable field-level provenance without resolved values.
- **WP4 — portable command execution:** added canonical
`akm command run <ref>`, implemented one-pass `$ARGUMENTS` substitution, and
reject unsupported native template constructs before dispatch. Stored
commands have one execution surface; `akm agent --command` is not retained.
- **WP6 — task v3:** added the strict shipped task schema, command/workflow/script
targets, closed shell and working-directory rules, portable schedules, and a
fail-closed task-v2 migrator with no-write preview, per-file status, backups,
validation, and blocked argv-array handling.
- **WP7 — peer workflow sources and durable v4:** compile Markdown and the
approved GitHub-shaped `.yml` subset through source IR v1; new runs atomically
persist immutable v4 targets, guarded source reads, symbolic environments,
and durable dispatch attempts. Pre-v4 stored plans are rejected rather than
maintaining a second runtime.
- **WP8 — release diagnostics:** added zero-write, secret-safe command dry-run
output, stderr-only verbose provenance, offline `selected-model-aliases` and
`configured-engines` health advisories, self-contained migration guidance,
and npm tar/install/link release gates.
### Changed
- **WP5 — engine lowering convergence:** direct command/agent execution,
improve/proposal dispatch, tasks, and workflows now share the resolved-request
boundary and implementation-derived lowerers. Exact model and inference
settings reach registered harnesses; unsupported fields produce safe notices,
while provider capability rejection remains a runtime result.
- Scheduled workflow fires re-read the current peer source and create a fresh
durable-v4 freeze. Resume remains journaled replay, and crash recovery uses
at-least-once attempt semantics with stable reclaim identities.
- Task scheduler synchronization is transactional across supported adapters and
preserves exact ownership, rollback evidence, enabled state, parameters,
timeouts, redaction metadata, and resolver overrides.
### Fixed
- Ordinary managed database opens no longer silently apply released migration
`018-drop-dead-lane-schema`, which drops retired tables and a column. Every
state migration now has an explicit safety classification; migration 002's
row-preserving table rebuild remains automatic, while migration 018 requires
a successful `akm upgrade` and a verified sibling SQLite snapshot created
immediately before its immutable SQL runs. The ledger recheck, WAL-inclusive
snapshot, and 018 transaction now share one writer-exclusion window; fresh
files and randomized backup paths are atomically reserved and inode-verified.
Pre-existing unversioned files—including an absent or empty migration
ledger—are rejected without writes by ordinary opens and snapshotted before
migration 001 by explicit upgrade. That snapshot, ledger initialization, and
migrations 001–002 share one writer-exclusion transaction, preventing an old
schema writer from landing data between the snapshot and the 002 rebuild.
Migration locks now reject phantom `BEGIN IMMEDIATE` results before running
SQL and fail clearly if the transaction disappears after a body. Snapshot
source and target handles are inode-bound, and failed reserved paths are
retained rather than removed by raced cleanup.
## [0.9.1] - 2026-08-18
### Fixed
- Preserve multiline frontmatter descriptions when `akm lint --fix` quotes
colons, recover already-malformed quoted descriptions, and report a fix only
when the file actually changes.
- Skip consolidation promotion proposals whose body already exists in a live
knowledge asset, preventing exact-content duplicates from recurring in the
proposal backlog.
- Derive utility `last_used_at` values only from real user retrieval events
(`search`, `show`, and `curate`) instead of stamping assets with index time.
- Compute the salience-distribution health metric over every positive,
non-missing salience value rather than a top-ranked 100-row slice, and report
the evaluated sample size.
## [0.9.1-beta.2] - 2026-08-17
### Breaking changes & migration
The 0.9.x series carries breaking changes as it works toward the 0.10.x
stabilization line. Every item here is detailed further down; this section is
what an upgrader reads first.
- **A data directory akm cannot READ is now an error, not an empty result.**
Commands that previously returned `hits: []` / `entryCount: 0` / "nothing
eligible" at exit 0 for an index, lockfile or database they lacked permission
on now raise `DATA_DIR_UNREADABLE` (exit 78) naming the path, errno, mode,
owner and running uid. *Affected:* anyone whose data dir is partly unreadable
— most often a `$XDG_DATA_HOME` shared across uids. *Remedy:* fix the
ownership or mode the error names, or point `AKM_DATA_DIR` somewhere this
user owns. The old behaviour was a false success, so a script that treated
exit 0 as "no results" was already being lied to.
- **Lockfile writes refuse to run against an unreadable `akm.lock`.**
`akm bundle add` / `remove` / `update` now fail closed instead of reading the
lock as empty and writing the single incoming entry over the whole record.
*Remedy:* as above. This one prevented real data loss — see Fixed.
- **`akm workflow run` exits 1 when a run ends `blocked`.** Previously 0.
*Affected:* CI steps and scheduled wrappers that branched only on `failed`.
*Remedy:* treat nonzero as "not verified"; resume with
`akm workflow resume <id>`.
- **`akm index --clean` no longer deletes entries whose file it cannot read.**
It keeps and names them. *Affected:* anyone relying on `--clean` to prune
aggressively; it is now conservative where it cannot see.
- **Workflow documents are bounds-checked at authoring time.** `engine:` name
grammar, `retry.max` 0–100, `gate.max_loops` 1–100, `map.concurrency` and
`engines.<name>.concurrency` 1–64, and any `timeout:` ≤ 2 147 483 647 ms are
now enforced by the parser. *Affected:* documents that parsed at 0.9.0 but
could never actually run — the frozen-plan decoder already refused them.
*Remedy:* edit the offending field; the error is now line-anchored.
- **`akm health` no longer emits `secret-file-perms`, and no longer exits 4 for
it.** The check is gone. *Affected:* anything parsing health output for that
check name.
- **Command-target task logs are now redacted.** Output that previously
persisted verbatim may now contain `[REDACTED]`. *Affected:* anything
grepping task logs for values that are now recognised as secrets.
- **Leftover `isolation: worktree` trees are garbage-collected after 7 days.**
*Remedy:* copy anything you want to keep out of a retained worktree within a
week.
### Added
- **`exec` workflow units — run a shell command as a workflow step.** A step
whose `unit:` block declares `exec:` runs a command directly instead of
dispatching to an LLM or an agent, so deterministic work (test suites,
builds, lint, scripts) no longer costs a model dispatch, its latency, its
tokens, or its nondeterminism.
```yaml
- id: test
unit:
exec:
command: ["bun", "run", "test:unit"]
pass_env: [CARGO_HOME] # optional: widen the default env allowlist
timeout: "10m"
retry: { max: 1, on: [timeout] }
```
- **`command:` is an argv array; there is no shell-string spelling.** The
child is spawned directly, so `;`, `|`, `&&`, `$(…)` and `*` inside an
argument are inert literal bytes — the quoting/injection class is
structurally absent, not defended against. Write `["bash", "-lc", "…"]`
when a pipeline is genuinely wanted, and own that choice in the diff.
- **An exec unit names no engine.** It rejects `engine`/`model`/`llm`, spends
no tokens, and a workflow made only of exec steps runs on an install with
no engine configured at all.
- **Everything else about a unit still applies:** `timeout`, `retry`,
`on_error`, `output`, `env`, `isolation: worktree`, `map` fan-out and its
concurrency limits, the unit journal, budget accounting, and replay/reuse
(a completed exec unit is never re-run on resume).
- **Output rule:** stdout is the promoted artifact with trailing newlines
stripped (like shell `$(…)`); with an `output:` schema on the unit, stdout
must be exactly one JSON value, strictly parsed and validated. stderr is a
diagnostic channel only. A schema miss is *not* re-prompted — a fixed argv
cannot answer feedback, but re-running it could deploy twice.
- **Exit codes:** non-zero → `non_zero_exit`, wall-clock expiry → `timeout`,
cancellation → `aborted`, failure to start → `spawn_failed`. Those are
pre-existing `retry.on` reasons. With the default `on_error: fail`, a
non-zero exit fails the step and the run, which is what makes a `test` step
a gate.
- **A partial capture is never promoted as the artifact.** Exiting 0 does not
prove stdout was read to the end: a pipe can error, and a background
descendant holding the stdout handle open after the command leader exits
keeps the pipe alive past the drain deadline. Both leave a *prefix* of the
real output, so the unit fails — with its own reason,
`exec_capture_incomplete`, which is deliberately **not** a `retry.on` value.
The command already ran; re-dispatching identical argv to fix a capture
problem would run its side effects a second time.
- **Everything the command can spend is bounded — without inventing failures.**
Alongside the wall-clock timeout, akm bounds the memory it spends on the
command's behalf and the environment it can hand the command. Both bounds are
built so that they only ever *explain* a failure that was going to happen
anyway; neither fails a run that would otherwise have succeeded.
**Retained output: 8 MiB per stream, drain-and-discard.** akm keeps at most
8 MiB of stdout and 8 MiB of stderr. Past the cap it keeps *reading* the pipe
and throws the extra bytes away, so the child never blocks on backpressure:
the command runs to completion and its real exit code decides the unit. A
verbose-but-passing test suite is not failed over its log volume. What
overflow costs is completeness of the artifact, and that is never hidden —
a step with **no** `output:` schema succeeds and its artifact is the retained
head with a `__akm_exec_output_truncated__` block appended (naming bytes
written vs bytes retained), so truncated data can never be mistaken for
complete data by `steps.<id>.output`, a gate judge, or a human. A step **with**
an `output:` schema still fails `exec_output_limit`: stdout must parse as
exactly one JSON value, a truncated prefix cannot, and promoting it would
corrupt every downstream reference to the typed artifact.
**Context environment: this platform's ceiling, not the smallest one.** The
engine-authored `AKM_*` context is capped at **96 KiB per variable / 128 KiB
total** on Linux, macOS and BSD, and at **32 767 bytes per variable / 64 000
bytes total** on Windows. The numbers cite their sources: Linux's
`MAX_ARG_STRLEN` (`32 * PAGE_SIZE` = 131 072 bytes per `argv`/`environ`
string), macOS's 256 KiB `ARG_MAX` over argv + environ combined, and Win32
`SetEnvironmentVariable`'s 32 767-character per-variable limit. Crossing the
bound fails `exec_context_too_large` *before* the spawn, with an error naming
the variable, its size, this platform's limit and where that limit comes from
— replacing a bare `E2BIG` from the spawn syscall that named neither the
variable nor the data behind it. Converting that inevitable failure into an
actionable one is the check's *only* job, so it uses the ceiling of the
platform the run is on: previously it applied Windows' limit everywhere and
refused spawns Linux and macOS would have accepted. Workflows that must also
run on Windows should stay under the smaller bound — that is documented
guidance now, not something a Linux host enforces.
`exec_output_limit` and `exec_context_too_large` keep their meanings and
their place outside the `retry.on` vocabulary, alongside `exec_cwd_escape`:
each is deterministic, so re-dispatching could only spend the budget again.
`PROGRAM_RETRY_REASONS` is unchanged.
- **A failing command's stderr survives to a durable surface.** The unit
journal now keeps each failed unit's redacted diagnostic (clipped to 2000
characters), and the step summary carries the first failure's. For an exec
unit that is the difference between `akm workflow status --units` saying
`non_zero_exit` and it saying *why* — a command that explains itself only on
stderr with empty stdout previously left no diagnostic anywhere durable.
This is an output surface only: the unit input hash is computed from
plan-frozen inputs, so no completed unit re-dispatches because of it.
- **The child's environment is an ALLOWLIST, not an inheritance.** The
command starts from an empty environment and receives `PATH`, `HOME`, the
identity/locale/temp/terminal variables, the Windows process-creation
essentials (`SystemRoot`, `SystemDrive`, `WINDIR`, `COMSPEC`, `PATHEXT`)
and the Windows home/config roots, plus `AKM_EVENT_SOURCE` — then the
unit's `env:` bindings, then the `AKM_*` context. `exec.pass_env: [NAME…]`
adds a few more names (for a per-machine toolchain variable like
`CARGO_HOME`, which a committed `env:` asset cannot express);
`exec.inherit_env: true` opts all the way back into akm's whole
environment. Both keys live inside `exec:` because the unit-level `env:`
key already means "env asset binding refs", and both are dispatch-
significant, so both are in the input hash.
This is not a claim to stop a determined attacker — a command that runs at
all can read the same credentials off disk. It bounds **accidental**
exposure (the invoking shell or CI job routinely exports tokens for
unrelated services), makes the environment surface **explicit and
reviewable**, and **matches the convention akm already applies** to
agent-harness children (`profile.envPassthrough`), which now share one
mechanism with exec units instead of two.
- **Security:** commands run inside the existing workflow trust model.
Secrets come from `env:` bindings by NAME — the frozen plan and the replay
hash carry only ref names, and resolved values are scrubbed from stdout,
stderr, and failure diagnostics by the same redaction contract every other
dispatch uses, before anything is journaled. `cwd:` is relative and
`..`-free, re-checked against the resolved base (symlinks included) before
spawning.
- **Cancellation is real:** the child is spawned in its own process group and
gets a SIGTERM→SIGKILL ladder on timeout or abort, so `--timeout` / Ctrl-C
stop a running command without orphaning its children.
- **No replay churn:** the exec spec was added to the unit input-hash preimage
as a key present only on exec units, so `hashVersion` stays 4 and every
previously-frozen llm/agent/sdk unit hashes byte-identically — runs already
in flight neither re-dispatch nor diverge. The env-scope keys are inside
that same spec and are frozen only in their non-default form (`inherit_env`
only when `true`, `pass_env` only when non-empty), so an exec unit that says
nothing about its environment hashes byte-identically too.
See [Workflow Schema: Exec (shell) units](docs/reference/workflow-schema.md#exec-shell-units)
and the worked example in
[Author's Guide](https://github.com/itlackey/akm/blob/main/docs/guides/author-workflows.md#deterministic-steps-run-a-command-gate-on-it).
### Changed
- **`akm workflow run` now exits non-zero when the run ends `blocked`.** A
verification judge that throws, cannot be resolved, or returns a malformed
verdict stops the run `blocked` — unverified, and resumable with `akm
workflow resume <id>`. That previously exited 0, so a CI step or scheduled
wrapper read an unverified run as a passing one. It now exits 1, matching
`failed` and gate rejections, and matching how the scheduled-task path
already reported it.
- **Workflow dispatch bounds are enforced at authoring time, not only by the
frozen-plan decoder.** `engine:` names must match the decoder's own grammar
(lowercase dash-separated letters/digits, starting with a letter, ≤63
chars); `retry.max` is 0–100; `gate.max_loops` is 1–100; `map.concurrency`
and `engines.<name>.concurrency` are 1–64; any `timeout:` must resolve to at
most 2 147 483 647 ms (~24.8 days, `setTimeout`'s 32-bit ceiling). Every one
of these was already refused by the frozen-plan decoder, so such a document
could never actually run — but it *parsed*, so `akm lint`, `akm workflow
show` and `akm workflow create` all reported it clean and the failure arrived
at `workflow run` as an unlocated "Invalid frozen workflow plan". The error is
now line-anchored at parse time. Nothing changes for a document already
inside the bounds.
- **`akm lint` gained an advisory channel.** The result envelope carries
`warnings: LintIssue[]` alongside `fixed`/`flagged`, `summary` gains a
`warnings` count, and text output prints a `warnings` section. Advisories
never route into `flagged`, so `--fail-on-flagged` cannot fail a run over
one. Workflow compile advisories (`workflow-warning`) are surfaced for the
first time — a step with no `output:` schema, a `params.<name>` reference to
an undeclared param, a `gate.max_loops` above 1 on an `exec` step — so a
bundle that linted clean at 0.9.0 may now report warnings without becoming a
failure. Findings that know a location carry `line` in `--format json` and
render as `file:line` in text. A new `lint-failed` code reports a file the
sweep reached but could not finish.
- **Leftover `isolation: worktree` trees are now garbage-collected.** A run
that crashed, or one whose worktree was retained after a dirty unit, used to
leave its tree under the worktrees root forever. akm now opportunistically
removes such trees once they are 7 days old, confined to the worktrees root,
symlinks skipped, containment re-checked. A worktree still in use is never
collected: every live tree carries a liveness marker (pid, host, resolved
path) in git's administrative directory for it, and the sweep skips a
candidate whose holder is still running here.
- **The workflow JSON Schema subset now enforces `allOf`/`anyOf`/`oneOf`/`not`.**
A step `output:` or `params:` schema may use the combinators, and the runtime
now evaluates them. Previously it ignored them: a schema using one was
accepted and simply constrained less than it appeared to. Evaluation stays
bounded — nesting is capped at 64 levels and one validation at 100 000 checks,
and exhausting either is reported as an error rather than a truncated pass.
**This one reaches runs already in flight.** The combinators live in the
frozen plan, which the decoder still accepts unchanged, so a run frozen before
the upgrade is resumed against the *new* evaluation: an artifact that passed
when the combinators were ignored can fail validation now. There is no
`irVersion` bump to gate it, because the plan bytes did not change — only what
they mean. Runs whose schemas use no combinators are unaffected, as is every
step already completed.
`pattern` is **not** part of the subset. It is a recognized-but-unsupported
keyword like `format` or `const`: using one is a loud, line-anchored authoring
error naming the keyword, so no schema silently fails to constrain what it
looks like it constrains. Enforcing it would mean screening every author
regex for catastrophic backtracking before the match — and any such screen
also refuses regexes authors legitimately write (the usual hand-rolled email
pattern among them), which is authoring friction with no workflow asking for
it. Where a string's shape matters, `enum` lists the allowed values,
`minLength`/`maxLength` bound the size, and a step's `### gate` rubric can
check a shape and explain a mismatch. The `format` hint now points at `enum`
rather than at `pattern`.
**Existing workflows that use one of these keywords must be edited before
they load again.** They previously parsed — the keyword was silently
non-constraining — so a workflow carrying `format: date-time` or `pattern:`
ran fine and now fails to parse for every caller: `workflow run`, `workflow
show`, `workflow create`, and `akm lint`. The quietest surface is `akm index`,
which skips an asset it cannot parse with a scan warning, so the workflow
simply stops appearing in the stash index. A run already frozen from such an
asset still resumes — the frozen-plan decoder does not re-screen keywords —
so resuming works while re-creating the same workflow errors until it is
edited.
- **A document-level `defaults.llm` is now rejected at freeze when any step
resolves onto an agent engine**, naming the step and the engine. The guard
existed before but was unreachable: overrides were computed only for `llm`
engines, so `defaults.llm` on a document with an agent step was silently
DROPPED for that step — the run proceeded with the author's sampling settings
quietly discarded. Failing loudly is the point, but it means a document that
mixes `defaults.llm` with any agent-engine step no longer freezes.
There is no per-step opt-out: `llm: {}` is a no-op, `llm: null` is a parse
error, and the layer merge is additive. Move the `llm:` block from
`defaults:` onto the `unit:` of each LLM step that wants it.
- **A scheduled workflow task now gets a 6-hour whole-run timeout by default.**
This applies to task files that declare no `timeoutMs:` — which is every task
file written before this release, since the key was previously rejected on
workflow targets. An unattended run that legitimately takes longer will be
aborted and the attempt reported failed on every firing until the task is
edited. `timeoutMs: null` opts out entirely, and any number overrides the
default. The abort itself is graceful: it lands at a step boundary, the
journal and lease are kept, and the run stays resumable with
`akm workflow resume <id>` — which the failure message names.
- **Workflow `map` steps now fan out in parallel by default.** A `map` step
that declares no `concurrency:` freezes a width of **4** instead of 1, and an
LLM engine that declares no `engines.<name>.concurrency` freezes **4** for a
remote endpoint (loopback endpoints stay at **1** — a local model server holds
one loaded model and returns HTTP 500 under concurrent inference). Both
defaults previously froze 1, which made every fan-out serial unless the author
opted in at two independent layers, and left `workflow.maxConcurrency` and the
host CPU cap binding on nothing.
This is a behavior change on a patch release, so every escape hatch is
explicit:
- `map.concurrency: 1` on a step is honored exactly as before — an authored
`1` is kept distinct from an unset field and always wins.
- New config key **`workflow.defaultMapConcurrency`** sets the default for
every workflow on the machine. `akm config set workflow.defaultMapConcurrency 1`
restores the pre-0.9.1 serial default wholesale.
- `engines.<name>.concurrency` pins any engine's own limit (and is now clamped
to `1..64` at freeze time instead of freezing a plan the decoder would then
refuse to load).
- **Runs already in flight are unaffected.** Both values are frozen into
`plan_json` when a run starts and the frozen-plan decoder requires them, so
a resumed run keeps the widths it began with. The new defaults apply only to
runs started after the upgrade.
The effective width remains the minimum of the step's `concurrency`, the run's
frozen `workflow.maxConcurrency`, the selected engine's concurrency, and the
current host's CPU cap.
- **`--max-steps` now counts steps, not engine-loop iterations.** The budget is
spent by the DISTINCT spine steps that finished — completed, failed, or
gate-rejected with the loop budget spent. It was previously spent by entries
in the `executed` report,
which gains one per gate-loop iteration and one per route-skip, so
`--max-steps 3` against a step with `gate.max_loops: 3` could stop after a
single step had finished, and an unselected branch target consumed budget for
work that was never dispatched. Three steps now means three steps, which is
what the flag has always said (`Stop after executing this many steps`). A step
the invocation left unfinished — an abort, a judge outage — still consumes
nothing, because the work is still owed. The same accounting is what a
`--max-retries` reopen subtracts, so loops and skips no longer shrink a
retry's remaining budget either, and `maxSteps:` in a workflow task file is
the same knob and moves with it. The count is now reported: `akm workflow run`
carries a `stepsProcessed` field alongside `executed`, so the number the
budget is spent on is visible rather than inferred from a list that counts
something else.
**This loosens the dispatch exposure of one invocation, and the loosening is
cumulative across steps.** A step's whole bounded gate loop now costs one step
instead of one per iteration, so the rounds a single `akm workflow run` can
dispatch go from roughly `N + max_loops` to `N × max_loops`.
What did **not** change is what the flag bounds within one step. `--max-steps`
was never a cap on total dispatch rounds on either version: the budget is
tested only BETWEEN steps, so a single step's gate loop could always run out
its full `gate.max_loops` no matter how little budget was left. The per-step
ceiling is `gate.max_loops` (1–100); the whole-run ceilings are
`budget.max_units` and `budget.max_tokens`, which are seeded from the unit
journal and hold across resumes.
### Fixed
- **Upgrading Node after installing akm now explains itself.** A native binding
is built for the Node ABI present at install time, so upgrading Node major
versions afterwards leaves akm reporting a bare Node internals message —
*"The module … was compiled against a different Node.js version"*, or on a
second attempt the even less helpful *"Module did not self-register"*. akm now
recognises that failure and answers with the one command that fixes it
(`npm rebuild better-sqlite3`), names the ABI actually running, and says
plainly that this is not a broken install.
The diagnostic had to move to do this. It wrapped the `require`, but
`require("better-sqlite3")` **succeeds** against a mismatched binding — the
package resolves its `.node` file lazily — so the error lands at
`new Database(...)` and the loader's handler never saw it. The previous text
telling the user to look for a version mismatch "in the error below" was
unreachable. Found by installing the published build under Node 22 and running
it under Node 24.
- **akm's Node fallback no longer aborts at teardown on Node 24.** On Node
24.19.0 and later, any command that opened a database could intermittently
die with `node::RemoveEnvironmentCleanupHook … Assertion (env) != nullptr`
and exit 134 — after its work was done, so the failure looked random and
depended on garbage-collection timing.
The cause was upstream and nothing to do with akm's own code.
`better-sqlite3` ships one prebuilt binary per Node ABI and falls back to
`node-gyp rebuild` when none matches, and the 11.x line publishes no prebuild
for Node 24 — so installing it there silently compiled the driver from source.
Node 24.19.0 had just changed the public `node_object_wrap.h` so that
`ObjectWrap`'s constructor and destructor register and unregister an
environment cleanup hook; a binding compiled against those headers
unregisters the hook after the environment is already gone, and aborts from
V8's teardown path. Only the Node 24 line was affected, and only from that
release on.
akm now pins `better-sqlite3` to `12.11.1`, which publishes prebuilt binaries
for Node 22, 24, 25 and 26 — so no Node version akm supports compiles the
driver at all. This affected real installs, not just CI: an npm user on Node
24 LTS was getting the same crash-prone from-source build.
The Node-fallback CI job now installs the exact spec `package.json` declares
instead of carrying a range of its own, and both that job and the smoke
script fail loudly on a native crash banner — previously an abort was
reported only as missing output, and the one step that tolerates a non-zero
exit would not have failed at all.
- **A website source interrupted mid-refresh no longer loses the snapshot it
already had.** A refresh deleted the whole mirror and then rebuilt it page by
page, so a process killed inside that loop left an empty or partial directory
with the old content already gone — and the freshness marker still looked
recent, so the next `sync()` served the wreckage instead of rebuilding. The
new snapshot is built in a dot-prefixed sibling directory and swapped in with
renames: an interrupted refresh leaves the PREVIOUS complete snapshot
untouched. Abandoned staging directories are dot-prefixed so the indexer's
walk skips them, and are swept by the next refresh once an hour old.
- **A resumed workflow run no longer re-dispatches work that already ran.** The
single-driver guard was checked at the run level, so a run whose lease had
been stolen left its still-owned unit row `running` and discarded the real
outcome — the resume then re-dispatched a unit that had already executed its
side effects and already spent its tokens. The guard now lives on the row, so
a stale driver's finish matches nothing and a live outcome is never dropped.
- **Lowering `retry.max` no longer re-runs finished work.** The completed-attempt
scan matched only attempts the *current* retry policy could have produced, so
reducing `retry.max` between invocations hid a journaled `~rN` row and the
unit was dispatched again. It now matches any journaled attempt of the unit.
- **A scheduled `command` task no longer writes your secrets into its log.**
Task logs were scrubbed for credential *shapes* — `Bearer …`, `sk-…`, webhook
URLs — but only prompt- and workflow-target runs also scrubbed exact secret
*values*. A command that echoed a configured secret shaped like nothing in
particular persisted it verbatim into both the run `.log` and `logs.db`, for
the whole retention window. Exact-value redaction now runs in the one sink all
three target kinds share, so every task kind is covered.
akm treats a value as secret when your config declares it
(`engines.<name>.apiKey`, `embedding.apiKey`, and the
`AKM_ENGINE_<NAME>_API_KEY` / `AKM_LLM_API_KEY` / `AKM_EMBED_API_KEY`
recipes), and infers others from the variable name (`*_TOKEN`, `*_SECRET`,
`*_API_KEY`, `*_PASSWORD`, …) when the value is at least 8 characters. The
floor applies only to the *guesses*: a declared secret is redacted at any
length. Redaction replaces substrings, so an over-eager rule does real damage
— treating every non-allowlisted variable in the inherited environment as a
secret classified 127 of 132 variables as credentials, 25 of them one
character long, and turned `3 tests passed, 0 failed` into `[REDACTED] tests
passed, [REDACTED] failed`.
For a secret exported under a name none of those rules recognise, any task may
name it:
```yaml
command: ./deploy.sh
redact: [ACME_DEPLOY_TOKEN] # NAMES, never values — max 32
```
Names only, and a name that is unset at run time contributes nothing. A
literal secret in a task file would leak far more widely than the redaction
closes: task files are indexed into the search database, can be sent to an
embedding provider, are printed verbatim by `akm show`, and ship inside
bundles over git and npm — the same rule exec units' `pass_env:` follows.
- **Redacting a log can no longer explode it.** Exact-value redaction took a
fast path that rewrote the text once per secret, over an accumulator it had
already rewritten — so a secret containing any of the letters in `[REDACTED]`
matched the tokens it had just inserted, and the output grew geometrically.
Fifty characters against six single-letter values produced 32,450 characters,
a 649x blowup reachable from ordinary command output. Matches are now found
against the original text and the result emitted once. Overlapping matches
merge into a single `[REDACTED]`, and the two redaction paths no longer
disagree about output shape depending on whether the text happened to contain
a `%`.
- **Redacting a structured value no longer drops fields.** When two distinct
object keys redacted to the same string, the rebuilt object silently kept only
the last — `{a, b, ab}` came back with two entries, one of them simply gone
rather than redacted. Colliding keys are now suffixed, so the value survives
with its key still hidden. This affected persisted improve results and
journaled workflow outcomes.
- **`akm improve` auto-sync now commits exactly the files the run wrote.**
Every akm write path records the file it mutated into a run-scoped
write-provenance journal, and the end-of-run (and crash-path) commit stages
precisely those paths. A managed-directory file someone else edits while a
long run is in flight is left dirty for its author instead of being swept into
akm's commit, and a file that was already dirty when the run started and was
then rewritten by the run is now committed instead of being silently skipped.
Deletions are journaled like writes, so a path written and then reverted or
purged stages its final on-disk state — or produces no commit at all. The run
reports its journal as `writtenPaths` on the improve result, and the
`stash_synced` event gains `attributed` / `unattributed` counts. `akm sync` /
`akm push`, which supply no explicit path list, keep the managed-pathspec
fallback unchanged. (#652)
- **`akm lint` no longer reports a clean scan for a task file that cannot run.**
A `tasks/*.yml` whose YAML does not parse (bad indentation, an unterminated
quote, tab characters) produced `flagged: 0`: every task reader collapsed a
parse failure onto an empty mapping, and every task rule short-circuits on
one — so a CI gate on `--fail-on-flagged` passed a task that would die at
schedule time. The parse failure is now its own `invalid-task-yaml` finding.
A `tasks/*.yaml` file — a spelling akm never indexes and never schedules —
used to be skipped by the directory walk entirely; it is now collected and
flagged for the extension, with the rename in the message. Fixed on all three
task-lint surfaces (the CLI sweep, the `akm` adapter's `validate`, and the
`akm-task` format adapter) from one shared parse, so they cannot disagree.
- **`akm lint --fix` refuses a bundle configured `writable: false`.** Every
other mutating command checks the flag before touching disk; `--fix` wrote
directly and never consulted it, so it rewrote frontmatter in a bundle
explicitly marked read-only. It is now a usage error raised before any file
is modified.
- **A `--fix` write failure no longer aborts the run and hides the fixes that
already landed.** One unwritable file (read-only file, full disk) threw
straight out of `akm lint`, so the caller got an exception instead of a
result — with no way to tell which earlier files in the same sweep had
already been rewritten. A failed fix is now reported in-band on its own file
as `fixed: "failed"`, and the sweep continues through the rest of the bundle.
- **`akm lint --type` says so when it does nothing.** For a non-akm bundle the
adapter validates the whole bundle regardless of `--type`, so scoping a run
silently had no effect. It now warns, naming the flag and the adapter.
Findings are unchanged (full-bundle validation was already a superset), and
it is deliberately a warning, not an error, so scripts passing one `--type`
across mixed-adapter bundle sets keep working.
- **`missing-skill-md` fires again for an `agent-skills` package with no
manifest.** The check iterated pending CHANGES, and a change is always a
file — so a package directory holding resources but no `SKILL.md`
contributed nothing it could see, and a skills pack with a broken package
linted clean. It is now a real directory pass over the bundle root. Related:
under opencode's supported singular `skill/` alias the same package went
unflagged while an identical one under `skills/` was caught; both spellings
are now checked.
- **An index akm cannot read no longer reports as an index that does not
exist.** `fs.existsSync()` answers `false` for a permission error exactly as
it does for a missing file, and the read path used it as its "is there an
index?" gate — so `akm search` and `akm curate` returned no hits at **exit 0**
with the tip *"No search index available. Run 'akm index' to build one."* for
a populated index sitting right there on disk, and `akm info` reported
`entryCount: 0, vecAvailable: false` for the same index. Nothing said
"permission". A consuming agent had no way to tell that from a genuine empty
result, so it relayed the false answer to its user with an explanation it had
invented.
Absent and inaccessible are now distinct everywhere it matters:
- `search` / `curate` / the index openers raise a `ConfigError`
(`DATA_DIR_UNREADABLE`, exit 78) naming the path, the errno, the mode and
owner, and the uid actually running — instead of an empty success.
- `akm info` reports an `indexStats.unreadable` diagnostic rather than zeros
that look healthy. The field is absent on every healthy run.
- `akm health` now *diagnoses* an unreadable `state.db` as a failing
`state-db-readable` check instead of dying on the open before it could
report anything — it is the command you reach for when this happens.
- `probeLock` returns a distinct `inaccessible` state instead of classifying a
permission error as a stale lock. "I cannot read this lock" and "the holder
is dead" are opposite facts, and `akm improve` now stops rather than
reclaiming a lease that may be genuinely held.
The same conflation existed on the write paths, where the consequence was
worse than a wrong answer:
- **An unreadable `akm.lock` could destroy every bundle record in it.** The
lockfile read that exists specifically so a write path never sees `[]`
returned `[]` for *any* read failure, permission errors included — and
every lockfile write is read-modify-write, so the next atomic write
replaced the operator's whole lock record with the single entry being
added. Verified by probe: the symlink was replaced by a regular file
holding one entry. Lockfile writes now refuse to run against a lock they
cannot read.
- **The migration recovery gate failed open.** "I cannot tell whether a
recovery is pending" cleared the gate exactly as "no recovery is pending"
did, so akm would open the canonical databases on top of a half-applied
migration. It now fails closed.
- **`akm index --clean` deleted rows for files it merely could not look at**,
and reported the deletions as a clean success. Unreadable entries are now
kept and named.
- `indexWrittenAssets` returned `true` — "the index is as you expect" — for
an index it could not open, on the strength of which `acceptProposal`
advanced its journal to `index-finalized`.
- `akm improve` eligibility, `akm feedback`, `akm bundle list` and the graph
loaders each turned a permission fault into an empty result, a zero count,
or the advice to "Run `akm index` first".
- **akm no longer manages permissions on your data directory, its databases, or
your task logs — and no longer reports on them either.** Those take your
process umask; their mode is yours to set, and `chmod`/`umask` are your
levers.
This is scoped, not blanket: akm still creates a handful of files at
restrictive modes *at creation time*, as it always has — `env` and `secret`
assets and config backups at `0600`, their directories at `0700`, and the
scheduler invocation files it writes for cron/launchd/schtasks. Those are
files akm authors itself and whose contents are credentials; setting their
mode when creating them is not the same as re-permissioning a directory you
already owned.
Two 0.9.1 pre-release changes are gone. The first chmodded akm's databases
and task logs to `0600`/`0700` on every open — reverted because
re-permissioning a directory akm did not create silently broke installs that
share `$XDG_DATA_HOME` between two uids (agent sandboxes, containers, service
accounts). If a pre-release tightened your data directory, `chmod` it back.
The second was an `akm health` advisory (`secret-file-perms`) that reported
group/other-readable `env`, `secrets` and `config-backups` paths — removed
too: it is meaningless on Windows, and nagging about modes akm does not set
is not health reporting. `akm health` no longer emits this check, and no
longer exits `4` on account of it.
- **`timeout: none` on an exec unit is genuinely unbounded again.** The
stream-drain safety net — a one-hour bound on a pipe still being read after
the child is gone — was armed when capture STARTED, so a command that ran
past an hour had its output reader cancelled mid-run and was then failed for
an incomplete capture even though it exited 0. It is now armed from the
child's exit, which is the only window it was ever meant to bound.
- **A bounded exec unit no longer waits out its whole `timeout` after the
command has already exited.** The drain deadline for a unit WITH a wall budget
ran from the moment capture started — budget plus a 2 s grace — so a command
that exited in milliseconds while a background descendant held a pipe open
kept the unit, and with it a fan-out slot, occupied for the entire declared
timeout before reporting. It now runs from the moment nothing living owns the
pipe: the child's exit, or (for a child that outlived its own kill ladder) the
budget's expiry, plus the same 2 s grace. A command that really does spend its
whole budget sees the identical ceiling it saw before; only the case that used
to stall stopped stalling.
- **A stderr drain that never finished no longer fails an exec unit whose
command succeeded.** `exec_capture_incomplete` was raised when EITHER pipe
failed to drain, so a command that exited 0 with its stdout captured whole was
failed — and a valid artifact thrown away — because a background descendant
was still holding STDERR open. stderr is a diagnostic channel that never
contributes to the artifact, so only an incomplete STDOUT capture fails the
unit now; an incomplete stderr drain is reported on the warn stream instead,
naming the unit and warning that any stderr shown for it may be missing its
tail.
- **A step artifact larger than 1 MiB no longer breaks the next step of the run
that produced it.** Step evidence is clipped to bound one SQLite row, and the
engine rebuilt each downstream `steps.<id>.output` scope by re-reading those
rows — so a large artifact (an exec unit's stdout retains up to 8 MiB) reached
the very next step as a truncation marker: a path reference failed with a
missing-property error that never mentioned truncation, and a whole-value
reference silently handed the marker to the unit as its input. The run now
carries its own complete values forward; the row stays clipped for resume,
where a reference into a clipped artifact fails by name.
- **A workflow run that completed is no longer reported as timed out.** The
deadline is observed between steps, so one landing during a run's final
bookkeeping set the timed-out flag on a run that then finished. On a scheduled
workflow task that recorded the attempt as failed, with a hint to resume a run
that had nothing left to resume; under `akm workflow run --timeout` it
rendered a `timedOut` marker on a `completed` run and exited nonzero. Both
surfaces now drop the marker once the run reached `completed` — a deadline
that lands with nothing left to abort has nothing to report.
- **A rejected gate on an `exec` step no longer re-runs the command.** A gate
loop earns its re-dispatch by handing the judge's feedback to a unit that can
answer it. An exec unit cannot: its argv is frozen and never interpolated, and
the exec context environment carries no feedback variable — so the loop could
only re-run the byte-identical command, performing a deploy, a publish, or a
migration a second time for a verdict that could not change. The gate still
EVALUATES on an exec step and can still fail it: a rejection is final on the
first evaluation, carrying the judge's missing criteria and feedback exactly
as in the one-shot case. What an author sees is
that `gate.max_loops` is capped at 1 on a step whose unit is `exec:` — not an
authoring error, and no change at all to an engine step, where a declared
`max_loops` is still honored in full. This is the same reasoning that already
makes an exec unit's `output:` schema miss fail without a corrective
re-dispatch.
- **The stale-worktree sweep no longer collects a worktree that is still in
use.** The opportunistic age-based GC of leftover `isolation: worktree` trees
judged staleness from the worktree root's mtime, which a unit writing only
inside subdirectories never touches — so another akm process minting a
worktree could delete the tree a long-running unit was working in. Every live
worktree now carries a liveness marker (pid, host, resolved path) in git's own
administrative directory for it, and the sweep skips a candidate whose holder
is still running here. A marker from a dead pid, from another host, or for a
different path is not liveness: crashed runs and retained dirty trees stay
collectible, which is what the sweep exists for.
- On Windows, an agent CLI, gate judge, or prompt task was spawned into an
environment the loader cannot start from: the shared passthrough allowlist
named no `SystemRoot`/`SystemDrive`/`WINDIR`, and without `PATHEXT` a
`bin: "bun"` profile was unresolvable — while an exec unit on the same host
worked, because its own allowlist names them. Those variables are now added
when any allowlisted child environment is built.
- Scheduler PATH repair skipped itself in the environments it exists for. It
decided a PATH was "interactive" by testing whether any entry began with the
user's home directory as a *string*, so a home of `/` — system crontab,
launchd, service accounts — matched every absolute entry, and a sibling home
(`/home/alice/bin` against `/home/al`) matched too.
- A directory whose name merely begins with two dots (`..data`) was treated as
a path escape. For a workflow exec `cwd` that meant the parser and the frozen
plan accepted a spelling the executor then failed as tampering, with a reason
no retry can clear.
- `appendEvent` resolved the state.db path outside its own error handling, and
did so even when the caller supplied an open connection — so a caller holding
a perfectly good handle could take a configuration error from a function whose
contract is that it never propagates one.
- Workflow freeze attributed per-step `engine`/`model`/`timeout`/`llm` overrides
by matching the compiled draft step list against the source document
**positionally**. That was correct only because compilation happens to be 1:1
and order-preserving; a compile pass that filtered or reordered steps would
have silently applied one step's overrides to another. Attribution is now
keyed by `stepId`.
### Security
- **A gate judge's response is now scrubbed before it is journaled.** The judge
verdict is written into the gate row's `result_json`, and a judge failure's
message becomes the blocked step's notes — but the judge dispatch bypassed
the redaction contract every unit dispatch goes through, so a judge that
echoed a credential out of the promoted artifact persisted it unredacted into
the workflow journal. Both judge paths (agent and llm) now wrap their
dispatch in the same scrub, with the sensitive-value set collected per
dispatch rather than at build time, so a credential rotated between the two
reads is still caught. The dispatch also carries the real run/step/gate ids
instead of a synthetic `"gate"` placeholder, so a gate row and its telemetry
describe the same thing.
- **Command-target task logs are scrubbed of exact secret values**, closing the
last redaction lane — see the `### Fixed` entry above for the full account.
## [0.9.0] - 2026-08-06
0.9.0 is the format-neutral **bundle / adapter** refactor: it replaces the flat
asset-type registry with per-format adapters, adopts one canonical ref grammar,
and consolidates the durable databases and config. This section consolidates and
supersedes the `0.9.0-rc.*` / `0.9.0-beta.*` development entries below.
### Breaking changes & migration
- **Installed non-akm bundles reclassify on your next `akm index`.** The
indexer now dispatches each installed bundle's *detected* adapter (Claude
tool dirs, LLM wikis, website snapshots, agent-skills packs, …) instead of
recognizing everything with the akm-stash adapter. Entries in such bundles
change type and ref spelling to the owning adapter's own scheme the first
time you reindex. No action needed — the index is a regenerable cache and
rebuilds itself — but searches/saved refs into those bundles may resolve to
the new spellings afterwards.
- **Ref grammar cutover — `type:name` → `[bundle//]conceptId`.** Every ref is
now a subdir-qualified concept id inside its bundle (`skills/code-review`,
`memories/vpn-note`, `env/prod`), optionally prefixed with a `bundle//`
installation slug and suffixed with `#fragment`. Durable state stores the
fully-qualified `bundle//conceptId`; the short bundle-omitted form is accepted
input only (resolved against `defaultBundle`, then installation-priority
order). The pre-0.9.0 `[origin//]type:name` grammar is removed — there is no
compatibility parser; the frozen migrator in `scripts/akm-migrate/migrate/`
is the only place it survives.
- **Explicit, crash-resumable cutover (`akm migrate apply`).** The
migrator re-keys all durable state to the new spelling, folds the former
`workflow.db` into `state.db` (four databases down to three: `state.db` /
`index.db` / a separate `logs.db`), and migrates config from the flat
`stashDir` / `sources` / `installed` / `wikiName` keys to `bundles` /
`defaultBundle`. A semantically verified, installation-scoped **backup manifest v4**
(covering the pre-rescue `index.db`) is taken before mutation. One phase-free
incomplete sentinel retains that backup and target; expected orphans are
quarantined, integrity failures fail closed, and the whole cutover reruns
idempotently after a crash. Normal commands refuse an
un-migrated or divergent durable schema rather than migrating as a side effect.
The retired `stashDir` / `sources` / `installed` keys are **hard-rejected** by
the 0.9.0 config schema whenever present (the error names `akm migrate apply`);
registry-installed bundles keep only their desired locator (`git`/`npm` +
`registryId`) in config, with resolved cache state living exclusively in the
lockfile.
- **`index.md` / `log.md` are reserved structural files.** Per the Open
Knowledge Format, `index.md` (directory listing) and `log.md` (update history)
are never indexed as concepts and are never valid write / `mv` targets at any
bundle depth. Existing stash files with those names are excluded from the
index (and renamed by the content migration when they hold a real concept).
- **`vault` asset type removed.** Use `env` (a whole `.env` group; key names
surfaced, values never) and `secret` (a single sensitive value), addressed as
`env/<name>` and `secrets/<name>`. `akm-migrate storage` performs the
non-destructive `vaults/` → `env/` copy for older stashes.
- **0.8-era CLI aliases removed.** The flat proposal verbs (`akm proposals`,
`akm accept`, `akm reject`, `akm diff`, `akm revert`, `akm show proposal`),
`akm save`, top-level `akm enable` / `akm disable`, `akm events`,
`--detail summary|agent`, `--for-agent`, `--note`, and `--source` (on
accept/reject/history) are gone — use the canonical spellings documented in
`STABILITY.md`.
See `docs/migration/v0.8-to-v0.9.md` and
`docs/migration/release-notes/0.9.0.md` for the full upgrade procedure.
### Removed
- **The experimental `akm workflow brief` / `akm workflow report`
external-driver protocol is removed**, along with the
`experimental.workflowEngine` config key that gated it, its
`WORKFLOW_ENGINE_NOT_ENABLED` error code, and the `workflowEngine` block in
`akm task doctor`. `akm workflow run` is now the single execution surface.
The protocol let a calling agent session execute a run's units itself
instead of akm dispatching them. Its stated justification was harness
neutrality, which measurement did not support: native dispatch already
covers **ten** harnesses (opencode, claude, opencode-sdk, codex, copilot,
pi, gemini, aider, amazonq, openhands) in 2,214 LOC total, while the
protocol cost 2,690 LOC on its own — more than supporting every harness
natively — and an eleventh harness is ~220 lines, not a protocol. Removing
it also drops the second consumer of `workflow_run_units` and the
cross-surface parity obligation on `step-work.ts`, both of which
constrained every future engine change. The analysis is recorded in
`docs/architecture/specs/driver-protocol-keep-or-cut.md`.
Legacy configs setting `experimental.workflowEngine` remain valid — the
config schema is `.passthrough()`, so the key is accepted and ignored.
### Added
- **`akm workflow run` and prompt tasks fall back to `opencode-sdk` instead of
refusing when no engine is configured.** A clean install that never ran
`akm setup` — a bare container, a CI image, an agent-operated session — used
to fail closed with `INVALID_CONFIG_FILE` (exit 78). When the `opencode`
binary is on PATH, akm now synthesizes a **config-free** `opencode-sdk`
engine: it carries no model, endpoint, or credential, so provider, model,
and auth all resolve from opencode's own configuration and akm never mirrors
or validates it. With `opencode` absent the failure is unchanged, and its
remedy now names both routes. An operator-configured `opencode-sdk` engine
always wins over the synthesized one.
The requirement is the **binary**, not the npm package: `@opencode-ai/sdk`
is an HTTP client that declares no dependencies and whose own
`createOpencodeServer` spawns `opencode serve`, so a host with the package
and no binary has no server to reach. Install it with `npm i -g opencode-ai`
or opencode's own installer.
The fallback is **announced, never silent** on every surface that applies
it: a workflow run surfaces it once at run creation in the result's
`warnings`, a prompt task writes it to the task run log, `akm agent`
carries it in its result `warnings` and on stderr, and `propose` and
`improve` reflect warn on stderr. The frozen plan records the engine
actually used, so a resume never re-announces a decision it did not make.
- **RSS, Bluesky, and X sources.** `akm bundle add` now recognizes three new
kinds of URL and snapshots them as knowledge assets instead of crawling
them as ordinary web pages:
```sh
akm bundle add https://blog.example/feed # RSS 2.0 / Atom / RDF
akm bundle add https://bsky.app/profile/<handle> # public, no auth
akm bundle add https://x.com/<user> # see token note below
```
Any of these falling through — a `/feed` URL that actually serves HTML, an
unresolvable Bluesky handle — degrades to the normal website crawl rather
than failing the command.
X needs credentials: set `X_BEARER_TOKEN` for the X API v2, or
`X_RSS_TEMPLATE` to an RSS bridge URL containing `{username}`. To keep the
token out of your shell history, store it as an akm secret and inject it
per-invocation:
```sh
akm secret set x-bearer-token
akm secret run secrets/x-bearer-token X_BEARER_TOKEN -- akm bundle add https://x.com/<user>
```
With neither set, the X fetcher emits one warning and falls through.
- **`akm-migrate` derives the 0.9 config from your 0.8 keys instead of
demanding one.** Upgrading used to require hand-authoring a complete 0.9
config before `migrate apply` would act. The first `apply` with no
`--config` now writes a validated starter config — `bundles`/`defaultBundle`
derived from the 0.8 `stashDir` / `sources` / `installed` keys — to a
predictable path under the backup root and stops, with config and durable
state byte-for-byte untouched; a second `apply` picks it up and performs the
cutover. Engine settings are never guessed: `profiles.*` and
`defaults.llm|agent|improve` are stripped and reported individually in
`droppedKeys` by their exact 0.8 dotted path. `status` and `apply --dry-run`
preview the same plan, and an explicit `--config` always wins and is never
overwritten. `akm migrate --format` now renders text/md/html/yaml through
the normal output pipeline instead of warning and printing JSON anyway.
- **Local downstream value attribution for memory inference and graph
extraction.** Private search-hit sidecars now write versioned, source-qualified
per-entry `usage_events.metadata` for emitted MI direct/surface value and the
active graph contributor's positive applied/capped contribution. Current plain
traffic is marked as control, brief/replaced MI surfaces and graph ablations do
not claim attribution, and nested curate reads avoid duplicate show rows. The
read-only `akm-eval-attribution-rollup` separates user-only exposure,
selection/show consumption, current controls, and historical unattributed rows
without emitting bodies, query text, or provenance content. Graph contribution
is an input attribution signal, not a causal claim that rank changed. No table,
migration, dashboard, or health schema was added.
- **Explicit, crash-resumable 0.9 migration coordination.** `akm migrate
status` classifies config, `state.db`, and `workflow.db` independently;
`akm migrate apply [--config <prepared>]` creates a semantically verified,
installation-scoped config/database backup before applying pending migrations.
Apply and restore use one phase-free incomplete sentinel, bounded control-file
reads, SQLite integrity and ordered-ledger checks, active-writer barriers,
WAL/SHM-safe publication, and idempotent replay. Legacy checksum columns are
inert. Routine reads and current database opens no longer depend on a
historical cutover bundle. See `docs/migration/release-notes/0.9.0.md`.
- **Workflow orchestration engine (experimental).** akm can now execute
multi-step workflows through a native engine or any agent session. Workflow
assets use the unified markdown format described above; the stable manual
CLI contract (`start`/`next`/`complete`/`status`/`list`) and the experimental
engine consume the same asset. What ships:
- **Authoring.** A workflow is a markdown asset whose frontmatter graph is
validated against `schemas/akm-workflow.json` and whose `## <step-id>` body
sections carry instructions and gate rubrics. `akm workflow create`
scaffolds that format; `akm lint --type workflows` parses and compiles it.
Bare references (`params.<name>` and `steps.<id>.output.<path>`) wire
`map.over`, `route.input`, and `inputs`; prose is never interpolated.
- **Compilation + frozen plans.** `akm workflow start` compiles the workflow
into a backend-agnostic Workflow Plan Graph IR (`src/workflows/ir/`) and
freezes it on the run row (`plan_json` + `plan_hash`); a run executes the
plan compiled at start, and edits to the source file require a new run.
- **Per-step orchestration.** A step can declare an engine, model, timeout,
fan-out (`map`/`over` with a `concurrency` cap and a `collect` | `vote`
reducer), a typed `output` JSON Schema (validated via a `runStructured`
retry-with-feedback loop), `env` bindings (resolved through the existing
`akm env run` machinery — secret tokens, dangerous-key policy, keys-only
audit events), and classify-and-dispatch `route` steps.
- **Determinism + replay.** Journaled unit identity is content-derived
(`<step>:<sha256(item)[:12]>`, `:solo` for a single unit), so cached
results survive item-list reordering; a completed unit whose recorded
inputs differ on replan is a hard **replay-divergence** failure naming the
unit, never a silent re-dispatch. Every unit is recorded in the new
`workflow_run_units` table behind a serialized writer queue.
- **Execution (`akm workflow run`).** A semaphore-bounded scheduler fans a
step's units out (concurrency defaults to 1 per the local-model
LLM-defaults rule and is the minimum of the map request, frozen workflow
limit, selected frozen LLM engine limit, and current host safety limit),
enforces per-unit
timeouts (default 10 m) and run **budget ceilings** (`budget.max_tokens` /
`budget.max_units`, seeded from the journal so they span resumes), and
advances the run **strictly through `completeWorkflowStep`** so completion
gates are never bypassed. Every dispatched unit gets a standard akm
preamble (run/unit ids, knowledge + env/secret + reporting contract).
- **Typed artifacts + honest gates.** A step's promoted artifact is
validated against its declared `output` schema before completion; a
criteria-bearing gate judges that **artifact** (canonical JSON, clipped)
rather than machine prose, and each engine-driven evaluation is journaled
as a gate unit row. `gate.max_loops` bounds an evaluator-optimizer retry
loop (feedback threaded into re-dispatched unit prompts). Gates are
optional validation: omitted/empty rubrics and unavailable or malformed
judges skip validation.
- **Failure policy.** Per-unit `on_error: fail | continue` (fail-fast
default) plus bounded `retry: { max, on: [<failure_reason>…] }` keyed on
the persisted failure taxonomy.
- **Isolation + leases.** `isolation: worktree` runs each file-mutating unit
in a fresh detached git worktree (journaled path; clean trees
auto-removed, dirty ones retained). A run **lease** (`engine_lease_*`)
ensures a run is driven by exactly one engine or one external driver at a
time; manual `complete` is refused while a live engine lease is held.
- **Harness-neutral driver protocol.** An orchestrated run can be driven by
ANY agent session (Claude Code, opencode, Codex, a human at a shell), not
only the native engine. **`akm workflow brief <run>`** is read-only (takes
no lease, mutates nothing) and emits the active step's expected work-list —
per-unit content-derived id, resolved instructions + input hash
(byte-identical to the engine's dispatch), output schema, env binding
NAMES only, and the exact `report` command lines. **`akm workflow report
<run> --unit <id> --status completed|failed|running`** is the one mutating
verb, ingesting a unit's result through the SAME shared step semantics the
engine uses (idempotent same-hash re-report, replay-divergence on a
differing hash, budget enforcement, schema validation, and the
artifact-judged gate/`max_loops` completion path). `--status running`
claims/heartbeats a unit for stale-driver detection without advancing the
spine; `--rerun` records a fresh attempt for a failed unit (carrying its
prior token total forward). Every report command carries `--expect-step`
(refused if the spine has moved since the brief), and `report --settle`
(no `--unit`) advances a step that dispatches no reportable units — a
params-only route, an empty fan-out, or an all-unresolvable work-list — so
a driver is never wedged. The engine and the brief/report surfaces are
proven to produce **identical unit graphs**
(`tests/workflows/conformance/driver-parity.test.ts`).
- **Observability.** `akm workflow watch <run>` tails the run's `workflow_*`
/ `workflow_unit_*` events as NDJSON (`--stream` foreground-polls to a
terminal status, no daemon); `akm workflow status --units` lists per-unit
diagnostics (failure reason + result/error text) without feeding them into
the deterministic artifact graph; unit lifecycle emits
`workflow_unit_started` / `workflow_unit_finished` events carrying
ids/status/enums only. `akm show workflow:<name>` summarizes each step's
orchestration.
- **Harness adapters.** Seven local coding-agent CLIs are first-class
dispatch targets — Codex, Copilot CLI, Pi, Gemini, Aider, Amazon Q, and
OpenHands — each registered in `HARNESS_REGISTRY` with a command builder +
result extractor; agent-identity detection and the session-log provider
list are derived from the registry, and harness-native session ids are
journaled opportunistically for future session reuse.
- **Storage.** Additive `workflow.db` migrations 004–010 (unit journal,
harness session ids, frozen plans + run leases, check-in heartbeats,
attempt counter, unit claims); migrations 001–003 are untouched and linear
workflows behave exactly as before.
See "Orchestrated steps" and "Driving a run from any agent" in
`docs/features/workflows.md`, the redesign addendum in
`docs/archive/akm-workflows-orchestration-plan.md`, and `STABILITY.md`
(Experimental).
- **`fable` built-in model alias** — resolves to `claude-fable-5`
(`opencode/claude-fable-5` on opencode); recommended resolution target for
the `deep` workflow model tier.
- **`akm lint` now checks the frontmatter xref channels for broken refs.**
The existing `missing-ref` check additionally scans the `xrefs:`,
`supersededBy:`, and `contradictedBy:` frontmatter keys of non-wiki markdown
assets (memories, knowledge, lessons, facts, agents, commands, skills,
workflows) — the channels the stash back-linking conventions route
provenance and correction links through, and previously the only ref channel
with zero checking. Dangling refs are flagged with a detail naming the key
(`missing ref: <ref> (frontmatter <key>; resolved to <relPath>)`). The
`refs: []` body-scan carve-out does not suppress the new pass; `lint_skip:
[missing-ref]` suppresses both; non-ref values (URLs, `raw/<slug>`,
`<placeholder>` templates, shell vars) are ignored; refs resolving in a
configured extra stash root stay clean. **Note for `--fail-on-flagged` CI
users:** stashes with already-dangling xrefs (e.g. from past renames) will
gain new `missing-ref` findings on upgrade — fix the refs or add
`lint_skip: [missing-ref]` per file. `sources:`, `source_refs:`, and
`evidenceSources:` are deliberately not checked (wiki `sources:` is covered
by `akm wiki lint`; the latter two legitimately point at merged-away
assets).
- **`--xref <ref>` on `akm remember` and `akm import` — write-time
cross-references with validation.** The stash back-linking conventions route
provenance and associative links through `xrefs:` frontmatter, but neither
CLI write flow could express them (remember always generated its own
frontmatter block; import wrote content verbatim). The new repeatable flag
records refs in the written asset's `xrefs:` frontmatter list, which the
indexer folds into search hints — the new asset becomes findable from
searches for its source. `remember` merges the refs into its generated
frontmatter (composes with `--tag`/scope flags; does not trigger the
tags-required check); `import` dedupe-appends into the document's existing
frontmatter, or adds a block when the document has none — never a nested
second block. A document whose existing frontmatter is not a parseable YAML
mapping aborts the import (exit 2, nothing written) rather than being
rewritten lossily; importing it without `--xref` still preserves it
verbatim. Every ref is validated before anything is written, against the
write target plus all configured sources (read-only cross-stash sources
count): an unresolvable ref fails with the standard usage envelope (exit 2)
and leaves the stash untouched. The conventions' ~5-xref cap stays soft —
exceeding it warns on stderr but still writes. Additionally, a type-root
write (no `--path`, flat name) into a stash carrying convention facts now
returns an additive `hint` output key pointing at the stash's placement
conventions (`facts/conventions/organization` when that fact exists), so CLI
writers see the conventions that LLM flows already receive by injection.
- **`--supersedes <ref>` on `akm remember` and `akm import` — atomic
correction + demotion of the superseded asset.** The stash conventions'
corrections pattern needs TWO writes (the new correction asset with an xref
to what it corrects, plus a metadata edit demoting the old asset), which
previously meant hand-editing the old file's frontmatter and remembering to
reindex it. The new repeatable flag does both: the correction is written
with the old ref folded into its `xrefs:` (correction provenance), and the
old asset gains `beliefState: superseded` +
`supersededBy: [<new ref>]` via the shared `writeSupersededEdge` primitive
(sibling of `writeContradictEdge`) — a metadata-only frontmatter edit that
preserves every other key and the body byte-for-byte, sorted-set-appended
and idempotent across re-runs. The mutated old file is reindexed by the
write path, so `--belief current` hides it and ranking demotes it
immediately. An unresolvable ref is input validation: exit 2 with the
standard `{ok:false,error,code}` envelope and NOTHING written or demoted
(no partial correction); a ref resolving to the asset being written itself
(self-supersede via `--force` overwrite) is rejected the same way instead
of letting a correction demote itself. An old asset that resolves only
outside the write target and the working stash (in a read-only source, or
in a writable source that is not this write's target) is not mutated: the
correction still writes, stderr warns, and the JSON output reports the
additive `superseded: [{ref, applied: false, reason}]` key (`applied: true`
on success) — the reason names the `--target` remedy when one exists. An
old asset whose existing frontmatter is not parseable YAML is likewise
skipped (`applied: false`) rather than rewritten through the lossy lenient
parser. On a git write target the demotion is ordered before the
batch-at-boundary commit, so the correction and the demoted old asset land
in one commit.
- **Ref-prefix search queries — `akm search "<subdir>/<prefix>/"` now enumerates
that subtree.** A query shaped like a ref prefix (trailing slash required:
`memories/projectA/`; a bare `memories/` lists the whole type) translates to a
typed index enumeration narrowed to entry names under the prefix, instead of
degenerating into the AND-token FTS query its sanitized form used to produce
(`"memory projectA"` — noise, since `entry_type` is not an FTS column). The
listing is recursive and `/`-boundary exact (`projectA/` cannot leak a
sibling `projectAlpha/…` scope), matches names case-insensitively (the CLI
lowercases queries; on-disk scope directories may carry mixed case), and
composes with `--limit`, `--belief`, `--filter`, and named `--source`
narrowing exactly like the existing empty-query enumeration — hits carry the
fixed browse score `1` in deterministic listing order, not a relevance
ranking. The parsed type is explicit intent: a bare `sessions/` enumerates
sessions just like `--type session` (the default session exclusion is an
untyped-path policy), while an explicit `--type` flag always wins over the
type parsed from the query (the branch fires only on untyped searches). A
full ref without the trailing slash (`memories/projectA/auth-tip`) stays an
ordinary keyword search — resolving a single ref is `akm show`'s job.
**Stable-surface note:** `akm search` is Stable; this changes results for a
query shape that previously returned noise or nothing. A user literally
keyword-searching for the string `memories/x/` loses the old fuzzy token
behavior — accepted as negligible.
- **The `category:` frontmatter key is now captured into the index** as
`entry.category` (entry_json only — no schema migration). The key already
drives convention-fact prompt injection (`resolveStashStandards`) and the
fact linter, but the indexer never captured it, so no category-keyed search
or ranking policy was implementable. Captured for all markdown asset types
alongside `beliefState` (trimmed; blank/non-string values ignored; no
default invented), captured directly onto the index entry. Search results
and ranking are unchanged — this is capture
only (a unit test pins that `category` never enters the FTS search
fields). **Requires a reindex to take effect** for existing entries. The
companion rank-time demotion of `category: convention` facts on untyped
queries was NOT shipped: the prescribed measurement (full skeleton
convention facts plus a real `knowledge/auth` asset, untyped `auth` query,
semantic off) shows no crowding — FTS is exact-first, so prefix expansion
onto the facts' tokens only happens when nothing matches the query exactly,
and a real domain asset always outranks the facts. That invariant is pinned
by `tests/search-convention-fact-demotion.test.ts`, which becomes the
regression guard if a demotion contributor is ever revisited.
- **One progressive lexical retrieval path and searchable Markdown prose.**
FTS now tokenizes Unicode letters/numbers, deduplicates and caps the query,
then runs strict AND, prefix-AND, and a single OR/prefix-OR recovery only
after both conjunctive forms miss. Every stage keeps the existing BM25 field
weights and downstream ranker; callers do not maintain stopword lists or
parallel result collections. AKM-native Markdown contributes a normalized,
16,384-character body projection through the existing lowest-weight
`content` field. Frontmatter, comments, fenced code, and link destinations
are removed; secret/env values and raw session/checkpoint bodies never enter
the projection. Structured fields remain first in the bounded embedding
input, so exact names and metadata continue to dominate body-only matches.
- **`akm mv <ref> <new-name>` — rename with inbound-xref rewrite and
utility-history preservation (Experimental).** The stash conventions'
forced-rename procedure ("grep and fix inbound xrefs in the same pass") was
agent-executable except for the part only the CLI can do: a rename used to
mint a new index row, orphaning the `utility_scores` /
`utility_scores_scoped` / embeddings / salience rows keyed by entry id —
the "rename resets learned ranking" cost the conventions warn about. The
new verb does the whole pass: it moves the file (a memory's `.derived.md`
twin moves together, keeping the `entry_key + ".derived"` belief-inheritance
coupling), rewrites inbound refs across the writable stash's markdown files
— body prose, frontmatter ref-list keys (`xrefs:`/`refs:`/`supersededBy:`/
…), and fenced code blocks — with complete-ref boundary matching (a longer
ref sharing the old ref as a prefix is untouched), and re-keys the index
row **in place** so the row id and every id-keyed ranking table survive;
the moved row and rewritten citers are FTS-refreshed so search reflects the
new name immediately. Scope v1: flat-markdown asset types (`memory`,
`knowledge`, `command`, `agent`, `workflow`, `lesson`, `session`, `fact`)
in the primary writable stash only, and the source ref must be the
canonical spelling — a ref that resolves only through one of lint's
fallback resolutions (knowledge-subdir alias, direct-path) is rejected
naming the canonical ref, since a fallback-keyed move would strand the
index row and dangle canonical citers. Wiki refs, cross-type targets,
existing targets, unresolvable refs, type-root escapes, `.derived` twin
refs as the source (rename the base — the twin follows), and target names
ending in `.derived` (reserved twin suffix) are rejected with the
standard envelope (exit 2, nothing moved). Read-only sources are scanned
but never written — their citing files are reported in `readOnlyCiters` as
manual follow-ups. Output:
`{ok, from, to, rewrote: [{file, count}], readOnlyCiters, utilityPreserved}`;
a successful move appends an exactly-once `mv` event. A durable mutation
journal stages citer rewrites and the asset publication, preserves
source-qualified utility/salience history, and resumes index/state
finalization after interruption. Divergent citers and late-created targets
fail closed instead of being overwritten. Added to the v1 §9.4 command
surface as an Experimental-tier additive entry (see `STABILITY.md`).
### Changed
- **X source tokens now resolve from the secret store during bundle update.**
The `secrets/x-bearer-token` akm secret is honored on the provider
`sync()` / bundle-update path, not just when adding or importing a URL —
closing a gap where a refresh saw only the `X_BEARER_TOKEN` environment
variable. Implemented as a `SecretResolver` capability injected from above
the source-provider import cycle; internals are documented in
`docs/architecture/reviews/env-secret-access.md`.
- **`website` crawls now have a hard time limit.** `crawlTimeoutMs` (default
600000 — 10 minutes) bounds the entire crawl, and unlike the previous
between-page check it aborts work already in flight: a `Retry-After` sleep
could previously park `akm bundle add` for as long as a rate-limiting server
asked, well past the advertised cap. Raise it for a large site, or set
`"crawlTimeoutMs": 0` to disable the cap. Relatedly, `fetchWithRetry` now
honors its caller's `AbortSignal` during retry backoff, so any operation that
passes a signal can actually interrupt a long wait.
- **Website snapshots now extract the page's main content.** Conversion moved
from a hand-rolled regex converter to a DOM parse plus Turndown, scoped to
the page's content region (`<main>`, `<article>`, `[role=main]`, then common
content ids/classes, falling back to `<body>` minus nav/header/footer/aside).
Navigation, ads, and boilerplate no longer land in snapshots, and tables,
nested lists, and fenced code blocks with language hints now survive
conversion. **Existing website snapshots will change on their next refresh**
— expect them to get shorter and cleaner. Link discovery still scans the
whole page, so crawl coverage is unchanged.
- **`website` sources now respect `robots.txt` by default.** Before crawling
an origin, akm fetches and parses that origin's `/robots.txt` and skips
paths disallowed for the `akm`/`akm-cli` product tokens (or `*`), honoring
`Crawl-delay` (clamped to 10s) between page fetches. This is a deliberate
behavior change: **existing website sources may return fewer pages, or
fail with an error if the start URL itself is disallowed, after
upgrading.** Re-running `akm bundle update` on a website source is what
surfaces it. Opt out with `"respectRobots": false` on the website
descriptor to restore the exact pre-upgrade behavior (no `/robots.txt`
request at all):
```json
{ "bundles": { "docs": { "website": { "url": "https://docs.example.com", "respectRobots": false } } } }
```
- **`akm lint` now routes through each bundle adapter's own `validate()`.**
`validate()` was a required member of the adapter interface that nothing
called: `akm lint` branched on adapter id and re-implemented OKF's checks
inline (with drifted semantics for `missing-type`), OKF's `missing-ref`
never ran at all (a bundle with a dangling link reported nothing), and
llm-wiki's `uncited-raw` / `broken-xref` / `broken-source` /
`missing-description` checks were unreachable dead code. **Existing OKF and
llm-wiki bundles may surface new lint findings after upgrading.** akm-bundle
lint output is byte-identical. Proposal promotion also runs the adapter
check immediately before the write — advisory-only: it warns and never
rejects, because the adapter resolver and the legacy promotion gate still
disagree on foreign-typed cross-bundle refs.
- **Improve-stage extraction and proactive maintenance now ship opt-in.** The
built-in `default` and `frequent` strategies resolve extract off, while
`default` and `reflect-distill` resolve `proactiveMaintenance` off. The
dedicated `proactive-maintenance` strategy remains enabled. Built-ins such as
`thorough` that omit these fields inherit the new `default` off values; user
overrides are merged last, so explicit `enabled: true` values still win.
Standalone extraction remains independent of the improve-stage toggle but
still requires `--type <harness>` or `--auto`. The bundled, unselected
`core/extract` task now uses `akm extract --auto`; existing scheduled tasks
with invalid bare `akm extract` commands must be updated explicitly.
- **Indexing dispatches each bundle's detected adapter.** The indexer's per-
directory scan now resolves the component's adapter (`adapterForId`) and runs
THAT adapter's `recognize`, instead of always using the `akm` adapter. A
component whose adapter id is unknown is skipped with a warning. Adapter-owned
filtering moves the AKM-stash sensitive/infra exclusions (env/secret
`.sensitive`-marker skips, the legacy `vaults/` skip, wiki infra files) out of
the core scan and into the `akm` adapter's own recognition, so each adapter
owns its bundle's filtering. **Reindex note:** any non-`akm` bundle that was
previously probed as one adapter id but still recognized by `akm` will
re-index under its own adapter on the next `akm index` — the index is a
regenerable cache, so no migration is required.
- **Improve target identity is now end-to-end and source-qualified.** Explicit
targets govern reads, generated proposals, triage promotion, consolidation,
retrieval signals, cooldowns, and replay state. Duplicate bare refs in other
sources no longer affect the selected corpus. Generated lessons and
provenance follow stash placement conventions and canonical `xrefs`.
- **Writable Git boundaries commit only operation-owned paths.** Improve,
proposal, supersedes, and direct write flows preserve unrelated staged or
dirty work, including files beside generated assets in `content/` layouts.
- **Directory (scope/domain) tokens now always merge into `tags` at index
time**, even when an asset sets explicit `tags:` frontmatter. Previously
explicit tags suppressed all path-derived tags, so a nested asset like
`memories/projectA/auth-tip` with `tags: [auth]` silently lost the exact
tag-match ranking boost for its scope token unless the author restated it.
The merged tokens come from the canonical ref subpath
(`extractDirTagsFromName`), which also fixes the flat-walk indexing path
losing directory segments in the empty-tags fallback. Filename tokens are
still auto-derived only when `tags` is empty (they already live in the FTS
name column and aliases), and the empty-tags fallback itself is unchanged.
**Operator notes:** the change takes effect on the next reindex and alters
indexed tag text for nested assets with explicit tags, so collapse-detector
canary recall baselines may shift — re-mint them with `akm improve canary
--refresh`. Embeddings are not regenerated when indexed text changes; the
drift here is small (the merged tokens already appear in the name field),
but a purge/re-embed picks up the new text exactly.
- **Demoting belief states now cap an entry's final search score**
(superseded ≤ 0.25, contradicted ≤ 0.2, archived ≤ 0.15, deprecated ≤
0.28). The existing additive belief penalties are applied inside the
multiplicative boost sum on a min-max-normalized FTS base (rank-1 vs rank-2
base can differ by up to 0.7), so a superseded incumbent that was the best
keyword match stayed clamp-pinned at 1.0 above its own correction — the
demotion was invisible exactly when the corrections pattern needs it. The
ceiling is applied once at the end of the single scoring pipeline (sort
order and displayed scores stay consistent); demoted entries remain listed
under the default `--belief all`, keep their relative ordering, and the
`--belief` filter axis is unchanged. Semantic-only hits are judged against
the `search.minScore` floor by their pre-ceiling score, so a ceiling below
the floor (archived 0.15 < default 0.2) ranks the hit last instead of
silently dropping it. Ordering changes only for stashes containing
belief-flagged assets.
- **`mutateFrontmatter` (belief-edge writers: supersede/contradict edges,
belief refresh) now preserves the body bytes verbatim** when the file
already has a frontmatter block, instead of re-normalizing the
fence-to-body separator through `assembleAsset`. A metadata edit is no
longer a (whitespace-level) content edit; files gaining their first
frontmatter block still use the canonical shape.
### Fixed
- **Fresh 0.8 installs can actually upgrade.** A config that 0.8.x wrote
itself carries no `configVersion` key at all (0.8 stamped it only when a
0.7-era migration did substantive work), and the migrator read the absent
key as `inconsistent` — an unconditional blocker. `migrate status` reported
`blocked` and `migrate apply` refused with exit 78 for every fresh 0.8
install; reproduced end to end against the published `akm-cli@0.8.14`. An
absent `configVersion` on a positively pre-cutover-shaped config now
classifies as `old`; a present-but-unparseable version still fails closed.
Relatedly, `migrate` reports `not-applicable` (exit 0) instead of `blocked`
when there is no akm installation to migrate at all, and `apply` warns when
an active workflow run targets an asset that fails 0.9 structural
validation, naming the asset and `akm workflow abandon <run-id>`.
- **`akm lint` fails closed on mistyped invocations.** A nonexistent `--dir`,
or an unknown `--type` on an akm bundle (the classic singular/plural typo,
`--type workflow`), used to scan nothing and report a clean
`ok:true, flagged:0` — silently passing scripted `--fail-on-flagged`
gates. Both are now usage errors (exit 2), the `--type` error listing the
valid values.
- **Registry search survives a briefly unreachable registry.** Once the
cached registry index aged past its refresh TTL, a failed fetch
hard-failed the command even though a serviceable index sat in the cache.
A failed fetch now serves the last cached index — past its TTL — with a
warning naming the fetch error.
- **`akm upgrade` verifies the package manager actually delivered the new
version.** A lagging `@latest` dist-tag (partial publish, registry mirror
lag) exits 0 while leaving the old version on PATH; upgrade used to report
success anyway — and then run `migrate apply` against the old binary. It
now re-reads `akm --version` after the install: a confirmed mismatch
reports `upgraded: false` with an exact-version pin command, and a
verified match is named in the success message.
- **`akm info` no longer overstates semantic-search health.** After a run
with partial sqlite-vec fast-path insert failures, the verification
reported `ready-vec` ("sqlite-vec active") even though search had already
routed to the slower JS-cosine fallback. The status now reflects the path
search actually takes, with an `akm index --full` hint when the fast path
is degraded. Relatedly, `embedding.dimension` is now bounded to the
vec table's own 1–4096 limit at config validation, so an out-of-range
value fails at `akm config set` with a clear message instead of crashing
`akm index` mid-run.
- **Standalone `akm remember --enrich` actually enriches.** With no other
metadata flag, `--enrich` fell through to the zero-flag raw-write hot path
and never attempted the LLM call — an unenriched memory with no warning.
`--enrich` now routes to the enrichment dispatch exactly like `--auto`;
the fail-soft contract is unchanged (no configured LLM still warns and
writes without enrichment).
- **Read paths no longer plant a broken `index.db` on a fresh install.**
The fire-and-forget usage telemetry behind `search` / `show` / `curate`
opened `index.db` with create-on-open: with no index built yet, the open
itself left an empty, schema-less `index.db` behind, and every later
command then saw an existing-but-broken index ("no such table: entries") —
hard-failing proposal acceptance among others. `openExistingDatabase` now
refuses to create the file (a missing index throws, naming `akm index` as
the remedy) and the telemetry paths skip cleanly instead.
- **Improve RC stabilization.** Restored one ownership-safe whole-run lock from
triage through final sync; `--skip-if-locked` is a true no-op; the run deadline
now starts before indexing and reaches index waits, generation, reindexing, and
quality judges; reflect judges the sanitized final candidate with bounded
changed-region context; write-target selectors no longer replace durable source
identity; and vLLM thinking controls cannot be overridden through `extraParams`.
- **Proposal promotion, reversion, and rejection are durable and recoverable.**
Acceptance and reversion persist target ownership and content fingerprints,
publish atomically across filesystem layouts, index immediately, commit exact
Git paths, and emit idempotent lifecycle events. Crash recovery and legacy
accepted proposals fail closed on ambiguous targets instead of clobbering
another source.
- **Engine/setup/health behavior now matches the effective improve plan.**
Built-in strategies compose over one baseline, setup preserves independent
general and LLM defaults, native OpenCode SDK execution does not require an
unused fallback, and health checks each enabled process and credential.
- **Check-in directives now survive plain-text output and `workflow
status`** (check-in review C2/M1): `formatWorkflowNextPlain` and
`formatWorkflowStatusPlain` render the `CONTINUE` directive, and every
run-detail response (status/start/complete) evaluates the check-in instead
of only `workflow next`.
- Workflow frontmatter validator error message now lists the actually-allowed
keys (`name`, `updated` were missing); removed the documented-but-nonexistent
`akm workflow step` alias from `docs/features/workflows.md`.
## [0.9.0-rc.13] - 2026-07-31
### Security
- **`akm update` no longer deletes a previous install directory without
confirmation.** When a managed source's resolved content location moves,
`update` removed the old directory outright, while `akm remove` had always
required `--yes` in non-interactive mode. Only that destructive branch is
gated — a normal refresh, where the location does not move, still needs no
prompt and no flag, so existing CI invocations are unaffected. Pass
`-y`/`--yes` to allow the deletion non-interactively. A cleanup that fails
now warns instead of failing silently.
- **The dangerous-env-key install gate now scans `env/` recursively.** It
previously read only the top level, so a stash carrying `LD_PRELOAD` in
`env/nested/inner.env` installed cleanly with no warning. Files without a
`.env` suffix are still not scanned — no akm code path loads them as
environment variables.
### Added
- **The `okf` adapter reads OKF v0.2's trust/provenance and lifecycle
frontmatter families.** `generated: {by, at}` (with `generated.at` taking
precedence over the legacy `timestamp` field, which remains a valid
fallback), `verified` (a list, or v0.2's permitted single-mapping
shorthand), `sources` (an object list — `resource` required; `id`/`title`/
`author`/`usage_count`/`last_modified` optional), `status`
(`draft`/`stable`/`deprecated`), and `stale_after` are now parsed leniently
from any OKF concept's frontmatter and surfaced on new, namespaced
`IndexDocument` fields (`provenance`, `lifecycleStatus`, `staleAfter`,
`okfVersion`) that never overload the pre-existing AKM-native `sources`
(wiki citation strings), `generation` (consolidation depth), or `quality`
fields. As with every other optional OKF field, a missing or malformed
value never rejects the document. The `okf` adapter remains consumer-only.
- **Accepting a proposal now stamps OKF v0.2 provenance onto the written
asset's frontmatter**, for AKM-native writes only (never through the `okf`
adapter, which stays consumer-only and unaffected by this). `promoteProposal`
projects the proposal system's own `source`/`sourceRun`/`gateDecision`/
`review` bookkeeping — already tracked in `state.db` but previously never
written to disk. `generated: {by, at}` and `verified: [{by, at}]` are written
**bare at the top level**, exactly as OKF v0.2 spells them, so a third-party
OKF v0.2 reader pointed at an AKM stash sees conformant trust metadata;
`sources` alone is namespaced as `provenance: {sources}`, because a bare
`sources:` collides with the pre-existing wiki citation-string convention.
`generated.by` records whether the content came from an automated pipeline
(`akm/<version>`) or a human-initiated source (`human:<id>`); `verified`
records whether the promotion itself was an automated gate decision or a
direct human accept, and accumulates rather than overwriting across
re-promotions; `evidenceSources`, when present, projects as
`provenance.sources`. AKM's own adapter rereads what it wrote, so `akm show`
surfaces it. Every AKM-native markdown type is stamped, `workflow` included.
Two consequences worth knowing: promotion re-serializes the whole frontmatter
block, so YAML **comments** in a hand-written proposal's frontmatter are not
preserved (values and body bytes are); and for a human-attributed promotion
with no configured actor id, `by` falls back to `human:<OS username>`, which
puts that username into content you may later commit and share.
- **Internal: a `capturedAtHead` integrity guard**
(`scripts/lint-golden-captured-at-head.ts`, wired into `bun run lint`) now
checks every golden fixture's recorded `capturedAtHead` commit SHA — it must
exist in the local object database and be reachable from at least one known
branch. Post-hoc review of this PR found all four new OKF format-family
goldens pointed at a commit that existed locally but was unreachable from
any ref (a pre-amend duplicate left behind by an interrupted git operation),
which would have 404'd on GitHub and vanished under a local `git gc`; a
human fixed that one by hand because nothing caught it. This guard is that
catch, going forward. In CI's shallow (`fetch-depth: 1`) checkout, a merely
*absent* commit object is inconclusive (indistinguishable from "just not
fetched") and only warns; a commit that *exists but is unreachable from any
branch* — the actual bug class above — still fails there too, since a
shallow clone can tell presence apart from absence just fine.
- **`akm log list --limit <n>`** returns the most recent N events. The flag was
documented but silently ignored, and there was no limiting mechanism at all
in the read path — the command returned the entire events table regardless of
history size. The default remains unlimited.
- **`--track-usage` (default on) on `akm search`, `akm curate`, and `akm show`.**
Pass `--no-track-usage` for a read-only lookup that does not feed usage
telemetry or the utility-score ranking signal. Previously a bare `akm search`
silently wrote a `utility_scores` row that influenced future ranking, with no
disclosure and no way to opt out.
- **`akm show` returns the canonical `ref` in every shape.** It was present only
under `--shape agent`, so a `--shape summary` consumer had to make a second
call at a different shape just to learn which asset it was looking at.
- **`akm info` gained `stashDir`, `defaultBundle`, and `indexStats.byType`.**
Answering "which stash is primary" previously required a separate
`akm sources list`.
- **`instruction` is a stash-resident asset type.** It was already in
`KNOWN_TYPES` and had a presentation entry, but had no placement spec — so
there was nowhere to put one and the indexer never recognized one. `akm bundle create`
now creates an `instructions/` directory, `.md` files under it index as
`instruction`, and `--type instruction` is accepted and tab-completable
everywhere `--type` is. A compile-time assertion now pins
`placementTypes() ⊆ KnownType`, so the half-registered state this fixes
cannot recur silently.
- **Schedule tasks from any configured bundle via `--bundle <bundle>`** (#711).
`akm task add`, `run`, `sync`, and `history` accept `--bundle` to
operate on a non-default bundle instead of only the primary stash. `add`
resolves through the normal writable-target rules; `run --bundle X` resolves
the task file and relative asset refs from bundle X. A non-default bundle is
recorded in the scheduler entry as `--bundle <bundle>`, so scheduled
`akm task run` resolves the right bundle. Scheduler ids stay bare and a
collision with another bundle is a hard error rather than a silent clobber.
- **Orphan-GC pass for unresolvable `asset_salience` / `asset_outcome` state
rows** (#733). A new improve maintenance pass (`runOrphanStateGcPass`, run
next to the existing orphan-proposal purge) stamps `missing_since` on any
state row whose ref no longer resolves against `entries.item_ref`, clears
the stamp the moment the ref resolves again, and — only when
`improve.stateGc.collect` is set to `true` (**default `false`**) — deletes
rows whose stamp is older than a fixed 7-day grace window
(`STATE_GC_GRACE_MS`). The pass always runs and always reports counts via
the new `asset_state_gc` event (`{pending, collected, byTable}`), emitted
only when there is something to report, so live data can prove the report
clean before `collect` is ever turned on. Additive migration
`021-asset-state-missing-since` adds the `missing_since` column to both
tables. Deliberately lean by design (Workstream C): no quarantine archive,
no circuit breaker, no health-advisory plumbing, no new tables — "ref not
present in `entries.item_ref`" is trusted as the authoritative-deletion
predicate because the indexer already preserves a source's last-known-good
rows when its scan is incomplete, so a temporarily unreachable source never
contributes false candidates. `usage_events` is out of scope (already
covered by cascade-on-delete plus its own 90-day retention purge).
### Changed
- **Workflow execution is consolidated on stable `akm workflow run`.** The
public `workflow start`, `next`, and `complete` commands are removed with
explicit `UNKNOWN_COMMAND` migration hints; `run <ref|run-id>` now owns
creation, active-run continuation, native dispatch, completion, and durable
replay. It is no longer gated by `experimental.workflowEngine`; only the
experimental `brief`/`report` external-driver protocol retains that opt-in.
Workflow parameters move from the opaque `--params '<json>'` bag to exact
declared flags (`--version 1.2.3`, repeated array flags, JSON object/array
values) coerced through the frozen parameter schemas. New invocation controls
add bounded failed-step retries (`--max-retries`) and a whole-run timeout
(`--timeout N|Nms|Ns|Nm`); failures, gate rejection, timeout, and signals now
produce non-zero process statuses while leaving interrupted work resumable.
Criteria-bearing gates now require `workflow.judgeEngine`, which may name a
configured LLM or agent engine and is frozen into the run. Verification is
fail-closed: a missing/failing verifier or malformed verdict rejects instead
of silently advancing. Scheduled workflow tasks now execute through the same
native orchestrator rather than stopping after run creation. Migration:
replace `workflow start/next/complete` loops with `workflow run`, replace
`--params` with exact declared flags, and configure `workflow.judgeEngine`
before running a workflow with a non-empty `### gate` rubric.
- **The two workflow authoring formats — markdown documents and YAML
orchestration programs — are unified into one format**, per
`docs/architecture/specs/workflow-format-unification.md`. A workflow is
now always a single markdown asset: the standard AKM frontmatter envelope
carries the whole orchestration graph (`params`, `steps` with
`unit`/`map`/`route`, `inputs`, `output`, `gate`, `defaults`, `budget`),
and the body carries each step's instructions under a bare `## <step-id>`
heading, joined to the frontmatter by step id. `.yaml`/`.yml` workflow
files, the `# Workflow:` / `## Step:` / `Step ID:` markdown headings, and
`akm workflow create <name>.yaml` are all gone; `akm workflow create`
always writes the one unified template
(`src/assets/workflows/workflow-template.md`).
**Prose is never interpolated.** The YAML program's `${{ … }}` template
language, and the markdown format's decorative — and never
substituted — `{{ … }}` moustaches, are both removed. Data reaches a
dispatched unit as *attached context* instead: the run's params, its
item and index for a map unit, and the artifacts its step's new
`inputs:` key declares. Instructions refer to that context in plain
language ("clone the repository named by the `repo` parameter") rather
than splicing a value into the instruction string. Bare reference
strings (two roots, `params.<name>` and `steps.<id>.output…`) now appear
only in three frontmatter positions: `map.over`, `route.input`, and
`inputs:`.
**Gate rubrics move to the body.** A step's completion criteria are no
longer a frontmatter `gate.criteria` list or a `### Completion Criteria`
bullet section — they live under a step's `### gate` sub-heading, the
format's one reserved marker, as full prose a judge receives byte-exact.
Frontmatter `gate:` now carries only optional `max_loops` configuration.
Omitted or empty rubric text skips validation; a non-empty rubric requires
the frozen `workflow.judgeEngine`, and unavailable or malformed judges reject
the gate.
This is a **pre-1.0 format change**. The ten example workflows under
`scripts/akm-eval/example-stash/workflows/` are rewritten to the unified
format in this change; existing user-authored workflow assets must be updated
manually before execution.
- **akm is described as a knowledge toolkit, not a package manager** (R-048).
The npm one-liner, the README lede, and the `concepts.md` opener all led with
"a package manager for AI agent capabilities", which misstates the product to
its distribution channel and sets package-manager expectations for verbs
(`update` / `upgrade` / `sync`) that don't mean what a package manager's do.
- **BREAKING: a command group invoked with no subcommand is now always a usage
error, exit 2** (owner ruling 12). The eleven `akm <group>` groups did three
different things when invoked bare: `graph`, `config`, `env`, `secret`,
`task`, `workflow`, and `proposal` ran an implicit default action and exited
0 (bare `akm graph` silently rendered `graph summary`); `registry`, `log`, and
`lessons` printed citty's human usage banner to stdout; only `migrate` raised
a structured error. All eleven now emit the same
`MISSING_REQUIRED_ARGUMENT` envelope on stderr, naming the available
subcommands, and exit 2 — matching STABILITY.md's exit-code table (2 =
usage) and the exit code already used for unknown commands. Matching exit
codes alone was not enough: a script could not parse the failure uniformly
while three groups answered on stdout in prose.
Migration: name the subcommand. `akm graph` → `akm graph summary`,
`akm config` → `akm config list`, `akm env` → `akm env list`, `akm secret` →
`akm secret list`, `akm task` → `akm task doctor`, `akm workflow` →
`akm workflow list --active`, `akm proposal` → `akm proposal list` (which
takes the same `--status`/`--queue`/`--ref`/`--type` flags the bare form did).
- **BREAKING: `akm sync` persists `eventType: "sync"`, not the legacy
`"save"`.** The event name now matches the command name. Historical
`state.db` rows are left as-is — `akm log` and `akm log tail` treat `"save"`
and `"sync"` as synonyms on **read**, so `akm log --type save` keeps
returning both old and new rows. Only newly written events use `"sync"`.
Migration: none for `akm log --type save`. A script matching raw event rows
by `eventType === "save"` — reading state.db directly, bypassing `akm log` —
should also match `"sync"` to see new syncs.
- **BREAKING: dropped the dead `installedKitCount` field from the `add`,
`remove`, and `update` JSON envelopes.** It was a raw lockfile-entry count
that nothing — internal code or test — ever read.
Migration: a script parsing `.config.installedKitCount` should stop; the
field is gone, not renamed. `config.sourceCount` remains and is unaffected.
- **BREAKING: dropped the dead `graphPath` field from every `akm graph *` JSON
envelope** (`summary`, `entities`, `relations`, `export`, `related`, `entity`,
`orphans`). It always resolved to the shared state.db path, never a
per-graph artifact, and carried nothing `stashPath` did not already provide.
Migration: a script reading `.graphPath` from any `akm graph` subcommand
should stop; `stashPath` remains.
- **BREAKING: `semanticSearchMode` now defaults to `"off"`.** A bare or
headless install (`akm init`, `akm setup --yes`, `akm setup --config`) was
silently downloading the ~130 MB local embedding model on its first `akm
index`, because the fallback used when the key is absent was `"auto"`. The
interactive `akm setup` wizard still pre-selects semantic search **on** — a
human is present to decide — and now shows the asset/download warning
*before* the prompt rather than after, so the pre-checked box is an informed
choice. When a remote `embedding.endpoint` is configured, enabling semantic
search downloads nothing.
Migration: existing saved configs are unaffected — the flip only changes the
fallback used when the key is absent. To keep semantic search on for a
headless or CI install, set `semanticSearchMode: "auto"` explicitly, or point
`embedding.endpoint` at a remote embedder.
- **BREAKING: `akm workflow run|brief|report` refuse to run until
`experimental.workflowEngine` is set** (0.9.0 decision Q-05). The native
workflow executor — including fan-out scheduling and worktree isolation —
is experimental, and shipping it enabled by default would have made an
unreviewed execution engine reachable from a plain `akm workflow run`. The
gated surfaces now exit `78` with a `ConfigError` naming the exact key, and
`akm task doctor` reports the gate's state. Authoring and linting the unified
markdown format, along with every other `akm workflow` subcommand, remain
ungated.
Migration: `akm config set experimental.workflowEngine true`.
- **BREAKING: the `env:<name>` / `secret:<name>` colon ref spelling is
rejected** (0.9.0 decision Q-08). Refs are slash conceptIds only — `env/foo`,
`secrets/deploy-key`. The colon form previously resolved as an undocumented
alias in some places and fell through as a literal filename in others. It now
fails with a usage error naming the slash replacement, rather than silently
doing the wrong thing.
Migration: rewrite `env:<name>` as `env/<name>` and `secret:<name>` as
`secrets/<name>`. The error message prints the exact replacement.
- **`akm improve` is review-first by default; autonomy is opt-in** (0.9.0
decision D8). The command stays ON — schedules, reflect/distill proposals, and
graph extraction are unchanged — but the lanes that mutate assets *without*
review now require `akm config set experimental.improveAutonomy true`:
memory-inference writes, the memory-cleanup pass, and triage
`applyMode: "promote"` (which downgrades to `queue` rather than disabling
triage). Consolidation remains review-oriented and is not gated.
A gated lane is never a silent no-op: it warns on stderr naming the lane and
the key, appends an `improve_skipped` event with `reason: "autonomy_gated"`,
and is counted in `akm health`'s improve skip-reason summary.
Migration: set `experimental.improveAutonomy: true` to restore the previous
behavior. `sync.push` is **not** affected — it keeps its `true` default and its
own `sync.push: false` / `--no-push` controls. Two other direct writes stay
ungated by design: `extract`'s additive session indexing and distill's
encoding-salience frontmatter stamp. Because the gate is applied before the LLM
preflight, a review-first workspace may now need fewer engines configured than
before.
Also: `akm improve` no longer rejects the global `--format`. It emits an
envelope through `output()` (always under `--dry-run`, otherwise under
`--json-to-stdout`), so `--format` applies to that envelope; progress output
stays on stderr. Previously it exited 2 with `INVALID_FLAG_VALUE`, which made
it the one command that rejected a valid global flag.
- **`akm health --report` replaces the html-only full report** (D7
follow-through). The full health report — per-run rows, trend deltas vs the
prior window, and the pending proposal queue — is now a **data** flag, not a
side effect of asking for html: `akm health --report --format html` renders
the rich report, and the identical dataset comes back under `--format json`
(previously that data was reachable only as html). The registered md/html
renderers fire on the shape of the result, and `akm health` no longer reads
`--format` at all.
Migration: `akm health --format html` → `akm health --report --format html`
(the bare form now renders the plain check generically); the html-only
`--compare` flag is removed — use `--window-compare`, which with `--report`
defaults to the `--since` window so trend deltas stay like-for-like.
- **Global output flags parse correctly next to positionals.** citty parses
each command level against only its own declared args, so a root-declared
global flag was unknown at the leaf and its space-separated value fell
through as a positional — `akm sync --format json` synced a bundle named
"json", and `akm env unset env:x KEY --format json` tried to unset a key
named "json". The global output flags (`--format`, `--detail`, `--shape`,
`--output`) are now declared on every leaf command so their values are
consumed by the parser; the two bespoke argv-inspection workarounds this
replaces are deleted. Three more non-exempt commands (`akm health`, `akm
index`, `akm lint`) now declare these flags too, purely for `--help`
visibility — all three already parsed `--format`/`--detail`/`--shape`/
`--output` correctly, since none of them has a positional a stray value
could fall into.
- **BREAKING: unknown commands and missing required arguments now exit `2`
(usage), not `1`.** citty's own command-dispatch wrapper unconditionally
called `process.exit(1)` for any error it raised before a command's own
body ever ran — `akm totally-bogus` (unknown command), bare `akm log` /
`akm lessons` (a subcommand group invoked with no subcommand), and a
command missing a required positional (e.g. bare `akm import`) all exited
`1`, contradicting the documented exit-code table (`1` = general error /
not found, `2` = usage / bad input). The CLI now drives command dispatch
directly instead of going through that wrapper, so it can reclassify this
one error family as `2` while leaving `--help`, `--version`, and every
other exit code unchanged.
Migration: a script that treated exit `1` as "something went wrong" for a
mistyped command or missing argument should check for `2` instead (or
keep treating any non-zero exit as failure, which was already correct).
- **BREAKING: `akm completions --shell <unsupported>` now exits `2` with the
standard JSON error envelope, not `1` with a raw stack trace.**
`completions` stays format-exempt (its own output is shell-script source,
not a result envelope — see STABILITY.md), but its body is now wrapped in
the same error-classification path every other command uses.
Migration: a script parsing this failure should now expect
`{"ok":false,"error":"...","code":"INVALID_FLAG_VALUE","hint":...}` on
stderr and exit code `2` in place of a stack trace and exit code `1`.
- **BREAKING: `akm index --dry-run` without `--clean` now exits `2` instead
of running a real index.** The flag only ever gated the `--clean`
stale-entry removal pass — every other phase (walk, LLM enrichment,
embeddings, FTS, the adapter-detection config write) ran for real
regardless, so `akm index --dry-run` alone silently performed a full index
despite its name. The combination is now rejected with the standard usage
envelope instead of quietly doing something other than what "dry run"
promised.
Migration: a script or cron invoking bare `akm index --dry-run` was
already getting a real index, so nothing there needs to change in effect —
but it will now fail loudly instead. Pass `akm index --clean --dry-run` to
preview the stale-entry removal pass, or `akm index --clean` to apply it;
drop `--dry-run` entirely to keep running a plain real index.
- **BREAKING: a corrupt or unparseable `akm.lock` now makes lockfile WRITES
throw, instead of silently destroying every entry.** The previous lenient
reader returned `[]` on unparseable JSON; a write path that upserted a
single entry onto that `[]` then overwrote the file, permanently deleting
every other tracked bundle's lock entry. Install/update/remove write paths
now use a strict reader that throws on the same corruption instead of
reaching the destructive overwrite.
Migration: if a write now fails with a lockfile-parse error, `akm.lock` is
genuinely corrupt — inspect and repair it by hand, or restore it from a
backup (e.g. git history), before retrying the write. Reads elsewhere are
unaffected; the lenient read contract is unchanged.
- **BREAKING: `AKM_NPM_REGISTRY` now redirects npm package METADATA lookups,
not just the trusted-tarball allowlist.** Previously the override only
widened which tarball hosts were trusted for download while metadata
queries stayed hardcoded to `registry.npmjs.org`, so a configured private
mirror was never actually consulted for package info — the error hint that
points users at this variable was false. The override now also replaces
the metadata registry base, matching how a private npm registry is meant
to work (like npm's own `--registry` flag: wholesale replacement, not a
merge with the public registry).
Migration: an operator who set `AKM_NPM_REGISTRY` expecting only tarball
downloads to be redirected, with metadata still served from the public
registry, should confirm the mirror actually serves equivalent package
metadata — `akm add`/`akm update` for npm-sourced bundles now resolve
entirely against the configured mirror when it is set.
- **`akm remember --show-similar` and `akm migrate apply --dry-run` are the
documented, canonical spellings** (previously `--showSimilar` /
`--dryRun`), matching every other multi-word flag in the CLI. Not a
breaking change: citty registers both the camelCase and kebab-case
spelling of any declared flag name automatically, so `--showSimilar` /
`--dryRun` keep working — they're now explicit, documented aliases instead
of an undocumented accident.
- **`--detail` and `--shape` help text is scoped honestly.** The per-command
`--detail` description now names `info`, `list`, and `remember` as the
commands where it has no effect (verified byte-identical output at every
level — `akm show` is not one of these; it has three distinct
brief/normal/full payloads). `--shape`'s per-command help now repeats the
"`summary` is only valid on `akm show`" caveat the root help already
documented.
- **All six `--format` values work on every command** (0.9.0 decision D7).
`json|jsonl|yaml|text|md|html` are now universal. Previously there were three
inconsistent behaviours: `md` silently emitted the JSON envelope everywhere
except `akm health`, `html` was rejected with exit 2 everywhere except
`akm health`, and `akm health` reached neither because it intercepted the
format itself. Rendering is now registry-driven — a command may register a
renderer for a document format, and anything unregistered falls back to a real
rendering of its own envelope (headings, tables for arrays of uniform objects,
lists otherwise). `akm health` keeps its per-run/window-compare tables and its
full HTML report by registering them; the output is unchanged.
Migration: none required for `json|jsonl|yaml|text`. `--format md` on a
non-health command previously returned JSON and now returns Markdown; a script
that parsed that JSON should ask for `--format json` explicitly. `--format
html` previously exited 2 on non-health commands and now succeeds.
Also: `akm graph export --format` is **removed** — it declared `--format`
locally as well as globally (one token, two parsers). The artifact payload
now follows the `--out` extension (`--out g.jsonl` writes JSONL, anything
else JSON); the global flag only renders the command's own envelope. A dead
local `--format` declaration on `akm history` was removed too (it was never
read). Commands
whose output is not an envelope (`completions`, `setup`, `env run`,
`secret run`, `agent`, `workflow template`, `help migrate`) are declared
format-exempt in `src/output/format-exempt.ts` and now warn when given
`--format` instead of ignoring it silently. `output.format` in config accepts
all six values.
- **Subtree browse is a conceptId prefix, not `<type>:`** (0.9.0 decision D4).
`akm search` enumerates on `memories/`, `memories/projecta/`, `bundle//`, and
`bundle//skills/`; a trailing `/` is still required. The prefix now matches the
**conceptId** rather than the item name, so a ref copied out of search output
can be truncated to a prefix and pasted straight back in — previously that
round-trip degraded silently into a keyword search. Enumeration no longer
validates against the `akm` adapter's placement types, so items from every
adapter browse the same way, and `bundle//` lists a whole bundle (the
replacement for the removed `akm bundle items`).
Migration: `akm search "memory:"` → `akm search "memories/"`;
`akm search "memory:projectA/"` → `akm search "memories/projectA/"`;
`akm search "session:"` → `akm search "sessions/"`. The retired spelling is
now an ordinary keyword search; when it returns nothing, the tip names the
conceptId spelling that replaces it. `scripts/lint-shipped-assets.ts` no
longer exempts the old spelling, so it is an offense in agent-facing assets.
- **`akm task sync [--bundle <bundle>]` reconciles a single bundle.** Sync now
attributes each installed scheduler entry to its bundle (parsed from the
`--bundle` token; absent ⇒ primary) and reconciles only the entries for the
bundle being synced. A plain (primary) sync never installs from, updates, or
removes another bundle's entries, and sync never scans all bundles — task
activation stays explicit (`add --bundle` or `sync --bundle`), so registering a bundle
still never activates code. When the target is the default bundle (or omitted),
installed scheduler entries are byte-identical to before, so upgrading shows no
spurious drift.
- **The R2 salience ranking boost no longer applies to default `search`/`curate`
ranking** (#692). `asset_salience.rank_score` (an encoding + outcome +
retrieval projection, recomputed every `improve` run) previously composed
into every default search as a bounded multiplicative boost
(`salience-ranking`, ×[1.0–1.2]), loaded best-effort from `state.db` on the
hot path. On live data it measured as noise (max observed multiplier
×1.071, mean ×1.016): the boost was retrieval-dominated with no source
filter — double-counting the same `usage_events` the utility-score
contributor already reinforces — warm-started non-zero with no outcome
evidence, and had zero pack coverage, so it could only ever favor
self-generated personal assets over an equally-relevant pack asset.
Removing the default `state.db` load also fixes a confirmed hot-path
defect: whenever `state.db` already existed, every default search
synchronously waited on the maintenance-activity barrier before ranking
could even start — up to a 5-second stall on a blocking wait loop, plus a
lock-file create, before the load's own 250ms SQLite `busy_timeout` ever
applied. No config gate was added: a key for a term being removed would be
dead surface for the upcoming 1.0 contract freeze to carry forever.
`rank_score` itself, and everything `improve` computes and does with it
internally, are unchanged — only its promotion into user-facing ranking is
removed. The contributor stays in the codebase (unwired) for a future
gated, outcome-backed experiment.
- **Internal: `asset_salience` / `asset_outcome` state.db access moved behind
`src/storage/repositories/{salience,outcome}-repository.ts`** (#672 part 2).
Mirrors the existing state.db repository precedents
(`proposals-repository.ts`, `improve-runs-repository.ts`,
`events-repository.ts`): the raw SQL, row-mapping, and the #644
encoding-provenance CASE guards are extracted verbatim, only relocated —
`commands/improve/salience.ts` and `outcome-loop.ts` re-export the moved
functions, so no importer or test churns. A new `state-table-sql` rule in
`scripts/lint-repository-sql.ts` now fails the build if raw
`asset_salience`/`asset_outcome` SQL reappears outside the repository
directory (or `core/state/migrations.ts`). Not a user-visible behavior
change: `rank_score`, `outcome_score`, and everything `improve`/`health`
compute from them are identical.
### Fixed
- **The compiled standalone binary can run `akm migrate`.** Release binaries
compiled only `src/cli.ts`, and the migrator was resolved as a sibling file
and spawned — neither candidate exists inside a compiled executable, so the
documented `./akm-0.9 migrate status/apply` upgrade path always failed with
`FILE_NOT_FOUND`. Standalone builds now compile `scripts/akm-standalone.ts`,
a wrapper that embeds both the CLI and the migrator (src never imports
scripts/ — the dist build's tsc forbids it); `akm migrate` re-execs the
binary with an `AKM_MIGRATE_ENTRY` marker the wrapper dispatches on. The
repo and npm layouts keep the subprocess path.
- **Quarantined migration rows are retained in full, not reduced to a count.**
When the 0.8→0.9 cutover met a durable ref it could not map, it recorded
surface/ref/count in `legacy_state` and then deleted the rows — destroying
proposal payloads, event and task history, fingerprints, and canary anchors,
contrary to the migration guide's "quarantined, not dropped". Complete rows
are now preserved as JSON in `legacy_state_rows` before leaving the live
tables.
- **A failed content migration fails the apply instead of reporting success.**
Root discovery, sidecar folding, or the legacy-proposal import throwing was
swallowed and logged; the apply then advanced and cleared its journal, and —
because 0.9 removed the live `.stash.json` and filesystem-proposal readers —
the affected metadata and pending proposals became permanently inaccessible
behind an apparently successful upgrade. The step now fails the apply with
the journal intact; the committed cutover is untouched and the next apply
retries.
- **Sidecar provenance survives the fold.** Folding a `.stash.json` into
frontmatter dropped `xrefs` and `sources` entirely and mapped legacy
`sourceRefs` to a `source_refs` key that could never fire (the validator
stopped copying the field) and that 0.9 never reads — then deleted the only
copy. `xrefs`/`sources` now fold through, and legacy `sourceRefs` merge into
`xrefs`.
- **A reserved-filename rename re-keys durable state.** The D-R6 rename of a
mis-named `index.md`/`log.md` concept ran after the cutover had keyed usage,
salience, and proposal rows to the old conceptId, stranding that learned
state. The rename now feeds the same re-key engine the cutover uses, with
the pairs persisted before re-keying so a crash between the two stays
retryable.
- **v1 tasks in a read-only bundle are surfaced with a remedy instead of being
silently skipped.** The 0.9 runtime removed the v1 task parser, so silently
skipping a `writable: false` bundle left tasks that would start failing after
an upgrade that reported current. The preflight now warns per bundle and
lists the stranded files in the plan (`readOnlyLegacyTasks`). It does not
block the apply: the migration deliberately never rewrites a read-only
bundle, and the fix for a lock-materialized git/npm bundle belongs upstream.
- **Lock resolution metadata survives migration.** Merging the migrator's
sparse lock entries replaced whole rows by id, discarding
`resolvedVersion`/`resolvedRevision`/`integrity`/`installedAt` recorded by a
real install. Merge now preserves existing fields the incoming entry does
not define.
- **Migrating a pre-0.9 config no longer silently changes source policy.**
Three settings were dropped by the config-shape migration: an explicit
`writable: false` (an omitted filesystem `writable` reads as `true` in the
new shape, so a source the user deliberately protected became writable), an
explicit `enabled: false` (resuming refreshes and indexing for content the
operator had turned off), and a website source's `maxDepth` (silently
resetting crawl depth). All three now round-trip to the runtime source entry;
`bundles.<id>.enabled` is a supported key.
- **`akm mv` refuses a bundle marked `writable: false`.** It renamed the file
and rewrote citers anyway, because its preflight checked adapter
compatibility rather than writability — every other write command already
refused.
- **Memory belief edges written by `--supersedes` are no longer ignored.**
`writeSupersededEdge` persists a fully-qualified conceptId, but the belief
analyzer accepted only the internal `memory:<name>` spelling, so every edge
from `akm remember --supersedes` / `akm import --supersedes` was dropped and
a superseded memory read back as active.
- **`akm env run <ref> -- <cmd> --help` runs the command.** The builtin
help-flag scan read the child tail after `--` and printed akm's own usage
instead.
- **`akm mv` works under an `AKM_STASH_DIR` override again.** A valid override
not owned by a configured bundle failed with `No configured bundle owns move
source`.
- **An unexpected internal error exits 70 with the JSON failure envelope.** The
residual dispatch boundary exited 1 with an unstructured message, so
automation could not tell an internal defect from an ordinary failure.
- **Concurrent `akm config set` processes no longer give up prematurely.** The
contended-lock wait budget was 500ms total, so several concurrent writers on
a loaded machine could exhaust it and fail with "Timed out waiting for config
lock" against a healthy but busy lock. Abandoned locks are still reclaimed by
the stale probe, which this budget does not gate.
- **Config keys named in indexer output and comments now exist.** Four sites
pointed at a top-level `llm.*` namespace that the config schema has no such
key for — including the user-facing "Increase llm.timeoutMs" warning on an
exceeded enrichment budget. The enrichment budget lives at
`index.enrichment.timeoutMs` (or `index.defaults.timeoutMs`). Indexing
concurrency is auto-derived (2 remote / 1 local) and currently has no config
override on that path: `engines.<name>.concurrency` is a valid schema field
but the engine resolver does not forward it (documented in
`docs/architecture/internals/indexing.md`).
- **The bundle-identity-drift warning stops naming a command that doesn't
exist.** It told users to "rekey it atomically via the bundle-rename
command"; 0.9.0 ships no such command. It now gives the two remedies that
work: restore the previous bundle id in `config.json`, or keep the new id and
`akm index --full` to re-mint, accepting the loss of learned state keyed to
the old id.
- **The scaffolded `organization.md` convention no longer contradicts `akm
mv`.** It told authoring agents "there is no command that preserves an
asset's identity or learned state" across a rename and showed a raw `mv`.
`akm mv` does exactly that — it rewrites inbound refs and re-keys the index
row, usage history, and state.db salience/outcome rows. The convention now
points at it, flagged Experimental.
- **`setup.taskSchedules` is no longer documented.** The key was removed from
the schema in 0.9.0 (nothing ever read or wrote it), but
`docs/reference/configuration.md` still described its two sub-keys.
- **A freshly scaffolded stash passes its own `akm lint`.** All 12 shipped
`facts/conventions/**` convention templates carry frontmatter but none
carried an `updated` field, so the first `akm lint` after `akm init` flagged
12 `missing-updated` issues on files the user never wrote. The templates now
ship the field, and a regression test lints a freshly scaffolded stash and
requires nothing flagged.
- **`akm show akm//meta` is the documented spelling for the primary stash.**
`docs/reference/cli.md` and `docs/guides/concepts.md` showed
`akm show local//meta`, which errors with `ASSET_NOT_FOUND` — `local//` is no
longer a scoping prefix, so it reads as a bundle named `local`.
- **`akm sync` emits `shape: "sync"`.** The envelope kept the `"save"` shape
from the command's pre-rename name even after the persisted `eventType` was
renamed. Unlike the event log, the shape is per-invocation and never
persisted, so it needs no read-side synonym.
- **`akm add <pkg> --provider npm` adds an npm source instead of a broken
filesystem bundle.** `--provider` was only read inside the remote-URL branch,
so any non-URL target fell through to the filesystem path with the flag
ignored, producing a bundle pointed at `<cwd>/<pkg>`. A URL target with
`--provider npm` is now rejected at add time rather than storing the URL as a
package spec and failing much later at first sync.
- **`akm add --provider` no longer prints `Installed undefined`.** Two
incompatible result shapes reached one text formatter; each is now rendered
honestly, including whether a follow-up `akm update` or `akm index` is needed.
- **`akm update --all` accounts for every configured source.** It previously
considered only registry-managed installs and reported `nothing to update`
for a stash full of plain sources — nothing was updated because nothing was
looked at. Plain git and npm sources are now synced (npm is promoted to a
lock-backed install on first sync) and website/filesystem sources are
reported through a new `skipped` field with the reason. A successful update of
a plain source no longer renders as `nothing to update` either.
- **`akm search` with no query browses**, as `--help` has always documented,
instead of exiting 2.
- **`akm curate --type <t>` curates within the type instead of bypassing
curation.** The filter skipped ranking, intent nudges, the score floor, and
family collapse entirely — and could return a hit of the *wrong* type while
dropping a higher-scoring correct one.
- **`akm curate` respects `--limit` for registry hits**, which were capped at a
hard-coded 2 regardless.
- **`akm search --no-project-context` works.** citty strips a leading `--no-`
before consulting declared args, so a flag *declared* as `no-project-context`
could never be set — the ranking boost was identical with and without it. The
flag users type is unchanged.
- **`akm env run`, `akm secret run`, `akm migrate`, `akm agent`, `akm proposal new`,
`akm task run`, and `akm improve` no longer skip cleanup on exit.** They
called `process.exit()` directly — in two cases even on success — bypassing
teardown of spawned subprocesses. Exit codes, including forwarded non-zero
child codes, are unchanged.
- **The `blocked` semantic-search warning names the cause.** It emitted one
fixed string for every failure and discarded the status ledger's reason, so
"no embedding provider configured" and "the configured endpoint is failing"
read identically.
- **Shell completion for `--source` no longer suggests `stash|registry|both`
on commands where that enum doesn't apply.** `--source` means a closed
`stash|registry|both` enum on `akm search`/`akm curate`, but a free-form
stash name/path on every `akm graph` subcommand and a free-form URL/ref/
path on `akm remember`. The generated completion script keyed its value
list by flag name only, so the search/curate enum leaked onto `akm graph
--source <TAB>` and `akm remember --source <TAB>`. Value completion is now
scoped per command path; commands without a fixed value set get no
suggestion instead of the wrong one.
- **`akm setup --config <file>` / `--from <file>` no longer silently drops
six valid config keys** (`index`, `search`, `feedback`,
`archiveRetentionDays`, `workflow`, `experimental`). The allowlist was a
hand-copied set that had drifted out of sync with the config schema; a
user handing setup a config containing any of these keys got a different,
silently truncated config written back, with only a warning and exit `0`.
The allowlist is now derived from the schema's own key list so it cannot
drift again. Keys that remain genuinely retired (`profiles`, `llm`,
`agent`, `features`, `stashes`, `bindings`, `writable`) still warn-and-drop
as before.
Note: a config that previously relied on one of these six keys being
ignored (because the drop was silent) will now have it applied — re-check
`--config`/`--from` inputs if you were unknowingly depending on that gap.
- **`akm index` no longer persists adapter auto-detection to `config.json`
with zero disclosure.** Detecting and writing a bundle component's adapter
(`bundles.<id>.components.<component>.adapter`) previously happened
silently on every index run. It is now reported in the result envelope as
an additive `configUpdated.detectedAdapters` map and on stderr, and only
when a write actually happened.
- **`akm add owner/repo` now resolves as GitHub shorthand instead of failing
with "Local path not found".** Any ref containing a `/` was treated as an
explicit local path, so the local-ref resolver threw before the
GitHub-shorthand fallback ever ran, making the advertised `owner/repo` form
unreachable. A bare two-segment `owner/repo` (or `owner/repo#ref`) now
falls through to the registry resolver when no such directory exists on
disk; `./`, `../`, absolute, and three-or-more-segment paths still resolve
as explicit local paths exactly as before.
- **Internal output-shape command keys renamed `events-list`/`events-tail` →
`log-list`/`log-tail`**, matching the `akm log` command they back (the
command group used to be `akm events`, removed in 0.9.0). Internal-only:
the shape name is a registry lookup key that never reaches the wire (no
output field, no schema change), so this is not a user-visible behavior
change and carries no `schemaVersion` bump. The documented `[events-tail]`
stderr trailer text is deliberately left as-is pending a separate ruling.
### Removed
- **BREAKING: `akm upgrade --skip-checksum` is removed.** STABILITY.md has
always said checksum verification is not optional and that the recovery hatch
is an environment variable — but the flag shipped anyway, tab-completable,
while the documented variable existed nowhere in the source. The code now
matches the spec: set `AKM_UPGRADE_SKIP_CHECKSUM=1` if you must bypass a
genuinely broken `checksums.txt`. It is deliberately undiscoverable.
- **BREAKING: `akm config enable|disable` is removed.** It was a hard-coded
toggle for one target, the skills.sh registry, and the bare `akm enable` /
`akm disable` aliases were already removed in 0.9.0. Use
`akm registry add|remove`.
- **BREAKING: `akm mv` is removed.** No alias, no stub — `akm mv …` fails with
the standard unknown-command error. It claimed to preserve identity across a
rename, but its inbound-ref rewrite matched bare conceptIds rather than the
anchored `bundle//conceptId` prose form, so it could rewrite ordinary prose
while leaving real refs dangling. Renames are delete + create per
`STABILITY.md`: move the file, `akm index`, `akm lint`. The one capability
nothing else covered — carrying an asset's earned signal across the rename —
moves to `bun scripts/rekey-asset-ref.ts <old-ref> <new-ref>` (maintainer
tooling, `--dry-run` supported, idempotent), which re-keys the index
`entries` row in place plus the `asset_salience` / `asset_outcome` /
`usage_events` rows. The `mv` event type and output shape are gone; the
script emits a `rekey` event instead. A leftover `kind:"mv"` transaction
journal from an rc build is now swept by the recovery scanner rather than
failing it — an unregistered journal kind no longer bricks index refresh or
proposal accept/reject.
- **The CHURN alert class is removed from the collapse detector.** Its input was
a hard-coded `0` from the 0.9.0 confidence-gate deletion onward, so the alert
could never fire. The other three alert classes are unaffected. The
`improve_cycle_metrics.accepted_actions` column stays and is written as `0`
because deployed 0.8 `state.db` files already contain it.
- **`IndexResponse.graphQuality` is removed** from the `akm index` envelope — it
was declared but never assigned in any code path, so it was always absent.
- **`akm secret path` and `akm secret remove` are removed.** The two resolved a
secret ref through *different* stash-selection logic — `path` through the
read-side, all-sources resolver and `remove` through the write-target
resolver — so for a ref present in more than one stash they could name
different files: you could inspect one secret and delete another. Rather than
reconcile the resolvers, both subcommands are gone; `akm secret` now exposes
only `list`, `run`, and `set`. Both spellings exit 2 with `Unknown command`.
Migration: a ref's file lives at `<stash>/secrets/<name>` (run `akm sources
list` for stash roots) — locate or delete it directly, or use `akm secret run
<ref> <VAR> -- <command>` to consume the value without it touching disk. `akm
env path` and `akm env remove` are unaffected.
- Removed the dead `"backup"` output-shape registration left over from the
removed `akm backup` command (superseded by `akm-migrate backup`). Already
unreachable; no user-visible effect.
- **`akm task list`, `akm task show`, and `akm task remove` are removed** as
redundant with the generic asset commands. List and inspect tasks with `akm
search` / `akm show <bundle//tasks/id>` (both already cross-bundle); to remove a
scheduled task, delete its file in the owning bundle and run `akm task sync`
(sync uninstalls the orphaned scheduler entry). Run `akm task doctor` for
scheduler diagnostics — bare `akm task` is a usage error, see the canonical
bare-group change above.
- **The `akm show <ref> toc|section|lines|frontmatter|full` view-mode grammar is
removed** (0.9.0 decision D2). `#fragment` is now the only section selector,
and a positional after the ref is a usage error that names it. Migration:
| Old | New |
| --- | --- |
| `akm show knowledge/guide section "Auth"` | `akm show knowledge/guide#auth` |
| `akm show knowledge/guide full` | `akm show knowledge/guide` |
| `akm show knowledge/guide toc` | `akm show knowledge/guide#<unmatched>` — the error lists the available fragment slugs |
| `akm show knowledge/guide lines 10 30` | no replacement — every response carries `path`, so slice the file yourself |
| `akm show knowledge/guide frontmatter` | no replacement — if a raw-YAML projection proves necessary it returns as a `--shape` value |
The undocumented `--akmView` / `--akmHeading` / `--akmStart` / `--akmEnd`
flags the grammar injected into argv are gone with it.
## [0.9.0-rc.1] - 2026-06-30
### Fixed
- **improve/recombine: cap-aware decay — the `maxClustersPerRun` cap no longer
traps recurring hypotheses below `confirmThreshold` (#658).** Recombine is a
two-pass design: a cluster must be re-induced on `confirmThreshold` (=2) runs
before its `type:hypothesis` proposal promotes to an auto-accepted
`type:lesson`. But only the top-`maxClustersPerRun` (=5) clusters are
processed per run, and `decayUnseenRecombineHypotheses` hard-reset the
confirmation streak of every hypothesis not processed that run. A cluster that
genuinely re-forms every run but is displaced out of the top-5 (slots are tied
on member-count and broken by an arbitrary alphabetical tiebreak) had its
streak zeroed — it could never win two consecutive slots, so its proposal sat
pending forever (6 such proposals were stuck in one production stash).
`decayUnseenRecombineHypotheses` is now **cap-aware**: `recombine.ts` passes
the FULL pre-cap cluster set, and a hypothesis is spared from reset when its
cluster still Jaccard-matches a present cluster (same signature, overlap ≥ 0.7
— the same rule used for re-induction). Only hypotheses with no matching
current cluster (the corpus stopped supporting them) decay. This does **not**
lower the recurrence bar: the confirmation count is still advanced only by
genuine re-induction in the processed slice (`recordRecombineInduction`);
sparing merely avoids an artificial reset, so a genuinely non-recurring
hypothesis still decays to 0 and never confirms (no new bland-hypothesis churn,
cf. #632/#633). No schema change. The cap now lives in a new `capClusters`
helper split out of `buildRelatednessClusters` so the full ranked set stays
available for the decay sweep.
- **`improve` reflect no longer emits proposals doomed to fail the
`invalid-description` gate when the source asset has no frontmatter
`description` (#636).** Reflect echoed the source frontmatter, so for assets
that carry other keys but no `description` (notably scraped docs:
`source`/`title`/`scraped`) the proposal inherited the missing/empty
description and the promote-time validator (`isValidDescription`, 20–400
chars) rejected it — observed as ~14/16 rejects in one triage pass, blocking
the whole scraped-doc/knowledge cluster from reflect improvement. The fix is
**generation-time only**: (1) `buildReflectPrompt` now injects an explicit
"synthesize a `description`" instruction whenever the source lacks a non-empty
`description` and the asset type requires one (per `authoring-rules.ts`
`DESCRIPTION_TYPES`), telling the model it MUST author a valid 20–400-char
plain-prose description from the asset's `title:`/first `# Heading`/opening
body; and (2) a deterministic reflect-side belt-and-suspenders in
`sanitizeReflectPayload` — if a source that already had frontmatter still ends
up with a missing/empty description after generation, reflect derives one
deterministically from `title:`/first heading (validated against
`isValidDescription`, never free-form invention) **before** the proposal is
created. The validator, `authoring-rules.ts` bounds, `repairProposalContent`,
and the drain are unchanged — nothing in the validator/promote path fabricates
content to pass itself.
- **The high-salience improve admission lane (#608) now requires a
content-derived encoding score, not the per-type weight stub (#655,
#608/#644 follow-up).** The lane previously admitted any zero-feedback ref
whose `asset_salience.encoding_salience >= salienceThreshold` (default 0.75).
But for assets distill has not content-scored, `encoding_salience` is just the
per-type WEIGHT STUB (skill/agent 0.9, command/workflow 0.8, lesson 0.75), so
"high-salience" degenerated into "is a skill/agent/command/lesson" — which
selected the type-stub `lore-writer` agent on every run (prod: 1 content-scored
/ 37 type-stub / 1826 NULL-legacy rows). The gate now also requires
`isContentEncodingRow(row, parseAssetRef(ref).type)` (the #644 provenance
helper), so only genuinely content-scored assets qualify. This preserves
#608's intent — distilled assets, the lane's real targets, keep their real
content score and still qualify — while cutting the type-stub waste; type-stub
rows must earn retrieval/feedback signal via the other lanes. NULL-legacy rows
follow `isContentEncodingRow`'s differs-from-stub heuristic. An aggregated log
line now reports how many refs the lane admitted so lane composition is
observable. The threshold, type-weight table, 10% cap, and `isContentEncodingRow`
are unchanged.
- **Auto-sync no longer refuses to commit akm's own changes when unrelated
non-akm files are present in the stash working tree.** When a stash root is
shared with a project repo, stray files written into the stash root (e.g. a
`tasks.bak-…` backup dir or report artifacts like `data.js`,
`akm-health-report.html`, `reports/`) previously tripped the #476 safety
guard, which threw `refusing to push: … has uncommitted non-akm changes` on
**every** `akm improve` end-of-run auto-sync, `akm sync`, and `akm push`. In
one production incident this silently blocked all commits for ~1.5 days while
akm kept accepting proposals it never persisted. `saveGitStash` now **scopes
what it stages** instead of refusing: (1) an explicit modified-file list when
the caller passes `opts.paths`, else (2) the akm-managed pathspecs
(`TYPE_DIRS` values + `.akm`) that exist on disk — which by construction never
stages non-akm WIP, preserving the #476 protection without an all-or-nothing
refusal — and only as a last resort (3) `git add -A` when no managed pathspec
can be resolved. If nothing akm-managed is staged the run returns
`nothing to commit` (no empty commit, no throw). Unrelated non-akm files are
left untouched and uncommitted.
### Changed
- **BEHAVIOR CHANGE — `akm init --dir <path>` no longer silently repoints your
default stash.** Previously, `akm init --dir X` unconditionally wrote
`stashDir: X` to `config.json` whenever `X` differed from the configured
default — so initializing a throwaway or secondary stash (e.g.
`akm init --dir /tmp/scratch`) would hijack the user's real default stash
pointer (the footgun documented in `memory:akm-init-persists-stashdir-warning`).
Now `init` persists `stashDir` to config **only** when one of the following
holds: (a) **no `--dir`** was provided (the default `~/akm` setup flow —
unchanged), (b) `--dir` was provided and **no `stashDir` exists in config yet**
(first-time bootstrap), or (c) `--dir` was provided **with the new
`--set-default` flag** (explicit opt-in). Otherwise `init` still scaffolds and
backfills the target dir exactly as before, but **leaves your default stash
pointer untouched** and prints:
`Your default stash is unchanged (<existing>). Re-run with --set-default to make <dir> the default.`
The `InitResponse` JSON gains `defaultStashUpdated: boolean` and an optional
`previousStashDir`. To make a `--dir` target your default, pass
`akm init --dir <path> --set-default`. (`akm setup` is unaffected — it remains
the explicit configuration flow and always sets the default.)
### Added
- **Per-type SOFT authoring conventions are now user-editable stash facts.** A
third authoring-guidance layer joins the hard rules (#645) and general stash
standards (#642): a stash owner can author
`facts/conventions/assets/<type>.md` (e.g. `…/skill.md`, `…/command.md`) to
capture soft, type-specific guidance — voice, structure, length *preference*,
naming style. When an agent authors a `skill:x`, the body of
`fact:conventions/assets/skill` is injected (type-scoped — authoring
`command:y` pulls the `command` convention, never the `skill` one), labeled
as soft guidance and kept separate from the validator-enforced hard rules.
The basename must be a `getAssetTypes()`-validated asset type; facts are read
straight from disk (no index rebuild) and degrade to empty safely. When no
per-type fact exists, the built-in `TYPE_HINTS` fallback is unchanged (no
regression). These facts carry soft conventions only and can never weaken the
authoring contract the gate enforces (`authoringRulesForType` remains the sole
source of validator-rejecting rules). The general convention/meta resolver now
excludes `facts/conventions/assets/*` so per-type guidance never leaks
un-type-scoped into other authoring flows. (#646)
- **`akm init` now seeds default per-type SOFT convention templates.** Starter
`facts/conventions/assets/<type>.md` templates ship in the stash skeleton for
the authored types (`lesson, skill, command, agent, knowledge, memory,
workflow, script, fact`; `wiki`/`env`/`secret` excluded) so a stash owner has
an editable starting point. Each expands the matching built-in `TYPE_HINTS`
one-liner into soft starter guidance, carries `category: convention`
frontmatter, and states in-body that it is advice, not enforced — it carries
**no** validator-rejecting rules, so editing or deleting one cannot weaken the
gate (#645). The stash-skeleton copy is now recursive (preserving nested
subpaths), and `akm init` seeds **unconditionally** rather than only on first
create: re-running it on an existing stash backfills any missing skeleton,
convention, or `.meta/index.md` files. Seeding stays absent-only and never
overwrites a user-edited file. (#646)
## [0.9.0-beta.36] - 2026-06-22
### Added
- **Stash standards + wiki schemas are surfaced to authoring agents at write
time.** When an agent edits a page under `wikis/<name>/`, that wiki's
`schema.md` body is injected into the prompt; when it creates/edits a non-wiki
asset, the bodies of `category: convention`/`meta` `fact` assets are injected.
Two mutually-exclusive features selected by target type, sharing one
`standardsContext` prompt seam. Wired into reflect, propose, and every
improve authoring pass (distill, consolidate, recombine, procedural, extract,
schema-repair). (#642)
- **Unified, validator-sourced authoring-rules seam.** A new
`authoringRulesForType(type)` injects the hard authoring rules (no
pseudo-frontmatter in body, exactly two `---` fences, description/`when_to_use`
length + shape) into every authoring prompt. The numeric bounds live in one
module that the validators import, so the prompt can no longer drift from what
the gate enforces. (#645)
### Fixed
- **High-salience reflect lane now reflects each asset at most once.** The
`#608` admission gate lacked the cooldown its sibling high-retrieval gate has,
so zero-feedback assets were re-selected on every run (auto-accept emits
`promoted`, not `feedback`), burning LLM calls and churning assets. (#643)
- **Stuck validation-failing proposals no longer dead-end.** The triage drain no
longer overwrites an `auto-rejected` gate stamp with a misleading
`auto-accepted` (the failure stays truthful and visible). A bounded,
content-preserving auto-repair (strip pseudo-frontmatter / stray `---`, repair
truncated descriptions) runs at the promote boundary and re-validates — fixable
proposals promote; genuinely unrepairable ones stay `pending` for manual
review, with nothing fabricated and validation never bypassed. (#645)
- Corrected a prompt/validator drift where the distill system prompt asked for an
80–200 char description while the gate enforced 20–400. (#645)
## [0.9.0-beta.35] - 2026-06-21
### Fixed
- **Default extract discovery window is now "since the last run" (floored at 48h),
not a fixed 24h.** An intermittently-online host that was off for longer than
the old 24h window could permanently miss sessions that ended during the gap.
Discovery now looks back to the last recorded extract run for the harness, never
less than 48h. Widening is free of redundant LLM cost — the content-hash ledger
skips unchanged sessions with zero LLM calls. An explicit `--since`/`defaultSince`
still wins.
- **Per-session lock prevents concurrent double-extraction.** A session-end hook
firing `extract --session-id` while the periodic `akm improve` extract pass runs
discovery could both LLM-process the SAME session (duplicate spend + near-dup
proposals). A per-(harness, session) advisory lock (co-located with state.db,
PID + age staleness recovery) now makes the second run skip without any LLM call.
- **`minNewSessions` is read from the ACTIVE improve profile, not always `default`.**
A non-default profile (e.g. `frequent`) setting `minNewSessions` was silently
ignored because the gate (and its candidate-count discovery window) read
`profiles.improve.default`. They now read the resolved active profile, matching
how `extract.enabled` already resolves.
### Docs
- Documented that `processes.extract.indexSessions` (default on) makes a second
LLM call per processed session (the session summary); set it to `false` to halve
per-session extract cost. Unchanged/skipped sessions still cost zero.
## [0.9.0-beta.34] - 2026-06-21
### Fixed
- **`akm extract --type opencode` reads opencode's SQLite session store.** opencode
migrated session storage from per-file JSON (`storage/session/<projectId>/<id>.json`
+ `storage/message/<id>/*.json`) to a single Drizzle-managed database at
`<base>/opencode.db` (tables `session`/`message`/`part`; message text lives in
`part` rows with `data` JSON `type:"text"`). The legacy JSON layout went stale
~2026-02, so extract discovered 0 sessions on current opencode and the
`session.idle` extract hook had nothing to read. `OpenCodeProvider` now prefers
`opencode.db` when present (read-only, via the cross-driver `openDatabase` seam)
and falls back to the JSON layout. Verified end-to-end through the plugin's
`session.idle` hook.
## [0.9.0-beta.33] - 2026-06-21
### Fixed
- **`akm extract` decoupled from the improve-stage toggle.** `processes.extract.enabled`
now gates extract only as a STAGE of `akm improve` (the active improve profile, per
#593/#594); an explicit `akm extract` command always runs. Previously dropping extract
from the daily improve profile silently disabled the standalone command (and its LLM
calls, via the shared `session_extraction` feature gate).
- **`extract --session-id` now respects the content-hash ledger; `--force` overrides.**
Explicit single-session extraction previously bypassed the #602 already-extracted skip
unconditionally — re-paying the LLM on every call and risking double-extraction against
the cron. Now a targeted `extract --session-id <id>` is idempotent (skips an unchanged,
already-extracted session with zero LLM calls) and only `--force` re-extracts. This
makes a session-end hook firing `extract --session-id <id>` precise AND idempotent.
## [0.9.0-beta.32] - 2026-06-21
### Added
- **Recombine acceptance path — confirmed lessons now auto-accept.** Recombine
hypotheses that reach the confirmation threshold (promoted to `type: lesson`,
#625/#633) now flow to ACCEPTED by reusing the existing drain mechanism instead
of piling up pending forever: the `personal-stash` drain policy gains a
`{ generator: "recombine", requireType: "lesson", maxDiffLines: 200 }` rule, via
a new optional `requireType` frontmatter filter on `DrainAcceptRule`. Only
confirmed `type: lesson` proposals auto-accept; unconfirmed `type: hypothesis`
proposals stay pending; the existing proposal quality gate still applies.
- **`processes.reflect.lowValueFilter` (opt-in, default OFF)** — deterministic
semantic value-floor that defers trivial reflect rewrites (#639A).
- **`processes.extract.triage.proceduralAwareFloor` (opt-in, default OFF)** —
triage floor requiring markers/edits so real lessons always pass (#641).
### Fixed
- **Select-time proactive cooldown leak.** `selectProactiveMaintenanceRefs` plans
the due set BEFORE acquiring `reflect-distill.lock`, so overlapping/back-to-back
improve runs reused stale due-state and re-reflected the same asset repeatedly
(observed up to ~16× in a day). The orchestrator now re-applies the dueDays gate
with freshly-read timestamp maps INSIDE the lock (`filterProactiveDue`), dropping
refs a concurrent run already reflected.
## [0.9.0-beta.31] - 2026-06-20
### Changed
- **#632 — recombine now filters junk tags structurally.** Frontmatter tags that
are pure numbers, dates (`20260529`), short hex hashes (`002c624c`), version
strings (`0.8.0`, `v2`), single chars, or common English stopwords (`is`, `the`,
`for`, `when`, …) carry no topical signal and never form a recombine cluster.
Unlike `excludeTags` (a fixed project list), this catches the OPEN-ENDED junk —
every new date or commit hash — with no config upkeep. Exposed as `isJunkTag`.
On the live stash this turns the recombine cluster set from generic 66–171-member
buckets into tight topical clusters (`auth`, `architecture`, `patterns`, …).
## [0.9.0-beta.30] - 2026-06-20
### Changed / Fixed
- **#632 — recombine cluster tuning (opt-in, default-preserving).** Recombine
clustered memories by frontmatter tag and preferred the LARGEST buckets, so it
always picked the coarsest whole-stash tags (`session`/`claude`/`akm`, 63–171
members) and produced bland generalizations. Two new `processes.recombine` knobs:
`maxClusterSize` (skip clusters larger than N, so over-broad buckets no longer
reach/starve the largest-first slice) and `excludeTags` (tags that may never form
a tag cluster). Both UNSET = byte-identical to prior behavior.
- **#633 — recombine confirmation loop fixed.** The hypothesis confirmation streak
was keyed on a hash of the EXACT member set, so a growing stash drifted the key
every run → a fresh row at count 1 → `confirmThreshold` never reached → no
hypothesis ever promoted to a lesson (a dead two-pass loop). A freshly-induced
cluster now matches an existing pending row by signature + Jaccard
membership-overlap (≥ 0.7) and reuses its stable ref, so the streak accumulates
through membership drift. First/non-overlapping induction is unchanged.
## [0.9.0-beta.29] - 2026-06-20
### Reverted
- **#630 — `fact` asset type phase 2 reverted (#631).** The pinned-core assembly +
`akm fact` CLI shipped in beta.28 was reverted pending rework. Phase 1 (#629, the
`fact` asset type itself) remains in place.
## [0.9.0-beta.27] - 2026-06-20
All new behavior is **opt-in / default-preserving** — default runs are byte-identical.
### Added
- **#624 P2 — priority-ranked graph extraction.** `processes.graphExtraction.topN`:
when set, the graph-extraction pass ranks eligible files by asset utility
(`utility_scores`, read-only join) and processes only the top-N per run, so
high-value assets get graphed first instead of a ~55h full-corpus sweep. Unset
(default) = no ranking, byte-identical.
- **#624 P3 — lazy on-demand graph extraction.** New `graph_extraction_queue` table
+ `enqueueGraphExtraction`/`drainExtractionQueue`/`extractGraphForSingleFile`.
`akm curate` enqueues an ungraphed hit (non-blocking); `akm show` can extract a
missing graph inline — gated on `index.graph.lazyGraphExtraction: true`
(**default off**: `show` makes no LLM call by default), model-guarded, and bounded
by a 30s timeout so it never hangs. The pass drains the queue before the ranked
sweep. This **closes #624** (all three layers shipped).
- **#616 — bounded multi-cycle phasing.** `profiles.improve.<name>.maxCycles`
(default 1): when > 1, the improve passes run in an N-cycle loop so gate-accepted
output of cycle N feeds cycle N+1 within the same run (re-running ensureIndex +
ref selection each cycle), stopping at a fixed point and respecting the run budget.
`maxCycles: 1` = byte-identical to today.
### Fixed
- **Release CI unblocked.** `runCliCapture` (test harness) restored `process.exitCode`
to a captured `undefined`, which under `bun test` does not clear a previously-set
non-zero exit code — so the unit suite exited 1 with 0 failures at `TEST_PARALLEL=1`
(exactly how `release.yml` runs), silently blocking every npm publish since beta.11.
Fixed to restore to `0`. (This is why beta.26 was the first successful workflow publish.)
### Changed
- **CI/release tests sharded across runner jobs (~15 min → ~2 min).** Bun 1.3.x
in-process test parallelism (`--parallel=N`, N>1) hits an intermittent
`epoll_ctl EEXIST` race / busy-spin hang on the `--isolate` workers, which had
forced fully-sequential (`TEST_PARALLEL=1`) runs. Tests now shard across separate
runner jobs (each a separate process tree, so no cross-shard fd/epoll collisions)
with `--parallel=1` within each shard; the matrix runs shards concurrently. The
release gate runs the identical set of tests. Local `bun run check` defaults to
sequential too (the only safe mode on this Bun version). Coverage unchanged.
Each shard runs through `scripts/run-test-shard.sh`, which retries **only on a
hang/timeout** (the busy-spin can rarely fire even at `--parallel=1`) and never
on a real test failure, so genuine red tests still fail fast and are never masked.
## [0.9.0-beta.26] - 2026-06-20
### Added
- **#628 — configurable SQLite journal mode (`AKM_SQLITE_JOURNAL_MODE`) for network
filesystems.** AKM previously opened every database with `PRAGMA journal_mode = WAL`
unconditionally, which cannot run on a network filesystem (NFS/SMB/Azure Files) —
WAL's `-shm` shared-memory wal-index can't be `mmap`'d over a network mount. You can
now set `AKM_SQLITE_JOURNAL_MODE` to `WAL` (default), `DELETE`, or `TRUNCATE`, applied
at **all five** db openers (`state.db`, `index.db` ×2 paths, `workflow.db`, `logs.db`).
At the `WAL` default AKM auto-detects a network mount for the data dir and transparently
falls back to `DELETE` (rollback journal + `synchronous = FULL`) with a one-line warning;
invalid values warn once and fall back to `WAL`. **Default behavior is byte-identical.**
This lets the AKM database subtree live on a shared volume (e.g. Azure Files under
Azure Container Apps). New docs section "Hosting AKM databases on a network share
(NFS/SMB)" in `docs/configuration.md`.
## [0.9.0-beta.25] - 2026-06-19
Completes the recombine / extract-efficiency / graph thread. All new improve
passes are **opt-in (default off)**, so default behavior is unchanged.
### Added
- **#606 — event-driven extract (`akm extract --watch`).** Opt-in watch mode: an
injectable, debounced watcher triggers extraction shortly after a session file
appears, with a clean `stop()` handle. The `8,28,48` cron remains the fallback;
no daemon is auto-launched.
- **#625 — recombine second pass (hypothesis → lesson).** The opt-in `recombine`
process (#609) now consumes `confirmThreshold` (default 2): a generalization
re-induced that many consecutive runs is promoted from a `type: hypothesis`
proposal to a `type: lesson` proposal through the normal queue + quality gate
(never a direct stash write). Hypotheses that stop recurring decay. Backed by a
new `recombine_hypotheses` table in `state.db`.
### Changed
- **#624 (P1) — graph storage decoupled from `entries.id`.** `graph_files` is
re-keyed on `(stash_root, file_path, body_hash)`, so extracted graph data now
**survives a reindex** of unchanged files instead of being cascade-wiped. The
upgrade is migrated in a **targeted, graph-only path** that preserves existing
graph data and leaves the entry index, embeddings, FTS, and LLM-enrichment cache
untouched — **no full index rebuild and no re-embed** on upgrade. (P2 priority-
ranked extraction and P3 lazy/on-demand extraction remain deferred.)
### Fixed
- Graph re-key migration no longer triggers a destructive full-index rebuild: it
is a graph-scoped table migration (no `DB_VERSION` bump), and it **copies** the
existing graph rows into the new schema rather than dropping them.
- Test-suite `/tmp` hygiene: sandbox teardown now fires on `SIGINT`/`SIGTERM`/
`SIGHUP` (not just clean exit), and a `sweep:tmp` step reclaims stale `akm-*`
sandbox dirs left by force-killed workers — eliminating the tmpfs accumulation
that caused intermittent `EEXIST: epoll_ctl` test flakes.
## [0.9.0-beta.20] - 2026-06-18
### Fixed
- **`akm update --all` no longer fails for writable `github:` entries stored as `source:"git"`**. `updateRegistryEntry` was using `synced.source` (re-derived from the ref scheme as `"github"`) instead of the existing `entry.source`, causing the config validator to reject `writable:true` on every update cycle.
## [0.9.0-beta.19] - 2026-06-17
### Fixed
- **`akm feedback` now completes in ~0.3s** (was 3+ minutes). Root cause: the command was calling `ensureIndex` with `mode: "blocking"` inside `withIndexWriterLease`, triggering a full reindex on every feedback call. Fix: removed the `ensureIndex` call entirely (feedback only needs the index to exist, not be current — a stale index is fine for ref lookup); removed the application-level writer lock (SQLite WAL + `busy_timeout=30s` handles concurrent access with `akm improve`); added a fast DB-exists guard with a clear error for first-time users.
- **`akm health --format html` now completes in ~11s** (was ~18s). Root cause: `akmHealth()` was called twice — once for the main result and once to get `deltas`. Fix: merged into a single call passing both `groupBy: "run"` and `windowCompare` together.
## [0.9.0-beta.18] - 2026-06-17
### Changed
- **Health report: Recent Runs table now shows all filtered runs in descending order** (newest first) instead of capping at the last 10.
- **Health report: Removed "Command Set Used" section.**
- **Health report: All timestamps now display in the viewer's local timezone** (chart axis labels, runs table, freshness line, executive summary, footer). Server-rendered ISO strings are wrapped in `<time data-iso>` elements and converted to local time by client-side JS on page load.
### Changed (migration required)
- **WS-2 outcome loop (#613) — default-off weight change (state.db migration 010).**
Every `akm improve` run now writes an `asset_outcome` row per processed asset
(state.db migration `010`) and computes a differential usefulness signal
(`outcome_score`) per ref. The outcome signal is persisted and visible in the
health report, but the **weight change is gated behind a config flag** (see
below). Ranking is unchanged from WS-1 by default.
**Opt-in weight change.** The WS-2 projection weights (`w_e=0.25, w_o=0.15,
w_r=0.60`) affect ranking only when you explicitly set
`improve.salience.outcomeWeightEnabled: true` in your `akm.yaml`. The default
(`false`) keeps WS-1 parity weights (`w_e=0.30, w_r=0.70`, `w_o=0`), so
existing users see no ranking change on upgrade.
**Part-V measurement gate.** Before enabling the weight change, run the Part-V
T0 baseline (`scripts/akm-eval` + `akm health`; confirm proactive accept
≥ 0.9× reactive; reversion ≤ 0.15; retrieval-delta ≥ 0; coverage not
regressed). That gate requires a running production stash and cannot be
exercised in CI. Once confirmed, set
`improve.salience.outcomeWeightEnabled: true` to activate the three-way split.
**Outcome loop mechanics.** `outcome_score` is a differential prediction-error
signal: `(retrieval_delta − expected_delta) − PENALTY × retrieval_delta × (1 −
accepted_change_rate) + valence`, tracked via an EMA (α=0.3). New rows are
warm-started from the utility EMA score (clipped to 0.3) so the signal is
non-zero from launch. A stash-wide diversity floor (10% of the max score) prevents
rare-but-correct assets from being permanently outcompeted. An inverted-proxy
tripwire (`corr(outcome_score, accepted_change_rate) < −0.3`) emits an
`outcome_proxy_inverted` health event when the signal degrades.
`review_pressure` is computed and persisted per asset but is **not yet wired into
the admission policy** — that is deferred to a later work stream per plan §Part-VI
#613. The column is present and populated; routing it into the consolidation-
selection filter is the next step.
- **WS-1 salience vector (#618) — default-on ranking change.** The eligibility sort
for all `akm improve` runs (whole-stash, type, and ref scope) has changed from
`combinedEligibilityScore = utility·0.7 + negativeOnlyRatio·0.3` to
`rankScore = (0.3·encodingSalience + 0.7·retrievalSalience) × sizePenalty`
(feedback valence and utility EMA dropped from ordering until WS-2 re-introduces
outcome salience). Assets are now ranked by retrieval frequency × recency × type
importance rather than by feedback magnitude. Because the old
`combinedEligibilityScore` ordering was never persisted, a forgetting comparison is
not possible on the first run; instead a one-time `improve_salience_first_run` marker
event is emitted to record the transition. On every subsequent run a stash-wide
`improve_salience_rank_change` drift report (including `stashSize`) is emitted so
rank movement under the new scoring can be tracked over time.
The Part-V measurement protocol (T0 baseline via `scripts/akm-eval` + health report,
throughput/quality gate) is deferred to the WS-2 milestone, when outcome salience
re-joins the projection and re-tuning is triggered.
## [0.9.0-beta.12] - 2026-06-15
Improve-tuning work streams (all **default-off / parity-preserving** — no behavior
change until explicitly enabled).
### Added
- **#617 — deterministic near-duplicate memory dedup** (`processes.consolidate.dedup`,
default off). A cheap no-LLM pre-pass in front of consolidation collapses obvious
duplicates — `.derived`+origin pairs and content twins (normalized content-hash
equality, or embedding cosine ≥ `cosineThreshold`, default 0.97). Each dropped
variant is archived + backed up before deletion; hot memories are never
collapsed; distinct-but-related memories fall through to the LLM.
- **#581 — judged-state cache for consolidation** (`processes.consolidate.judgedCache`,
default off). New state.db table (`consolidation_judged`) records each memory's
content hash + outcome when the LLM judges it; subsequent runs skip
judged-unchanged memories, converting coverage from O(time-window) to
O(changed/new) so a run can sweep the full corpus. Fails open; failed chunks
and dry-runs never poison the cache. (state.db migration `007`.)
- **#612 — auto-accept gate calibration** (`improve.calibration`, auto-tune default
off). Joins predicted gate confidence to realized accept/reject outcomes into a
reliability table + calibration gap, surfaced in `akm health` (+ summary rows in
the HTML report). Opt-in bounded threshold auto-tune nudges the accept threshold
within a configured band toward a target accept rate, logged via a
`calibration_autotune` event. (Replay-prioritization from prediction error is
deferred — it depends on the #610 replay budget, a 0.10 item.)
### Fixed
- **#614 — symmetric valence weighting** (`profiles.improve.*.symmetricValence`,
default off). The eligibility sort weighted feedback negative-only; when enabled
it uses a symmetric `|valence|` magnitude so strong positive and strong negative
feedback both drive attention (utility stays the dominant factor), routing
high-negative → fix and high-positive → reinforce lanes.
## [0.9.0-beta.11] - 2026-06-15
### Added
- **`extract.maxSessionsPerRun`** (default 25) — caps the NEW sessions the
extract pass LLM-processes in a single run so a backlog (e.g. after downtime)
can't push one run past its scheduled-task timeout. Overflow sessions stay
unseen and are picked up by later runs, so coverage is preserved. `0` disables.
### Fixed
- **Auto-accept validation failures are no longer a blind leak.** When a
confidence-passing proposal fails promotion validation, the gate now captures
the reason (the `validateProposal` finding kind, e.g. `validation:description-quality`),
records it on the proposal (`akm proposal show` explains the rejection), logs
it, and exposes `failedByReason` on the gate result — so the ~5% leak is
diagnosable instead of silently warned-and-dropped.
- **Inflated skip-reason aggregates in `akm health`.** `no_new_signal` /
`profile_filtered_all_passes` are per-run snapshots of a stable set; the
window aggregator summed their per-run counts (≈2.7M / 3M). It now uses the
most recent run's count for these aggregated-snapshot reasons while still
summing genuine per-occurrence skips.
## [0.9.0-beta.10] - 2026-06-15
### Added
- **#603** — `akm health` pool-saturation advisory. Instead of alerting on the
raw `sessionsScanned` count (which false-alarmed on normal cadence changes),
a new `pool-saturation` advisory reports the ratio of new (unseen) sessions
to the total session pool: informational below 10% (expected steady state),
warning below 2% (possible discovery/dedup bug). Heuristic, never gates
overall status.
- **#576** — the `akm health` HTML report now renders the real per-stage LLM
token/time aggregate (a "🧠 LLM Work" KPI card + LLM token/call/wall-time
summary rows) from the captured `llm_usage` events, replacing the GPU-time
proxy.
- **Built-in `akm health --format html` report overhaul** — the report is now a
strict superset of (and supersedes) the external `akm-health-report` stash
skill. Restored the interactive filter bar (time-slice 1d–21d, task, status)
with client-side chart/table re-render and the Last-10 "Task" column;
reordered sections to a decision-first flow (verdict → action items → KPIs →
table → charts); added a synthesized one-sentence **Verdict** (status + 2–3
drivers) and a freshness line; merged the duplicate Advisories / What-to-Watch
into one prioritized, de-duplicated **Action Items** list (P1/P2/P3 +
remediation command); added a per-stage **LLM token** stacked-bar chart and
`dataZoom` sliders on dense charts; fixed the failed-run scatter x-alignment
(now shape-encoded); KPI-card colors are now health signals (not decoration);
added metric-glossary tooltips, chart `aria-label`s, contrast fixes, and
empty-state overlays. Deterministic output preserved.
### Fixed
- **Health report accuracy** (follow-ups to the overhaul): the per-run **Task**
column/filter now show the real scheduled task (`akm-improve-frequent`, …) via
a ±5min `task_history` join instead of the run's scope (which is `all` for
every scheduled run); the time-**slice** filter options are now derived from
the report's `--since` window (e.g. All/3d/1d/12h/6h for a 7d report) and
default to "All" — replacing the hard-coded 1d–21d list that didn't match the
window; and the trend **deltas** now default their compare window to `--since`
(like-for-like, e.g. last 7d vs prior 7d) instead of a fixed 24h, which had
produced nonsensical period-over-period percentages on multi-day reports.
- **Inflated stash-snapshot metrics in `akm health`.** `memorySummary`
(derived/eligible) and `profileFilteredRefs` are whole-stash snapshots recorded
on every run, but the window aggregator was **summing** them across all runs —
e.g. "915,258 of 1,226,025 eligible" and a 2.4M filtered-ref count. They now
take the most recent run's snapshot (the current state). Per-run *work* metrics
(promoted, MI written, graph entities, …) remain genuine window sums.
- **Health report polish:** the akm version is stamped in the header (under the
AKM logo) and footer; the steady-state `no new signal since last proposal`
distill reason is excluded from the skip-reason chart (it drowned out the
actionable reasons); and the Consolidation Output chart now draws Promoted as a
line on a secondary right-hand axis (it dwarfs merged/deleted) with merged and
deleted as bars on the left axis.
- **#598** — process-level tuning fields (`consolidate.incrementalSince`,
`minPoolSize`, `neighborsPerChanged`, `extract.minContentChars`, per-process
`enabled` flags) now survive an `akm config` rewrite. They are first-class
typed `ImproveProcessConfigSchema` fields, so the load→save round trip no
longer silently drops them. Unknown process sub-keys hard-error at load
(`ConfigError`) rather than being silently discarded — the deliberate,
documented resolution. Regression-guarded by
`tests/config-process-roundtrip.test.ts`.
## [0.9.0-beta.9] - 2026-06-14
Restore and instrument `akm improve` steady-state output. The reflect/distill
self-improvement lanes had been near-zero in steady state because the
signal-delta eligibility gate was the only lane (cache "no-access = no-work"
pathology) and the high-retrieval fallback was structurally dead. This release
revives proactive improvement, adds attribution + a measurement/kill-criterion
system so the lane must prove its value, and right-sizes reflect budgets to
their task timeouts.
### Added
- **Proactive maintenance selector** (`proactiveMaintenance` improve process):
due-gated, composite-priority (`importance × log(1+retrievalFreq) ×
recencyDecay / log(size)`), bounded rotating top-N reflect/distill over
stale/never-reflected assets. **Disabled by default**; enable per profile.
- **Eligibility attribution**: every reflect/distill proposal is stamped
`eligibilitySource ∈ {signal-delta, high-retrieval, proactive, scope,
unknown}` on `reflect_invoked`/`distill_invoked`/`promoted` events and the
proposal record, so outcomes are sliceable by lane.
- **Measurement system** under `scripts/akm-eval/`: a real-query retrieval suite
generated from `usage_events`, and `akm-eval-proactive-verdict` — a read-only
kill-criterion runner comparing the proactive lane (treatment) vs due-but-
untouched assets (control). Emits PASS/FAIL/INCONCLUSIVE and recommends
disabling the lane on FAIL. New `proactive_selected` event +
`proactiveSelected`/`proactiveDueTotal`/`proactiveNeverReflected` fields on
`improve_completed`.
### Fixed
- Revived the P0-A high-retrieval fallback: genuinely zero-feedback assets were
routed to the fully-skipped branch one phase before the fallback could see
them, so frequently-retrieved-but-never-rated assets were never improved.
- `getRetrievalCounts` now normalizes bare vs `origin//`-prefixed refs (it was
dropping ~half the retrieval signal) and counts `curate` events
(`akm curate` now records per-item `entry_ref`).
- The fully-skipped `no_new_signal` branch emitted one `improve_skipped` event
per ref (~11K writes/run, ~400K rows/day) — a contributor to 900s improve
timeouts and state.db bloat. Collapsed into one aggregated counted event.
## [0.9.0-beta.8] - 2026-06-13
Fix multi-process SQLite contention in `index.db` and harden concurrent proposal
queue mutations.
### Changed
- Added a global `index.db` writer lease used by foreground indexing,
background auto-index, improve maintenance index writers, graph updates, and
feedback writes.
- Replaced the racy background index PID-file dedup flow with lease-based
coordination and explicit handoff to the spawned worker.
- `akm feedback` now uses blocking index preparation and writes under the same
`index.db` lease, avoiding self-inflicted `database is locked` failures.
- Proposal queue create/archive/gate-decision mutations now run under
`BEGIN IMMEDIATE` state.db transactions so concurrent processes serialize on
live queue state.
## [0.9.0-beta.7] - 2026-06-13
Fix the `akm improve` regression introduced by background `ensureIndex`.
### Changed
- Added an explicit `ensureIndex` mode so callers choose `background` or
`blocking` behavior directly instead of relying on hidden environment state.
- `akm improve` now uses blocking index preparation before collecting eligible
refs, restoring the post-upgrade empty-index recovery path.
- Removed the `AKM_INDEX_INLINE` test-only override so tests exercise the same
index behavior model as production.
## [0.9.0-beta.6] - 2026-06-12
Pipeline optimization: new per-process config fields wire up the consolidation
and improve pipeline knobs exposed by the optimization report — incremental
consolidation, pool caps, distill gating, and memory inference throttling.
### Added
- **`consolidate.incrementalSince`** — profile config field that narrows the
consolidation candidate pool to memories modified within the given window
(e.g. `"1h"`, `"4h"`) plus their graph neighbours. Enables frequent
consolidation passes (e.g. `quick-shredder` every 15 min) without full-pool
sweeps. Absent = full-pool sweep (correct for nightly runs).
- **`consolidate.limit`** — hard cap on memories processed per consolidation
pass, applied after incremental narrowing. Prevents runaway full-pool sweeps
in the nightly default profile.
- **`consolidate.neighborsPerChanged`** — configurable graph-neighbour count
per changed memory during incremental consolidation (was hardcoded to 5).
`quick-shredder` sets this to 3 for a 40% candidate reduction per burst.
- **`distill.requirePlannedRefs`** — when `true`, the distill process is
skipped entirely for distill-only refs when the reflect phase produced zero
planned refs. Eliminates hundreds of `distill-skipped` events on quiet passes
where all refs are on reflect cooldown.
- **`memoryInference.minPendingCount`** — minimum pending split-parent memory
count below which the inference pass is skipped entirely (zero LLM calls).
Prevents lock acquisition on passes where there is nothing to infer.
- **`reflect.limit`** — per-process ref limit for the reflect/distill loop,
applied as the improve run limit when no CLI `--limit` is given.
- **New `reflect-distill` improve profile** — dedicated reflect + distill +
memoryInference + triage profile for the every-4h `akm-improve-frequent`
task. `reflect.limit: 25` bounds LLM cost per pass.
### Changed
- **`quick-shredder` profile tuned**: `incrementalSince` `4h` → `1h`,
`maxChunkSize` 25 → 35, added `minPoolSize: 10`, `neighborsPerChanged: 3`,
`memoryInference.minPendingCount: 5`. All `profile: "qwen-9b-shredder"`
process references removed — falls back to default LLM.
- **`default` improve profile** (nightly): extract disabled (dedicated
`akm-extract` task runs at 01:48), consolidate gets `limit: 500`,
reflect gets `limit: 100` and `allowedTypes`, distill gets
`requirePlannedRefs: true`, triage enabled at 50 accepts/run,
graphExtraction explicitly enabled.
- **Cron schedule optimised**: extract reverted to `8,28,48 * * * *` (3×/hr),
quick-shredder shifted to `4,19,34,49` (4-min extract gap), health-report
shifted to `:03` (avoids `:00` collision), `akm-improve-frequent` re-enabled
at `45 */4` with `reflect-distill` profile.
## [0.9.0-beta.3] - 2026-06-12
Stabilization batch closing the remaining 0.9.0 milestone: DB-locking and
improve-pipeline perf backports, extract/reflect gate fixes, SQLite-first
proposal and log storage, `--format html` output, and per-stage LLM telemetry.
### Added
- **`--format html` output with per-command templates** (#582). `akm health
--format html` renders the full interactive health report (ECharts inlined by
default, or via CDN with `AKM_ECHARTS=cdn`); every other command falls back to
a dark-mode default template that pretty-prints its JSON. A global `--output
<path>` flag writes the rendered HTML to a file instead of stdout. Token
replacement only — no template engine. The standalone health-report skill is
now folded into core.
- **Per-stage LLM telemetry** (#576). Every `chatCompletion` call now records
tokens (prompt/completion/total/reasoning), wall-time, model, and
finish_reason as an `llm_usage` event, attributed to the pipeline stage via an
ambient `AsyncLocalStorage` context (`withLlmStage`) set once per phase — no
`stage` parameter threaded through call sites. `akm health` exposes per-stage
token and time aggregates. Telemetry is best-effort and can never fail a run;
capture is forward-only.
- **Per-proposal gate decision + confidence** (#577). When a proposal passes
through the auto-accept/triage gate, its outcome (`auto-accepted` /
`deferred` / `auto-rejected`), reason, confidence, measured value, and the
thresholds in effect are persisted on the proposal (in the SQLite metadata).
`akm proposal show`/`list` surface them with reconstructable comparisons
(e.g. `0.72 < 0.90`), so tooling can explain *why* each proposal is pending
instead of relying on a run-level aggregate. Forward-only; legacy proposals
render `unknown`.
### Fixed
- **`SQLITE_BUSY` / "database is locked" under concurrent runs** (#584, #585,
#589). `busy_timeout` raised from 5 s to 30 s on every SQLite open path
(index.db and state.db); the improve maintenance pass now closes its index.db
handle before each reindex (which opens its own writer to the same WAL file);
and the post-loop purge reuses the long-lived events connection instead of
opening a second state.db writer. Together these eliminate all observed
lock failures from overlapping cron improve runs. (Backports of 0.8.8.)
- **Extract gate ignored the active profile's `extract.enabled: false`** (#593,
#594). The session-extraction gate hardcoded the `default` profile, so a
non-default profile (e.g. a quick pass) ran extract anyway — 300–600 s of
redundant work per run when a dedicated extract task also exists. The gate
now resolves `extract` against the active improve profile. (Backport of
0.8.11.)
- **Memory inference burned LLM calls on already-derived parents** (#588). The
primary pass now checks for the `<parent>.derived.md` child on disk *before*
the LLM/cache call, and opportunistically marks the parent processed so it
never re-pends. Previously ~55 % of the inference budget was spent
rediscovering children that already existed.
- **Reflect no longer queues empty-diff or cosmetic-only proposals** (#580).
A deterministic, LLM-free noise gate diffs each candidate against the current
asset; byte-identical edits are dropped and changes that are pure formatting
(whitespace reflow, hard-wrap changes, code-fence language hints, YAML scalar
re-folding) are suppressed, each recorded via summary events so suppression
rates are visible in `akm health`.
### Added
- **`minContentChars` pre-LLM extract gate** (#595, #596). Sessions whose raw
size is below `profiles.improve.<name>.processes.extract.minContentChars`
(default 10 — only truly empty sessions/journal files) skip the extract LLM
call entirely. Gates on raw input size, not post-noise-filter size.
(Backports of 0.8.12–0.8.14.)
- **Structured logs database** (#579). Task and run log lines now land in a
dedicated `logs.db` (WAL, 30 s busy_timeout) keyed by task, run, stream, and
time, with retention/purge wired into the existing purge pass and `ATTACH`
support for joining log lines to `state.db` rows (e.g. a failed
`task_history` row to its log output). The scattered-log audit and per-source
keep/move/drop decisions are documented in `docs/technical/logs-audit.md`.
### Changed
- **Proposals are now stored canonically in SQLite** (#578). The previously
bypassed `proposals` table in state.db is the single source of truth; all
proposal commands (`list`/`show`/`diff`/`accept`/`reject`/`revert`/`drain`),
the improve auto-accept gate, and health metrics read and write it through
one storage layer. Pending file-based proposals are imported on first read;
`akm proposal *` UX is unchanged. Design and migration notes live in
`docs/technical/proposal-storage.md`.
- **Improve planning no longer does per-ref DB lookups or per-ref skip events**
(#591, #592). Eligible refs carry a pre-resolved `filePath`, removing a
serial async lookup per ref (~500 s on 9 k-ref stashes), and the
profile-filtered skip loop emits one summary event with a count instead of
thousands of rows. (Backports of 0.8.9–0.8.10.)
## [0.9.0-beta.2] - 2026-06-09
### Fixed
- **Consolidation starved merge recall; the memory pool grew unbounded.** Commit
`633ece41` made the `incrementalSince` narrowing unconditional, so every
consolidation run only judged memories changed since the last run plus their
immediate vector-neighbors. Stale-but-unmerged duplicate clusters were never
re-examined, so the eligible pool grew monotonically and never shrank, and
contradiction detection (which rides on the consolidation pass) went dark.
Consolidation only runs on the nightly default-profile pass (`quick`/`frequent`
disable it), so a full-pool sweep is correct and affordable; the override is
removed. `lastConsolidateTs` still gates whether the pass runs. (Forward-port
of the 0.8.5 fix.)
- **`akm tasks sync` ignored schedule changes** — forward-ported from 0.8.4.
Sync classified any task already present in the OS scheduler as "unchanged"
without comparing its installed entry, so editing a task's `schedule:` in the
`.yml` never reached the crontab; the same gap affected `tasks enable`/`disable`
(toggled the comment, re-enabling a stale schedule). Sync now compares the
backend's installed signature against the signature the current definition
renders to and reinstalls on drift (new `updated[]` field); `enable`/`disable`
reinstall from the current `.yml`. The cron backend gains `expectedSignature()`
and a per-entry signature on `list()`; other backends fall back to an
idempotent reinstall.
### Added
- **`akm improve --skip-if-locked`** — forward-ported from 0.8.4. When another
improve run already holds the lock, the run logs and exits 0 with a no-op
result (`skipped.reason: "lock-held"`) instead of failing with the "already
running" config error (exit 78). Intended for high-frequency scheduled runs
(e.g. an every-30-min `quick` pass) that overlap a longer run. Default off.
### Removed
- **`akm config edit`** — the interactive menu-based editor was removed. A
prompt-driven drill-down was clunkier than just editing the file. Edit the
config directly (the path is shown by `akm config path`), use
`akm config set/get/unset` for scripted changes, and `akm config validate` to
check it.
## [0.9.0-beta.1] - 2026-06-08
### Fixed
- **`improve.lock` leaked on signal death (cron timeout)** — forward-ported from
0.8.3. The improve SIGTERM/SIGINT/SIGHUP handler calls `process.exit()`, which
skips `finally` blocks, so the `finally` releasing `improve.lock` never ran and
every timed-out cron run leaked the lock. It is now released from a
`process.on("exit", …)` handler registered at acquire time, via a new
ownership-checked `releaseLockIfOwned(path, pid)`.
- **`quick` profile was not quick** — forward-ported from 0.8.3. It did not
disable the default-ON session-`extract` process, so a `quick` run processed
the entire session backlog (~40 min). `quick` now sets
`processes.extract.enabled: false`.
- **`akm-eval` smoke suite adapted to the 0.9.0 CLI** (CI/tooling only). The
eval harness called `akm search --detail agent`, but 0.9.0 moved the
agent/summary projections to `--shape`; it now uses `--shape agent`.
Additionally, the improve-run history readers (`listRecentImproveRunIds` /
`resolveImproveRunId`) treated a missing `state.db` as an error rather than
"no runs", which broke the read-only smoke + replay-determinism gates on a
fresh checkout; a missing `state.db` is now handled as an empty history.
## [0.9.0-beta.0] - 2026-06-08
### Added
- **Cross-runtime: akm now runs on Node.js >= 22 in addition to Bun** (#560,
#465). A two-file runtime boundary (`src/storage/database.ts` owns SQLite via
`bun:sqlite` on Bun / `better-sqlite3` on Node; `src/runtime.ts` owns every
`Bun.*` API) contains all runtime-specific code, enforced by a lint guard so it
cannot leak back out. A CI `node-smoke` matrix runs the built CLI under Node
20 and 22. The prompts dependency (`@clack/core`) uses `node:util.styleText`,
added in Node 20.12; Node 18 is EOL and unsupported. The npm package uses Node
as its bootstrap and prefers a working Bun >= 1.0 for execution when both are
available. Old, unusable, or absent Bun installations fall back to Node.js;
standalone binaries remain runtime-free.
- **`session` asset type — agent sessions are now searchable** (#561). The
`extract` pass, after distilling memory proposals from a session, additionally
writes the session itself as a first-class `session` asset
(`sessions/<harness>/<id>.md`) with an LLM-generated `## Summary` /
`## Key topics` body plus `harness` / `session_id` / `started_at` / `ended_at`
/ `project` / `log_path` / `access` frontmatter. Sessions become discoverable
via `akm search --type session` and `akm curate`, and the `access` + `log_path`
fields tell any agent how to open the raw session log. The behaviour is
ADDITIVE, FAIL-OPEN, and config-gated via
`profiles.improve.default.processes.extract.indexSessions` (default on when an
LLM is configured; set `false` for byte-identical legacy extract behaviour) and
`…extract.minSessionDuration` (default 5 minutes). Session assets are not
graph-extracted. No new LLM call is made when no provider is configured.
- **`akm env set` / `akm env unset` — single-key `.env` management.** `akm env
set <ref> <KEY>` sets/updates one key (value from stdin by default, or
`--from-env <VAR>` / `--from-file <path>` — never argv, never echoed); `akm env
unset <ref> <KEY...>` removes one or more keys. Both do a minimal edit that
preserves existing comments and key order, and use `dotenv` as the
serialisation oracle: a value is only written if `dotenv.parse` reads it back
exactly, and the whole edit is re-verified so no sibling key is disturbed. This
reintroduces key-level management (the deprecated `vault set`/`vault unset`
pointed here); `akm env remove` still removes the whole file.
- **`--path` for subdirectory asset creation** (#503) — a consistent `--path
<relative-dir>` flag across the asset-creating command surface: `akm remember`,
`akm import`, `akm propose`, `akm workflow create`, `akm env create`, and
`akm secret set`. `--path` is a directory applied rooted at the asset's type
directory (e.g. `akm remember "buy milk" --path personal --name grocery-list`
→ `memories/personal/grocery-list.md`; `akm workflow create ship --path
release` → `workflows/release/ship.md`). The filename/name still comes from the
`--name`/name positional (or, for `remember`/`import`, the content/source slug).
The explicit name is now a **flat** name everywhere: a `/` in it is rejected
with guidance to use `--path`. System-derived names (e.g. a URL-path-derived
knowledge name from `akm import <url>`) may still nest. Shared semantics live in
`src/core/asset-create.ts`. (Replaces #503's earlier nested-`--name` approach.)
- **Workflow runs record agent harness + session identity** — `akm workflow start`
now persists the agent harness (e.g. `claude-code`, `opencode`) and the
platform-native session id that owns each run. Identity is resolved best-effort
from the environment (`AKM_AGENT_HARNESS` / `AKM_SESSION_ID`, falling back to the
harness-native session env var) or can be passed explicitly to `startWorkflowRun`.
Stored via additive migration `002-add-agent-identity` and surfaced on
`WorkflowRunSummary.agentHarness` / `.agentSessionId`. This is the first concrete,
scoped slice toward workflow session monitoring (#501).
- **Workflow agent check-in + step-summary validation** (#506) — workflow runs now
use a file-signal / command-loop check-in model (no resident background thread, per
the ADR in `docs/technical/workflow-agent-checkin-adr.md`). `akm workflow start`
arms a durable check-in timestamp; `akm workflow complete --summary` now **requires**
a per-step summary and runs it through an LLM completion-criteria validation gate —
on failure the step stays pending and structured corrective feedback is returned
(`workflow-complete-rejected`). A pure `evaluateCheckin` surfaces a strong `continue`
directive through `getNextWorkflowStep` when an active run looks stalled. Migration
`002` adds `agent_harness`, `agent_session_id`, `checkin_armed_at` on
`workflow_runs` and `summary` on `workflow_run_steps`.
- **Default improve profiles + scheduled task set** (#552) — three new bundled
profiles in `src/assets/profiles/` — `frequent` (extract + inference; distill /
consolidate excluded), `consolidate` (consolidation-only), and `catchup` (manual
recovery: consolidate + triage drain) — alongside the existing `default` / `quick` /
`thorough` / `memory-focus` / `graph-refresh`. `akm setup` and the new `akm tasks
init` register a multi-cadence task set **idempotently**: `akm-improve-frequent`
(60 min), `akm-improve-consolidate` (4 h), `akm-improve-nightly` (`thorough`, daily
2 am, server-gated), `akm-improve-catchup` (registered but unscheduled), and
`akm-graph-refresh-weekly` (Sun 3 am). Registration is CI-aware (skips when
`CI=true`) and asks a single "Is this a server install?" prompt to gate the nightly
task (default yes on Linux-without-battery, no on macOS/laptop).
### Design notes
- **#501 narrowed; superseded by #506 for the monitoring design.** Issue #501
("Add background thread for workflow command session monitoring and agent
prompting") was an epic. Per #506's stated preference to avoid always-on
background threads/daemons, the background-thread requirement is **not**
implemented here. #501 is narrowed to the one tractable, prerequisite sub-feature
— persisting harness + session identity on each workflow run — which any future
monitor needs regardless of design. The session-monitoring/agent-steering loop is
deferred to #506 and requires a separately approved design.
### Changed
- **`improve`: consolidation runs before extract + smarter pool-delta gate**
(#551). The consolidation phase now runs **before** the session-extract pass
in the improve pipeline. Extract auto-accept writes new memory `.md` files on
every run, which previously made the consolidation pool-delta gate
(`memoryUpdatedAfterLastConsolidate`) fire unconditionally — consolidation
never skipped and wastefully re-judged freshly-promoted single-source
memories with no merge/contradiction candidates yet. Running consolidation
first means it only ever sees memories from **prior** runs; current-run
extract promotions are not on disk yet. The pool-delta gate is additionally
narrowed: a memory whose only mtime bump since the last consolidate came from
its **own** auto-accept promotion (tracked via the `promoted` event's
`assetPath`) is excluded from the "work to do" check, so adjacent-run
promotions get a full improve cycle to settle before consolidation considers
them. When the gate now correctly skips, the existing
`improve_skipped` / `consolidation_no_memory_updates` event is emitted so
health reflects it. No event-shape changes; emitted-event order changes only
because consolidation moved earlier.
- **Unified git commit model — single batch-at-boundary commit** (#507). Writing
or deleting an asset on a git-backed source no longer commits (and optionally
pushes) **per asset**. `writeAssetToSource` / `deleteAssetFromSource` now
perform a plain filesystem write/unlink for every kind, and git-backed targets
are committed **once** at the operation boundary (`akm remember --target`,
proposal accept/revert, consolidate) as a single complete commit — `git add -A`
stages `.akm/` state + sibling assets together — pushed under the same
`writable + remote` gate as `akm save`/`akm sync`. This removes the noisy,
incomplete per-asset commits (~25 per improve run) and leaves no dirty
working-tree residue.
- **`improve/consolidate`: `minPoolSize` guard** (#553). Consolidation now skips
itself when the eligible memory pool is below `processes.consolidate.minPoolSize`
(default **500**), emitting a `consolidation_skipped` event with
`reason: pool_below_min_size` and making **zero** LLM calls — so the always-enabled
consolidate task self-activates only once a stash is large enough to have real
merge/contradiction candidates. `minPoolSize: 0` disables the guard. The skip
surfaces in `akm health` improve output. The bundled `consolidate` profile sets
`500`, `catchup` sets `0`.
- **`improve/extract`: `minNewSessions` gate** (#554). The extract phase now counts
in-window, not-yet-seen candidate sessions **before** any LLM call and skips the
pass (emitting `extract_skipped` / `reason: below_min_new_sessions`, visible in
`akm health`) when the count is below `processes.extract.minNewSessions`. The
in-code default is **0 (disabled)**, so existing profiles keep always-run behaviour;
only the new `frequent` profile opts in with `3`. This removes the ~22% of improve
runs that previously ran the full `ensureIndex` + extract pipeline for zero new
sessions.
### Deprecated
- **`options.pushOnCommit`** (#507). The per-asset push-on-commit knob is retired.
Existing configs still parse — its push intent is mapped onto the batch push
gate and a one-time deprecation warning is emitted when the option is
encountered. Remove it and rely on `writable: true` + a configured remote.
### Fixed
- **Memory inference re-queued `hot` parents forever** (#550). `markParentProcessed`
was only called when a derived child was newly written; when the child already
existed (`written = 0`), the parent never got `inferenceProcessed: true` and was
re-queued on every `akm improve` run (~37 wasted LLM calls/run on one production
stash). The child-exists path now marks the parent done (a genuine write failure
still leaves it unmarked for retry), while `skippedChildExists` accounting is
unchanged.
- **Auto-accept rejected truncated LLM descriptions** (#556). ~9.3% of proposals
failed auto-accept validation because the LLM cut the description mid-clause (ending
in `to`/`for`/`and`/a comma/etc.) or lost a YAML continuation line. A deterministic
post-generation repair pass (`repairTruncatedDescription` in
`src/core/text-truncation.ts`) now trims the truncated fragment to the last complete
clause or swaps in the first complete sentence from the body — never fabricating
text — wired into the extract and distill proposal-write paths before validation.
Already-valid descriptions pass through byte-identical. (Plus a one-line prompt
tightening requiring a complete sentence.)
- **Semantic index verification stuck on stashes with vault entries** (#502).
Verification compared the stored embedding count against the *full* entry count, but
the embedding phase intentionally excludes vault rows — so any index with vault
entries reported `embeddingCount < totalEntries` forever and stayed in
semantic-blocked / verification-failed state. A new `getEmbeddableEntryCount`
(`entry_type != 'vault'`) now feeds the zero-entry short-circuit, the readiness gate,
the "Semantic search ready (X/Y)" message, and the persisted `entryCount`; a
genuinely missing embedding on an embeddable entry still reports `ok:false`.
### Internal
- **#490 architecture refactor.** Decomposed `src/cli.ts` from **4,589 → 620 LOC**
across 16 per-family command modules under `src/commands/*-cli.ts` (adopting a
`defineJsonCommand` factory for byte-identical JSON envelopes); converted `akm
health` checks to an ordered `HealthCheck` registry; and turned the
`migrate-storage` bin's 54 hand-rolled `recordStep` sites into a `MigrationStep`
registry with 3 recursive copy helpers unified into one `copyTree`. Shipped as
serialized local merges with a zero-behaviour-change contract (byte-identical CLI
surface + JSON envelopes), each gated and reviewed; the secret-migrating
`migrate-storage` change is pinned by a sha256 + file-mode fixture-stash
differential test.
## [0.8.14] - 2026-06-11
### Fixed
- **`akm extract` minContentChars default lowered from 500 to 10.** The 500-char
threshold used inputCount (raw session size) but analysis showed 209 of 218
candidate-producing sessions had inputCount < 500 — tiny agent sessions (22–368
chars) regularly yield 1–5 candidates. The only reliably skippable sessions are
empty ones (0 chars, journal files). Default lowered to 10 to catch only
truly empty sessions while preserving all signal-bearing content. Closes #597.
## [0.8.13] - 2026-06-11
### Fixed
- **`akm extract` minContentChars gate filtered all sessions.** The threshold was
checked against `filtered.stats.outputCount` (post-noise-filter chars), but the
pre-filter strips so much boilerplate that even signal-bearing sessions end up
below 500 chars of output. All 75 sessions in the first post-deploy run were
filtered, dropping candidates from 4–13 to 0. Fix: gate on `inputCount` (raw
session size) instead — a session with < 500 raw chars has nothing worth
extracting regardless of what the pre-filter produces. Closes #596.
## [0.8.12] - 2026-06-11
### Fixed
- **`akm extract` calling the LLM for noise sessions that never yield candidates.**
96% of processed sessions (72/75 measured) produced zero candidates, consuming
~330 s of LLM time per run. The pre-filter had no minimum content threshold —
sessions as short as 50 chars were sent to the LLM regardless. A new
`minContentChars` gate (default 500) skips the LLM call when post-filter
content falls below threshold, cutting extract LLM calls by ~95% on typical
stashes. Configurable via `profiles.improve.<name>.processes.extract.minContentChars`.
Closes #595.
## [0.8.11] - 2026-06-11
### Fixed
- **`akm improve --profile <name>` ignored profile's `extract.enabled: false` setting.**
The session-extraction gate in the preparation stage called
`isLlmFeatureEnabled(config, "session_extraction")`, which hardcodes a lookup
against `profiles.improve.default.processes.extract.enabled`. Any non-default
profile that set `extract.enabled: false` (e.g. `quick-shredder`) was silently
ignored, causing the extract pass to run regardless. The fix adds a
`resolveProcessEnabled("extract", improveProfile)` check so the active
resolved profile gates the pass correctly. Closes #593.
## [0.8.10] - 2026-06-11
### Fixed
- **`akm improve` taking 8–10 minutes per run due to O(n) DB writes for
profile-filtered refs.** When a profile disables reflect and distill for
certain asset types, `collectEligibleRefs` marks those refs as
`profile_filtered_all_passes`. The caller then emitted one `improve_skipped`
event per ref — a sequential DB write for each. On a ~9 000-ref stash this
was ~500 s of SQLite writes before any consolidation or memory inference
began. The fix collapses the per-ref loop into a single summary event
carrying a `count` field, eliminating ~9 000 sequential writes per run.
Closes #590.
## [0.8.9] - 2026-06-11
### Fixed
- **`akm improve` validation pass was O(n) in stash size, causing ~510 s overhead
on large stashes.** For every indexed ref, the preparation phase called
`findAssetFilePath()` — an async round-trip to the index DB followed by a
filesystem probe — serially inside a `for…await` loop. With ~9 000 indexed
refs at ~55 ms each, this loop consumed the entire 600–900 s run budget before
any reflect, triage, or memory-inference work began. The fix threads
`filePath` from the planning stage (`collectEligibleRefs`) through
`ImproveEligibleRef` so the validation pass and the disk-existence guard can
use the pre-resolved path directly. The async lookup is retained only as a
fallback for refs that enter via a narrow scope (e.g. `--scope ref:foo`).
Closes #587.
## [0.8.8] - 2026-06-11
### Fixed
- **SQLite `SQLITE_BUSY` errors under concurrent improve runs.** `busy_timeout`
was set to 5 000 ms in all three database open paths (`openDatabase`,
`openExistingDatabase`, `openStateDatabase`). Under a busy cron schedule — or
when a reindex triggered by memory inference ran concurrently with an event
write — the 5 s window was routinely exhausted, producing "database is locked"
failures. Raised to 30 000 ms across all three paths so transient lock
contention is retried for up to 30 s before surfacing as an error.
## [0.8.7] - 2026-06-09
### Fixed
- **`incrementalSince` duration strings were silently ignored.** Values like
`"30m"`, `"24h"`, `"7d"` were passed raw to `narrowToIncrementalCandidates`,
which compared them against ISO timestamps via string sort. All `2026-...`
timestamps are lexicographically less than `"30m"` (`'2' < '3'`) and `"24h"`
(`"20" < "24"`), so `isChanged()` always returned `false` and the candidate
pool was silently emptied rather than filtered to the window. The fix adds
`parseSinceToIso()`, which resolves human duration strings to absolute ISO
timestamps before comparison. Values that already look like ISO timestamps
are passed through unchanged.
## [0.8.6] - 2026-06-09
### Added
- **`consolidate.incrementalSince` profile config field.** Setting
`incrementalSince: "7d"` (or any duration string) in the `consolidate` block
of an improve profile narrows the candidate pool to memories modified within
that window plus their top-5 graph neighbours, keeping each pass focused on
recent changes. This makes it practical to run consolidation more often than
once per day (e.g. via `akm-improve-consolidate` every 4 h) without
re-scanning the full pool every time. The nightly default profile leaves this
unset (full-pool sweep, same as before). The `incrementalSince` option already
existed in `akmConsolidate()` but was hardcoded off at the call site; the
field is now surfaced in the config schema and read from the profile.
## [0.8.5] - 2026-06-09
### Fixed
- **Consolidation starved merge recall; the memory pool grew unbounded.** Commit
`633ece41` made the `incrementalSince` narrowing unconditional, so every
consolidation run only judged memories changed since the last run plus their
immediate vector-neighbors. Stale-but-unmerged duplicate clusters were never
re-examined, so the eligible pool grew monotonically and never shrank, and
contradiction detection (which rides on the consolidation pass) went dark.
Consolidation only runs on the nightly default-profile pass (`quick`/`frequent`
disable it), so a full-pool sweep is correct and affordable; the override is
removed. `lastConsolidateTs` still gates whether the pass runs.
## [0.8.4] - 2026-06-08
### Fixed
- **`akm tasks sync` ignored schedule changes.** Sync classified any task already
present in the OS scheduler as "unchanged" without comparing its installed
entry, so editing a task's `schedule:` in the `.yml` never reached the crontab —
the only way to apply a new schedule was to `remove` and re-`add` the task. The
same gap affected `tasks enable`/`disable`, which merely toggled the existing
cron line's comment and so re-enabled a stale schedule. Sync now compares the
backend's installed signature against the signature the current definition would
produce and reinstalls on drift (reported in a new `updated[]` field);
`enable`/`disable` reinstall from the current `.yml` instead of toggling in
place. Backends that can't cheaply read their installed form fall back to an
idempotent reinstall, so the fix is correct on launchd/schtasks too. The cron
backend gains `expectedSignature()` and a signature on each `list()` entry.
### Added
- **`akm improve --skip-if-locked`.** When another improve run already holds the
lock, the run logs and exits 0 with a no-op result (`skipped.reason:
"lock-held"`) instead of failing with the "already running" config error
(exit 78). Intended for high-frequency scheduled runs (e.g. an every-30-min
`quick` pass) that would otherwise pile up exit-78 failures whenever a longer
run overlaps them. Default off — the hard error is preserved for interactive
use. The result is still recorded so the skip is auditable.
## [0.8.3] - 2026-06-08
### Fixed
- **`improve.lock` leaked on signal death (cron timeout).** The improve
SIGTERM/SIGINT/SIGHUP handler calls `process.exit()`, which skips `finally`
blocks — so the `finally` that releases `improve.lock` never ran, and every
timed-out cron run leaked the lock sentinel. (It wasn't a permanent deadlock
only because the next run reclaims a dead-PID lock, a path that PID reuse can
defeat.) The lock is now released from a `process.on("exit", …)` handler
registered at acquire time (exit handlers DO run on `process.exit()`), via a
new ownership-checked `releaseLockIfOwned(path, pid)` so a backstop release can
never delete a different run's lock. This generalizes to the budget watchdog
and any future exit path.
- **`quick` profile was not quick.** It was documented "Reflect-only" but did
not disable the session-`extract` process (which is default-ON), so a `quick`
run processed the entire unindexed-session backlog (~40 min) — guaranteeing a
5-minute cron timeout → SIGTERM → the lock leak above, every run. `quick` now
explicitly sets `processes.extract.enabled: false`.
## [0.8.2] - 2026-06-05
### Added
- **LM Studio auto-detection in setup wizard** — `akm setup` now probes
`localhost:1234/v1/models` at startup and, when the server is running, pre-fills
the LLM backend with the active model list, mirroring the existing Ollama detection
flow (#522).
- **Agent harness config import** — `akm setup` detects installed AI coding harnesses
(currently Claude Code and OpenCode) and pre-populates LLM provider, model, and
base-URL fields from the harness configuration. The importer registry
(`HARNESS_CONFIG_IMPORTERS`) makes adding future harnesses a single append (#523).
API key *values* are never read or stored — only the environment variable name is
imported.
- **Registry-driven stash selection** — the "Add Sources" step now fetches available
stashes from the official AKM registry at startup. `DEFAULT_SELECTED_STASH_IDS`
in `src/setup/registry-stash-loader.ts` is the single edit point for changing
which stashes are pre-checked. Falls back to a hardcoded list on network error (#520).
- **`improve.autoAccept.{promoted,validationFailed}` health metrics** — auto-accepted
proposals that pass the confidence threshold but fail validation (truncated
description, invalid frontmatter) are now counted as `gateAutoAcceptFailedCount`
in the improve result envelope and surfaced as `improve.autoAccept.validationFailed`
in `akm health` reports.
- **`auto-accept-validation` health advisory** — heuristic advisory that warns when
`validationFailed > 0` so malformed proposals are visible before they pile up in
the queue.
### Fixed
- **`akm-improve` tasks recorded as failed on budget exhaustion** — the budget
exhaustion timer called `process.exit(1)`, causing every budget-limited run to be
recorded as a task failure. Changed to `process.exit(0)`; budget exhaustion is a
normal exit condition.
- **`improve_runs.started_at` always equal to `completed_at`** — `writeImproveResultFile`
was called at end-of-run, so `new Date()` captured the completion time and both
columns held the same value (649/661 real runs affected, regressed ~May 26).
`started_at` now uses the timestamp captured at process launch, passed in from the
CLI entry point. A regex-based fallback decodes the timestamp embedded in the run ID
for any call site that does not supply an explicit value (#524).
- **`akm-health-report` task fails on transient DNS errors** — the Discord webhook
script caught `HTTPError` but not the parent `URLError`, so DNS blips caused the
task runner to record the health report as failed. `URLError` is now caught and
logged as a warning with a clean exit.
### Added
- **Stash `.meta/` convention** — a stash may carry an optional, human-authored
`.meta/` directory at its root for orientation: purpose, key assets, conventions,
and maintainer info. Surface it on demand with `akm show meta` (the working
stash's `.meta/index.md`), `akm show meta:<name>` (e.g. `.meta/about.md`), or
scope it to a specific stash with `akm show <origin>//meta[:<name>]`. Because
`.meta/` is a dot-directory, the indexer already skips it, so these docs never
pollute search results — they are direct-read on demand. Owners extend the
convention by dropping new files (`.meta/about.md`, `.meta/conventions.md`,
`.meta/license`) with no code changes. `akm init` scaffolds a `.meta/index.md`
template into newly created stashes.
- **Default stash skeleton** — `akm init` (and `akm setup`) now copies
`src/assets/stash-skeleton/` into every newly created stash. Currently ships
a `README.md` covering what the stash contains and how agents use `akm` to
access assets. Existing files are never overwritten. Add files to
`src/assets/stash-skeleton/` to extend what ships with a fresh install.
### Improved
- **Setup wizard pre-populates from existing config** — on re-run, `akm setup`
initialises every prompt default from the current saved configuration so users
only need to change what has actually changed (#519).
- **Config backup before every setup write** — `backupExistingConfig()` is now called
before each `saveConfig` in the setup wizard, ensuring the previous config is always
recoverable if a wizard run is interrupted (#521).
## [0.8.1] - 2026-06-05
### Added
- **`graph-refresh` improve profile** — new built-in profile that runs a full-corpus
graph extraction pass across all stash files (all other improve processes disabled).
Use `akm improve --profile graph-refresh` for a weekly relationship rebuild.
Pairs with the new `graph-refresh-weekly` task template (`akm tasks add --template graph-refresh-weekly`).
- **`session-extraction` health advisory** — new heuristic advisory backed by real
`akmExtract` outcomes: warns when the session-extraction process ran but produced
zero proposals across ≥ 5 sessions, or recorded warnings. Replaces the vestigial
`session-log-failures` warn signal.
- **`improve.sessionExtraction` health metrics** — `sessionsScanned`, `sessionsExtracted`,
`sessionsSkipped`, `proposalsCreated`, `warnings`, `durationMs` now tracked and
visible in `akm health` reports.
### Fixed
- **`akm info` indexStats** — `readIndexStats` errors are now surfaced and the resolved
DB path is passed correctly; `entryCount`, `hasEmbeddings`, and related fields are
no longer silently empty (#510).
- **Indexer timing fields** — `embedMs` and `ftsMs` in timing output had their
operands swapped, producing negative durations. Fixed (#516).
- **Incremental consolidation gate** — the `volumeTriggered` path bypassed the
incremental gate introduced in 0.8.0, causing consolidation to run on chunks it
had already processed in the same run. Fixed.
- **Improve budget exhaustion** — `improve.lock` was not released after budget
exhaustion, blocking subsequent runs until the lock TTL expired.
- **Consolidation chunk retry** — failed chunks are now retried once with a 2 s
backoff before being recorded as lost, reducing transient LLM errors from
propagating to `chunksFailed`.
- **`yieldRate` health metric** — `skippedAborted` refs were incorrectly counted in
`freshAttempts`, inflating the denominator and underreporting yield rate.
- **`session-log-failures` advisory** — demoted from `warn` to always `pass`
(informational only); the advisory was a raw regex counter with no LLM signal,
producing false positives on normal session content.
### Refactored
- All runtime assets consolidated under `src/assets/` with `dist/assets/` mirroring
the layout exactly. Built-in improve profiles moved from in-source object literals
to embedded JSON files (`src/assets/profiles/*.json`). The `copy-assets.ts` build
step now uses a precise `src/assets/**/*` glob instead of a broad catch-all.
- Vestigial Phase 0 (`getExecutionLogCandidates` / `ERROR_PATTERNS`) removed from
the improve pipeline. This regex scan collected a metric count but never fed an
LLM; `akmExtract` (Phase 0.4) is the real session extraction pipeline.
## [0.8.0] - 2026-05-28
### Performance
- **`akm consolidate`**: all-hot chunk early-exit. When every memory in a chunk
is `captureMode: hot` (user-explicit), the only operations the LLM could ever
propose are deletes — all refused unconditionally by the downstream guard.
Such chunks now skip the model entirely and are counted as `judgedNoAction`
up front, instead of relying on a prompt-level hint and spending a wasted
request. Mixed chunks are unaffected.
### Breaking changes (deprecation aliases, removed 0.9.0)
The 0.8 line is the clean-break window for CLI ergonomics. Every rename below
keeps the **old spelling working** as a deprecated alias that prints a stderr
warning (never on stdout, so JSON consumers are unaffected) and delegates to the
canonical form. **All of these deprecated aliases are removed in 0.9.0.** See
[`docs/migration/v0.8-to-v0.9.md`](docs/migration/v0.8-to-v0.9.md) for the full
old → new table.
- **Proposal queue is now a noun group**: `akm proposal {list,show,diff,accept,reject,revert}`.
The flat verbs `akm proposals`, `akm show proposal <id>`, `akm accept`,
`akm reject`, `akm diff`, and `akm revert` are deprecated aliases.
Bare `akm proposal` behaves as `akm proposal list`.
- **`--detail` is now verbosity only** (`brief|normal|full`). The output
*projection* moved to a new **`--shape`** flag (`human|agent|summary`).
`--detail summary` and `--detail agent` are deprecated aliases that map to
`--shape summary` / `--shape agent`.
- **`--for-agent`** is a deprecated alias for `--shape agent`.
- **`--generator`** replaces `--source` on `accept` / `reject` / `history`
(which generator produced the proposal/event). `--source` is a deprecated
alias on **those three commands only** — it is unchanged on
`search` / `curate` / `graph` / `remember`, where it means "read from here".
- **`akm save` → `akm sync`** (commit + optional push; `sync` connotes push
better). `akm save` is a deprecated alias. `akm sync` adds `--no-push`.
- **`akm enable` / `akm disable` → `akm config enable` / `akm config disable`**.
The top-level `enable` / `disable` are deprecated aliases.
- **`akm events` → `akm log`**: `log` is an additive alias for the same
state.db stream in 0.8 and becomes primary in 0.9.0. (`akm history` remains the
asset-scoped, cross-source analytical trail — a different surface.)
- **`akm wiki remove --force` → `-y` / `--yes`** for skipping the confirmation
prompt. `wiki remove` now also *prompts* interactively when a TTY is present;
`--force` is a deprecated alias for `-y`.
- **`akm feedback --note` → `--reason`**: `--note` is a deprecated alias and
warns when used without `--reason`.
- **`akm workflow next --dry-run` removed**: the flag is no longer declared, so
it no longer appears in `--help`. The explicit "next does not support
--dry-run" guard remains (read from argv) so existing callers still get a clear
message instead of silent acceptance.
- **Singular aliases added** (additive, non-breaking): `akm task` for
`akm tasks`, `akm lesson` for `akm lessons`.
### Safety
Two destructive paths that previously acted with no confirmation now guard
behind an interactive prompt (or `-y` / `--yes` in non-interactive use).
**Scripts that ran these non-interactively must add `-y`.**
- **`akm registry remove`** now confirms before splicing the registry out of the
config (`confirmDestructive`). Pass `-y` / `--yes` to skip the prompt;
non-interactive use without `-y` aborts.
- **Bulk `akm proposal accept --generator <g>`** (the multi-proposal branch) now
confirms before promoting every matching proposal, mirroring the existing
guard on bulk `reject`. Single-id accept stays unguarded (it is revertable).
### Fixed
- **Consolidation `delete_failed` on stale index entries** — when consolidation
successfully deleted a memory file, the index DB was not re-indexed between
runs. Subsequent runs loaded the stale DB entry into their memory map, the LLM
re-proposed the deletion, and `deleteAssetFromSource` threw "not found in
source" — appearing as `delete_failed` in skipReasons. Fix: `loadMemoriesForSource`
now filters entries whose file no longer exists on disk before building chunks,
so phantom memories are never sent to the LLM. A secondary catch in the delete
handler emits `delete_already_gone` instead of `delete_failed` when the file
is confirmed absent.
> **CI / Docker users:** the 0.8.0 storage split moved `akm.lock`, the event
> database, and the registry cache out of `$XDG_CONFIG_HOME/akm/` into
> `$XDG_DATA_HOME`, `$XDG_STATE_HOME`, and `$XDG_CACHE_HOME` respectively. If
> you override any of `AKM_CONFIG_DIR`, `AKM_DATA_DIR`, `AKM_STATE_DIR`,
> `AKM_CACHE_DIR` in CI to isolate per-job state, set **all four** (or none,
> and rely on XDG defaults). Overriding only `AKM_CONFIG_DIR` will leave the
> lock file / event DB pointing at the host's default `$XDG_DATA_HOME`,
> causing lock contention and bleed between jobs.
### Removed
- **Install-time security audit (`security.installAudit`) and the `--trust`
flag**. The audit scanned incoming stash assets for risky patterns (e.g.
`curl ... | bash`, "ignore previous instructions") and blocked installs on
critical findings. In practice it produced too many false positives on
benign documentation strings and forced first-time users to pass `--trust`
or twiddle config just to install the official stash. The whole feature is
gone:
- `akm add` and `akm update` no longer scan synced content.
- The `--trust` flag is removed from `akm add` and `akm wiki register`.
- The `security.installAudit.*` config keys (`enabled`, `blockOnCritical`,
`registryAllowlist`, `registryWhitelist`, `blockUnlistedRegistries`,
`allowedFindings`) are no longer recognised; the entire `security` block
is removed from the config schema.
- The `akm config set security.installAudit.*` keys now error as unknown.
- `audit` fields are removed from `AddResponse.installed` and
`SourceInstallStatus`.
### Breaking Changes
- **Project-level `.akm/config.json` files are no longer merged**. The
multi-layer config discovery introduced in the 0.7 line was deprecated
in late-0.8.x with a warning; that warning is now backed by removal.
`loadConfig` walks cwd-ancestors only to emit a one-time deprecation
warning per discovered file. Move any needed settings to
`~/.config/akm/config.json`. `stashInheritance` (a multi-layer-only
field) is removed from the schema.
- **`${VAR}` env-var expansion only resolves at the apiKey consumption
sites**. The recursive expansion walker that ran on the load path is
gone. Other config string values now round-trip verbatim: a literal
`${HOME}` in (say) `stashDir` is preserved as the literal `${HOME}`
on read. The new exported `resolveSecret(value)` helper is applied
only where authorization headers are built (`src/llm/client.ts`,
`src/llm/embedders/remote.ts`, `src/integrations/agent/sdk-runner.ts`).
Documented `${OPENAI_API_KEY}` recipes in `docs/configuration.md`
continue to work because expansion still happens at request time for
apiKey fields.
- **`AKM_FORCE_DOWNGRADE_CONFIG` env var removed**. The newer-than-binary
read-only guard (`configReadOnlyReason`, `markConfigReadOnlyIfNewer`,
`getConfigReadOnlyReason`) is gone. Configs declaring a `configVersion`
newer than the running binary now save through silently — unknown
fields are stripped on save by `sanitizeConfigForWrite` plus the
strict-walled Zod schema. Users on 0.9.x configs should not open them
with a 0.8.x binary in writable workflows.
### Changed
- **Rebrand**: the full name "Agent Kit Manager" is now **Agent Knowledge Manager** — `akm` stands for Agent Knowledge Manager going forward. The binary name, npm package (`akm-cli`), and all APIs remain unchanged.
- **Config layer rewrite** — single-source-of-truth Zod schema in
`src/core/config-schema.ts` replaces the per-field parse switch AND
the per-shape load-time parser. Adding a new config field is now one
line of schema + zero lines of CLI code. `loadConfig` now consists of
parse-text → migrate (pure JSON transforms) → Zod safeParse → overlay
defaults — a ~30-line pipeline that absorbs ~900 LOC of legacy
per-shape parsers (`parseLlmConfig`, `parseEmbeddingConfig`,
`parseIndexConfig`, `parseSourceConfigEntry`, and ~20 more).
- **#454**: `akm config set llm.apiKey` / `embedding.apiKey` /
`profiles.llm.<name>.apiKey` now throws `UsageError` pointing at the
corresponding env var (`AKM_LLM_API_KEY`, `AKM_EMBED_API_KEY`,
`AKM_PROFILE_<NAME>_API_KEY`). Was previously a silent strip.
- **#455**: every schema-leaf key is now reachable via `akm config set`.
Includes previously hand-listed gaps: `defaults.agent`, `search.minScore`,
`improve.eventRetentionDays`, `embedding.provider`, `llm.temperature`,
`profiles.llm.<name>.*`, `profiles.agent.<name>.*`, etc.
- **#456**: `akm config validate` and `akm config migrate` are now real
registered subcommands. The orphan implementations in `config-validate.ts`
have been removed; the new entry points live in `src/cli/`.
- **#457**: project-level `.akm/config.json` files are now flagged with a
deprecation warning ("will be ignored in 0.9.0+"). The merge still
happens in 0.8.x — one release of grace.
- **#458**: malformed JSON or non-object root in the config file now raises
`ConfigError("INVALID_CONFIG_FILE")` with the underlying parse error.
Was previously a silent fallback to `DEFAULT_CONFIG`, which masked
corruption. File-not-existing remains the legitimate cold-start case.
- **#459**: `~/.cache/akm/config-backups/` is now bounded to the 5 most
recent timestamped backups. Pruning runs on each `saveConfig`.
`config.latest.json` is preserved separately.
- **#460**: `UNKNOWN_CONFIG_KEY_HINT` is now auto-generated from the
schema via `listTopLevelConfigKeys()`. No more stale hand-maintained string.
- **#461**: if the auto-migration disk-write fails, `loadConfig` now throws
a hard error instead of returning the in-memory migrated shape. Eliminates
the silent infinite re-migrate loop on every `akm` command.
- **#462**: nested registries[], sources[], profiles.* objects are
`.strict()` — unknown keys are rejected with a path-pointing error at
both set time and saveConfig time.
- **#463**: `schemas/akm-config.json` is now auto-generated from the Zod
source via `bun scripts/gen-config-schema.ts`. A drift test fails CI if
the committed file disagrees with the regeneration output.
- **#464.a**: `defaultWriteTarget` is validated via Zod `.refine()` against
`sources[].name`. With no sources configured, save-time validation
rejects instead of silently accepting (no implicit "first writable" fallback).
- **#464.b**: generic unset works on `semanticSearchMode` and every other
key via the dotted-path walker.
- **#464.c**: all write paths route through `writeFileAtomic`.
- **#464.d**: duplicate `mergeSecurityConfig` / `mergeInstallAuditConfig`
in `config-cli.ts` are deleted; merging happens via re-parse through the
Zod schema.
See `docs/migration/v0.7-to-v0.8.md` for the user-facing migration guide.
## [0.7.5] - 2026-05-08
### Added
- **Feedback tag/filter filtering** — `akm feedback` and related event-reading paths now support richer filtering by tags and other event metadata, making it easier to inspect and reuse accumulated feedback signals.
- **Vault path/run UX improvements** — vault flows now better support path discovery and command-scoped secret injection without surfacing values, with expanded regression coverage for the path/run contract.
- **Reflect fallback improvements for external agents** — reflection/proposal flows now support a more robust fallback path for proposal content, including the file-write path used by the `opencode` agent integration.
### Changed
- **Workflow runs are now scoped to the current workspace** — ref-based workflow commands (`workflow next/status/list`) now resolve runs within the current project, worktree, or non-repo directory instead of sharing active-run state globally across the whole cache. Direct run-id commands still target the exact run.
- **Help, hints, and workflow docs now explain run scoping** — CLI descriptions, embedded hints, operator docs, and workflow guides now describe the current-scope semantics so users understand how ref-based run resolution behaves across repos and local sandboxes.
- **`akm show` auto-indexes stale state instead of falling back to raw filesystem reads** — show/search parity is tighter because stale index state now triggers refresh rather than silently drifting to a separate fallback path.
- **Release metadata lookup follows the published `CHANGELOG.md` layout** — migration-help, package publish metadata, and related docs now consistently reference the shipped changelog location at the package root.
- **Documentation refresh across README and posts** — README positioning, command-tour docs, workflow examples, and dev.to post organization were refreshed to better match the current CLI surface.
### Fixed
- **Cross-repo and cross-directory workflow leakage** — an active workflow run in one repo or sandbox no longer blocks or leaks into another when the same workflow ref is used from a different working directory.
- **`show` workflow hints now respect the current scope** — `show workflow:...` only surfaces the active workflow run for the current workspace instead of attaching the latest run from anywhere on the machine.
- **Agent-output and local-model JSON hardening** — reflect/propose and LLM-backed parsing paths are significantly more defensive against malformed JSON and partial local-model output.
- **Reflect draft-file isolation** — reflect no longer writes intermediate draft files into the stash itself; temporary draft output now lives in OS temp space instead of polluting user content.
- **Memory-inference token budgeting** — memory inference now respects the configured LLM token budget instead of overrunning long inputs.
- **Named git stash selectors in `akm save`** — save now resolves named git-backed stash selectors correctly.
- **Indexed script refs in search results** — script entries now surface the correct refs in indexed search results.
- **Feedback ref resolution and LLM indexing regressions** — feedback targeting and related LLM indexing paths were corrected.
- **Release workflow reruns and optional native dependency handling** — release automation is now rerunnable and avoids tripping over optional native dependency edges in CI/publish contexts.
- **Published static-file checks** — migration-help packaging/tests now verify the shipped changelog and bundled release-note files are present and loadable from the published layout.
### Documentation
- **Bundled migration notes now cover 0.7.5** — `akm help migrate 0.7.5` and `akm help migrate latest` now surface the full 0.7.5 operator summary alongside the changelog section.
## [0.7.4] - 2026-05-06
## [0.7.3] - 2026-05-05
### Added
- **`akm index --enrich` opt-in for LLM passes** — index-time enrichment work such as metadata enhancement, memory inference, and graph extraction now runs only when explicitly requested with `--enrich`. Default indexing is faster and no longer surprises operators with LLM-backed work during normal maintenance runs.
- **Config backup snapshots before writes** — config writes now create AKM cache backups so setup/config flows have a recovery path if a config is overwritten or corrupted during development or testing.
### Changed
- **Setup wizard UX refresh** — `akm setup` now better reflects the real configured state: source prompts are ordered more sensibly, configured and preserved stash information is surfaced, agent defaults can be selected explicitly (including disabled), and post-setup indexing does not implicitly enable enrichment.
- **CI workflows updated for current GitHub Actions runtimes** — CI, release, and publishing workflows now use current action majors (`checkout@v5`, `cache@v5`, `setup-node@v5`, `upload-artifact@v5`, `download-artifact@v6`) to stay off deprecated Node 20 action runtimes.
- **Technical investigation notes updated** — the index investigation note now reflects the latest `.stash.json` migration status, current green CI runs, and the narrowed remaining compatibility surface ahead of `v0.8.0`.
### Fixed
- **Embedding-dimension drift on read-only DB opens** — read/telemetry paths no longer mutate the live index schema with the default embedding dimension. `akm info`, search/show parity paths, and related readers now preserve the configured embedding shape instead of downgrading vector tables.
- **Incremental index churn across multiple source layouts** — incremental indexing is now significantly more stable for filename-less legacy metadata, wiki-root sources, repo-root git stash layouts, non-indexed companion files, and cross-source dedupe cases.
- **Git source indexing for repo-root stashes** — git-backed sources no longer assume a `<repo>/content` subtree; repo-root stash layouts are indexed correctly and cached mirrors are treated as fresh instead of being needlessly refreshed.
- **`show` metadata no longer depends on `.stash.json`** — command and skill summary/show metadata now comes from file-local frontmatter and renderer parsing rather than the deprecated disk fallback sidecar.
- **`.stash.json` no longer drives incremental stale detection** — editing `.stash.json` alone no longer forces directories to rescan during incremental indexing.
### Internal
- **Ranking and scoring fixtures migrated toward file-local metadata** — routine benchmark and regression fixtures now prefer markdown frontmatter or inline script metadata, with `.stash.json` retained only for intentional legacy-compatibility coverage that still exercises explicit-file override behavior.
- **Production-path ranking regression coverage** — ranking regression tests now build their fixture index through the production indexer rather than a custom `.stash.json` crawler, reducing fixture drift and improving confidence in the real indexing/search path.
### Added
- **One-shot URL ingest for `akm import` and `akm wiki stash`** — both commands now accept a single HTTP/HTTPS URL in addition to file paths and stdin. `akm import <url>` fetches the exact page, converts it to markdown, and writes it into `knowledge/` using a URL-path-derived default name. `akm wiki stash <wiki> <url>` fetches the exact page, converts it to markdown, and writes it into `wikis/<wiki>/raw/`. Neither command registers a persistent website source or crawls linked pages.
### Changed
- **Shared website ingest boundary** — website URL validation, single-page fetch/convert, and website mirror generation now live in a dedicated shared ingest module. The website source provider is a thin adapter, and `akm add`, `akm import`, and `akm wiki stash` all reuse the same core website-ingest path.
- **`.stash.json` docs deprecation timeline** — the docs now explicitly state that `.stash.json` is deprecated, remains only as a 0.7.x compatibility bridge, and will be removed in v0.8.0 to match the current aggressive pre-release phase-out posture.
## [0.7.0]
### Added
- **Proposal queue (`akm proposal *`)** (#225, #226, #233) — durable queue for proposal-producing commands. New verbs `akm proposal {list, show, diff, accept, reject, revert}`. Promotion runs full validation before routing through `writeAssetToSource()`. Multiple proposals for the same `ref` coexist without filesystem collisions. Auto-accept is gated per-source via `autoAcceptProposals: true` (default off; requires a writable source). See v1 spec §11.
- **`akm reflect`, `akm propose`, `akm distill`** (#225, #226, #227) — three new commands that write **only** to the proposal queue. `reflect` and `propose` shell out via the agent CLI (`agent.*` config); `distill` is the canonical bounded in-tree LLM call gated behind `llm.features.feedback_distillation`. Usage events `reflect_invoked`, `propose_invoked`, `distill_invoked`.
- **`lesson` asset type** (#227) — first-class well-known type with required frontmatter `description` and `when_to_use`, stored under `lessons/<name>.md`. Normally produced by `akm distill <ref>` as a `proposed`-quality proposal and promoted via `akm proposal accept`.
- **`llm.features.*` map with mixed defaults** (#227, #284) — every bounded in-tree LLM call site is gated behind exactly one feature flag. Four keys ship: `curate_rerank`, `feedback_distillation`, `memory_inference`, `graph_extraction`. `memory_inference` and `graph_extraction` default to `true`; the others default to `false`. Wrapper `tryLlmFeature(feature, config, fn, fallback)` in `src/llm/feature-gate.ts` guarantees disabled/throw/timeout fall back without crashing the call site. See v1 spec §14.
- **`quality: "proposed"` and `--include-proposed`** — `SearchHit.quality` open string set; `proposed` is excluded from default search and surfaces only via `akm search ... --include-proposed` or `akm proposal *`. Unknown values parse-warn-include. `SearchHit` gains optional `quality?` and `warnings?` fields.
- **`akm-bench` v1** (#234, PRs #266, #268, #269) — paired-utility benchmark framework. Track A runs each task with and without akm available and emits a comparable score pair; `akm-bench compare` aggregates paired runs into a delta report; `akm-bench attribute` maps utility deltas back to specific `[origin//]type:name` refs (Track B); `akm-bench evolve` is a stub for the closed-loop workflow that lands in 0.8.
- **Operator env-var documentation** (#284 Wave B, PR #285) — `docs/configuration.md` now documents `AKM_NPM_REGISTRY`, `AKM_REGISTRY_URL`, `AKM_CACHE_DIR`, `HF_HOME`, and `GH_TOKEN`.
- **Empty-state hints** (#284 Wave C, PR #286) — `akm proposal list`, `akm workflow list`, and `akm vault list` empty-state messages now include "how to create the first one" guidance.
- **Canned error hints** (#284 Wave C, PR #286) — four new typed error hints added: `INVALID_FLAG_VALUE`, `ASSET_NOT_FOUND`, `WORKFLOW_NOT_FOUND`, `FILE_NOT_FOUND`.
- **`--verbose` global flag in `--help`** (#284 Wave C, PR #286) — the flag was honoured at runtime but invisible in help output; now declared.
- **~90 new tests** (#284 Wave D, PR #285) — direct coverage for the proposal/reflect/propose/distill CLI integration paths, output-shape contracts, workflow-runs state machine, and lesson-init scaffolding.
### Security
- **Git message sanitization** (#270) — commit messages and remote URLs written by akm are sanitized to prevent shell-substitution and control-character injection through user-supplied content.
- **Bench env isolation** (#271) — `akm-bench` runs each agent invocation in a scrubbed environment so host secrets do not leak into bench transcripts or paired-run logs.
- **LLM body redact + npm tarball host validation** (#272) — outbound LLM request/response bodies are redacted in error reporting before surfacing to stderr or warnings; `akm add npm:…` validates the tarball download host against the configured npm registry rather than following arbitrary `dist.tarball` URLs.
### Changed
- **Workflow noise gate, sources deprecation warn, setup `--help`** (#273) — `akm workflow next/complete/status` no longer print spurious progress noise on quiet runs; the legacy `stashes[]` key emits a single deprecation warning per process (was: per call site); `akm setup --help` renders the same help block as `akm setup` with no args plus the agent-detection summary.
- **tsconfig + HF pin + shapes throw** (#274) — `tsconfig.json` now includes `tests/` so `bunx tsc --noEmit` covers test files; the HF embeddings model is pinned to a specific revision to avoid silent upstream changes; the output-shape registry throws on a missing shape rather than silently `JSON.stringify`-ing.
- **Bench tmp redirect** (#276) — `akm-bench` no longer writes scratch state under `/tmp`; everything lands under the AKM cache dir (`~/.cache/akm/bench/`) so cleanup is bounded and CI sandboxes that ban `/tmp` writes work out of the box.
- **Registry-build tmp redirect** (#284 Wave E, PR #285) — `inspectArchive` now mkdtemps under `${getCacheDir()}/registry-build/` instead of `os.tmpdir()`. Mirrors the bench-only redirect from #276 for non-bench code. `vault load` retains its `/tmp` mode-0600 sentinel by design.
### Fixed
- **Agent spawn timeout** (#284 Wave A, PR #285, BUG-H1) — stdin write could hang past `agent.timeoutMs`; the write now races against `proc.exited` so the timeout is always honoured.
- **Captured-stdio leak on spawn failure** (#284 Wave A, PR #285, BUG-H2) — stream readers no longer leak as floating promises on the spawn-failed path.
- **`defaultWriteTarget` writability check** (#284 Wave A, PR #285, BUG-H3) — resolving `defaultWriteTarget` was missing the writability gate that the `--target` path enforces; now mirrored.
- **Schema-upgrade row loss** (#284 Wave A, PR #285, BUG-H4) — `restoreUsageEventsBackup` silently dropped rows when the new schema added a NOT-NULL column without DEFAULT; now projects rows onto the column intersection and warns loudly.
- **Bench cleanup registry running flag** (#284 Wave A, PR #285, BUG-H5) — `runAllAndExit` now resets `registry.running` in a `try/finally` so a synchronous throw cannot deadlock subsequent SIGINT handlers.
- **`akm search` with no query** (#284 Wave C, PR #286) — error hint now references `--type`/`--limit` instead of show-style ref grammar.
- **`akm workflow next <bogus-id>`** (#284 Wave C, PR #286) — surfaces `WORKFLOW_NOT_FOUND` with `Run \`akm workflow list --active\`` instead of a cryptic ref-parse error.
- **`akm add /missing/path`** (#284 Wave C, PR #286) — throws typed `NotFoundError("FILE_NOT_FOUND")` with hint instead of a bare `Error`.
- **`akm update <bogus>`** (#284 Wave C, PR #286) — now uses `SOURCE_NOT_FOUND` (with the existing hint pointing at `akm list`) instead of the default `ASSET_NOT_FOUND`.
- **Setup wizard source count + embedding-dim prompt** (#284 Wave C, PR #286) — the wizard now reads `newConfig.sources ?? newConfig.stashes` to count configured sources (was reading the dropped legacy key); the embedding-dimension prompt now explains what the value is for.
- **`formatPlain` null fallback** (#284 Wave C, PR #286) — text renderers now exist for every command that calls `output()`; no more silent JSON when an operator passes `--format text`.
- **Arity guards** (#284 Wave C, PR #286) — `propose`, `feedback`, `curate`, and `help migrate` no longer exit 0 with citty's help screen when required positionals are missing; they now exit 2 with `MISSING_REQUIRED_ARGUMENT`.
### Removed
- **Legacy registry `curated` boolean** — legacy v2 index JSON parses and silently ignores it; renderers no longer surface a `curated` column. The per-asset `quality` field replaces it. Publishers do not need to migrate existing JSON.
- **Phantom config keys** (#284 Wave B, PR #285): `llm.features.{tag_dedup, memory_consolidation, embedding_fallback_score}`, `llm.capabilities.{longContext, toolUse}`, and `llm.contextWindow`. These were parsed and persisted by the loader but never read at any call site, and the docs that described their behaviour were misleading. Operators with these keys in `config.json` will see them silently ignored — `akm config get llm.features.tag_dedup` (etc.) will return undefined.
- **`disableGlobalStashes`** (#284 Wave B, PR #285) — legacy config key removed; the one-cycle deprecation window from the v1 spec has expired.
- **`stashes[]` config-key migration shim** (#284 Wave B, PR #285) — the `stashes[]` → `sources[]` migration was advertised for one release cycle in 0.6.x; that cycle has now expired. 0.5.x configs that have not been touched since will produce a `ConfigError` on parse instead of auto-migrating. Run `akm setup` (or rename the key by hand) to migrate.
- **`searchPaths` legacy migration** (#284 Wave B, PR #285) — pre-0.5.x config key; deprecation window long expired.
- **`context-hub` source-kind migration paths** (#284 Wave B, PR #285) — `STASH_TYPE_ALIASES`, the `parseSourceSpec` `case "context-hub"` arm, the `context-hub-${key}` git rename migration, and the `normalizeToggleTarget("context-hub")` arm are all gone. Per CLAUDE.md, `context-hub` is just a git repo and was never a first-class kind.
- **Legacy lockfile migration** (#284 Wave B, PR #285) — `migrateLegacyLockfileIfNeeded` (the `stash.lock` → `akm.lock` rename) is removed; the rename ran for at least two release cycles.
### Internal
- 9 `console.warn` sites migrated to `warn()` from `src/core/warn.ts` for uniform `--quiet` honoring (#284 Waves A/B, PR #285).
- 6 unused exports removed: `StashLockEntry`, `listProviderTypes`, `resetBuiltinsCache`, and two `GraphRelation` re-exports (#284 Wave A, PR #285).
- ~472 LoC net deletion from `src/core/config.ts` from removing the legacy migration paths above (#284 Wave B, PR #285).
- `--for-agent` deprecation note retained in `docs/technical/akm-core-principles.md` and `docs/technical/search-updated.md` for at least one more cycle.
- Workflow-runs state machine, lesson-init scaffolding, and the proposal/reflect/propose/distill CLI now have direct test coverage (#284 Wave D, PR #285).
### Migration
- See [`docs/migration/release-notes/0.7.0.md`](docs/migration/release-notes/0.7.0.md) for the operator summary and the [archived pre-1.0 plan](https://github.com/itlackey/akm/blob/be3a6a632b0cbe7a63ce71b7d093d8ac266e857c/docs/archive/pre-1.0-migration.md) for the historical per-surface delta from any 0.6.x baseline.
## [0.6.0] - 2026-04-23
### Added
- **`akm workflow validate <ref|path>`** — new subcommand that validates a workflow markdown file or ref, surfacing every error in one pass (without running a full reindex).
- **`akm feedback` now accepts any indexed ref** — previously type-restricted. `memory:`, `vault:`, `workflow:`, `wiki:` refs all work. Vault feedback never echoes vault values.
- **`akm upgrade` runs post-upgrade tasks automatically.** After a successful upgrade, the new binary is invoked as a child process running `akm index`, which auto-migrates any legacy `stashes` → `sources` config keys via `loadConfig` and rebuilds the index against the new schema (`DB_VERSION` 8 → 9 forces a rebuild). Pass `--skip-post-upgrade` to opt out (config migration still runs on the next `akm` invocation; you'd just need to run `akm index` yourself). Result is reported in the `postUpgrade` field of the upgrade response.
- **`writable` flag on sources.** New optional `SourceConfigEntry.writable` controls whether write commands (`akm remember`, `akm import`, `akm save`, `akm clone`) may target the source. Defaults: `true` for `filesystem`, `false` for `git` / `website` / `npm`. `writable: true` on `website` or `npm` is rejected at config load with `ConfigError("writable: true is only supported on filesystem and git sources")`.
- **`defaultWriteTarget` root config key.** Names the source that receives writes when no `--target` flag is given. Resolution order: `--target` → `defaultWriteTarget` → `stashDir` (working stash) → `ConfigError("no writable source configured; run \`akm init\`")`. There is no implicit "first writable in `sources[]` order" fallback.
### Changed
- **Workflows are now stored as validated `WorkflowDocument` JSON** — workflows are compiled into a validated `WorkflowDocument` JSON shape with line-anchored `SourceRef`s back into the source markdown, cached in a new `workflow_documents` table in `index.db`. The run engine reads from the cache on `akm workflow next` instead of re-parsing markdown each step.
- **Feedback events flow into utility recomputation** — positive/negative feedback signals now feed utility scoring alongside search/show events. Telemetry records both `entry_ref` and `entry_id` so feedback signals survive a reindex.
### Changed (breaking)
- **v1 architecture refactor.** The internal architecture was rebuilt around a single minimal `SourceProvider` interface (`{ name, kind, init, path, sync? }`), a unified FTS5 index that owns search and show, and a single `writeAssetToSource` helper that owns all writes. The CLI command surface and all user-visible config keys are unchanged. See `docs/archive/pre-1.0-migration.md` for the historical guide.
- **Config key `stashes[]` renamed to `sources[]`.** Configs with the legacy key load with one deprecation warning and are auto-migrated in memory; the new key is persisted on the next `akm config` write. New configs should use `sources[]`. Configs that contain both keys are rejected with `ConfigError`.
- **Error hints surface without `--verbose`.** Error classes own their `hint()` text; the regex-on-message hint chain in `cli.ts` is removed. Hints print to stderr inline alongside the error message.
- **Registry providers loop through a uniform interface.** Context Hub is no longer a special-cased provider type. Add it as a regular git source (`akm add github:andrewyng/context-hub`) or include it as a kit in your registry index. Legacy `type: "context-hub"` entries normalize to `type: "git"` at load time.
- **Terminology cleanup — clean break from "kit" → "stash"** (#148). Pre-v1, no fallback period.
- **Wire format**: `RegistryIndex.kits[]` renamed to `RegistryIndex.stashes[]`. Schema version bumped to **v3** — `akm-cli >= 0.6.0` only parses indexes with `version: 3`. v1/v2 indexes are no longer accepted. Every static-index registry must regenerate its `index.json` with `version: 3` to be readable. The official `akm-registry` ships a regenerated index alongside this release.
- **Discovery**: npm packages and GitHub repos are now discovered via the `akm-stash` keyword/topic only. Legacy `akm-kit` and `agentikit` keywords/topics are no longer honored. Publishers must retag.
- **Schemas**: `schemas/registry-index.json` and `docs/technical/registry-index.schema.json` updated (`RegistryKit` → `RegistryStash`, `kits` → `stashes`).
- **Internal types**: `RegistryKitEntry` → `RegistryStashEntry`, `InstalledKitEntry` → `InstalledStashEntry`, `KitInstallStatus` → `StashInstallStatus`, `KitSource` → `StashSource`. Files `src/kit-include.ts` → `src/stash-include.ts` and `src/installed-kits.ts` → `src/installed-stashes.ts`.
- **Asset hit field**: `RegistryAssetSearchHit.kit` → `RegistryAssetSearchHit.stash`.
- **Docs**: `docs/kit-makers.md` → `docs/stash-makers.md`. All user-facing "kit" references in docs and the README replaced with "stash".
- **Preserved**: the *Agent Kit Manager* tagline, the `akm-cli` npm package name, and the `akm.include` package.json field.
- **Migration**: a curated registry author should regenerate their `index.json` (rename `kits` → `stashes`, drop legacy keyword filtering). Publishers should add the `akm-stash` keyword/topic and remove `akm-kit`/`agentikit`.
- **`akm registry` description**: changed from "Manage kit registries" to "Manage stash registries".
### Migration / Breaking
- **`DB_VERSION` bumped 8 → 9.** On first run after upgrade, the version-mismatch path in `ensureSchema()` drops + recreates all `index.db` tables (preserving `usage_events` via a typed backup); the next `akm index` rebuilds the index. `workflow.db` (run state) is unaffected.
### Removed (breaking)
- **OpenViking source provider.** The `openviking` source kind is no longer supported. Configs that contain one fail to load with `ConfigError("openviking is not supported in akm v1. …")` and a hint pointing to `akm config sources remove <name>`. API-backed sources will return as a separate `QuerySource` tier post-v1. To downgrade in the meantime, pin to `akm-cli@0.5`.
- **`akm enable context-hub` / `akm disable context-hub` toggles.** Add Context Hub as a regular git source (`akm add github:andrewyng/context-hub`) or list it as a kit entry in your registry; remove or disable it via `akm config sources remove context-hub` or by editing the entry's `enabled` flag.
- **Legacy re-export shims** `src/llm.ts`, `src/registry-provider.ts`, and `src/ripgrep.ts`. akm has no public API (CLI-only package, no barrel exports), so external consumers should be unaffected.
### Internal
- **`src/` reorganized into purpose-named subdirectories** (`commands/`, `core/`, `indexer/`, `output/`, `registry/`, `setup/`, `sources/`, `wiki/`, `workflows/`). No public API surface change.
- **Single `writeAssetToSource` helper** under `src/core/write-source.ts` is the only place that branches on `source.kind` to add behaviour. All write call sites (`remember`, `import`, `clone`, `save`) route through it.
- **`SourceProvider` interface simplified** to `{ name, kind, init, path, sync? }`. The previous `LiveStashProvider` / `SyncableStashProvider` split is gone.
## [0.5.0] - 2026-04-22
### Added
- **Multi-wiki support** (#119, #121, #136, #139, #144): new `wiki` asset type with ten CLI verbs under `akm wiki …` (`create`, `register`, `list`, `show`, `remove`, `pages`, `search`, `stash`, `lint`, `ingest`). Each wiki lives at `<stashDir>/wikis/<name>/` with `schema.md`, `index.md`, `log.md`, `raw/`, and agent-authored pages. Wiki pages are first-class in stash-wide `akm search`. `akm index` regenerates each wiki's `index.md` as a side effect and is resilient to malformed workflow assets. Raw sources under `raw/` and the `schema.md` / `index.md` / `log.md` infrastructure files are intentionally excluded from the search index. See `docs/wikis.md` for the full guide. Design principle: **akm surfaces, the agent writes** — no LLM calls, no network access; akm owns only operations with invariants an agent can't reliably enforce (lifecycle, raw-slug uniqueness, structural lint, index regeneration, workflow discovery).
- **External wiki registration** (#139, #144): `akm wiki register <name> <path-or-repo>` and `akm add --type wiki --name <name> <source>` register an existing directory or git/website repo as a first-class wiki without copying or mutating it; source and wiki search state are refreshed immediately and refs/state are normalized on subsequent indexing.
- **Workflow asset type** (#118): new `workflow` type with `akm workflow` subcommands `template`, `create`, `start`, `next`, `complete`, `status`, `list`, and `resume` for authoring and stepping through multi-step workflows stored in the stash. Runs snapshot their step list at start so edits to the source workflow do not affect an in-flight run.
- **Vault asset type** (#117): new `vault` type backed by `.env` files; `akm vault` subcommand with `list`, `show`, `create`, `set`, `unset`, and `load` (emits a `source` snippet for the current shell via a mode-0600 temp file); values never appear in structured output.
- **`--trust` flag for installs**: `akm add <source> --trust` performs a one-off trusted install, bypassing the install audit for that source. Blocked install errors now include a `hint` pointing to `--trust` as a remediation option.
- **Writable git stash + `akm save`** (#114): `akm add … --writable` opts a remote git-backed stash into push-on-save; `akm save [name] [-m message]` commits (and pushes when writable + remote is set); default stash is auto-initialized as a git repo; git stash provider now uses `git clone` instead of HTTP tarball download.
- **`akm help migrate <version>`** (#132): prints the release notes and migration guidance for a given version (accepts `0.5.0`, `v0.5.0`, or `latest`). Pulls the matching section from `CHANGELOG.md` when available and supplements it with embedded migration notes for major releases.
- **Broader `akm upgrade` coverage** (#132, #134): self-update now detects and upgrades npm, bun, pnpm, and standalone-binary installs (previously binary-only). Runtime assets covered by the upgrade flow were also expanded so newly shipped asset types stay current.
### Fixed
- **0.5.0 QA follow-ups** (#130): fixes across the new wiki, workflow, vault, and save/trust surfaces surfaced during release-candidate QA.
### Removed (breaking)
- The unreleased single-wiki LLM POC: removes `akm lint` command, `akm import --llm` / `--dry-run` flags, `knowledge.pageKinds` config, and the `ingestKnowledgeSource` / `lintKnowledge` LLM prompts. Users of the POC should migrate to the new `akm wiki …` surface; raw content can be manually moved to `wikis/<name>/raw/`.
### Documentation
- **Technical docs refresh** (#138): stash and search architecture docs updated to match the current implementation.
- **Wiki configuration guide** (#115): new docs page covering wiki configuration and ingest flow.
## [0.4.1] - 2026-04-21
### Added
- **`akm enable` / `akm disable`** (#108): toggle optional components (`skills.sh`, `context-hub`) on/off without manually editing config
- **`akm remember` and `akm import` commands** (#110): capture in-session knowledge directly from the CLI; `akm remember` records a memory to the default stash (supports stdin); `akm import` ingests a file or stdin as a knowledge asset
- **Karpathy-style wiki workflow in knowledge assets** (#113): `akm show knowledge:<doc>` now surfaces an `ingest` workflow for knowledge documents; `--dry-run` flag added; `pageKind` taxonomy made extensible
- Documentation: expanded `agent-install.md`, added `info` and `feedback` command docs, global flags reference (#106)
### Fixed
- Remote embedding endpoint URL normalization — trailing slashes and path segments now handled correctly (#112)
- Reduced fallback capture-name collisions in `akm remember`
## [0.4.0] - 2026-04-19
### Added
- **Install security audit**: new pre-install scanner inspects kit contents for dangerous patterns and executable scripts before install; configurable via `config` CLI
- **Project-level config stash merging**: `.akm.json` in a project directory merges its stash/registry entries with user config during CLI runs
- **Disable inherited project stashes**: project config can disable stashes inherited from parent/user scopes
- **`akm curate` command**: new subcommand for curating assets from the stash (initial skeleton)
### Fixed
- Index nested agent markdown files as agents so `akm search agent:...` finds them
- `install-audit` now reads at most `MAX_SCANNED_FILE_BYTES` per file using `Buffer.alloc`, with the file descriptor always closed via `try/finally`, and corrects the `scannedBytes` counter
## [0.3.1] - 2026-04-01
### Added
- **Website stash provider**: add a URL directly as a stash source with `akm stash add <url>`; crawls the site and indexes pages as knowledge assets
- Website provider options: `--max-pages` and `--depth` flags to bound crawling
### Fixed
- Relaxed HTTP warnings for localhost website sources
- Addressed review feedback around website provider routing and security heuristics
## [0.3.0] - 2026-03-30
### Added
- Regression tests for vector/semantic search readiness, install, and setup flows
- `CONTRIBUTING.md` and "Why akm" section in documentation
- Three draft SEO blog posts
### Changed
- **Unified source model**: replaced the `kit` vs `stash` split with a single source concept; `akm add` works for all source types
- Removed `stash` and `kit` subcommand groups; their behaviors fold into the top-level CLI (`akm list`, `akm add`, etc.)
- Refactored semantic search readiness tracking for clearer state transitions
- Aligned documentation voice and updated older posts for the current CLI surface
### Fixed
- Embedding fingerprint is purged on model change and `usage_events` are re-linked correctly
- Local embedder dtype selection
- Release validation workflow
- Prereleases (versions with suffixes) are marked as such on GitHub releases and published to npm with `--tag next`
## [0.2.2] - 2026-03-28
### Fixed
- Binary install detection in `akm upgrade` self-update; centralized `AKM_VERSION` declaration with binary detection tests
## [0.2.1] - 2026-03-25
### Added
- Docker-based install tests covering multiple OS configurations (skipped in CI)
- Detailed error reporting in embedding availability checks
- Actionable guidance when `sqlite-vec` fails to open the DB
### Changed
- **Rename**: project renamed from `Agent-i-Kit` to `akm` across docs and links
- Local embeddings switched to `@huggingface/transformers`
- `@huggingface/transformers` moved to `optionalDependencies`, then promoted to a runtime dependency
- Improved semantic search setup and index UX
## [0.2.0] - 2026-03-18
### Added
- **Extensible asset type system**: `AkmAssetType` (formerly `AgentIKitAssetType`) is now `string` instead of a fixed union; new types can be registered at runtime via `registerAssetType()`
- **Memory asset type**: built-in `memory` type stored in `memories/`, with `memory-md` renderer and directory/parent-dir-hint matchers
- **OpenViking stash provider**: `openviking` provider type for searching OpenViking servers via REST; add with `akm stash add <url> --provider openviking`
- **Remote show for `viking://` URIs**: `akm show viking://resources/my-doc` fetches content directly from an OpenViking server (returns `editable: false`)
- **`--options` flag** for `akm registry add` and `akm stash add`: pass provider-specific JSON config (e.g., `--options '{"apiKey":"key"}'`)
- **`akm registry build-index` command**: generates a v2 registry index JSON from npm/GitHub discovery with `--out`, `--manual`, `--npmRegistry`, `--githubApi`, and `--format` flags
- Exact-name match, type-relevance, and alias boosts in the search scoring pipeline
- Ranking regression tests with a synthetic fixture stash and a 41-case benchmark suite (MRR / Recall@5)
- `estimatedTokens` on context-hub provider search results and in `--for-agent` output
- Architecture docs and test fixture for OpenViking manual testing (`tests/fixtures/openviking/`)
### Changed
- Unified context-hub indexing and fair provider scoring: local FTS scores are preserved everywhere and remote provider scores compete on equal footing
- Replaced RRF with normalized BM25 scoring across all merge paths
- EMA utility decay is now time-proportional instead of tied to index frequency
- Replaced the `(Bun as any).YAML` hack with a proper `yaml` package dependency
- YAML output format fixed; local registry refs now use a `file:` prefix
### Removed
- `manifest` subcommand (adds no value over `search`)
- URI schemes (`viking://`, `context-hub://`) from user-facing refs — assets are addressed as `type:name`; sources use URLs
- Stale audit/ergonomics markdown from the repo
### Fixed
- `skills.sh` install refs now produce valid `akm add` commands (#82)
- Prevented `akm remove` and `akm update --force` from deleting user-owned local source directories installed via path refs
- `usage_events` reverted to `DELETE` on full reindex
## [0.1.0] - 2026-03-10
Major internal overhaul and rebrand. This release simplifies the asset model,
cleans up the CLI surface, and renames the package from `agent-i-kit` to `akm-cli`.
### Added
- `--verbose` flag on `search` for detailed scoring output
- ExecHints system (`run`, `cwd`, `setup`) for script assets, replacing the old tool-runner
- New environment variable overrides: `AKM_CONFIG_DIR`, `AKM_CACHE_DIR`, `AKM_STASH_DIR`
- CI workflow running lint, type-check, and tests on every push/PR
- Biome linter and formatter configuration
- README badges (npm version, CI status, license)
### Changed
- **Rebrand**: npm package `agent-i-kit` renamed to `akm-cli`; binary remains `akm`
- **Rebrand**: config field `"agent-i-kit"` renamed to `"akm"` in `package.json`
- **Rebrand**: plugin `agent-i-kit-opencode` renamed to `akm-opencode`
- **Rebrand**: registry `agent-i-kit-registry` renamed to `akm-registry`
- **Rebrand**: default paths changed (`~/agent-i-kit` to `~/akm`, `~/.config/agent-i-kit` to `~/.config/akm`)
- **Rebrand**: environment variables `AGENT_I_KIT_*` renamed to `AKM_*`
- Removed `tool` asset type entirely; `script` is the only script-like type
- `.stash.json` field renames: `intents` to `searchHints`, `entry` to `filename`; removed `generated` boolean
- `show` command: `--view` flag replaced with positional syntax (`akm show <ref> toc`)
- Collapsed `AssetTypeHandler` handlers into a unified renderer pipeline
- Dropped provider presets (raw JSON config only)
- Pinned `sqlite-vec` to exact version `0.1.7-alpha.2` (removed caret range)
- Replaced `(Bun as any).YAML` cast with proper type guard in CLI
- Version now injected at compile time via `--define AKM_VERSION` with safe runtime fallback
### Removed
- `submit` command
- Provider presets (configure providers with raw JSON)
- `generated` boolean from `.stash.json`
### Fixed
- CLI crash on macOS when running as compiled binary (`package.json` not embedded)
- Cleaned up search output formatting
## [0.0.17] - 2026-03-12
Registry refactor and documentation overhaul. This release introduces a
first-class registry management CLI, modernizes the config schema, and
rewrites all documentation against the final asset model.
### Added
- `akm registry` subcommand group with `list`, `add`, `remove`, and `search` subcommands
- `akm registry search --assets` flag for asset-level search against v2 registry indexes
- `registries` config field (`RegistryConfigEntry[]`) with `url`, `name`, and `enabled` properties
- Registry Index v2 schema with optional `assets` array on kit entries for asset-level discovery
- Official registry pre-configured by default in new installations
- Type names: `KitSource`, `InstalledKitEntry`, `KitInstallResult`, `KitInstallStatus`, `InstalledKitListEntry`
### Changed
- Config: `installed` is now a top-level field (`config.installed`) instead of nested under `config.registry.installed`
- Config: registry URLs configured via `registries` array instead of `registryUrls`
- Documentation: complete rewrite of concepts, registry, CLI reference, README, and all technical docs
- Documentation: added "Mental Model" (registries --> kits --> stash --> assets) to concepts
- Documentation: added asset classification taxonomy description
- Documentation: merged ref format documentation into concepts (removed "opaque handle" framing)
- Documentation: revised apt analogy in core principles to map registries, kits, stash, and assets
- Documentation: added `akm registry` subcommand group to CLI reference
- Documentation: added registry hosting and v2 index format guides
### Removed
- `tool` asset type (fully removed across all documentation and code)
- `registryUrls` config field (replaced by `registries`)
- `config.registry.installed` nesting (replaced by `config.installed`)
- All `tools/` directory references from documentation
## [0.0.13] - 2026-03-09
Initial public release of Agent-i-Kit (`akm` CLI).
### Added
- CLI tool (`akm`) for searching, showing, and running Agent-i-Kit stash assets
- Hybrid search with FTS5 full-text and optional vector similarity scoring
- Registry support for discovering, installing, and updating community kits
- Multiple install sources: npm, GitHub, git URLs, and local directories
- Self-update via `akm upgrade`
- Multiple output formats: plain text, YAML, and JSON (`--json`)
- Knowledge asset navigation with TOC, section, and line-range views
- `akm clone` to fork installed assets into your working stash
- Configuration system with embedding and LLM provider management
- Standalone binary distribution (no runtime dependencies)