akm docs

Migrating from akm 0.9.1 to 0.9.2

AKM 0.9.2 makes task source v4 the only executable task source and introduces peer Markdown and GitHub-shaped YAML workflow sources, compiled through a shared source IR to durable plan irVersion 5. Authored task-v2 and task-v3 files require an explicit, fail-closed migration — akm migrate apply now runs both generations in one pass. Durable workflow plans created by 0.9.1, and any plan frozen before this release's irVersion 5, cannot resume in 0.9.2 — see Before you upgrade below for the exact recovery steps; no run data is lost.

The released 0.9.1 state ledger already includes historical migration 018, so the normal 0.9.1→0.9.2 database step is additive and automatic. If a pre-release database has an exact prefix before 018, ordinary commands stop before its destructive dead-lane cleanup. Run akm migrate apply (or akm upgrade, which runs it after its install step regardless of whether an install happened): AKM creates and verifies a sibling pre-018 SQLite safety copy before applying that immutable released migration. Its locked ledger recheck, snapshot, and migration share one writer-exclusion window, so another WAL writer cannot commit between the copy and 018. Unknown or divergent ledgers remain unsupported. See Bundling akm for the full migration contract.

Before you upgrade

  1. Check for workflow runs in flight and either let them finish or abandon them deliberately — a run frozen at a pre-irVersion-5 plan cannot resume/next/complete/run after the upgrade (it can still be inspected and abandoned). See Pre-irVersion-5 stored plans below for the exact recovery sequence if you upgrade with runs still active.

    akm workflow list --active
    
  2. Commit or otherwise snapshot your authored task and workflow bundles before running any migrator — both the task-v2→v3 and v3→task-source-v4 generations back up every file they touch, but a snapshot at the repo level is cheap insurance regardless.

  3. Do not downgrade below 0.9.2 once you've recorded task history on it. Task run history's target.kind vocabulary changed in this release, tagged with a metadata marker a pre-0.9.2 akm's decoder does not recognize — a pre-0.9.2 akm reading a row this release (or later) wrote throws rather than misreading it. Upgrading is always safe (a 0.9.2-or-later akm reads older rows correctly); only downgrading, or otherwise pointing an older binary at a state.db a newer one has already written to, is the hazard. See Task history result vocabulary below.

Everything else below can happen after the binary upgrade, at your own pace: an unmigrated v2/v3 task source keeps reading and running through the in-memory read shim, with a one-line deprecation warning, until you migrate it (nothing silently breaks or half-runs, and only a genuinely unmigratable shape fails closed with an actionable hint), and pre-irVersion-5 runs stay inspectable indefinitely. (0.9.17-alpha.7 removed that shim: a v2/v3 task source now fails on its own, naming akm migrate apply, which akm upgrade runs after an install.)

Task sources

