All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
- Preserve multiline frontmatter descriptions when
akm lint --fixquotes 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_atvalues only from real user retrieval events (search,show, andcurate) 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.
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 raiseDATA_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_HOMEshared across uids. Remedy: fix the ownership or mode the error names, or pointAKM_DATA_DIRsomewhere 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/updatenow 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 runexits 1 when a run endsblocked. Previously 0. Affected: CI steps and scheduled wrappers that branched only onfailed. Remedy: treat nonzero as "not verified"; resume withakm workflow resume <id>. -
akm index --cleanno longer deletes entries whose file it cannot read. It keeps and names them. Affected: anyone relying on--cleanto prune aggressively; it is now conservative where it cannot see. -
Workflow documents are bounds-checked at authoring time.
engine:name grammar,retry.max0–100,gate.max_loops1–100,map.concurrencyandengines.<name>.concurrency1–64, and anytimeout:≤ 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 healthno longer emitssecret-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: worktreetrees are garbage-collected after 7 days. Remedy: copy anything you want to keep out of a retained worktree within a week.
-
execworkflow units — run a shell command as a workflow step. A step whoseunit:block declaresexec: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.- 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,mapfan-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 anoutput: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-existingretry.onreasons. With the defaulton_error: fail, a non-zero exit fails the step and the run, which is what makes ateststep 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 aretry.onvalue. 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 bysteps.<id>.output, a gate judge, or a human. A step with anoutput:schema still failsexec_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'sMAX_ARG_STRLEN(32 * PAGE_SIZE= 131 072 bytes perargv/environstring), macOS's 256 KiBARG_MAXover argv + environ combined, and Win32SetEnvironmentVariable's 32 767-character per-variable limit. Crossing the bound failsexec_context_too_largebefore the spawn, with an error naming the variable, its size, this platform's limit and where that limit comes from — replacing a bareE2BIGfrom 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_limitandexec_context_too_largekeep their meanings and their place outside theretry.onvocabulary, alongsideexec_cwd_escape: each is deterministic, so re-dispatching could only spend the budget again.PROGRAM_RETRY_REASONSis 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 --unitssayingnon_zero_exitand 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, plusAKM_EVENT_SOURCE— then the unit'senv:bindings, then theAKM_*context.exec.pass_env: [NAME…]adds a few more names (for a per-machine toolchain variable likeCARGO_HOME, which a committedenv:asset cannot express);exec.inherit_env: trueopts all the way back into akm's whole environment. Both keys live insideexec:because the unit-levelenv: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
hashVersionstays 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_envonly whentrue,pass_envonly when non-empty), so an exec unit that says nothing about its environment hashes byte-identically too.
See Workflow Schema: Exec (shell) units and the worked example in Author's Guide.
-
-
akm workflow runnow exits non-zero when the run endsblocked. A verification judge that throws, cannot be resolved, or returns a malformed verdict stops the runblocked— unverified, and resumable withakm 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, matchingfailedand 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.maxis 0–100;gate.max_loopsis 1–100;map.concurrencyandengines.<name>.concurrencyare 1–64; anytimeout: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, soakm lint,akm workflow showandakm workflow createall reported it clean and the failure arrived atworkflow runas 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 lintgained an advisory channel. The result envelope carrieswarnings: LintIssue[]alongsidefixed/flagged,summarygains awarningscount, and text output prints awarningssection. Advisories never route intoflagged, so--fail-on-flaggedcannot fail a run over one. Workflow compile advisories (workflow-warning) are surfaced for the first time — a step with nooutput:schema, aparams.<name>reference to an undeclared param, agate.max_loopsabove 1 on anexecstep — so a bundle that linted clean at 0.9.0 may now report warnings without becoming a failure. Findings that know a location carrylinein--format jsonand render asfile:linein text. A newlint-failedcode reports a file the sweep reached but could not finish. -
Leftover
isolation: worktreetrees 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 stepoutput:orparams: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
irVersionbump 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.patternis not part of the subset. It is a recognized-but-unsupported keyword likeformatorconst: 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,enumlists the allowed values,minLength/maxLengthbound the size, and a step's### gaterubric can check a shape and explain a mismatch. Theformathint now points atenumrather than atpattern.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-timeorpattern:ran fine and now fails to parse for every caller:workflow run,workflow show,workflow create, andakm lint. The quietest surface isakm 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.llmis 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 forllmengines, sodefaults.llmon 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 mixesdefaults.llmwith any agent-engine step no longer freezes.There is no per-step opt-out:
llm: {}is a no-op,llm: nullis a parse error, and the layer merge is additive. Move thellm:block fromdefaults:onto theunit: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: nullopts 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 withakm workflow resume <id>— which the failure message names. -
Workflow
mapsteps now fan out in parallel by default. Amapstep that declares noconcurrency:freezes a width of 4 instead of 1, and an LLM engine that declares noengines.<name>.concurrencyfreezes 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 leftworkflow.maxConcurrencyand the host CPU cap binding on nothing.This is a behavior change on a patch release, so every escape hatch is explicit:
map.concurrency: 1on a step is honored exactly as before — an authored1is kept distinct from an unset field and always wins.- New config key
workflow.defaultMapConcurrencysets the default for every workflow on the machine.akm config set workflow.defaultMapConcurrency 1restores the pre-0.9.1 serial default wholesale. engines.<name>.concurrencypins any engine's own limit (and is now clamped to1..64at 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_jsonwhen 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 frozenworkflow.maxConcurrency, the selected engine's concurrency, and the current host's CPU cap. -
--max-stepsnow 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 theexecutedreport, which gains one per gate-loop iteration and one per route-skip, so--max-steps 3against a step withgate.max_loops: 3could 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-retriesreopen subtracts, so loops and skips no longer shrink a retry's remaining budget either, andmaxSteps:in a workflow task file is the same knob and moves with it. The count is now reported:akm workflow runcarries astepsProcessedfield alongsideexecuted, 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 runcan dispatch go from roughlyN + max_loopstoN × max_loops.What did not change is what the flag bounds within one step.
--max-stepswas 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 fullgate.max_loopsno matter how little budget was left. The per-step ceiling isgate.max_loops(1–100); the whole-run ceilings arebudget.max_unitsandbudget.max_tokens, which are seeded from the unit journal and hold across resumes.
-
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, butrequire("better-sqlite3")succeeds against a mismatched binding — the package resolves its.nodefile lazily — so the error lands atnew 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) != nullptrand 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-sqlite3ships one prebuilt binary per Node ABI and falls back tonode-gyp rebuildwhen 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 publicnode_object_wrap.hso thatObjectWrap'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-sqlite3to12.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.jsondeclares 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
runningand 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.maxno longer re-runs finished work. The completed-attempt scan matched only attempts the current retry policy could have produced, so reducingretry.maxbetween invocations hid a journaled~rNrow and the unit was dispatched again. It now matches any journaled attempt of the unit. -
A scheduled
commandtask 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.logandlogs.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 theAKM_ENGINE_<NAME>_API_KEY/AKM_LLM_API_KEY/AKM_EMBED_API_KEYrecipes), 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 turned3 tests passed, 0 failedinto[REDACTED] tests passed, [REDACTED] failed.For a secret exported under a name none of those rules recognise, any task may name it:
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 improveauto-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 aswrittenPathson the improve result, and thestash_syncedevent gainsattributed/unattributedcounts.akm sync/akm push, which supply no explicit path list, keep the managed-pathspec fallback unchanged. (#652) -
akm lintno longer reports a clean scan for a task file that cannot run. Atasks/*.ymlwhose YAML does not parse (bad indentation, an unterminated quote, tab characters) producedflagged: 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-flaggedpassed a task that would die at schedule time. The parse failure is now its owninvalid-task-yamlfinding. Atasks/*.yamlfile — 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, theakmadapter'svalidate, and theakm-taskformat adapter) from one shared parse, so they cannot disagree. -
akm lint --fixrefuses a bundle configuredwritable: false. Every other mutating command checks the flag before touching disk;--fixwrote 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
--fixwrite failure no longer aborts the run and hides the fixes that already landed. One unwritable file (read-only file, full disk) threw straight out ofakm 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 asfixed: "failed", and the sweep continues through the rest of the bundle. -
akm lint --typesays 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--typeacross mixed-adapter bundle sets keep working. -
missing-skill-mdfires again for anagent-skillspackage with no manifest. The check iterated pending CHANGES, and a change is always a file — so a package directory holding resources but noSKILL.mdcontributed 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 singularskill/alias the same package went unflagged while an identical one underskills/was caught; both spellings are now checked. -
An index akm cannot read no longer reports as an index that does not exist.
fs.existsSync()answersfalsefor a permission error exactly as it does for a missing file, and the read path used it as its "is there an index?" gate — soakm searchandakm curatereturned 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, andakm inforeportedentryCount: 0, vecAvailable: falsefor 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 aConfigError(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 inforeports anindexStats.unreadablediagnostic rather than zeros that look healthy. The field is absent on every healthy run.akm healthnow diagnoses an unreadablestate.dbas a failingstate-db-readablecheck instead of dying on the open before it could report anything — it is the command you reach for when this happens.probeLockreturns a distinctinaccessiblestate instead of classifying a permission error as a stale lock. "I cannot read this lock" and "the holder is dead" are opposite facts, andakm improvenow 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.lockcould 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 --cleandeleted rows for files it merely could not look at, and reported the deletions as a clean success. Unreadable entries are now kept and named.indexWrittenAssetsreturnedtrue— "the index is as you expect" — for an index it could not open, on the strength of whichacceptProposaladvanced its journal toindex-finalized.akm improveeligibility,akm feedback,akm bundle listand the graph loaders each turned a permission fault into an empty result, a zero count, or the advice to "Runakm indexfirst".
-
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/umaskare your levers.This is scoped, not blanket: akm still creates a handful of files at restrictive modes at creation time, as it always has —
envandsecretassets and config backups at0600, their directories at0700, 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/0700on every open — reverted because re-permissioning a directory akm did not create silently broke installs that share$XDG_DATA_HOMEbetween two uids (agent sandboxes, containers, service accounts). If a pre-release tightened your data directory,chmodit back. The second was anakm healthadvisory (secret-file-perms) that reported group/other-readableenv,secretsandconfig-backupspaths — removed too: it is meaningless on Windows, and nagging about modes akm does not set is not health reporting.akm healthno longer emits this check, and no longer exits4on account of it. -
timeout: noneon 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
timeoutafter 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_incompletewas 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>.outputscope 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 --timeoutit rendered atimedOutmarker on acompletedrun and exited nonzero. Both surfaces now drop the marker once the run reachedcompleted— a deadline that lands with nothing left to abort has nothing to report. -
A rejected gate on an
execstep 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 thatgate.max_loopsis capped at 1 on a step whose unit isexec:— not an authoring error, and no change at all to an engine step, where a declaredmax_loopsis still honored in full. This is the same reasoning that already makes an exec unit'soutput: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: worktreetrees 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 withoutPATHEXTabin: "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/binagainst/home/al) matched too. -
A directory whose name merely begins with two dots (
..data) was treated as a path escape. For a workflow execcwdthat meant the parser and the frozen plan accepted a spelling the executor then failed as tampering, with a reason no retry can clear. -
appendEventresolved 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/llmoverrides 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 bystepId.
-
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
### Fixedentry above for the full account.
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.
- 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 abundle//installation slug and suffixed with#fragment. Durable state stores the fully-qualifiedbundle//conceptId; the short bundle-omitted form is accepted input only (resolved againstdefaultBundle, then installation-priority order). The pre-0.9.0[origin//]type:namegrammar is removed — there is no compatibility parser; the frozen migrator inscripts/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 formerworkflow.dbintostate.db(four databases down to three:state.db/index.db/ a separatelogs.db), and migrates config from the flatstashDir/sources/installed/wikiNamekeys tobundles/defaultBundle. A semantically verified, installation-scoped backup manifest v4 (covering the pre-rescueindex.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 retiredstashDir/sources/installedkeys are hard-rejected by the 0.9.0 config schema whenever present (the error namesakm 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.mdare reserved structural files. Per the Open Knowledge Format,index.md(directory listing) andlog.md(update history) are never indexed as concepts and are never valid write /mvtargets 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).vaultasset type removed. Useenv(a whole.envgroup; key names surfaced, values never) andsecret(a single sensitive value), addressed asenv/<name>andsecrets/<name>.akm-migrate storageperforms the non-destructivevaults/→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-levelakm enable/akm disable,akm events,--detail summary|agent,--for-agent,--note, and--source(on accept/reject/history) are gone — use the canonical spellings documented inSTABILITY.md.
See docs/migration/v0.8-to-v0.9.md and
docs/migration/release-notes/0.9.0.md for the full upgrade procedure.
-
The experimental
akm workflow brief/akm workflow reportexternal-driver protocol is removed, along with theexperimental.workflowEngineconfig key that gated it, itsWORKFLOW_ENGINE_NOT_ENABLEDerror code, and theworkflowEngineblock inakm task doctor.akm workflow runis 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_unitsand the cross-surface parity obligation onstep-work.ts, both of which constrained every future engine change. The analysis is recorded indocs/architecture/specs/driver-protocol-keep-or-cut.md.Legacy configs setting
experimental.workflowEngineremain valid — the config schema is.passthrough(), so the key is accepted and ignored.
-
akm workflow runand prompt tasks fall back toopencode-sdkinstead of refusing when no engine is configured. A clean install that never ranakm setup— a bare container, a CI image, an agent-operated session — used to fail closed withINVALID_CONFIG_FILE(exit 78). When theopencodebinary is on PATH, akm now synthesizes a config-freeopencode-sdkengine: 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. Withopencodeabsent the failure is unchanged, and its remedy now names both routes. An operator-configuredopencode-sdkengine always wins over the synthesized one.The requirement is the binary, not the npm package:
@opencode-ai/sdkis an HTTP client that declares no dependencies and whose owncreateOpencodeServerspawnsopencode serve, so a host with the package and no binary has no server to reach. Install it withnpm i -g opencode-aior 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 agentcarries it in its resultwarningsand on stderr, andproposeandimprovereflect 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 addnow recognizes three new kinds of URL and snapshots them as knowledge assets instead of crawling them as ordinary web pages: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
/feedURL 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_TOKENfor the X API v2, orX_RSS_TEMPLATEto 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: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-migratederives the 0.9 config from your 0.8 keys instead of demanding one. Upgrading used to require hand-authoring a complete 0.9 config beforemigrate applywould act. The firstapplywith no--confignow writes a validated starter config —bundles/defaultBundlederived from the 0.8stashDir/sources/installedkeys — to a predictable path under the backup root and stops, with config and durable state byte-for-byte untouched; a secondapplypicks it up and performs the cutover. Engine settings are never guessed:profiles.*anddefaults.llm|agent|improveare stripped and reported individually indroppedKeysby their exact 0.8 dotted path.statusandapply --dry-runpreview the same plan, and an explicit--configalways wins and is never overwritten.akm migrate --formatnow 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.metadatafor 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-onlyakm-eval-attribution-rollupseparates 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 statusclassifies config,state.db, andworkflow.dbindependently;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. Seedocs/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.jsonand whose## <step-id>body sections carry instructions and gate rubrics.akm workflow createscaffolds that format;akm lint --type workflowsparses and compiles it. Bare references (params.<name>andsteps.<id>.output.<path>) wiremap.over,route.input, andinputs; prose is never interpolated. - Compilation + frozen plans.
akm workflow startcompiles 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/overwith aconcurrencycap and acollect|votereducer), a typedoutputJSON Schema (validated via arunStructuredretry-with-feedback loop),envbindings (resolved through the existingakm env runmachinery — secret tokens, dangerous-key policy, keys-only audit events), and classify-and-dispatchroutesteps. - Determinism + replay. Journaled unit identity is content-derived
(
<step>:<sha256(item)[:12]>,:solofor 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 newworkflow_run_unitstable 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 throughcompleteWorkflowStepso 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
outputschema 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_loopsbounds 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 boundedretry: { max, on: [<failure_reason>…] }keyed on the persisted failure taxonomy. - Isolation + leases.
isolation: worktreeruns 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; manualcompleteis 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 exactreportcommand lines.akm workflow report <run> --unit <id> --status completed|failed|runningis 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_loopscompletion path).--status runningclaims/heartbeats a unit for stale-driver detection without advancing the spine;--rerunrecords 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), andreport --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'sworkflow_*/workflow_unit_*events as NDJSON (--streamforeground-polls to a terminal status, no daemon);akm workflow status --unitslists per-unit diagnostics (failure reason + result/error text) without feeding them into the deterministic artifact graph; unit lifecycle emitsworkflow_unit_started/workflow_unit_finishedevents 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_REGISTRYwith 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.dbmigrations 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 indocs/archive/akm-workflows-orchestration-plan.md, andSTABILITY.md(Experimental). - Authoring. A workflow is a markdown asset whose frontmatter graph is
validated against
-
fablebuilt-in model alias — resolves toclaude-fable-5(opencode/claude-fable-5on opencode); recommended resolution target for thedeepworkflow model tier. -
akm lintnow checks the frontmatter xref channels for broken refs. The existingmissing-refcheck additionally scans thexrefs:,supersededBy:, andcontradictedBy: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>)). Therefs: []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-flaggedCI users: stashes with already-dangling xrefs (e.g. from past renames) will gain newmissing-reffindings on upgrade — fix the refs or addlint_skip: [missing-ref]per file.sources:,source_refs:, andevidenceSources:are deliberately not checked (wikisources:is covered byakm wiki lint; the latter two legitimately point at merged-away assets). -
--xref <ref>onakm rememberandakm import— write-time cross-references with validation. The stash back-linking conventions route provenance and associative links throughxrefs: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'sxrefs:frontmatter list, which the indexer folds into search hints — the new asset becomes findable from searches for its source.remembermerges the refs into its generated frontmatter (composes with--tag/scope flags; does not trigger the tags-required check);importdedupe-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--xrefstill 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 additivehintoutput key pointing at the stash's placement conventions (facts/conventions/organizationwhen that fact exists), so CLI writers see the conventions that LLM flows already receive by injection. -
--supersedes <ref>onakm rememberandakm 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 itsxrefs:(correction provenance), and the old asset gainsbeliefState: superseded+supersededBy: [<new ref>]via the sharedwriteSupersededEdgeprimitive (sibling ofwriteContradictEdge) — 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 currenthides 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--forceoverwrite) 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 additivesuperseded: [{ref, applied: false, reason}]key (applied: trueon success) — the reason names the--targetremedy 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 barememories/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, sinceentry_typeis not an FTS column). The listing is recursive and/-boundary exact (projectA/cannot leak a siblingprojectAlpha/…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--sourcenarrowing exactly like the existing empty-query enumeration — hits carry the fixed browse score1in deterministic listing order, not a relevance ranking. The parsed type is explicit intent: a baresessions/enumerates sessions just like--type session(the default session exclusion is an untyped-path policy), while an explicit--typeflag 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 isakm show's job. Stable-surface note:akm searchis Stable; this changes results for a query shape that previously returned noise or nothing. A user literally keyword-searching for the stringmemories/x/loses the old fuzzy token behavior — accepted as negligible. -
The
category:frontmatter key is now captured into the index asentry.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 alongsidebeliefState(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 thatcategorynever enters the FTS search fields). Requires a reindex to take effect for existing entries. The companion rank-time demotion ofcategory: conventionfacts on untyped queries was NOT shipped: the prescribed measurement (full skeleton convention facts plus a realknowledge/authasset, untypedauthquery, 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 bytests/search-convention-fact-demotion.test.ts, which becomes the regression guard if a demotion contributor is ever revisited. -
Config-gated indexing of the self-situating body opening —
index.indexBodyOpening(defaultfalse). Body prose is not indexed (the FTScontentcolumn carries only TOC headings and parameters), which is why the stash conventions route orientation intodescription:/when_to_use:. With the new flag enabled, the metadata pass captures the first prose paragraph of each markdown asset body — skipping headings (ATX and setext), fenced code blocks, thematic breaks, and a leading nested frontmatter block (only when its content is actually frontmatter-shaped: prose wrapped in decorative---lines is captured, not discarded); capped at 280 chars with word-boundary truncation and a trailing ellipsis — intoentry.bodyOpening, which folds into the lowest-weightcontentFTS column (bm25 weight 1.0, so a name match always outranks a body-opening-only match) and into the search/embedding text. Secret and env files are never read for it, and session-kind memories (akm_memory_kindin outer or nested inner frontmatter) are excluded — their bodies are raw transcripts. Both indexing walks and write-path indexing honor the flag (the metadata pass reads the user config directly). With the flag absent orfalse, entries and search fields stay byte-identical to before. Costs of toggling (either direction): indexed text changes, so collapse-detector canary recall baselines shift — re-mint viaakm improve canary --refresh— and embeddings are NOT regenerated for entries that already have one, while incremental runs re-extract only changed files. Runakm index --fullafter toggling: it re-extracts every entry and wipes embeddings so they rebuild from the new text; until thenakm indexwarns that the flag differs from the state the index was built with. The conventions'description:/when_to_use:orientation routing remains primary — this flag makes body openings additionally pay retrieval rent, it does not replace structured metadata. Seedocs/configuration.md. -
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 theutility_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.mdtwin moves together, keeping theentry_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,.derivedtwin 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 inreadOnlyCitersas manual follow-ups. Output:{ok, from, to, rewrote: [{file, count}], readOnlyCiters, utilityPreserved}; a successful move appends an exactly-oncemvevent. 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 (seeSTABILITY.md).
-
X source tokens now resolve from the secret store during bundle update. The
secrets/x-bearer-tokenakm secret is honored on the providersync()/ bundle-update path, not just when adding or importing a URL — closing a gap where a refresh saw only theX_BEARER_TOKENenvironment variable. Implemented as aSecretResolvercapability injected from above the source-provider import cycle; internals are documented indocs/architecture/reviews/env-secret-access.md. -
websitecrawls 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: aRetry-Aftersleep could previously parkakm bundle addfor as long as a rate-limiting server asked, well past the advertised cap. Raise it for a large site, or set"crawlTimeoutMs": 0to disable the cap. Relatedly,fetchWithRetrynow honors its caller'sAbortSignalduring 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. -
websitesources now respectrobots.txtby default. Before crawling an origin, akm fetches and parses that origin's/robots.txtand skips paths disallowed for theakm/akm-cliproduct tokens (or*), honoringCrawl-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-runningakm bundle updateon a website source is what surfaces it. Opt out with"respectRobots": falseon the website descriptor to restore the exact pre-upgrade behavior (no/robots.txtrequest at all):{ "bundles": { "docs": { "website": { "url": "https://docs.example.com", "respectRobots": false } } } } -
akm lintnow routes through each bundle adapter's ownvalidate().validate()was a required member of the adapter interface that nothing called:akm lintbranched on adapter id and re-implemented OKF's checks inline (with drifted semantics formissing-type), OKF'smissing-refnever ran at all (a bundle with a dangling link reported nothing), and llm-wiki'suncited-raw/broken-xref/broken-source/missing-descriptionchecks 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
defaultandfrequentstrategies resolve extract off, whiledefaultandreflect-distillresolveproactiveMaintenanceoff. The dedicatedproactive-maintenancestrategy remains enabled. Built-ins such asthoroughthat omit these fields inherit the newdefaultoff values; user overrides are merged last, so explicitenabled: truevalues still win. Standalone extraction remains independent of the improve-stage toggle but still requires--type <harness>or--auto. The bundled, unselectedcore/extracttask now usesakm extract --auto; existing scheduled tasks with invalid bareakm extractcommands 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'srecognize, instead of always using theakmadapter. 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 legacyvaults/skip, wiki infra files) out of the core scan and into theakmadapter's own recognition, so each adapter owns its bundle's filtering. Reindex note: any non-akmbundle that was previously probed as one adapter id but still recognized byakmwill re-index under its own adapter on the nextakm 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
tagsat index time, even when an asset sets explicittags:frontmatter. Previously explicit tags suppressed all path-derived tags, so a nested asset likememories/projectA/auth-tipwithtags: [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 whentagsis 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 withakm 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--belieffilter axis is unchanged. Semantic-only hits are judged against thesearch.minScorefloor 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 throughassembleAsset. A metadata edit is no longer a (whitespace-level) content edit; files gaining their first frontmatter block still use the canonical shape.
-
Fresh 0.8 installs can actually upgrade. A config that 0.8.x wrote itself carries no
configVersionkey at all (0.8 stamped it only when a 0.7-era migration did substantive work), and the migrator read the absent key asinconsistent— an unconditional blocker.migrate statusreportedblockedandmigrate applyrefused with exit 78 for every fresh 0.8 install; reproduced end to end against the publishedakm-cli@0.8.14. An absentconfigVersionon a positively pre-cutover-shaped config now classifies asold; a present-but-unparseable version still fails closed. Relatedly,migratereportsnot-applicable(exit 0) instead ofblockedwhen there is no akm installation to migrate at all, andapplywarns when an active workflow run targets an asset that fails 0.9 structural validation, naming the asset andakm workflow abandon <run-id>. -
akm lintfails closed on mistyped invocations. A nonexistent--dir, or an unknown--typeon an akm bundle (the classic singular/plural typo,--type workflow), used to scan nothing and report a cleanok:true, flagged:0— silently passing scripted--fail-on-flaggedgates. Both are now usage errors (exit 2), the--typeerror 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 upgradeverifies the package manager actually delivered the new version. A lagging@latestdist-tag (partial publish, registry mirror lag) exits 0 while leaving the old version on PATH; upgrade used to report success anyway — and then runmigrate applyagainst the old binary. It now re-readsakm --versionafter the install: a confirmed mismatch reportsupgraded: falsewith an exact-version pin command, and a verified match is named in the success message. -
akm infono longer overstates semantic-search health. After a run with partial sqlite-vec fast-path insert failures, the verification reportedready-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 anakm index --fullhint when the fast path is degraded. Relatedly,embedding.dimensionis now bounded to the vec table's own 1–4096 limit at config validation, so an out-of-range value fails atakm config setwith a clear message instead of crashingakm indexmid-run. -
Standalone
akm remember --enrichactually enriches. With no other metadata flag,--enrichfell through to the zero-flag raw-write hot path and never attempted the LLM call — an unenriched memory with no warning.--enrichnow 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.dbon a fresh install. The fire-and-forget usage telemetry behindsearch/show/curateopenedindex.dbwith create-on-open: with no index built yet, the open itself left an empty, schema-lessindex.dbbehind, and every later command then saw an existing-but-broken index ("no such table: entries") — hard-failing proposal acceptance among others.openExistingDatabasenow refuses to create the file (a missing index throws, namingakm indexas 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-lockedis 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 throughextraParams. -
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):formatWorkflowNextPlainandformatWorkflowStatusPlainrender theCONTINUEdirective, and every run-detail response (status/start/complete) evaluates the check-in instead of onlyworkflow next. -
Workflow frontmatter validator error message now lists the actually-allowed keys (
name,updatedwere missing); removed the documented-but-nonexistentakm workflow stepalias fromdocs/features/workflows.md.
-
akm updateno longer deletes a previous install directory without confirmation. When a managed source's resolved content location moves,updateremoved the old directory outright, whileakm removehad always required--yesin 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/--yesto 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 carryingLD_PRELOADinenv/nested/inner.envinstalled cleanly with no warning. Files without a.envsuffix are still not scanned — no akm code path loads them as environment variables.
-
The
okfadapter reads OKF v0.2's trust/provenance and lifecycle frontmatter families.generated: {by, at}(withgenerated.attaking precedence over the legacytimestampfield, which remains a valid fallback),verified(a list, or v0.2's permitted single-mapping shorthand),sources(an object list —resourcerequired;id/title/author/usage_count/last_modifiedoptional),status(draft/stable/deprecated), andstale_afterare now parsed leniently from any OKF concept's frontmatter and surfaced on new, namespacedIndexDocumentfields (provenance,lifecycleStatus,staleAfter,okfVersion) that never overload the pre-existing AKM-nativesources(wiki citation strings),generation(consolidation depth), orqualityfields. As with every other optional OKF field, a missing or malformed value never rejects the document. Theokfadapter 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
okfadapter, which stays consumer-only and unaffected by this).promoteProposalprojects the proposal system's ownsource/sourceRun/gateDecision/reviewbookkeeping — already tracked instate.dbbut previously never written to disk.generated: {by, at}andverified: [{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;sourcesalone is namespaced asprovenance: {sources}, because a baresources:collides with the pre-existing wiki citation-string convention.generated.byrecords whether the content came from an automated pipeline (akm/<version>) or a human-initiated source (human:<id>);verifiedrecords 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 asprovenance.sources. AKM's own adapter rereads what it wrote, soakm showsurfaces it. Every AKM-native markdown type is stamped,workflowincluded.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,
byfalls back tohuman:<OS username>, which puts that username into content you may later commit and share. -
Internal: a
capturedAtHeadintegrity guard (scripts/lint-golden-captured-at-head.ts, wired intobun run lint) now checks every golden fixture's recordedcapturedAtHeadcommit 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 localgit 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) onakm search,akm curate, andakm show. Pass--no-track-usagefor a read-only lookup that does not feed usage telemetry or the utility-score ranking signal. Previously a bareakm searchsilently wrote autility_scoresrow that influenced future ranking, with no disclosure and no way to opt out. -
akm showreturns the canonicalrefin every shape. It was present only under--shape agent, so a--shape summaryconsumer had to make a second call at a different shape just to learn which asset it was looking at. -
akm infogainedstashDir,defaultBundle, andindexStats.byType. Answering "which stash is primary" previously required a separateakm sources list. -
instructionis a stash-resident asset type. It was already inKNOWN_TYPESand had a presentation entry, but had no placement spec — so there was nowhere to put one and the indexer never recognized one.akm bundle createnow creates aninstructions/directory,.mdfiles under it index asinstruction, and--type instructionis accepted and tab-completable everywhere--typeis. A compile-time assertion now pinsplacementTypes() ⊆ 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, andhistoryaccept--bundleto operate on a non-default bundle instead of only the primary stash.addresolves through the normal writable-target rules;run --bundle Xresolves the task file and relative asset refs from bundle X. A non-default bundle is recorded in the scheduler entry as--bundle <bundle>, so scheduledakm task runresolves 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_outcomestate rows (#733). A new improve maintenance pass (runOrphanStateGcPass, run next to the existing orphan-proposal purge) stampsmissing_sinceon any state row whose ref no longer resolves againstentries.item_ref, clears the stamp the moment the ref resolves again, and — only whenimprove.stateGc.collectis set totrue(defaultfalse) — 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 newasset_state_gcevent ({pending, collected, byTable}), emitted only when there is something to report, so live data can prove the report clean beforecollectis ever turned on. Additive migration021-asset-state-missing-sinceadds themissing_sincecolumn 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 inentries.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_eventsis out of scope (already covered by cascade-on-delete plus its own 90-day retention purge).
-
Workflow execution is consolidated on stable
akm workflow run. The publicworkflow start,next, andcompletecommands are removed with explicitUNKNOWN_COMMANDmigration hints;run <ref|run-id>now owns creation, active-run continuation, native dispatch, completion, and durable replay. It is no longer gated byexperimental.workflowEngine; only the experimentalbrief/reportexternal-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: replaceworkflow start/next/completeloops withworkflow run, replace--paramswith exact declared flags, and configureworkflow.judgeEnginebefore running a workflow with a non-empty### gaterubric. -
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,stepswithunit/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/.ymlworkflow files, the# Workflow:/## Step:/Step ID:markdown headings, andakm workflow create <name>.yamlare all gone;akm workflow createalways 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 newinputs:key declares. Instructions refer to that context in plain language ("clone the repository named by therepoparameter") rather than splicing a value into the instruction string. Bare reference strings (two roots,params.<name>andsteps.<id>.output…) now appear only in three frontmatter positions:map.over,route.input, andinputs:.Gate rubrics move to the body. A step's completion criteria are no longer a frontmatter
gate.criterialist or a### Completion Criteriabullet section — they live under a step's### gatesub-heading, the format's one reserved marker, as full prose a judge receives byte-exact. Frontmattergate:now carries only optionalmax_loopsconfiguration. Omitted or empty rubric text skips validation; a non-empty rubric requires the frozenworkflow.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.mdopener 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, andproposalran an implicit default action and exited 0 (bareakm graphsilently renderedgraph summary);registry,log, andlessonsprinted citty's human usage banner to stdout; onlymigrateraised a structured error. All eleven now emit the sameMISSING_REQUIRED_ARGUMENTenvelope 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/--typeflags the bare form did). -
BREAKING:
akm syncpersistseventType: "sync", not the legacy"save". The event name now matches the command name. Historicalstate.dbrows are left as-is —akm logandakm log tailtreat"save"and"sync"as synonyms on read, soakm log --type savekeeps 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 byeventType === "save"— reading state.db directly, bypassingakm log— should also match"sync"to see new syncs. -
BREAKING: dropped the dead
installedKitCountfield from theadd,remove, andupdateJSON envelopes. It was a raw lockfile-entry count that nothing — internal code or test — ever read.Migration: a script parsing
.config.installedKitCountshould stop; the field is gone, not renamed.config.sourceCountremains and is unaffected. -
BREAKING: dropped the dead
graphPathfield from everyakm 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 nothingstashPathdid not already provide.Migration: a script reading
.graphPathfrom anyakm graphsubcommand should stop;stashPathremains. -
BREAKING:
semanticSearchModenow 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 firstakm index, because the fallback used when the key is absent was"auto". The interactiveakm setupwizard 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 remoteembedding.endpointis 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 pointembedding.endpointat a remote embedder. -
BREAKING:
akm workflow run|brief|reportrefuse to run untilexperimental.workflowEngineis 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 plainakm workflow run. The gated surfaces now exit78with aConfigErrornaming the exact key, andakm task doctorreports the gate's state. Authoring and linting the unified markdown format, along with every otherakm workflowsubcommand, 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>asenv/<name>andsecret:<name>assecrets/<name>. The error message prints the exact replacement. -
akm improveis 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 requireakm config set experimental.improveAutonomy true: memory-inference writes, the memory-cleanup pass, and triageapplyMode: "promote"(which downgrades toqueuerather 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_skippedevent withreason: "autonomy_gated", and is counted inakm health's improve skip-reason summary.Migration: set
experimental.improveAutonomy: trueto restore the previous behavior.sync.pushis not affected — it keeps itstruedefault and its ownsync.push: false/--no-pushcontrols. 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 improveno longer rejects the global--format. It emits an envelope throughoutput()(always under--dry-run, otherwise under--json-to-stdout), so--formatapplies to that envelope; progress output stays on stderr. Previously it exited 2 withINVALID_FLAG_VALUE, which made it the one command that rejected a valid global flag. -
akm health --reportreplaces 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 htmlrenders 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, andakm healthno longer reads--formatat all.Migration:
akm health --format html→akm health --report --format html(the bare form now renders the plain check generically); the html-only--compareflag is removed — use--window-compare, which with--reportdefaults to the--sincewindow 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 jsonsynced a bundle named "json", andakm env unset env:x KEY --format jsontried 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--helpvisibility — all three already parsed--format/--detail/--shape/--outputcorrectly, since none of them has a positional a stray value could fall into. -
BREAKING: unknown commands and missing required arguments now exit
2(usage), not1. citty's own command-dispatch wrapper unconditionally calledprocess.exit(1)for any error it raised before a command's own body ever ran —akm totally-bogus(unknown command), bareakm log/akm lessons(a subcommand group invoked with no subcommand), and a command missing a required positional (e.g. bareakm import) all exited1, 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 as2while leaving--help,--version, and every other exit code unchanged.Migration: a script that treated exit
1as "something went wrong" for a mistyped command or missing argument should check for2instead (or keep treating any non-zero exit as failure, which was already correct). -
BREAKING:
akm completions --shell <unsupported>now exits2with the standard JSON error envelope, not1with a raw stack trace.completionsstays 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 code2in place of a stack trace and exit code1. -
BREAKING:
akm index --dry-runwithout--cleannow exits2instead of running a real index. The flag only ever gated the--cleanstale-entry removal pass — every other phase (walk, LLM enrichment, embeddings, FTS, the adapter-detection config write) ran for real regardless, soakm index --dry-runalone 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-runwas already getting a real index, so nothing there needs to change in effect — but it will now fail loudly instead. Passakm index --clean --dry-runto preview the stale-entry removal pass, orakm index --cleanto apply it; drop--dry-runentirely to keep running a plain real index. -
BREAKING: a corrupt or unparseable
akm.locknow 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.lockis 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_REGISTRYnow 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 toregistry.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--registryflag: wholesale replacement, not a merge with the public registry).Migration: an operator who set
AKM_NPM_REGISTRYexpecting 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 updatefor npm-sourced bundles now resolve entirely against the configured mirror when it is set. -
akm remember --show-similarandakm migrate apply --dry-runare 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/--dryRunkeep working — they're now explicit, documented aliases instead of an undocumented accident. -
--detailand--shapehelp text is scoped honestly. The per-command--detaildescription now namesinfo,list, andrememberas the commands where it has no effect (verified byte-identical output at every level —akm showis not one of these; it has three distinct brief/normal/full payloads).--shape's per-command help now repeats the "summaryis only valid onakm show" caveat the root help already documented. -
All six
--formatvalues work on every command (0.9.0 decision D7).json|jsonl|yaml|text|md|htmlare now universal. Previously there were three inconsistent behaviours:mdsilently emitted the JSON envelope everywhere exceptakm health,htmlwas rejected with exit 2 everywhere exceptakm health, andakm healthreached 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 healthkeeps 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 mdon a non-health command previously returned JSON and now returns Markdown; a script that parsed that JSON should ask for--format jsonexplicitly.--format htmlpreviously exited 2 on non-health commands and now succeeds.Also:
akm graph export --formatis removed — it declared--formatlocally as well as globally (one token, two parsers). The artifact payload now follows the--outextension (--out g.jsonlwrites JSONL, anything else JSON); the global flag only renders the command's own envelope. A dead local--formatdeclaration onakm historywas 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 insrc/output/format-exempt.tsand now warn when given--formatinstead of ignoring it silently.output.formatin config accepts all six values. -
Subtree browse is a conceptId prefix, not
<type>:(0.9.0 decision D4).akm searchenumerates onmemories/,memories/projecta/,bundle//, andbundle//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 theakmadapter's placement types, so items from every adapter browse the same way, andbundle//lists a whole bundle (the replacement for the removedakm 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.tsno 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--bundletoken; 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 --bundleorsync --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/curateranking (#692).asset_salience.rank_score(an encoding + outcome + retrieval projection, recomputed everyimproverun) previously composed into every default search as a bounded multiplicative boost (salience-ranking, ×[1.0–1.2]), loaded best-effort fromstate.dbon 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 sameusage_eventsthe 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 defaultstate.dbload also fixes a confirmed hot-path defect: wheneverstate.dbalready 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 SQLitebusy_timeoutever 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_scoreitself, and everythingimprovecomputes 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_outcomestate.db access moved behindsrc/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.tsandoutcome-loop.tsre-export the moved functions, so no importer or test churns. A newstate-table-sqlrule inscripts/lint-repository-sql.tsnow fails the build if rawasset_salience/asset_outcomeSQL reappears outside the repository directory (orcore/state/migrations.ts). Not a user-visible behavior change:rank_score,outcome_score, and everythingimprove/healthcompute from them are identical.
-
The compiled standalone binary can run
akm migrate. Release binaries compiled onlysrc/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/applyupgrade path always failed withFILE_NOT_FOUND. Standalone builds now compilescripts/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 migratere-execs the binary with anAKM_MIGRATE_ENTRYmarker 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_stateand 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 inlegacy_state_rowsbefore 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.jsonand 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.jsoninto frontmatter droppedxrefsandsourcesentirely and mapped legacysourceRefsto asource_refskey that could never fire (the validator stopped copying the field) and that 0.9 never reads — then deleted the only copy.xrefs/sourcesnow fold through, and legacysourceRefsmerge intoxrefs. -
A reserved-filename rename re-keys durable state. The D-R6 rename of a mis-named
index.md/log.mdconcept 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: falsebundle 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/installedAtrecorded 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 filesystemwritablereads astruein the new shape, so a source the user deliberately protected became writable), an explicitenabled: false(resuming refreshes and indexing for content the operator had turned off), and a website source'smaxDepth(silently resetting crawl depth). All three now round-trip to the runtime source entry;bundles.<id>.enabledis a supported key. -
akm mvrefuses a bundle markedwritable: 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
--supersedesare no longer ignored.writeSupersededEdgepersists a fully-qualified conceptId, but the belief analyzer accepted only the internalmemory:<name>spelling, so every edge fromakm remember --supersedes/akm import --supersedeswas dropped and a superseded memory read back as active. -
akm env run <ref> -- <cmd> --helpruns the command. The builtin help-flag scan read the child tail after--and printed akm's own usage instead. -
akm mvworks under anAKM_STASH_DIRoverride again. A valid override not owned by a configured bundle failed withNo 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 setprocesses 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 atindex.enrichment.timeoutMs(orindex.defaults.timeoutMs). Indexing concurrency is auto-derived (2 remote / 1 local) and currently has no config override on that path:engines.<name>.concurrencyis a valid schema field but the engine resolver does not forward it (documented indocs/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 andakm index --fullto re-mint, accepting the loss of learned state keyed to the old id. -
The scaffolded
organization.mdconvention no longer contradictsakm mv. It told authoring agents "there is no command that preserves an asset's identity or learned state" across a rename and showed a rawmv.akm mvdoes 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.taskSchedulesis no longer documented. The key was removed from the schema in 0.9.0 (nothing ever read or wrote it), butdocs/reference/configuration.mdstill described its two sub-keys. -
A freshly scaffolded stash passes its own
akm lint. All 12 shippedfacts/conventions/**convention templates carry frontmatter but none carried anupdatedfield, so the firstakm lintafterakm initflagged 12missing-updatedissues 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//metais the documented spelling for the primary stash.docs/reference/cli.mdanddocs/guides/concepts.mdshowedakm show local//meta, which errors withASSET_NOT_FOUND—local//is no longer a scoping prefix, so it reads as a bundle namedlocal. -
akm syncemitsshape: "sync". The envelope kept the"save"shape from the command's pre-rename name even after the persistedeventTypewas renamed. Unlike the event log, the shape is per-invocation and never persisted, so it needs no read-side synonym. -
akm add <pkg> --provider npmadds an npm source instead of a broken filesystem bundle.--providerwas 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 npmis now rejected at add time rather than storing the URL as a package spec and failing much later at first sync. -
akm add --providerno longer printsInstalled undefined. Two incompatible result shapes reached one text formatter; each is now rendered honestly, including whether a follow-upakm updateorakm indexis needed. -
akm update --allaccounts for every configured source. It previously considered only registry-managed installs and reportednothing to updatefor 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 newskippedfield with the reason. A successful update of a plain source no longer renders asnothing to updateeither. -
akm searchwith no query browses, as--helphas 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 curaterespects--limitfor registry hits, which were capped at a hard-coded 2 regardless. -
akm search --no-project-contextworks. citty strips a leading--no-before consulting declared args, so a flag declared asno-project-contextcould 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, andakm improveno longer skip cleanup on exit. They calledprocess.exit()directly — in two cases even on success — bypassing teardown of spawned subprocesses. Exit codes, including forwarded non-zero child codes, are unchanged. -
The
blockedsemantic-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
--sourceno longer suggestsstash|registry|bothon commands where that enum doesn't apply.--sourcemeans a closedstash|registry|bothenum onakm search/akm curate, but a free-form stash name/path on everyakm graphsubcommand and a free-form URL/ref/ path onakm remember. The generated completion script keyed its value list by flag name only, so the search/curate enum leaked ontoakm graph --source <TAB>andakm 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 exit0. 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/--frominputs if you were unknowingly depending on that gap. -
akm indexno longer persists adapter auto-detection toconfig.jsonwith 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 additiveconfigUpdated.detectedAdaptersmap and on stderr, and only when a write actually happened. -
akm add owner/reponow 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 advertisedowner/repoform unreachable. A bare two-segmentowner/repo(orowner/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 theakm logcommand they back (the command group used to beakm 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 noschemaVersionbump. The documented[events-tail]stderr trailer text is deliberately left as-is pending a separate ruling.
-
BREAKING:
akm upgrade --skip-checksumis 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: setAKM_UPGRADE_SKIP_CHECKSUM=1if you must bypass a genuinely brokenchecksums.txt. It is deliberately undiscoverable. -
BREAKING:
akm config enable|disableis removed. It was a hard-coded toggle for one target, the skills.sh registry, and the bareakm enable/akm disablealiases were already removed in 0.9.0. Useakm registry add|remove. -
BREAKING:
akm mvis 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 anchoredbundle//conceptIdprose form, so it could rewrite ordinary prose while leaving real refs dangling. Renames are delete + create perSTABILITY.md: move the file,akm index,akm lint. The one capability nothing else covered — carrying an asset's earned signal across the rename — moves tobun scripts/rekey-asset-ref.ts <old-ref> <new-ref>(maintainer tooling,--dry-runsupported, idempotent), which re-keys the indexentriesrow in place plus theasset_salience/asset_outcome/usage_eventsrows. Themvevent type and output shape are gone; the script emits arekeyevent instead. A leftoverkind:"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
0from the 0.9.0 confidence-gate deletion onward, so the alert could never fire. The other three alert classes are unaffected. Theimprove_cycle_metrics.accepted_actionscolumn stays and is written as0because deployed 0.8state.dbfiles already contain it. -
IndexResponse.graphQualityis removed from theakm indexenvelope — it was declared but never assigned in any code path, so it was always absent. -
akm secret pathandakm secret removeare removed. The two resolved a secret ref through different stash-selection logic —paththrough the read-side, all-sources resolver andremovethrough 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 secretnow exposes onlylist,run, andset. Both spellings exit 2 withUnknown command.Migration: a ref's file lives at
<stash>/secrets/<name>(runakm sources listfor stash roots) — locate or delete it directly, or useakm secret run <ref> <VAR> -- <command>to consume the value without it touching disk.akm env pathandakm env removeare unaffected. -
Removed the dead
"backup"output-shape registration left over from the removedakm backupcommand (superseded byakm-migrate backup). Already unreachable; no user-visible effect. -
akm task list,akm task show, andakm task removeare removed as redundant with the generic asset commands. List and inspect tasks withakm search/akm show <bundle//tasks/id>(both already cross-bundle); to remove a scheduled task, delete its file in the owning bundle and runakm task sync(sync uninstalls the orphaned scheduler entry). Runakm task doctorfor scheduler diagnostics — bareakm taskis a usage error, see the canonical bare-group change above. -
The
akm show <ref> toc|section|lines|frontmatter|fullview-mode grammar is removed (0.9.0 decision D2).#fragmentis 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#authakm show knowledge/guide fullakm show knowledge/guideakm show knowledge/guide tocakm show knowledge/guide#<unmatched>— the error lists the available fragment slugsakm show knowledge/guide lines 10 30no replacement — every response carries path, so slice the file yourselfakm show knowledge/guide frontmatterno replacement — if a raw-YAML projection proves necessary it returns as a --shapevalueThe undocumented
--akmView/--akmHeading/--akmStart/--akmEndflags the grammar injected into argv are gone with it.
-
improve/recombine: cap-aware decay — the
maxClustersPerRuncap no longer traps recurring hypotheses belowconfirmThreshold(#658). Recombine is a two-pass design: a cluster must be re-induced onconfirmThreshold(=2) runs before itstype:hypothesisproposal promotes to an auto-acceptedtype:lesson. But only the top-maxClustersPerRun(=5) clusters are processed per run, anddecayUnseenRecombineHypotheseshard-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).decayUnseenRecombineHypothesesis now cap-aware:recombine.tspasses 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 newcapClustershelper split out ofbuildRelatednessClustersso the full ranked set stays available for the decay sweep. -
improvereflect no longer emits proposals doomed to fail theinvalid-descriptiongate when the source asset has no frontmatterdescription(#636). Reflect echoed the source frontmatter, so for assets that carry other keys but nodescription(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)buildReflectPromptnow injects an explicit "synthesize adescription" instruction whenever the source lacks a non-emptydescriptionand the asset type requires one (perauthoring-rules.tsDESCRIPTION_TYPES), telling the model it MUST author a valid 20–400-char plain-prose description from the asset'stitle:/first# Heading/opening body; and (2) a deterministic reflect-side belt-and-suspenders insanitizeReflectPayload— if a source that already had frontmatter still ends up with a missing/empty description after generation, reflect derives one deterministically fromtitle:/first heading (validated againstisValidDescription, never free-form invention) before the proposal is created. The validator,authoring-rules.tsbounds,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_salienceis 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-stublore-writeragent on every run (prod: 1 content-scored / 37 type-stub / 1826 NULL-legacy rows). The gate now also requiresisContentEncodingRow(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 followisContentEncodingRow'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, andisContentEncodingRoware 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 likedata.js,akm-health-report.html,reports/) previously tripped the #476 safety guard, which threwrefusing to push: … has uncommitted non-akm changeson everyakm improveend-of-run auto-sync,akm sync, andakm push. In one production incident this silently blocked all commits for ~1.5 days while akm kept accepting proposals it never persisted.saveGitStashnow scopes what it stages instead of refusing: (1) an explicit modified-file list when the caller passesopts.paths, else (2) the akm-managed pathspecs (TYPE_DIRSvalues +.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 -Awhen no managed pathspec can be resolved. If nothing akm-managed is staged the run returnsnothing to commit(no empty commit, no throw). Unrelated non-akm files are left untouched and uncommitted.
- BEHAVIOR CHANGE —
akm init --dir <path>no longer silently repoints your default stash. Previously,akm init --dir Xunconditionally wrotestashDir: Xtoconfig.jsonwheneverXdiffered 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 inmemory:akm-init-persists-stashdir-warning). NowinitpersistsstashDirto config only when one of the following holds: (a) no--dirwas provided (the default~/akmsetup flow — unchanged), (b)--dirwas provided and nostashDirexists in config yet (first-time bootstrap), or (c)--dirwas provided with the new--set-defaultflag (explicit opt-in). Otherwiseinitstill 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.TheInitResponseJSON gainsdefaultStashUpdated: booleanand an optionalpreviousStashDir. To make a--dirtarget your default, passakm init --dir <path> --set-default. (akm setupis unaffected — it remains the explicit configuration flow and always sets the default.)
- 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 askill:x, the body offact:conventions/assets/skillis injected (type-scoped — authoringcommand:ypulls thecommandconvention, never theskillone), labeled as soft guidance and kept separate from the validator-enforced hard rules. The basename must be agetAssetTypes()-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-inTYPE_HINTSfallback is unchanged (no regression). These facts carry soft conventions only and can never weaken the authoring contract the gate enforces (authoringRulesForTyperemains the sole source of validator-rejecting rules). The general convention/meta resolver now excludesfacts/conventions/assets/*so per-type guidance never leaks un-type-scoped into other authoring flows. (#646) akm initnow seeds default per-type SOFT convention templates. Starterfacts/conventions/assets/<type>.mdtemplates ship in the stash skeleton for the authored types (lesson, skill, command, agent, knowledge, memory, workflow, script, fact;wiki/env/secretexcluded) so a stash owner has an editable starting point. Each expands the matching built-inTYPE_HINTSone-liner into soft starter guidance, carriescategory: conventionfrontmatter, 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), andakm initseeds unconditionally rather than only on first create: re-running it on an existing stash backfills any missing skeleton, convention, or.meta/index.mdfiles. Seeding stays absent-only and never overwrites a user-edited file. (#646)
- Stash standards + wiki schemas are surfaced to authoring agents at write
time. When an agent edits a page under
wikis/<name>/, that wiki'sschema.mdbody is injected into the prompt; when it creates/edits a non-wiki asset, the bodies ofcategory: convention/metafactassets are injected. Two mutually-exclusive features selected by target type, sharing onestandardsContextprompt 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_uselength + 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)
- High-salience reflect lane now reflects each asset at most once. The
#608admission gate lacked the cooldown its sibling high-retrieval gate has, so zero-feedback assets were re-selected on every run (auto-accept emitspromoted, notfeedback), burning LLM calls and churning assets. (#643) - Stuck validation-failing proposals no longer dead-end. The triage drain no
longer overwrites an
auto-rejectedgate stamp with a misleadingauto-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 staypendingfor 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)
- 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/defaultSincestill wins. - Per-session lock prevents concurrent double-extraction. A session-end hook
firing
extract --session-idwhile the periodicakm improveextract 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. minNewSessionsis read from the ACTIVE improve profile, not alwaysdefault. A non-default profile (e.g.frequent) settingminNewSessionswas silently ignored because the gate (and its candidate-count discovery window) readprofiles.improve.default. They now read the resolved active profile, matching howextract.enabledalready resolves.
- Documented that
processes.extract.indexSessions(default on) makes a second LLM call per processed session (the session summary); set it tofalseto halve per-session extract cost. Unchanged/skipped sessions still cost zero.
akm extract --type opencodereads opencode's SQLite session store. opencode migrated session storage from per-file JSON (storage/session/<projectId>/<id>.jsonstorage/message/<id>/*.json) to a single Drizzle-managed database at<base>/opencode.db(tablessession/message/part; message text lives inpartrows withdataJSONtype:"text"). The legacy JSON layout went stale ~2026-02, so extract discovered 0 sessions on current opencode and thesession.idleextract hook had nothing to read.OpenCodeProvidernow prefersopencode.dbwhen present (read-only, via the cross-driveropenDatabaseseam) and falls back to the JSON layout. Verified end-to-end through the plugin'ssession.idlehook.
akm extractdecoupled from the improve-stage toggle.processes.extract.enablednow gates extract only as a STAGE ofakm improve(the active improve profile, per #593/#594); an explicitakm extractcommand always runs. Previously dropping extract from the daily improve profile silently disabled the standalone command (and its LLM calls, via the sharedsession_extractionfeature gate).extract --session-idnow respects the content-hash ledger;--forceoverrides. 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 targetedextract --session-id <id>is idempotent (skips an unchanged, already-extracted session with zero LLM calls) and only--forcere-extracts. This makes a session-end hook firingextract --session-id <id>precise AND idempotent.
- 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: thepersonal-stashdrain policy gains a{ generator: "recombine", requireType: "lesson", maxDiffLines: 200 }rule, via a new optionalrequireTypefrontmatter filter onDrainAcceptRule. Only confirmedtype: lessonproposals auto-accept; unconfirmedtype: hypothesisproposals 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).
- Select-time proactive cooldown leak.
selectProactiveMaintenanceRefsplans the due set BEFORE acquiringreflect-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.
- #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. UnlikeexcludeTags(a fixed project list), this catches the OPEN-ENDED junk — every new date or commit hash — with no config upkeep. Exposed asisJunkTag. On the live stash this turns the recombine cluster set from generic 66–171-member buckets into tight topical clusters (auth,architecture,patterns, …).
- #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 newprocesses.recombineknobs:maxClusterSize(skip clusters larger than N, so over-broad buckets no longer reach/starve the largest-first slice) andexcludeTags(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 →
confirmThresholdnever 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.
- #630 —
factasset type phase 2 reverted (#631). The pinned-core assembly +akm factCLI shipped in beta.28 was reverted pending rework. Phase 1 (#629, thefactasset type itself) remains in place.
All new behavior is opt-in / default-preserving — default runs are byte-identical.
- #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_queuetableenqueueGraphExtraction/drainExtractionQueue/extractGraphForSingleFile.akm curateenqueues an ungraphed hit (non-blocking);akm showcan extract a missing graph inline — gated onindex.graph.lazyGraphExtraction: true(default off:showmakes 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.
- Release CI unblocked.
runCliCapture(test harness) restoredprocess.exitCodeto a capturedundefined, which underbun testdoes not clear a previously-set non-zero exit code — so the unit suite exited 1 with 0 failures atTEST_PARALLEL=1(exactly howrelease.ymlruns), silently blocking every npm publish since beta.11. Fixed to restore to0. (This is why beta.26 was the first successful workflow publish.)
- 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 intermittentepoll_ctl EEXISTrace / busy-spin hang on the--isolateworkers, 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=1within each shard; the matrix runs shards concurrently. The release gate runs the identical set of tests. Localbun run checkdefaults to sequential too (the only safe mode on this Bun version). Coverage unchanged. Each shard runs throughscripts/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.
- #628 — configurable SQLite journal mode (
AKM_SQLITE_JOURNAL_MODE) for network filesystems. AKM previously opened every database withPRAGMA journal_mode = WALunconditionally, which cannot run on a network filesystem (NFS/SMB/Azure Files) — WAL's-shmshared-memory wal-index can't bemmap'd over a network mount. You can now setAKM_SQLITE_JOURNAL_MODEtoWAL(default),DELETE, orTRUNCATE, applied at all five db openers (state.db,index.db×2 paths,workflow.db,logs.db). At theWALdefault AKM auto-detects a network mount for the data dir and transparently falls back toDELETE(rollback journal +synchronous = FULL) with a one-line warning; invalid values warn once and fall back toWAL. 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)" indocs/configuration.md.
Completes the recombine / extract-efficiency / graph thread. All new improve passes are opt-in (default off), so default behavior is unchanged.
- #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 cleanstop()handle. The8,28,48cron remains the fallback; no daemon is auto-launched. - #625 — recombine second pass (hypothesis → lesson). The opt-in
recombineprocess (#609) now consumesconfirmThreshold(default 2): a generalization re-induced that many consecutive runs is promoted from atype: hypothesisproposal to atype: lessonproposal through the normal queue + quality gate (never a direct stash write). Hypotheses that stop recurring decay. Backed by a newrecombine_hypothesestable instate.db.
- #624 (P1) — graph storage decoupled from
entries.id.graph_filesis 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.)
- Graph re-key migration no longer triggers a destructive full-index rebuild: it
is a graph-scoped table migration (no
DB_VERSIONbump), and it copies the existing graph rows into the new schema rather than dropping them. - Test-suite
/tmphygiene: sandbox teardown now fires onSIGINT/SIGTERM/SIGHUP(not just clean exit), and asweep:tmpstep reclaims staleakm-*sandbox dirs left by force-killed workers — eliminating the tmpfs accumulation that caused intermittentEEXIST: epoll_ctltest flakes.
akm update --allno longer fails for writablegithub:entries stored assource:"git".updateRegistryEntrywas usingsynced.source(re-derived from the ref scheme as"github") instead of the existingentry.source, causing the config validator to rejectwritable:trueon every update cycle.
akm feedbacknow completes in ~0.3s (was 3+ minutes). Root cause: the command was callingensureIndexwithmode: "blocking"insidewithIndexWriterLease, triggering a full reindex on every feedback call. Fix: removed theensureIndexcall 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=30shandles concurrent access withakm improve); added a fast DB-exists guard with a clear error for first-time users.akm health --format htmlnow completes in ~11s (was ~18s). Root cause:akmHealth()was called twice — once for the main result and once to getdeltas. Fix: merged into a single call passing bothgroupBy: "run"andwindowComparetogether.
- 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.
-
WS-2 outcome loop (#613) — default-off weight change (state.db migration 010). Every
akm improverun now writes anasset_outcomerow per processed asset (state.db migration010) 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 setimprove.salience.outcomeWeightEnabled: truein yourakm.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, setimprove.salience.outcomeWeightEnabled: trueto activate the three-way split.Outcome loop mechanics.
outcome_scoreis 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 anoutcome_proxy_invertedhealth event when the signal degrades.review_pressureis 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 improveruns (whole-stash, type, and ref scope) has changed fromcombinedEligibilityScore = utility·0.7 + negativeOnlyRatio·0.3torankScore = (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 oldcombinedEligibilityScoreordering was never persisted, a forgetting comparison is not possible on the first run; instead a one-timeimprove_salience_first_runmarker event is emitted to record the transition. On every subsequent run a stash-wideimprove_salience_rank_changedrift report (includingstashSize) is emitted so rank movement under the new scoring can be tracked over time. The Part-V measurement protocol (T0 baseline viascripts/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.
Improve-tuning work streams (all default-off / parity-preserving — no behavior change until explicitly enabled).
- #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 migration007.) - #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 inakm 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 acalibration_autotuneevent. (Replay-prioritization from prediction error is deferred — it depends on the #610 replay budget, a 0.10 item.)
- #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.
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.0disables.
- 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
validateProposalfinding kind, e.g.validation:description-quality), records it on the proposal (akm proposal showexplains the rejection), logs it, and exposesfailedByReasonon 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_passesare 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.
- #603 —
akm healthpool-saturation advisory. Instead of alerting on the rawsessionsScannedcount (which false-alarmed on normal cadence changes), a newpool-saturationadvisory 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 healthHTML 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 capturedllm_usageevents, replacing the GPU-time proxy. - Built-in
akm health --format htmlreport overhaul — the report is now a strict superset of (and supersedes) the externalakm-health-reportstash 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 anddataZoomsliders 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, chartaria-labels, contrast fixes, and empty-state overlays. Deterministic output preserved.
-
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 ±5mintask_historyjoin instead of the run's scope (which isallfor every scheduled run); the time-slice filter options are now derived from the report's--sincewindow (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) andprofileFilteredRefsare 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 proposaldistill 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-processenabledflags) now survive anakm configrewrite. They are first-class typedImproveProcessConfigSchemafields, 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 bytests/config-process-roundtrip.test.ts.
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.
- Proactive maintenance selector (
proactiveMaintenanceimprove 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}onreflect_invoked/distill_invoked/promotedevents and the proposal record, so outcomes are sliceable by lane. - Measurement system under
scripts/akm-eval/: a real-query retrieval suite generated fromusage_events, andakm-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. Newproactive_selectedevent +proactiveSelected/proactiveDueTotal/proactiveNeverReflectedfields onimprove_completed.
- 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.
getRetrievalCountsnow normalizes bare vsorigin//-prefixed refs (it was dropping ~half the retrieval signal) and countscurateevents (akm curatenow records per-itementry_ref).- The fully-skipped
no_new_signalbranch emitted oneimprove_skippedevent per ref (~11K writes/run, ~400K rows/day) — a contributor to 900s improve timeouts and state.db bloat. Collapsed into one aggregated counted event.
Fix multi-process SQLite contention in index.db and harden concurrent proposal
queue mutations.
- Added a global
index.dbwriter 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 feedbacknow uses blocking index preparation and writes under the sameindex.dblease, avoiding self-inflicteddatabase is lockedfailures.- Proposal queue create/archive/gate-decision mutations now run under
BEGIN IMMEDIATEstate.db transactions so concurrent processes serialize on live queue state.
Fix the akm improve regression introduced by background ensureIndex.
- Added an explicit
ensureIndexmode so callers choosebackgroundorblockingbehavior directly instead of relying on hidden environment state. akm improvenow uses blocking index preparation before collecting eligible refs, restoring the post-upgrade empty-index recovery path.- Removed the
AKM_INDEX_INLINEtest-only override so tests exercise the same index behavior model as production.
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.
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-shredderevery 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-shreddersets this to 3 for a 40% candidate reduction per burst.distill.requirePlannedRefs— whentrue, the distill process is skipped entirely for distill-only refs when the reflect phase produced zero planned refs. Eliminates hundreds ofdistill-skippedevents 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--limitis given.- New
reflect-distillimprove profile — dedicated reflect + distill + memoryInference + triage profile for the every-4hakm-improve-frequenttask.reflect.limit: 25bounds LLM cost per pass.
quick-shredderprofile tuned:incrementalSince4h→1h,maxChunkSize25 → 35, addedminPoolSize: 10,neighborsPerChanged: 3,memoryInference.minPendingCount: 5. Allprofile: "qwen-9b-shredder"process references removed — falls back to default LLM.defaultimprove profile (nightly): extract disabled (dedicatedakm-extracttask runs at 01:48), consolidate getslimit: 500, reflect getslimit: 100andallowedTypes, distill getsrequirePlannedRefs: true, triage enabled at 50 accepts/run, graphExtraction explicitly enabled.- Cron schedule optimised: extract reverted to
8,28,48 * * * *(3×/hr), quick-shredder shifted to4,19,34,49(4-min extract gap), health-report shifted to:03(avoids:00collision),akm-improve-frequentre-enabled at45 */4withreflect-distillprofile.
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.
--format htmloutput with per-command templates (#582).akm health --format htmlrenders the full interactive health report (ECharts inlined by default, or via CDN withAKM_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
chatCompletioncall now records tokens (prompt/completion/total/reasoning), wall-time, model, and finish_reason as anllm_usageevent, attributed to the pipeline stage via an ambientAsyncLocalStoragecontext (withLlmStage) set once per phase — nostageparameter threaded through call sites.akm healthexposes 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/listsurface 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 renderunknown.
SQLITE_BUSY/ "database is locked" under concurrent runs (#584, #585, #589).busy_timeoutraised 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 thedefaultprofile, 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 resolvesextractagainst 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.mdchild 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.
minContentCharspre-LLM extract gate (#595, #596). Sessions whose raw size is belowprofiles.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 andATTACHsupport for joining log lines tostate.dbrows (e.g. a failedtask_historyrow to its log output). The scattered-log audit and per-source keep/move/drop decisions are documented indocs/technical/logs-audit.md.
- Proposals are now stored canonically in SQLite (#578). The previously
bypassed
proposalstable 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 indocs/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.)
- Consolidation starved merge recall; the memory pool grew unbounded. Commit
633ece41made theincrementalSincenarrowing 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/frequentdisable it), so a full-pool sweep is correct and affordable; the override is removed.lastConsolidateTsstill gates whether the pass runs. (Forward-port of the 0.8.5 fix.) akm tasks syncignored 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'sschedule:in the.ymlnever reached the crontab; the same gap affectedtasks 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 (newupdated[]field);enable/disablereinstall from the current.yml. The cron backend gainsexpectedSignature()and a per-entry signature onlist(); other backends fall back to an idempotent reinstall.
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-minquickpass) that overlap a longer run. Default off.
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 byakm config path), useakm config set/get/unsetfor scripted changes, andakm config validateto check it.
improve.lockleaked on signal death (cron timeout) — forward-ported from 0.8.3. The improve SIGTERM/SIGINT/SIGHUP handler callsprocess.exit(), which skipsfinallyblocks, so thefinallyreleasingimprove.locknever ran and every timed-out cron run leaked the lock. It is now released from aprocess.on("exit", …)handler registered at acquire time, via a new ownership-checkedreleaseLockIfOwned(path, pid).quickprofile was not quick — forward-ported from 0.8.3. It did not disable the default-ON session-extractprocess, so aquickrun processed the entire session backlog (~40 min).quicknow setsprocesses.extract.enabled: false.akm-evalsmoke suite adapted to the 0.9.0 CLI (CI/tooling only). The eval harness calledakm 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 missingstate.dbas an error rather than "no runs", which broke the read-only smoke + replay-determinism gates on a fresh checkout; a missingstate.dbis now handled as an empty history.
-
Cross-runtime: akm now runs on Node.js >= 22 in addition to Bun (#560, #465). A two-file runtime boundary (
src/storage/database.tsowns SQLite viabun:sqliteon Bun /better-sqlite3on Node;src/runtime.tsowns everyBun.*API) contains all runtime-specific code, enforced by a lint guard so it cannot leak back out. A CInode-smokematrix runs the built CLI under Node 20 and 22. The prompts dependency (@clack/core) usesnode: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. -
sessionasset type — agent sessions are now searchable (#561). Theextractpass, after distilling memory proposals from a session, additionally writes the session itself as a first-classsessionasset (sessions/<harness>/<id>.md) with an LLM-generated## Summary/## Key topicsbody plusharness/session_id/started_at/ended_at/project/log_path/accessfrontmatter. Sessions become discoverable viaakm search --type sessionandakm curate, and theaccess+log_pathfields tell any agent how to open the raw session log. The behaviour is ADDITIVE, FAIL-OPEN, and config-gated viaprofiles.improve.default.processes.extract.indexSessions(default on when an LLM is configured; setfalsefor 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.envmanagement.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 usedotenvas the serialisation oracle: a value is only written ifdotenv.parsereads it back exactly, and the whole edit is re-verified so no sibling key is disturbed. This reintroduces key-level management (the deprecatedvault set/vault unsetpointed here);akm env removestill removes the whole file. -
--pathfor 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, andakm secret set.--pathis 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, forremember/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 fromakm import <url>) may still nest. Shared semantics live insrc/core/asset-create.ts. (Replaces #503's earlier nested---nameapproach.) -
Workflow runs record agent harness + session identity —
akm workflow startnow 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 tostartWorkflowRun. Stored via additive migration002-add-agent-identityand surfaced onWorkflowRunSummary.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 startarms a durable check-in timestamp;akm workflow complete --summarynow 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 pureevaluateCheckinsurfaces a strongcontinuedirective throughgetNextWorkflowStepwhen an active run looks stalled. Migration002addsagent_harness,agent_session_id,checkin_armed_atonworkflow_runsandsummaryonworkflow_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), andcatchup(manual recovery: consolidate + triage drain) — alongside the existingdefault/quick/thorough/memory-focus/graph-refresh.akm setupand the newakm tasks initregister 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), andakm-graph-refresh-weekly(Sun 3 am). Registration is CI-aware (skips whenCI=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).
- #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.
-
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.mdfiles 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 thepromotedevent'sassetPath) 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 existingimprove_skipped/consolidation_no_memory_updatesevent 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/deleteAssetFromSourcenow 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 -Astages.akm/state + sibling assets together — pushed under the samewritable + remotegate asakm save/akm sync. This removes the noisy, incomplete per-asset commits (~25 per improve run) and leaves no dirty working-tree residue. -
improve/consolidate:minPoolSizeguard (#553). Consolidation now skips itself when the eligible memory pool is belowprocesses.consolidate.minPoolSize(default 500), emitting aconsolidation_skippedevent withreason: pool_below_min_sizeand 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: 0disables the guard. The skip surfaces inakm healthimprove output. The bundledconsolidateprofile sets500,catchupsets0. -
improve/extract:minNewSessionsgate (#554). The extract phase now counts in-window, not-yet-seen candidate sessions before any LLM call and skips the pass (emittingextract_skipped/reason: below_min_new_sessions, visible inakm health) when the count is belowprocesses.extract.minNewSessions. The in-code default is 0 (disabled), so existing profiles keep always-run behaviour; only the newfrequentprofile opts in with3. This removes the ~22% of improve runs that previously ran the fullensureIndex+ extract pipeline for zero new sessions.
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 onwritable: true+ a configured remote.
- Memory inference re-queued
hotparents forever (#550).markParentProcessedwas only called when a derived child was newly written; when the child already existed (written = 0), the parent never gotinferenceProcessed: trueand was re-queued on everyakm improverun (~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), whileskippedChildExistsaccounting 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 (repairTruncatedDescriptioninsrc/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 < totalEntriesforever and stayed in semantic-blocked / verification-failed state. A newgetEmbeddableEntryCount(entry_type != 'vault') now feeds the zero-entry short-circuit, the readiness gate, the "Semantic search ready (X/Y)" message, and the persistedentryCount; a genuinely missing embedding on an embeddable entry still reportsok:false.
- #490 architecture refactor. Decomposed
src/cli.tsfrom 4,589 → 620 LOC across 16 per-family command modules undersrc/commands/*-cli.ts(adopting adefineJsonCommandfactory for byte-identical JSON envelopes); convertedakm healthchecks to an orderedHealthCheckregistry; and turned themigrate-storagebin's 54 hand-rolledrecordStepsites into aMigrationStepregistry with 3 recursive copy helpers unified into onecopyTree. Shipped as serialized local merges with a zero-behaviour-change contract (byte-identical CLI surface + JSON envelopes), each gated and reviewed; the secret-migratingmigrate-storagechange is pinned by a sha256 + file-mode fixture-stash differential test.
akm extractminContentChars 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.
akm extractminContentChars gate filtered all sessions. The threshold was checked againstfiltered.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 oninputCount(raw session size) instead — a session with < 500 raw chars has nothing worth extracting regardless of what the pre-filter produces. Closes #596.
akm extractcalling 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 newminContentCharsgate (default 500) skips the LLM call when post-filter content falls below threshold, cutting extract LLM calls by ~95% on typical stashes. Configurable viaprofiles.improve.<name>.processes.extract.minContentChars. Closes #595.
akm improve --profile <name>ignored profile'sextract.enabled: falsesetting. The session-extraction gate in the preparation stage calledisLlmFeatureEnabled(config, "session_extraction"), which hardcodes a lookup againstprofiles.improve.default.processes.extract.enabled. Any non-default profile that setextract.enabled: false(e.g.quick-shredder) was silently ignored, causing the extract pass to run regardless. The fix adds aresolveProcessEnabled("extract", improveProfile)check so the active resolved profile gates the pass correctly. Closes #593.
akm improvetaking 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,collectEligibleRefsmarks those refs asprofile_filtered_all_passes. The caller then emitted oneimprove_skippedevent 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 acountfield, eliminating ~9 000 sequential writes per run. Closes #590.
akm improvevalidation pass was O(n) in stash size, causing ~510 s overhead on large stashes. For every indexed ref, the preparation phase calledfindAssetFilePath()— an async round-trip to the index DB followed by a filesystem probe — serially inside afor…awaitloop. 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 threadsfilePathfrom the planning stage (collectEligibleRefs) throughImproveEligibleRefso 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.
- SQLite
SQLITE_BUSYerrors under concurrent improve runs.busy_timeoutwas 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.
incrementalSinceduration strings were silently ignored. Values like"30m","24h","7d"were passed raw tonarrowToIncrementalCandidates, which compared them against ISO timestamps via string sort. All2026-...timestamps are lexicographically less than"30m"('2' < '3') and"24h"("20" < "24"), soisChanged()always returnedfalseand the candidate pool was silently emptied rather than filtered to the window. The fix addsparseSinceToIso(), which resolves human duration strings to absolute ISO timestamps before comparison. Values that already look like ISO timestamps are passed through unchanged.
consolidate.incrementalSinceprofile config field. SettingincrementalSince: "7d"(or any duration string) in theconsolidateblock 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. viaakm-improve-consolidateevery 4 h) without re-scanning the full pool every time. The nightly default profile leaves this unset (full-pool sweep, same as before). TheincrementalSinceoption already existed inakmConsolidate()but was hardcoded off at the call site; the field is now surfaced in the config schema and read from the profile.
- Consolidation starved merge recall; the memory pool grew unbounded. Commit
633ece41made theincrementalSincenarrowing 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/frequentdisable it), so a full-pool sweep is correct and affordable; the override is removed.lastConsolidateTsstill gates whether the pass runs.
akm tasks syncignored schedule changes. Sync classified any task already present in the OS scheduler as "unchanged" without comparing its installed entry, so editing a task'sschedule:in the.ymlnever reached the crontab — the only way to apply a new schedule was toremoveand re-addthe task. The same gap affectedtasks 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 newupdated[]field);enable/disablereinstall from the current.ymlinstead 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 gainsexpectedSignature()and a signature on eachlist()entry.
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-minquickpass) 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.
improve.lockleaked on signal death (cron timeout). The improve SIGTERM/SIGINT/SIGHUP handler callsprocess.exit(), which skipsfinallyblocks — so thefinallythat releasesimprove.locknever 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 aprocess.on("exit", …)handler registered at acquire time (exit handlers DO run onprocess.exit()), via a new ownership-checkedreleaseLockIfOwned(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.quickprofile was not quick. It was documented "Reflect-only" but did not disable the session-extractprocess (which is default-ON), so aquickrun processed the entire unindexed-session backlog (~40 min) — guaranteeing a 5-minute cron timeout → SIGTERM → the lock leak above, every run.quicknow explicitly setsprocesses.extract.enabled: false.
- LM Studio auto-detection in setup wizard —
akm setupnow probeslocalhost:1234/v1/modelsat 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 setupdetects 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_IDSinsrc/setup/registry-stash-loader.tsis 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 asgateAutoAcceptFailedCountin the improve result envelope and surfaced asimprove.autoAccept.validationFailedinakm healthreports.auto-accept-validationhealth advisory — heuristic advisory that warns whenvalidationFailed > 0so malformed proposals are visible before they pile up in the queue.
akm-improvetasks recorded as failed on budget exhaustion — the budget exhaustion timer calledprocess.exit(1), causing every budget-limited run to be recorded as a task failure. Changed toprocess.exit(0); budget exhaustion is a normal exit condition.improve_runs.started_atalways equal tocompleted_at—writeImproveResultFilewas called at end-of-run, sonew Date()captured the completion time and both columns held the same value (649/661 real runs affected, regressed ~May 26).started_atnow 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-reporttask fails on transient DNS errors — the Discord webhook script caughtHTTPErrorbut not the parentURLError, so DNS blips caused the task runner to record the health report as failed.URLErroris now caught and logged as a warning with a clean exit.
- 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 withakm 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 withakm 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 initscaffolds a.meta/index.mdtemplate into newly created stashes. - Default stash skeleton —
akm init(andakm setup) now copiessrc/assets/stash-skeleton/into every newly created stash. Currently ships aREADME.mdcovering what the stash contains and how agents useakmto access assets. Existing files are never overwritten. Add files tosrc/assets/stash-skeleton/to extend what ships with a fresh install.
- Setup wizard pre-populates from existing config — on re-run,
akm setupinitialises 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 eachsaveConfigin the setup wizard, ensuring the previous config is always recoverable if a wizard run is interrupted (#521).
graph-refreshimprove profile — new built-in profile that runs a full-corpus graph extraction pass across all stash files (all other improve processes disabled). Useakm improve --profile graph-refreshfor a weekly relationship rebuild. Pairs with the newgraph-refresh-weeklytask template (akm tasks add --template graph-refresh-weekly).session-extractionhealth advisory — new heuristic advisory backed by realakmExtractoutcomes: warns when the session-extraction process ran but produced zero proposals across ≥ 5 sessions, or recorded warnings. Replaces the vestigialsession-log-failureswarn signal.improve.sessionExtractionhealth metrics —sessionsScanned,sessionsExtracted,sessionsSkipped,proposalsCreated,warnings,durationMsnow tracked and visible inakm healthreports.
akm infoindexStats —readIndexStatserrors 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 —
embedMsandftsMsin timing output had their operands swapped, producing negative durations. Fixed (#516). - Incremental consolidation gate — the
volumeTriggeredpath 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.lockwas 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. yieldRatehealth metric —skippedAbortedrefs were incorrectly counted infreshAttempts, inflating the denominator and underreporting yield rate.session-log-failuresadvisory — demoted fromwarnto alwayspass(informational only); the advisory was a raw regex counter with no LLM signal, producing false positives on normal session content.
- All runtime assets consolidated under
src/assets/withdist/assets/mirroring the layout exactly. Built-in improve profiles moved from in-source object literals to embedded JSON files (src/assets/profiles/*.json). Thecopy-assets.tsbuild step now uses a precisesrc/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.
akm consolidate: all-hot chunk early-exit. When every memory in a chunk iscaptureMode: 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 asjudgedNoActionup front, instead of relying on a prompt-level hint and spending a wasted request. Mixed chunks are unaffected.
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 for the full
old → new table.
- Proposal queue is now a noun group:
akm proposal {list,show,diff,accept,reject,revert}. The flat verbsakm proposals,akm show proposal <id>,akm accept,akm reject,akm diff, andakm revertare deprecated aliases. Bareakm proposalbehaves asakm proposal list. --detailis now verbosity only (brief|normal|full). The output projection moved to a new--shapeflag (human|agent|summary).--detail summaryand--detail agentare deprecated aliases that map to--shape summary/--shape agent.--for-agentis a deprecated alias for--shape agent.--generatorreplaces--sourceonaccept/reject/history(which generator produced the proposal/event).--sourceis a deprecated alias on those three commands only — it is unchanged onsearch/curate/graph/remember, where it means "read from here".akm save→akm sync(commit + optional push;syncconnotes push better).akm saveis a deprecated alias.akm syncadds--no-push.akm enable/akm disable→akm config enable/akm config disable. The top-levelenable/disableare deprecated aliases.akm events→akm log:logis an additive alias for the same state.db stream in 0.8 and becomes primary in 0.9.0. (akm historyremains the asset-scoped, cross-source analytical trail — a different surface.)akm wiki remove --force→-y/--yesfor skipping the confirmation prompt.wiki removenow also prompts interactively when a TTY is present;--forceis a deprecated alias for-y.akm feedback --note→--reason:--noteis a deprecated alias and warns when used without--reason.akm workflow next --dry-runremoved: 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 taskforakm tasks,akm lessonforakm lessons.
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 removenow confirms before splicing the registry out of the config (confirmDestructive). Pass-y/--yesto skip the prompt; non-interactive use without-yaborts.- Bulk
akm proposal accept --generator <g>(the multi-proposal branch) now confirms before promoting every matching proposal, mirroring the existing guard on bulkreject. Single-id accept stays unguarded (it is revertable).
- Consolidation
delete_failedon 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, anddeleteAssetFromSourcethrew "not found in source" — appearing asdelete_failedin skipReasons. Fix:loadMemoriesForSourcenow 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 emitsdelete_already_goneinstead ofdelete_failedwhen 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_HOMErespectively. If you override any ofAKM_CONFIG_DIR,AKM_DATA_DIR,AKM_STATE_DIR,AKM_CACHE_DIRin CI to isolate per-job state, set all four (or none, and rely on XDG defaults). Overriding onlyAKM_CONFIG_DIRwill leave the lock file / event DB pointing at the host's default$XDG_DATA_HOME, causing lock contention and bleed between jobs.
- Install-time security audit (
security.installAudit) and the--trustflag. 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--trustor twiddle config just to install the official stash. The whole feature is gone:akm addandakm updateno longer scan synced content.- The
--trustflag is removed fromakm addandakm wiki register. - The
security.installAudit.*config keys (enabled,blockOnCritical,registryAllowlist,registryWhitelist,blockUnlistedRegistries,allowedFindings) are no longer recognised; the entiresecurityblock is removed from the config schema. - The
akm config set security.installAudit.*keys now error as unknown. auditfields are removed fromAddResponse.installedandSourceInstallStatus.
-
Project-level
.akm/config.jsonfiles 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.loadConfigwalks 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)stashDiris preserved as the literal${HOME}on read. The new exportedresolveSecret(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 indocs/configuration.mdcontinue to work because expansion still happens at request time for apiKey fields. -
AKM_FORCE_DOWNGRADE_CONFIGenv var removed. The newer-than-binary read-only guard (configReadOnlyReason,markConfigReadOnlyIfNewer,getConfigReadOnlyReason) is gone. Configs declaring aconfigVersionnewer than the running binary now save through silently — unknown fields are stripped on save bysanitizeConfigForWriteplus the strict-walled Zod schema. Users on 0.9.x configs should not open them with a 0.8.x binary in writable workflows.
-
Rebrand: the full name "Agent Kit Manager" is now Agent Knowledge Manager —
akmstands 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.tsreplaces 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.loadConfignow 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>.apiKeynow throwsUsageErrorpointing 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 validateandakm config migrateare now real registered subcommands. The orphan implementations inconfig-validate.tshave been removed; the new entry points live insrc/cli/. - #457: project-level
.akm/config.jsonfiles 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 toDEFAULT_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 eachsaveConfig.config.latest.jsonis preserved separately. - #460:
UNKNOWN_CONFIG_KEY_HINTis now auto-generated from the schema vialistTopLevelConfigKeys(). No more stale hand-maintained string. - #461: if the auto-migration disk-write fails,
loadConfignow throws a hard error instead of returning the in-memory migrated shape. Eliminates the silent infinite re-migrate loop on everyakmcommand. - #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.jsonis now auto-generated from the Zod source viabun scripts/gen-config-schema.ts. A drift test fails CI if the committed file disagrees with the regeneration output. - #464.a:
defaultWriteTargetis validated via Zod.refine()againstsources[].name. With no sources configured, save-time validation rejects instead of silently accepting (no implicit "first writable" fallback). - #464.b: generic unset works on
semanticSearchModeand every other key via the dotted-path walker. - #464.c: all write paths route through
writeFileAtomic. - #464.d: duplicate
mergeSecurityConfig/mergeInstallAuditConfiginconfig-cli.tsare deleted; merging happens via re-parse through the Zod schema.
- #454:
See docs/migration/v0.7-to-v0.8.md for the user-facing migration guide.
- Feedback tag/filter filtering —
akm feedbackand 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
opencodeagent integration.
- 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 showauto-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.mdlayout — 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.
- 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.
showworkflow 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.
- Bundled migration notes now cover 0.7.5 —
akm help migrate 0.7.5andakm help migrate latestnow surface the full 0.7.5 operator summary alongside the changelog section.
akm index --enrichopt-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.
- Setup wizard UX refresh —
akm setupnow 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.jsonmigration status, current green CI runs, and the narrowed remaining compatibility surface ahead ofv0.8.0.
- 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>/contentsubtree; repo-root stash layouts are indexed correctly and cached mirrors are treated as fresh instead of being needlessly refreshed. showmetadata 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.jsonno longer drives incremental stale detection — editing.stash.jsonalone no longer forces directories to rescan during incremental indexing.
- Ranking and scoring fixtures migrated toward file-local metadata — routine benchmark and regression fixtures now prefer markdown frontmatter or inline script metadata, with
.stash.jsonretained 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.jsoncrawler, reducing fixture drift and improving confidence in the real indexing/search path.
- One-shot URL ingest for
akm importandakm 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 intoknowledge/using a URL-path-derived default name.akm wiki stash <wiki> <url>fetches the exact page, converts it to markdown, and writes it intowikis/<wiki>/raw/. Neither command registers a persistent website source or crawls linked pages.
- 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, andakm wiki stashall reuse the same core website-ingest path. .stash.jsondocs deprecation timeline — the docs now explicitly state that.stash.jsonis 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.
- Proposal queue (
akm proposal *) (#225, #226, #233) — durable queue for proposal-producing commands. New verbsakm proposal {list, show, diff, accept, reject, revert}. Promotion runs full validation before routing throughwriteAssetToSource(). Multiple proposals for the samerefcoexist without filesystem collisions. Auto-accept is gated per-source viaautoAcceptProposals: 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.reflectandproposeshell out via the agent CLI (agent.*config);distillis the canonical bounded in-tree LLM call gated behindllm.features.feedback_distillation. Usage eventsreflect_invoked,propose_invoked,distill_invoked.lessonasset type (#227) — first-class well-known type with required frontmatterdescriptionandwhen_to_use, stored underlessons/<name>.md. Normally produced byakm distill <ref>as aproposed-quality proposal and promoted viaakm 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_inferenceandgraph_extractiondefault totrue; the others default tofalse. WrappertryLlmFeature(feature, config, fn, fallback)insrc/llm/feature-gate.tsguarantees disabled/throw/timeout fall back without crashing the call site. See v1 spec §14.quality: "proposed"and--include-proposed—SearchHit.qualityopen string set;proposedis excluded from default search and surfaces only viaakm search ... --include-proposedorakm proposal *. Unknown values parse-warn-include.SearchHitgains optionalquality?andwarnings?fields.akm-benchv1 (#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 compareaggregates paired runs into a delta report;akm-bench attributemaps utility deltas back to specific[origin//]type:namerefs (Track B);akm-bench evolveis a stub for the closed-loop workflow that lands in 0.8.- Operator env-var documentation (#284 Wave B, PR #285) —
docs/configuration.mdnow documentsAKM_NPM_REGISTRY,AKM_REGISTRY_URL,AKM_CACHE_DIR,HF_HOME, andGH_TOKEN. - Empty-state hints (#284 Wave C, PR #286) —
akm proposal list,akm workflow list, andakm vault listempty-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. --verboseglobal 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.
- 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-benchruns 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 arbitrarydist.tarballURLs.
- Workflow noise gate, sources deprecation warn, setup
--help(#273) —akm workflow next/complete/statusno longer print spurious progress noise on quiet runs; the legacystashes[]key emits a single deprecation warning per process (was: per call site);akm setup --helprenders the same help block asakm setupwith no args plus the agent-detection summary. - tsconfig + HF pin + shapes throw (#274) —
tsconfig.jsonnow includestests/sobunx tsc --noEmitcovers 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 silentlyJSON.stringify-ing. - Bench tmp redirect (#276) —
akm-benchno 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/tmpwrites work out of the box. - Registry-build tmp redirect (#284 Wave E, PR #285) —
inspectArchivenow mkdtemps under${getCacheDir()}/registry-build/instead ofos.tmpdir(). Mirrors the bench-only redirect from #276 for non-bench code.vault loadretains its/tmpmode-0600 sentinel by design.
- Agent spawn timeout (#284 Wave A, PR #285, BUG-H1) — stdin write could hang past
agent.timeoutMs; the write now races againstproc.exitedso 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.
defaultWriteTargetwritability check (#284 Wave A, PR #285, BUG-H3) — resolvingdefaultWriteTargetwas missing the writability gate that the--targetpath enforces; now mirrored.- Schema-upgrade row loss (#284 Wave A, PR #285, BUG-H4) —
restoreUsageEventsBackupsilently 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) —
runAllAndExitnow resetsregistry.runningin atry/finallyso a synchronous throw cannot deadlock subsequent SIGINT handlers. akm searchwith no query (#284 Wave C, PR #286) — error hint now references--type/--limitinstead of show-style ref grammar.akm workflow next <bogus-id>(#284 Wave C, PR #286) — surfacesWORKFLOW_NOT_FOUNDwithRun \akm workflow list --active`` instead of a cryptic ref-parse error.akm add /missing/path(#284 Wave C, PR #286) — throws typedNotFoundError("FILE_NOT_FOUND")with hint instead of a bareError.akm update <bogus>(#284 Wave C, PR #286) — now usesSOURCE_NOT_FOUND(with the existing hint pointing atakm list) instead of the defaultASSET_NOT_FOUND.- Setup wizard source count + embedding-dim prompt (#284 Wave C, PR #286) — the wizard now reads
newConfig.sources ?? newConfig.stashesto count configured sources (was reading the dropped legacy key); the embedding-dimension prompt now explains what the value is for. formatPlainnull fallback (#284 Wave C, PR #286) — text renderers now exist for every command that callsoutput(); no more silent JSON when an operator passes--format text.- Arity guards (#284 Wave C, PR #286) —
propose,feedback,curate, andhelp migrateno longer exit 0 with citty's help screen when required positionals are missing; they now exit 2 withMISSING_REQUIRED_ARGUMENT.
- Legacy registry
curatedboolean — legacy v2 index JSON parses and silently ignores it; renderers no longer surface acuratedcolumn. The per-assetqualityfield 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}, andllm.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 inconfig.jsonwill 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) — thestashes[]→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 aConfigErroron parse instead of auto-migrating. Runakm setup(or rename the key by hand) to migrate.searchPathslegacy migration (#284 Wave B, PR #285) — pre-0.5.x config key; deprecation window long expired.context-hubsource-kind migration paths (#284 Wave B, PR #285) —STASH_TYPE_ALIASES, theparseSourceSpeccase "context-hub"arm, thecontext-hub-${key}git rename migration, and thenormalizeToggleTarget("context-hub")arm are all gone. Per CLAUDE.md,context-hubis just a git repo and was never a first-class kind.- Legacy lockfile migration (#284 Wave B, PR #285) —
migrateLegacyLockfileIfNeeded(thestash.lock→akm.lockrename) is removed; the rename ran for at least two release cycles.
- 9
console.warnsites migrated towarn()fromsrc/core/warn.tsfor uniform--quiethonoring (#284 Waves A/B, PR #285). - 6 unused exports removed:
StashLockEntry,listProviderTypes,resetBuiltinsCache, and twoGraphRelationre-exports (#284 Wave A, PR #285). - ~472 LoC net deletion from
src/core/config.tsfrom removing the legacy migration paths above (#284 Wave B, PR #285). --for-agentdeprecation note retained indocs/technical/akm-core-principles.mdanddocs/technical/search-updated.mdfor 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).
- See
docs/migration/release-notes/0.7.0.mdfor the operator summary and the archived pre-1.0 plan for the historical per-surface delta from any 0.6.x baseline.
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 feedbacknow accepts any indexed ref — previously type-restricted.memory:,vault:,workflow:,wiki:refs all work. Vault feedback never echoes vault values.akm upgraderuns post-upgrade tasks automatically. After a successful upgrade, the new binary is invoked as a child process runningakm index, which auto-migrates any legacystashes→sourcesconfig keys vialoadConfigand rebuilds the index against the new schema (DB_VERSION8 → 9 forces a rebuild). Pass--skip-post-upgradeto opt out (config migration still runs on the nextakminvocation; you'd just need to runakm indexyourself). Result is reported in thepostUpgradefield of the upgrade response.writableflag on sources. New optionalSourceConfigEntry.writablecontrols whether write commands (akm remember,akm import,akm save,akm clone) may target the source. Defaults:trueforfilesystem,falseforgit/website/npm.writable: trueonwebsiteornpmis rejected at config load withConfigError("writable: true is only supported on filesystem and git sources").defaultWriteTargetroot config key. Names the source that receives writes when no--targetflag is given. Resolution order:--target→defaultWriteTarget→stashDir(working stash) →ConfigError("no writable source configured; run \akm init`"). There is no implicit "first writable insources[]` order" fallback.
- Workflows are now stored as validated
WorkflowDocumentJSON — workflows are compiled into a validatedWorkflowDocumentJSON shape with line-anchoredSourceRefs back into the source markdown, cached in a newworkflow_documentstable inindex.db. The run engine reads from the cache onakm workflow nextinstead 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_refandentry_idso feedback signals survive a reindex.
- v1 architecture refactor. The internal architecture was rebuilt around a single minimal
SourceProviderinterface ({ name, kind, init, path, sync? }), a unified FTS5 index that owns search and show, and a singlewriteAssetToSourcehelper that owns all writes. The CLI command surface and all user-visible config keys are unchanged. Seedocs/archive/pre-1.0-migration.mdfor the historical guide. - Config key
stashes[]renamed tosources[]. Configs with the legacy key load with one deprecation warning and are auto-migrated in memory; the new key is persisted on the nextakm configwrite. New configs should usesources[]. Configs that contain both keys are rejected withConfigError. - Error hints surface without
--verbose. Error classes own theirhint()text; the regex-on-message hint chain incli.tsis 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. Legacytype: "context-hub"entries normalize totype: "git"at load time. - Terminology cleanup — clean break from "kit" → "stash" (#148). Pre-v1, no fallback period.
- Wire format:
RegistryIndex.kits[]renamed toRegistryIndex.stashes[]. Schema version bumped to v3 —akm-cli >= 0.6.0only parses indexes withversion: 3. v1/v2 indexes are no longer accepted. Every static-index registry must regenerate itsindex.jsonwithversion: 3to be readable. The officialakm-registryships a regenerated index alongside this release. - Discovery: npm packages and GitHub repos are now discovered via the
akm-stashkeyword/topic only. Legacyakm-kitandagentikitkeywords/topics are no longer honored. Publishers must retag. - Schemas:
schemas/registry-index.jsonanddocs/technical/registry-index.schema.jsonupdated (RegistryKit→RegistryStash,kits→stashes). - Internal types:
RegistryKitEntry→RegistryStashEntry,InstalledKitEntry→InstalledStashEntry,KitInstallStatus→StashInstallStatus,KitSource→StashSource. Filessrc/kit-include.ts→src/stash-include.tsandsrc/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-clinpm package name, and theakm.includepackage.json field. - Migration: a curated registry author should regenerate their
index.json(renamekits→stashes, drop legacy keyword filtering). Publishers should add theakm-stashkeyword/topic and removeakm-kit/agentikit.
- Wire format:
akm registrydescription: changed from "Manage kit registries" to "Manage stash registries".
DB_VERSIONbumped 8 → 9. On first run after upgrade, the version-mismatch path inensureSchema()drops + recreates allindex.dbtables (preservingusage_eventsvia a typed backup); the nextakm indexrebuilds the index.workflow.db(run state) is unaffected.
- OpenViking source provider. The
openvikingsource kind is no longer supported. Configs that contain one fail to load withConfigError("openviking is not supported in akm v1. …")and a hint pointing toakm config sources remove <name>. API-backed sources will return as a separateQuerySourcetier post-v1. To downgrade in the meantime, pin toakm-cli@0.5. akm enable context-hub/akm disable context-hubtoggles. 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 viaakm config sources remove context-hubor by editing the entry'senabledflag.- Legacy re-export shims
src/llm.ts,src/registry-provider.ts, andsrc/ripgrep.ts. akm has no public API (CLI-only package, no barrel exports), so external consumers should be unaffected.
src/reorganized into purpose-named subdirectories (commands/,core/,indexer/,output/,registry/,setup/,sources/,wiki/,workflows/). No public API surface change.- Single
writeAssetToSourcehelper undersrc/core/write-source.tsis the only place that branches onsource.kindto add behaviour. All write call sites (remember,import,clone,save) route through it. SourceProviderinterface simplified to{ name, kind, init, path, sync? }. The previousLiveStashProvider/SyncableStashProvidersplit is gone.
- Multi-wiki support (#119, #121, #136, #139, #144): new
wikiasset type with ten CLI verbs underakm wiki …(create,register,list,show,remove,pages,search,stash,lint,ingest). Each wiki lives at<stashDir>/wikis/<name>/withschema.md,index.md,log.md,raw/, and agent-authored pages. Wiki pages are first-class in stash-wideakm search.akm indexregenerates each wiki'sindex.mdas a side effect and is resilient to malformed workflow assets. Raw sources underraw/and theschema.md/index.md/log.mdinfrastructure files are intentionally excluded from the search index. Seedocs/wikis.mdfor 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>andakm 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
workflowtype withakm workflowsubcommandstemplate,create,start,next,complete,status,list, andresumefor 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
vaulttype backed by.envfiles;akm vaultsubcommand withlist,show,create,set,unset, andload(emits asourcesnippet for the current shell via a mode-0600 temp file); values never appear in structured output. --trustflag for installs:akm add <source> --trustperforms a one-off trusted install, bypassing the install audit for that source. Blocked install errors now include ahintpointing to--trustas a remediation option.- Writable git stash +
akm save(#114):akm add … --writableopts 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 usesgit cloneinstead of HTTP tarball download. akm help migrate <version>(#132): prints the release notes and migration guidance for a given version (accepts0.5.0,v0.5.0, orlatest). Pulls the matching section fromCHANGELOG.mdwhen available and supplements it with embedded migration notes for major releases.- Broader
akm upgradecoverage (#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.
- 0.5.0 QA follow-ups (#130): fixes across the new wiki, workflow, vault, and save/trust surfaces surfaced during release-candidate QA.
- The unreleased single-wiki LLM POC: removes
akm lintcommand,akm import --llm/--dry-runflags,knowledge.pageKindsconfig, and theingestKnowledgeSource/lintKnowledgeLLM prompts. Users of the POC should migrate to the newakm wiki …surface; raw content can be manually moved towikis/<name>/raw/.
- 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.
akm enable/akm disable(#108): toggle optional components (skills.sh,context-hub) on/off without manually editing configakm rememberandakm importcommands (#110): capture in-session knowledge directly from the CLI;akm rememberrecords a memory to the default stash (supports stdin);akm importingests a file or stdin as a knowledge asset- Karpathy-style wiki workflow in knowledge assets (#113):
akm show knowledge:<doc>now surfaces aningestworkflow for knowledge documents;--dry-runflag added;pageKindtaxonomy made extensible - Documentation: expanded
agent-install.md, addedinfoandfeedbackcommand docs, global flags reference (#106)
- Remote embedding endpoint URL normalization — trailing slashes and path segments now handled correctly (#112)
- Reduced fallback capture-name collisions in
akm remember
- Install security audit: new pre-install scanner inspects kit contents for dangerous patterns and executable scripts before install; configurable via
configCLI - Project-level config stash merging:
.akm.jsonin 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 curatecommand: new subcommand for curating assets from the stash (initial skeleton)
- Index nested agent markdown files as agents so
akm search agent:...finds them install-auditnow reads at mostMAX_SCANNED_FILE_BYTESper file usingBuffer.alloc, with the file descriptor always closed viatry/finally, and corrects thescannedBytescounter
- 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-pagesand--depthflags to bound crawling
- Relaxed HTTP warnings for localhost website sources
- Addressed review feedback around website provider routing and security heuristics
- Regression tests for vector/semantic search readiness, install, and setup flows
CONTRIBUTING.mdand "Why akm" section in documentation- Three draft SEO blog posts
- Unified source model: replaced the
kitvsstashsplit with a single source concept;akm addworks for all source types - Removed
stashandkitsubcommand 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
- Embedding fingerprint is purged on model change and
usage_eventsare 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
- Binary install detection in
akm upgradeself-update; centralizedAKM_VERSIONdeclaration with binary detection tests
- Docker-based install tests covering multiple OS configurations (skipped in CI)
- Detailed error reporting in embedding availability checks
- Actionable guidance when
sqlite-vecfails to open the DB
- Rename: project renamed from
Agent-i-Kittoakmacross docs and links - Local embeddings switched to
@huggingface/transformers @huggingface/transformersmoved tooptionalDependencies, then promoted to a runtime dependency- Improved semantic search setup and index UX
- Extensible asset type system:
AkmAssetType(formerlyAgentIKitAssetType) is nowstringinstead of a fixed union; new types can be registered at runtime viaregisterAssetType() - Memory asset type: built-in
memorytype stored inmemories/, withmemory-mdrenderer and directory/parent-dir-hint matchers - OpenViking stash provider:
openvikingprovider type for searching OpenViking servers via REST; add withakm stash add <url> --provider openviking - Remote show for
viking://URIs:akm show viking://resources/my-docfetches content directly from an OpenViking server (returnseditable: false) --optionsflag forakm registry addandakm stash add: pass provider-specific JSON config (e.g.,--options '{"apiKey":"key"}')akm registry build-indexcommand: generates a v2 registry index JSON from npm/GitHub discovery with--out,--manual,--npmRegistry,--githubApi, and--formatflags- 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)
estimatedTokenson context-hub provider search results and in--for-agentoutput- Architecture docs and test fixture for OpenViking manual testing (
tests/fixtures/openviking/)
- 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).YAMLhack with a properyamlpackage dependency - YAML output format fixed; local registry refs now use a
file:prefix
manifestsubcommand (adds no value oversearch)- URI schemes (
viking://,context-hub://) from user-facing refs — assets are addressed astype:name; sources use URLs - Stale audit/ergonomics markdown from the repo
skills.shinstall refs now produce validakm addcommands (#82)- Prevented
akm removeandakm update --forcefrom deleting user-owned local source directories installed via path refs usage_eventsreverted toDELETEon full reindex
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.
--verboseflag onsearchfor 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)
- Rebrand: npm package
agent-i-kitrenamed toakm-cli; binary remainsakm - Rebrand: config field
"agent-i-kit"renamed to"akm"inpackage.json - Rebrand: plugin
agent-i-kit-opencoderenamed toakm-opencode - Rebrand: registry
agent-i-kit-registryrenamed toakm-registry - Rebrand: default paths changed (
~/agent-i-kitto~/akm,~/.config/agent-i-kitto~/.config/akm) - Rebrand: environment variables
AGENT_I_KIT_*renamed toAKM_* - Removed
toolasset type entirely;scriptis the only script-like type .stash.jsonfield renames:intentstosearchHints,entrytofilename; removedgeneratedbooleanshowcommand:--viewflag replaced with positional syntax (akm show <ref> toc)- Collapsed
AssetTypeHandlerhandlers into a unified renderer pipeline - Dropped provider presets (raw JSON config only)
- Pinned
sqlite-vecto exact version0.1.7-alpha.2(removed caret range) - Replaced
(Bun as any).YAMLcast with proper type guard in CLI - Version now injected at compile time via
--define AKM_VERSIONwith safe runtime fallback
submitcommand- Provider presets (configure providers with raw JSON)
generatedboolean from.stash.json
- CLI crash on macOS when running as compiled binary (
package.jsonnot embedded) - Cleaned up search output formatting
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.
akm registrysubcommand group withlist,add,remove, andsearchsubcommandsakm registry search --assetsflag for asset-level search against v2 registry indexesregistriesconfig field (RegistryConfigEntry[]) withurl,name, andenabledproperties- Registry Index v2 schema with optional
assetsarray on kit entries for asset-level discovery - Official registry pre-configured by default in new installations
- Type names:
KitSource,InstalledKitEntry,KitInstallResult,KitInstallStatus,InstalledKitListEntry
- Config:
installedis now a top-level field (config.installed) instead of nested underconfig.registry.installed - Config: registry URLs configured via
registriesarray instead ofregistryUrls - 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 registrysubcommand group to CLI reference - Documentation: added registry hosting and v2 index format guides
toolasset type (fully removed across all documentation and code)registryUrlsconfig field (replaced byregistries)config.registry.installednesting (replaced byconfig.installed)- All
tools/directory references from documentation
Initial public release of Agent-i-Kit (akm CLI).
- 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 cloneto fork installed assets into your working stash- Configuration system with embedding and LLM provider management
- Standalone binary distribution (no runtime dependencies)