akm 0.9.2 has shipped three task source generations. Task source v4 is the current, standing grammar; an older version: 2 or version: 3 file still reads and runs through an in-memory read shim that converts it to v4 on the same bytes akm migrate apply would produce, with a one-line stderr deprecation warning and no disk write. Only a v2/v3 document the deterministic conversion itself cannot resolve fails to load. akm migrate apply remains the way to rewrite the file on disk and silence the warning. (A later release — see the Tasks reference for the current grammar — extended the same kind of shim to a version: 4 document whose schedule[] still carries a per-entry enabled key, retired from task source v4's own grammar since; the field is stripped without being read, never re-added. 0.9.17-alpha.7 removed both shims: akm now reads only task source v4, and each older file fails on its own, naming akm migrate apply.)

Before — 0.9.1 task v2:

version: 2
name: Contract review
schedule: "15 4 * * 1"
prompt: Review the execution contract.
enabled: false
engine: reviewer
model: exact-model-id
timeoutMs: 45000
redact: [REVIEW_TOKEN]

An intermediate generation — task v3 (shipped earlier in the 0.9.x line; also read only through the shim now, not as a standing grammar):

version: 3
name: Contract review
uses: akm/command
with:
  content: Review the execution contract.
akm:
  schedule: "15 4 * * 1"
  enabled: false
  engine: reviewer
  model: exact-model-id
  timeout: 45000
  redact: [REVIEW_TOKEN]

After — 0.9.2 task source v4, the migration destination:

version: 4
name: Contract review
uses: akm/command
with:
  content: Review the execution contract.
schedule:
  - cron: "15 4 * * 1"
engine: reviewer
model: exact-model-id
timeout: 45000
redact: [REVIEW_TOKEN]

What changed between v3 and v4 (v2's changes to v3 are unchanged from earlier 0.9.x releases and are summarized further down):

See Tasks for the complete grammar reference, including the retired v3 grammar (kept purely so you can read an old file while migrating it).

The migration procedure

akm migrate status and akm migrate apply [--dry-run] run both generations in one pass against the same tree: task-v2 → task-v3 first, then task-v3 → task source v4 against the resulting files. Each generation keeps its own lock, O_EXCL backup, prevalidation, TOCTOU recheck, atomic replace, and reverse rollback — a file blocked in generation 1 does not stop generation 2 from converting files that are already version: 3.

  1. Commit or otherwise snapshot your authored bundles (see Before you upgrade).

  2. Preview the pure migration plan. This performs no source write:

    akm migrate apply --dry-run
    
  3. Review every input file. The report is stable and names an exact status for each file, per generation: changed, skipped, or blocked.

  4. Resolve every blocked file manually, then preview again.

  5. Apply the same planner:

    akm migrate apply
    

For each changed file, AKM validates the complete replacement bytes before writing. At apply time it rechecks the planned generation, backs up that file immediately before replacement, preserves its file mode, and then installs the validated replacement. A race or validation failure stops instead of applying stale output.

v2 → v3 blocked cases

A v2 argv array has no unambiguous v3 shell-string equivalent:

version: 2
schedule: "@daily"
command: [node, scripts/release.js, "--exact value"]

Shell-sensitive strings — assignments, shell builtins, reserved words, and similar constructs — are also blocked when translating them would invent shell semantics. The original bytes remain untouched, and akm migrate status/apply names the block as argv-array-has-no-portable-shell-string (or a sibling shell-safety reason) rather than guessing: this is manual conversion, not a case the migrator can be re-run to fix. Rewrite the file by hand using this field mapping:

v2 v4
command: (array, argv style) run: (string) plus shell:
timeoutMs: timeout:
enabled: (document level) removed — use host-local scheduler.enabled (akm task enable / disable)
schedule: (cron string) still accepted as a bare string, or as the list form above

validate it, and rerun the preview. You can either write the replacement directly as version: 4 (generation 1 then reports it already-v4 and leaves it alone) or as a version: 3 run: string, letting the second generation carry it the rest of the way to v4 in the same akm migrate apply run.

Migrating task v3 to task source v4

The second generation converts an eligible version: 3 task source to version: 4. It translates structure, never intent: it never invents an inputs: declaration on a file's behalf, regardless of how inferable a with: value's shape looks — declaring inputs: (and rewriting a step to bind it) is left to the person editing the migrated file by hand.

Common blocked reasons and what to do about each:

Reason Meaning Fix
github-action-target-removed The task's uses: is a GitHub Action locator (owner/repo[/path]@ref); that spelling has no task source v4 equivalent. Rewrite the target as commands/, scripts/, workflows/, or akm/command by hand.
with-on-non-command-target A with: block is authored on a target other than uses: akm/command. Declare inputs: on the task instead; a workflow step composing it binds them with its own with:.
ambiguous-scheduling-source The document declares both akm.schedule and on:. Pick one; the migrator will not guess which one wins.
read-only-source The owning source or file is not writable. Move or re-source the file somewhere writable, or edit it by hand.
invalid-v3-task The v3 document itself is structurally invalid (unknown fields, missing selector, malformed trigger, etc). Fix the underlying v3 document first — the migrator translates structure, it does not repair it.

The migrator is also installed as its own executable, akm-migrate, with the same status / apply [--dry-run] surface as akm migrate. A tree that is already all version: 3 simply reports the first generation as current and runs this one.

Task history result vocabulary (target.kind)

This is about run history, not task source files — nothing here is a document you author or migrate. akm task history and akm task run's result envelope both carry a target.kind field, and its vocabulary changed:

Old (0.9.1) New (0.9.2) Meaning
"prompt" "command" A prepared command dispatched to an agent/LLM engine.
"command" (shared) "shell" or "script" A native shell or script execution — previously one shared "command" label for both.

"prompt" mislabeled an LLM-routed dispatch as a literal prompt, and one shared "command" conflated two materially different execution shapes. 0.9.2 renames the vocabulary going forward and, at the same time, adds a per-row marker (targetVocab: 2, stored in each row's metadata) so a reader can always tell which generation a row belongs to.

The read side is a permanent legacy mapping, not a one-time conversion. task_history is an append-only log — there is no "convert existing rows in place" step, and there never will be. akm task history keeps reading BOTH generations correctly forever: a row with no targetVocab marker (written before this release) is read with the OLD meaning — "prompt" → {kind: "command", engine}, "command" → {kind: "shell"} — and a row carrying targetVocab: 2 is read with the NEW meaning directly. You do not need to do anything for your existing history; this mapping is built in and will not be removed.

Mixed-fleet ordering hazard (one-way, hard failure). The metadata decoder rejects any field it does not recognize — it is a closed allowlist, not a permissive parser that ignores extras. A pre-0.9.2 akm's decoder does not have targetVocab in that allowlist, because the field did not exist yet. If a task's history lives in a state.db that both a 0.9.2 (or later) akm and a pre-0.9.2 akm read — for example, a global install and a pinned npx akm@<old> pointed at the same state directory, or a downgrade — the OLDER binary throws invalid task_history metadata_json: unknown fields: targetVocab (an uncaught error, not a handled UsageError) the first time it tries to read a row a 0.9.2-or-later akm wrote. A 0.9.2-or-later akm has no such problem: it reads a legacy (unmarked) row correctly using the mapping above, so upgrading is always safe in that direction. Only downgrading, or otherwise pointing an older binary at a state directory a newer one has already written to, is the hazard. Keep one akm version reading a given state.db at a time; do not alternate versions against the same state directory, and do not downgrade below 0.9.2 once a 0.9.2-or-later akm has recorded task history there.

Harness id rename: claude-code -> claude

0.9.2 also renamed the Claude Code harness id from claude-code to claude — the id used for both agent dispatch and the per-session extraction ledger (state.db's extract_sessions_seen.harness and workflow_runs.agent_harness). The 0.9.2 release did not carry a state migration for this rename, so any row a pre-0.9.2 akm wrote stayed keyed under claude-code, invisible to anything querying by the new name — a script or dashboard filtering extract_sessions_seen or workflow_runs on harness = 'claude-code' (or agent_harness = 'claude-code') sees those rows disappear from that query, not deleted, once you're on a release carrying the 0.9.12 fix. 0.9.12 adds state migration 027-extract-sessions-seen-harness-rename, which runs automatically on the next managed state.db open (no separate command needed) and renames every such row to claude in place — conflict-tolerant against a session already recorded under claude, which is kept as the authoritative row. After upgrading to 0.9.12 or later, re-point any external query at harness = 'claude' / agent_harness = 'claude'.

Workflow cutover

Markdown .md and GitHub-shaped .yml are peer workflow source formats in 0.9.2 and compile to source IR version 1. New workflow starts freeze durable plan irVersion 5, the sole executable plan format. A resume does not re-read authored workflow/command/agent source, configuration, model maps, or the asset index.

The inherit_env removal is breaking because every new start rejects it. Use named environment bindings and pass_env as the bounded replacement for fixed, secret, and per-machine values. There is no compatibility reader for the historical flag.

The GitHub-shaped source format is intentionally local and bounded: AKM YAML uses a familiar GitHub-step-shaped syntax but is an AKM workflow format, executed by AKM's native engine. It accepts schedule and empty workflow_dispatch triggers, runs-on: [self-hosted], and the documented local step subset. Full expressions, contexts, remote/local or Docker actions, service-event automation, and arbitrary runners remain unsupported. A step composing another workflow (uses: workflows/<ref>, or a task whose own target is a workflow) is new in 0.9.2 — see Child workflows below.

Multi-job YAML is rejected at the adapter boundary

A GitHub-shaped document whose jobs: map does not contain exactly one job now fails to compile at all — it never reaches lint, plan, or run as a partially-valid document. Before this release, a multi-job document parsed and ordered its jobs cleanly, and was refused only much later, in two different places, with two different shapes:

# 0.9.1: parsed clean, then refused at freeze with a bare thrown error
Multi-job workflow cannot execute until job boundaries and needs have a
durable runtime representation.
# 0.9.2: refused at compile, with the offending job count and a `line`
AKM workflow YAML requires exactly one job; this document declares 2.
AKM's YAML is an AKM workflow format executed by AKM's native engine, not
GitHub Actions — split the jobs into separate workflows.

surfaced from akm workflow run (and akm workflow plan) as UsageError code COMPOSITION_INVALID, exit 2. Fix: split a multi-job document into separate single-job workflows and compose them with a child-workflow step (uses: workflows/<ref>) — see Child workflows.

GitHub Action locators are no longer recognized anywhere

A workflow step's uses: owner/repo[/path]@ref (e.g. uses: actions/checkout@v4) used to be recognized and rejected with a locator-specific message:

# 0.9.1
Remote action acquisition is out of scope for "actions/checkout@v4".

In 0.9.2 the locator grammar itself is gone from native classification; the same value now fails the same way any other unrecognized uses: shape does — the canonical target-ref classifier's own rejection:

# 0.9.2
Target ref "actions/checkout@v4" must be a canonical commands/, scripts/,
tasks/, or workflows/ asset ref.

with code unsupported-uses-target. Nothing acquired or executed a remote action in any akm release, so this is a message and classification change, not a capability removal. A task's own uses: GitHub Action locator fails the same way, at parse, with TASK_SOURCE_INVALID — see Task sources above. The migrator still names the target explicitly when it blocks a v3 → v4 conversion (github-action-target-removed).

Pre-irVersion-5 stored plans: complete or abandon before upgrading

Every 0.9.1 (and pre-P3a 0.9.2-alpha) durable plan was frozen at an older irVersion. Upgrading does not delete or migrate those runs, but it does retire them as executable:

There is no second executor and no compatibility replay layer for an old plan version — a blocked run's only way forward is a fresh start.

Before upgrading, check for runs in flight and let them finish, or abandon them deliberately (see Before you upgrade).

After upgrading, if a run is blocked by this policy, recover it with the two-command sequence the error message itself names:

akm workflow abandon <id>
akm workflow run <ref>

No data is lost either way: the blocked run's row, its step spine, and its journaled events all remain readable through akm workflow status/list indefinitely — only resume/next/complete/run against that specific run id are refused.

with: on a task-composed step now binds — or rejects

A workflow step's uses: tasks/<ref> target with an authored with: mapping used to decode without error and then have that mapping silently dropped when the workflow was frozen — the authored inputs never reached the task, with no error and no warning. 0.9.2 makes this fail closed, and, where the target supports it, actually deliver the mapping:

A step targeting any task with no with: at all is unaffected and keeps freezing exactly as before. with: on uses: akm/command is a different, unaffected path — it is still required to supply the builtin action's arguments and continues to work as documented. with: on a child-workflow target (direct or task-wrapped) is different again: it binds the child's declared params: — see Child workflows.

Resume identity. A reference binding's resolved value is part of the unit's durable input-identity hash, alongside the reference's own text (from: "steps.discover.output.files") inside the frozen target: any unit whose target carries bindings hashes the effective values it actually receives (the same values delivered through the ## Task inputs prompt block and AKM_TASK_INPUTS). In the ordinary case this changes nothing — a frozen plan never re-reads source, so the same reference resolves to the same journaled upstream output on every attempt, the recomputed hash matches, and a resume reuses completed rows exactly as before. What it closes is the stale-reuse hole: journaled workflow_run_units rows are still read-only durable state, and if an earlier, already-completed step's journaled output is altered before a resume, a later bound unit now recomputes a different input hash and the run fails loudly with the executor's replay-divergence error ("journaled with different inputs") instead of silently reusing a row bound to the now-stale value. Units without bindings are unaffected.

See with: on a task-composed step for the full binding grammar, delivery surfaces (AKM_TASK_INPUTS, the ## Task inputs prompt block), and akm task explain for inspecting what a task-composed step would actually receive.

Child workflows

Nothing to migrate here in the strict sense: composing a child workflow is itself new in 0.9.2, so no run from before this release ever has a stored child. There is no pre-existing child-run data to convert or backfill. But if you are restructuring a multi-job document to work around the new one-job limit (above), this is the mechanism you restructure into.

Two authoring forms, both lowering to the same child-workflow target:

Composition is bounded, checked entirely at freeze, before the parent run is published, and failing with UsageError code COMPOSITION_INVALID when violated:

The child workflow is compiled, validated, and frozen completely — its own complete plan embedded inside the parent's — before the parent run exists, so editing the child's source afterward cannot affect an already-frozen parent, and the child's transitive sources join the parent's guarded source read set.

Execution. Running a step whose target is a child workflow drives that child to completion (or as far as it gets) inline, in the parent's own process, with the same engine akm workflow run uses on the child's frozen plan — not a separately scheduled job. Publication is idempotent, so a retried or resumed composing step reuses the same child rather than starting a new one.

Status mapping. The child's final status maps onto the composing step and the parent run: completed promotes the child's declared outputs: (see Workflow outputs) — or {runId, status} when it declares none — as the step's output, and the parent continues; failed fails the step and the run; blocked blocks the step and the run.

Cancellation propagates because the drive is inline. Whatever aborts the parent's own dispatch — Ctrl-C, a --timeout, a budget ceiling, or the parent losing its run lease — also aborts the child drive, since it is the same process. Both runs are left resumable, never partially torn down.

Independent resume. A blocked child blocks its composing step; AKM does not resume a child for you, because a gate is a gate for a child workflow too. The step's notes name the exact three-command sequence — resume the child first, then resume and re-run the parent, since re-driving the parent is what re-enters the composing step and drives the now-resumed child:

akm workflow resume <childRunId>
akm workflow resume <parentRunId>
akm workflow run <parentRunId>

A child run id always works directly with status/resume/abandon/run, whether or not it is listed. akm workflow status on a run that composes children renders a children: tree. akm workflow list excludes child runs by default now that they exist at all — pass --children to include them.

See Workflow Schema: Child workflows and Workflow Schema: Child execution for the complete grammar, status-mapping table, and nested-block recovery sequence, and Running Workflows: Child runs for an operational walkthrough.

Workflow outputs

A workflow may declare a run-level export in its Markdown frontmatter:

outputs:
  summary:
    from: steps.review.output.summary
  fileCount:
    from: steps.scan.output.files
    schema: { type: array }

Up to 64 entries, each {from: steps.<id>.output(.<segment>)*, schema?}, resolved once, from persisted step evidence, at run completion. An unresolvable reference, a truncated step artifact, or a schema violation rolls the completion back — UsageError code WORKFLOW_OUTPUT_INVALID: the run stays active and its final step stays pending rather than completing with missing exports. A run with no outputs: declaration exports {runId, status} instead. When this run is itself a composed child, its exported result (whichever of the two shapes above) becomes the composing parent step's own output — see Child workflows.

This is a Markdown-frontmatter-only key — a GitHub-shaped workflow's closed root key set (name, on, jobs) has no extension surface for it, the same reason it cannot declare params: either. See Workflow Schema: Workflow outputs.

New commands

Both are read-only and zero-write — neither spawns anything, writes history, or publishes a run. akm workflow plan is secret-free by construction: it never prints a resolved reference value, request content, script byte, or credential, because that data never reaches the command in the first place. akm task explain instead redacts secret-shaped input values on a best-effort heuristic basis (see below) — a value that doesn't match the heuristic (short, low-entropy, or unusually named) can still print unredacted, so don't treat its output as a guaranteed-safe paste target.

akm workflow plan <ref> compiles, resolves, and freezes a workflow exactly as starting a run would, then stops. It prints the canonical step graph, per-step frozen target kinds, task/child expansion, input bindings, the source read set, and freeze-time lowering notices.

akm workflow plan release --format json

Use it before committing to a run — especially after restructuring a multi-job document into a composed set of workflows (above) — to confirm the plan looks the way you expect, including which children it would compose.

akm task explain <ref> [input flags] prints a task's source path and version, its declared inputs: (with defaults — a secret-shaped default prints as <redacted>), the supplied values with provenance (default | flag | schedule-binding, likewise redacted when secret-shaped), the resolved target kind/ref, effective execution settings with field-level provenance, and schedule bindings.

akm task explain nightly-review --scope all  # doclint:ignore

Its default output (no --format flag) is the same raw JSON as --format json, byte for byte. --format text is a separate renderer: it flattens the envelope into dotted.path=value lines instead of printing JSON. See Tasks: akm task explain.

Diagnostics

INVALID_FLAG_VALUE is now rare in task or workflow domain failures, with two named exceptions (below). Every OTHER task-source, workflow-source, target-classification, and composition failure now reports a phase-specific code:

Code Domain
TASK_SOURCE_INVALID A task document's field- or semantic-level validation failure, or a malformed/oversized/too-deep YAML front end failure.
TASK_SCHEMA_VERSION_UNSUPPORTED A task document's version: is 3 or 2 and the in-memory read shim's deterministic conversion cannot resolve it — a human decision is needed (see Task sources); a convertible v2/v3 document reads through the shim instead of failing. From 0.9.17-alpha.7, every v2/v3 document fails this way, naming akm migrate apply.
TARGET_REF_INVALID A value is not a canonical commands/, scripts/, tasks/, or workflows/ asset ref (malformed shapes, GitHub locators, other asset families).
WORKFLOW_SOURCE_INVALID A workflow-source compile failure other than the one below.
COMPOSITION_INVALID A composition-policy rejection: a rejected with:, a multi-job document, a composition cycle/depth/size violation.
INPUT_BINDING_INVALID A with: binding, or a task's declared inputs: flag, fails its schema or names something that doesn't exist.
TASK_TARGET_UNSUPPORTED A recognized-but-unsupported task-execution construct (e.g. an interpreter task source v4 does not support).
WORKFLOW_OUTPUT_INVALID A declared outputs: entry could not be resolved at run completion.
WORKFLOW_IR_VERSION_UNSUPPORTED A stored plan predates irVersion 5 (see Workflow cutover).

Two failures are deliberately not re-coded and still report INVALID_FLAG_VALUE, so an existing pinned test's code and message stay byte-unchanged: a task's workflow-target env: composition rejection (a uses: workflows/<ref> task that also authors env:), and a workflow child-ref asset-resolution failure (Workflow source target <ref> was not found.). Beyond those two, the remaining INVALID_FLAG_VALUE sites in the task/workflow domains (38 total, across src/tasks/** and src/workflows/**) are scalar CLI-argument parsing (a cron expression, a task id, a workflow parameter flag) and one code-allowlist membership entry — genuine flag-value validation, not a re-codable task/workflow source or composition failure. Scripts branching on code for a task/workflow domain error should switch on the specific code above rather than assuming INVALID_FLAG_VALUE — except for the two named exceptions, which still report INVALID_FLAG_VALUE. Exit codes are unchanged — every code above is exit 2, same as before.

Improve triage judgment

improve.strategies.<name>.processes.triage.judgment now accepts an explicit boolean or object. Set judgment: false to disable the judgment pass without requiring an engine or credentials. Object form defaults to enabled for newly authored configuration and rejects unknown object keys. The standalone akm proposal ... --judgment option remains an explicit invocation override; it does not silently rewrite the configured strategy.

Validate the upgrade

After migration:

akm index
akm task sync
akm task doctor
akm health

Review scheduler changes before activation. Use akm task sync --rebind only when the installed runtime path intentionally changed. Spot-check a migrated task or a restructured workflow with the two new read-only commands before trusting it in production:

akm task explain <migrated-task-ref>
akm workflow plan <restructured-workflow-ref>

See Tasks, Workflow schema, and the 0.9.2 terminal migration note.