akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
646 lines (537 loc) • 39.3 kB
Markdown
# Review #2 — `task-workflow-format-unification.md` (v7) vs. the implementation
Reviewer: independent reviewer #2 of 3.
Repo: `/home/user/akm` @ `claude/akm-markdown-tasks-history-75l6du` (baselined on
`claude/release-0-9-0-polish-d6sycl`, HEAD `3c5fadb`).
Scope: the spec in full, plus `src/tasks/`, `src/workflows/{ir,exec,program}`,
`schemas/*.json`, config engine schema, model aliases, the task adapter, and the
0.9.0 migrator.
**Overall.** The *direction* is right and several of the spec's diagnoses are
accurate (the `unit:` bag is awkward; `resolvePromptSource()` sniffing is bad;
`akm-task.json` really is dead weight; freeze really does resolve by layers).
But the document repeatedly presents **new mechanism as existing mechanism**.
The three load-bearing "this already exists" claims — the configuration cascade,
the single dispatch seam, and "no IR/freeze/journal changes" — do not survive
contact with the code. `run:` shell units cannot be represented in IR v3 at all;
the cascade that "already exists" is two layers deep with no persona layer and
no `defaults.model`; and the `output:` "duplicate" the flatten deletes is two
semantically distinct schemas that the *sibling spec it cites* explicitly
distinguishes. The migration section's losslessness claim is false in at least
four concrete ways.
Verdict: **do not sign off decision 14 (or §3.6/§10) as written.** The design is
salvageable, but §10's non-goals must be withdrawn and the hidden cost
(IR v4, plan-hash/journal migration, a persona-layer freeze path, a placeholder
grammar decision, a redaction-policy change) has to be priced in before this is
scoped as "authoring-surface changes".
---
## Findings table
| # | Sev | Title | Key refs |
|---|---|---|---|
| C1 | critical | `unit.output` and `step.output` are not duplicates; the flatten collides them | `src/workflows/ir/compile.ts:204,229`; `src/workflows/exec/step-work.ts:560`; `src/workflows/exec/report.ts:1621`; `workflow-format-unification.md` §2.3b |
| C2 | critical | `run:` shell units are unrepresentable in IR v3 / dispatch; §10 non-goals are false | `src/workflows/exec/unit-dispatch.ts:19`; `src/workflows/ir/schema.ts:29,48,65,171`; `src/workflows/exec/native-executor.ts:638-645`; `src/workflows/ir/freeze.ts:52-62` |
| C3 | critical | Prose-append rule can change the prompt without changing the input hash | `src/workflows/exec/step-work.ts:376-392,362-368`; spec §3.3 bullet 3 |
| C4 | critical | `params` as a cascade value field collides with `params:` = param *declarations* | `schemas/akm-workflow.json:22-32`; `src/workflows/ir/params.ts:39-53`; spec §3.4 |
| M1 | major | The "existing cascade" is 2 layers; `defaults.model` and the persona layer don't exist | `src/core/config/config-schema.ts:102-108`; `src/workflows/ir/freeze.ts:50,163-191`; `schemas/akm-config.json` defaults |
| M2 | major | The kind-gated `llm` error is already dead code in workflows (silent drop, not error) | `src/workflows/ir/freeze.ts:67-73`; `src/tasks/runner.ts:497-502` |
| M3 | major | Model aliases are not portable to LLM endpoints — §3.5's portability claim is wrong | `src/integrations/agent/model-aliases.ts:45-76,107-109` |
| M4 | major | `agent:` as a cascade selector needs systemPrompt + toolPolicy in the frozen plan, incl. the provenance ceiling | `src/output/renderers.ts:250-262`; `src/workflows/exec/native-executor.ts:1026`; `src/workflows/exec/unit-dispatch.ts:16` |
| M5 | major | Command-template filling: four placeholder conventions, one existing bug, silent behavior change | `src/output/renderers.ts:164-186`; `src/commands/agent/agent-dispatch.ts:63-72,125-133` |
| M6 | major | `uses: scripts/<name>` is net-new remote-code-execution surface, not plumbing reuse | `src/output/renderers.ts:118-142`; `src/core/activation-policy.ts` |
| M7 | major | Literal `env:` values will be over-redacted through the existing seam | `src/workflows/exec/native-executor.ts:1163-1193`; `src/core/redaction.ts:193-202` |
| M8 | major | `env:` list shape is an IR + input-hash + per-unit-env change | `src/workflows/ir/compile.ts:46`; `src/workflows/exec/step-work.ts:353-357,387`; `src/workflows/exec/native-executor.ts:449-457` |
| M9 | major | Markdown-task recognition collides with the `.md`-claiming workflow adapter; "DO NOT modify" goldens | `src/core/adapter/adapters/index.ts:73-90`; `akm-workflow-adapter.ts:60-76`; `akm-task-adapter.ts:24-26` |
| M10 | major | Migration is neither a byte-rewrite nor lossless; stray `.yml` becomes silently invisible | `scripts/akm-migrate/migrate/legacy/task-target-ref-migration.ts:44-66`; `src/core/asset/asset-placement.ts:159-170`; `src/tasks/parser.ts:331-338` |
| M11 | major | `defaults.on_error` is deleted by "graph keys are steps-only" with no migration row | `src/workflows/parser.ts:104`; `src/workflows/ir/compile.ts:231` |
| m1 | minor | "Trigger keys consumed only by `task sync`" — the runner gates on `enabled` at fire time | `src/tasks/runner.ts:169`; `src/core/activation-policy.ts` rule 3 |
| m2 | minor | §3.5's `akm agent` embodiment sentence is backwards | `src/commands/agent/contribute-cli.ts:87-88` |
| m3 | minor | "The `.yml` task path drops the model preference" understates: frontmatter is injected into the prompt | `src/tasks/runner.ts:582-593` |
| m4 | minor | "The engine node keeps its strict schema" — both engine schemas are `.passthrough()` | `src/core/config/schema/engines.ts:54,82,104` |
| m5 | minor | `status: draft` overloads OKF lifecycle with scheduler activation; a second unordered gate | `schemas/akm-asset-envelope.json` `status`; spec §4 |
| m6 | minor | `cwd:` × `isolation: worktree` interaction unspecified | `src/workflows/exec/native-executor.ts:1063-1075` |
| m7 | minor | `uses:` table covers 5 of ~15 asset types; the rest are unspecified | `src/core/asset/asset-placement.ts:89-178` |
| m8 | minor | Task id / placement inconsistency the plan inherits (subdir tasks index but cannot run) | `src/tasks/task-id.ts:7`; `src/core/asset/asset-placement.ts:162-169` |
| m9 | minor | Embedded-template plumbing is `.yml`+command-only and must change too | `src/tasks/embedded.ts:82,97` |
| ✓ | — | Claims that *do* check out (see §"Verified claims") | — |
---
## Critical
### C1. `unit.output` and `step.output` are two different schemas; the flatten collides them
Spec §3.6: *"`output:` is declared **both** on the step and inside the bag — a
live wart in `schemas/akm-workflow.json`"*, and *"The duplicate `output`
declaration disappears with the bag."* §6 repeats: *"separate `unit` defs with a
duplicated `output`"*.
They are not duplicates.
- `src/workflows/ir/compile.ts:229` — `unit.output` → `WorkflowUnitDraft.schema`.
- `src/workflows/ir/compile.ts:204` — `step.output` → `WorkflowStepDraft.outputSchema`.
- `schema` validates **one unit's structured result**, per dispatch attempt:
`src/workflows/exec/report.ts:1621` (`Unit "…" result failed validation against
its declared output schema`), threaded to harness native structured output at
`native-executor.ts:1038,1092`.
- `outputSchema` validates the **promoted step artifact** after reduction:
`src/workflows/exec/step-work.ts:558-561`, `native-executor.ts:55`,
`ir/schema.ts:217`.
And the spec this one claims consistency with says so in as many words —
`docs/architecture/specs/workflow-format-unification.md` §2.3b: *"For a map step,
`output:` validates the **promoted artifact**, i.e. what the reducer produced —
not one unit's result."*
Consequence: on a `map:` step, `map.unit.output` (per-item schema, which drives
each harness's native structured-output mode) and `step.output` (the collected
array schema) are **necessarily different documents**. Deleting the bag deletes
the ability to type a map step's per-item result at all — silently, since both
spellings are `output:`. Existing workflows that set both will migrate to a
conflict with no defined winner.
Recommendation: keep two spellings on the flattened step (e.g. `result:` for the
per-unit schema and `output:` for the step artifact), or explicitly declare
per-item schemas removed and accept losing harness-native structured output on
map steps. Either way, remove the "duplicate" framing from §3.6/§6 — it is the
justification for the change and it is wrong.
### C2. `run:` shell units cannot be represented in IR v3; §10's non-goals are false
Spec §3.6: *"A shell unit inside a workflow journals like any unit
(`workflow_run_units`): no tokens, but status, timing, attempts, and
`failure_reason` behave normally."* §2: *"`unit-dispatch.ts` is the one dispatch
seam."* §10: *"no changes to … IR, freeze, journal, leases, or replay."*
The code:
- `src/workflows/exec/unit-dispatch.ts:19` — `UnitDispatchRequest.engine:
FrozenEngineSnapshot` is **required**, as is `invocation: IrInvocation`.
- `src/workflows/ir/schema.ts:48,65,76` — `FrozenEngineSnapshot` is a closed
two-member union (`llm` | `agent`), validated by `decodeWorkflowPlanV3`
(`ir/schema.ts:171` — `irVersion must be 3`; `:341-374` field allowlists;
`:517` `assertUnitEngineCompatibility`).
- `src/workflows/exec/native-executor.ts:638-645` — a unit with no frozen engine
snapshot hard-fails `dispatch_error` before dispatch.
- `src/workflows/ir/freeze.ts:49-62` — **every** unit runs `freezeInvocation`,
which throws `ConfigError("No workflow engine is selected…")` when no engine
resolves. A workflow made entirely of `run:` steps therefore cannot even be
frozen on a machine with no agent CLI configured — the single most obvious
use case for inline shell.
Adding a shell unit kind requires: a third `FrozenEngineSnapshot` variant or a
new IR node kind, `decodeWorkflowPlanV3` changes, an `irVersion` bump (persisted
`plan_json`/`plan_hash` on run rows), a `hashVersion` bump in `step-work.ts:379`,
and a `frozenUnitRunner` branch. That is an IR + freeze + journal + replay
change, i.e. the exact four things §10 says are untouched. Nothing in the
codebase currently executes a shell string at all — `runCommandTask` spawns
pre-split argv with no shell (`src/tasks/runner.ts:277-289`,
`src/tasks/parser.ts:300-306`), which the spec itself notes in §6.
Recommendation: withdraw the §10 non-goal, state IR v4 explicitly, and decide
whether in-flight v3 runs are drained or migrated. Also state what
`freezeInvocation` does for a shell unit (it must be skipped, not defaulted —
otherwise every shell-only workflow requires an engine).
### C3. The prose-append rule can change the prompt without changing the input hash
Spec §3.3: *"the assembled prompt is a frozen string produced at one seam, so
appending is a deterministic concat — easy"*, and the appended section is
*"appended to the assembled prompt after a blank line, byte-exact."*
There are two seams, and the spec names the wrong one. `step-work.ts:322-332`
assembles the prompt via `buildUnitPrompt`; `step-work.ts:376-392` computes the
`inputHash` over **`template.instructions`** — the frozen template bytes — *not*
over the assembled prompt. The comment at `:342-350` states this deliberately.
The codebase has already been bitten by exactly this: `gateFeedback` had to be
added to the hash preimage (`:362-368`) because *"omitting it made loop 1 and
loop 2 journal identical hashes for different prompts, breaking the 'changed
inputs ⇒ changed hash' audit contract."*
If the `## <step-id>` section of a `uses: tasks/<id>` step is appended at
prompt-assembly time, two materially different prompts hash identically →
`classifyUnitReuse` (`native-executor.ts:690`) reuses a completed row for a
different ask, and editing only the step's section produces no re-dispatch.
Recommendation: the append must happen at **compile** time, folded into
`WorkflowUnitDraft.instructions` before freeze, so it is inside the hash
preimage by construction. Say so in §3.3. (This also makes the "byte-exact" and
"blank line" rule a compile-time, testable property.)
Secondary: the rule is conditioned on *"when the resolved work is
agent-dispatched"* — but for a `uses: tasks/<id>` step, whether the task
resolves to agent work or shell work is known only after resolving the task.
The same authored step therefore silently changes whether its prose is
load-bearing or documentation depending on the referenced task's target — and a
later edit to the *task* (prose → `run:`) silently demotes the *workflow's*
prose to a comment. That is a footgun worth an explicit lint.
### C4. `params` as a cascade value field collides with `params:` = declarations
§3.4 lists `params` in the flat value vocabulary, *"legal at every layer, merged
per-field with the nearest layer winning"*, and §3.3 says a referenced task is
*"one more layer"*.
But at the workflow-document layer, `params:` already means something else
entirely: the **declaration block**, name → JSON Schema
(`schemas/akm-workflow.json:22-32`; `compile.ts:132-139` → `plan.paramSchemas`).
A "document defaults" layer contributing `params` *values* into the same
frontmatter namespace is not expressible.
Worse, cascading param *values* down from config/engine/persona layers breaks
the declared-flag contract the spec cites in §2: `materializeWorkflowParameterFlags`
(`ir/params.ts:39-53`) rejects any name not in `plan.params`, and
`assertRunParamsSatisfyPlan` (`run-workflow.ts:531`) validates the journaled set.
A global `params` default would inject undeclared names into every run.
Also unspecified: `params` means three different things across §3.2 —
workflow param flags, command-template placeholder fills, and (per §3.4) a
cascading value field. Merging them per-field across five layers has no
coherent semantics (e.g. a persona node's `params.scope` leaking into an
unrelated workflow's param set).
Recommendation: remove `params` from the cascade vocabulary. Make it a
target-local input (`uses:`-adjacent only), and keep the document-level
`params:` declaration block as-is.
---
## Major
### M1. The "cascade that already exists" is two layers, with no persona layer and no `defaults.model`
§2 and §3.4 lean hard on *"Freeze already implements a configuration cascade"*
and *"exposing this existing mechanism"*. What exists:
- `freeze.ts:50` — `layers = [documentDefaults?, unit?]`. **Two** layers, both
from the workflow document. Verified.
- `freeze.ts:151-155` `selectedEngine`, `:163-191` `exactModel`,
`:193-209` `effectiveTimeout`, `:211-216` `mergedLlmOverrides` — nearest-wins /
deep-merge. Verified.
- The **engine node is not a layer**: `engine.model` is consulted only as a
fallback *after* the layers produce nothing (`freeze.ts:171`), and
`engine.timeoutMs` likewise (`:197`). Same relative order the spec wants, but
it is a two-branch fallback chain, not a layer list — generalizing it to five
layers is a rewrite of these three functions, not an exposure.
- **There is no persona layer at all.** Nothing in `freeze.ts` loads an agent
asset. This is the largest new piece of work in the whole proposal (see M4)
and §3.4 describes it as already-mechanism.
- **`defaults.model` does not exist.** `src/core/config/config-schema.ts:102-108`:
`DefaultsSchema = { engine?, llmEngine?, improveStrategy? }.passthrough()`.
`schemas/akm-config.json` `properties.defaults` has the same three keys with
`additionalProperties: true`. So the spec's §3.4 worked example
(`"defaults": { "engine": "claude", "model": "sonnet" }`) is a config a user can
write **today**, that validates, and that is **silently ignored**. That is a
live drift bug the plan should fix explicitly, not a documented layer.
Recommendation: rewrite §2's fifth bullet and §3.4's "the mechanism already
exists" framing to name what is new (persona layer, config `defaults.model`,
`env`/`cwd`/`shell` layering, per-field merge across five layers) versus what is
reused (nearest-wins over two workflow-document layers). Otherwise the estimate
is wrong by a large factor.
### M2. The kind-gated `llm` hard error is already dead code in the workflow path
§3.4: *"`llm:` … was a grouping whose only job was to scope a kind-gated hard
error"*; §6: *"a kind-gated hard error"* (singular).
There are two behaviors, and one is a bug:
```ts
// src/workflows/ir/freeze.ts:67-73
const llm = engine.kind === "llm" ? mergedLlmOverrides(layers) : undefined;
if (engine.kind !== "llm" && llm !== undefined) { throw new ConfigError(...) }
```
The guard is **unreachable** — `llm` is `undefined` whenever `kind !== "llm"`.
So a workflow that sets `defaults.llm.temperature` under an agent engine today
gets its overrides **silently discarded**, with no error and no warning. The
live hard error exists only on the task path
(`src/tasks/runner.ts:497-502`, `NotFoundError` — also the wrong error class for
a config problem).
This matters for the proposal in two ways: (a) the "convert the error to a
notice" work item is really "fix a silent-drop bug *and* convert a task-path
error", and (b) §3.4's argument that *"a global default `temperature` must not
brick every agent-engine dispatch"* is already true of workflows — the
motivating hazard is one path only.
Recommendation: fix `freeze.ts:67-73` independently (it is a bug on the current
branch), and correct §3.4/§6 to describe two divergent behaviors.
### M3. Model aliases are not portable to LLM endpoints
§3.5: *"Aliases are **per-platform**, which is what makes a shared asset
portable: `model: sonnet` resolves to the right string under a claude engine, an
opencode engine, **or an LLM endpoint**."*
`src/integrations/agent/model-aliases.ts:45-76` — the built-in table has exactly
two platform columns: `claude` and `opencode`. `resolveLlmModel` (`:107-109`)
calls `resolveModel(model, engineName, undefined, global, ["llm"])`, i.e. it
passes the **engine name** where `platform` is expected, then falls back to a
`"llm"` column and `"*"`. A user-supplied `modelAliases` table can supply those
columns; the **built-ins cannot**. So under an LLM engine, `model: sonnet`
returns `"sonnet"` verbatim (`:103` `entry?.platforms[platform] ?? model`) and is
sent as the model id to an OpenAI-compatible endpoint.
Since §3.4/§3.5 make aliases legal at *every* layer (including a shared persona
node in a third-party bundle), this failure now occurs at more layers, not
fewer, and only at dispatch.
Recommendation: either add an `llm`/`*` column to `BUILTIN_ALIASES`, or drop the
portability claim and add a freeze-time notice when an alias fails to resolve
against the selected engine.
### M4. The persona layer is not "a node whose fields join the cascade" — it carries a system prompt and a gated tool policy
§3.4 describes `agent:` as picking *"the persona node — who runs it (system
prompt, tool policy, model preference)"* and then only ever discusses `model`.
The other two are the hard part:
- Today an agent asset yields `{ prompt (system), modelHint, toolPolicy }`
(`src/output/renderers.ts:241-262`), consumed only by `akm agent`
(`contribute-cli.ts:72-90` → `AgentDispatchRequest`).
- The workflow path deliberately does **not** carry them:
`native-executor.ts:1026` — *"`systemPrompt`/`tools`/`cwd` come from the
profile/asset, not the workflow unit"*; `unit-dispatch.ts:16` documents
`systemPrompt` as *"used by frozen workflow gate judges"* only;
`buildAgentDispatchRequest` (`:1029-1039`) passes only prompt/model/schema.
`FrozenEngineSnapshot` has no tool-policy field (`ir/schema.ts:60-75`).
- **Security-relevant:** the agent asset's `tools:` is *self-declared
frontmatter*, and there is an explicit **provenance ceiling** applied at the
show layer that decides whether to honour it based on writable-own-stash vs
read-only third-party source (`renderers.ts:257-260`, tagged "07 P1-D").
Freeze has no notion of source provenance. Making `agent:` a cascade selector
resolved at freeze silently relocates that ceiling — or drops it.
Combined with §3.4's *"This widens agent assets from today's model-hint-only
frontmatter to the full value vocabulary"*, a third-party bundle's agent asset
would now be able to set `env`, `cwd`, `timeout` and a tool policy for a
workflow that merely names it. §10 says *"no multi-file config merging"* while
§3.4 makes bundle assets configuration layers; that tension should be resolved
explicitly with a provenance rule.
Recommendation: state that the persona layer contributes `systemPrompt` and
`toolPolicy` into the frozen plan (IR change), and carry the existing provenance
ceiling into freeze with a named test.
### M5. Command-template filling: four conventions, one live bug, and a silent behavior change
§3.2: *"`uses: commands/<name>` … `params:` fill the placeholders"*, *"a
placeholder with no matching param is an error at fill time"*; §6: *"one
implementation, both surfaces"* (from "n/a").
It is not "n/a" today, and the existing surface is inconsistent:
- `src/output/renderers.ts:164-186` (`extractParameters`) advertises **three**
conventions to consumers: `$ARGUMENTS`, `$1`…`$9`, and `{{name}}`.
- `src/commands/agent/agent-dispatch.ts:63-72` (`fillPlaceholders`) substitutes
**only** `{{<digits>}}`, **positionally** from `--args`, and leaves unmatched
placeholders verbatim (`:70`).
So a command asset declaring `{{scope}}` is reported as having a `scope`
parameter and can never be filled — an existing bug the plan inherits. And the
spec's rule changes behavior on two axes at once: positional → named, and
lenient → hard error. `$ARGUMENTS`-style command assets imported from Claude
Code (which the `agent:` frontmatter support at `renderers.ts:234` implies akm
already ingests) would become **errors** under "a placeholder with no matching
param is an error at fill time".
Recommendation: pick one grammar in the spec, state what happens to the other
three, and state the inverse rule (extra `params:` with no placeholder — error
or notice?). Treat this as a real work item, not a table row.
### M6. `uses: scripts/<name>` is new execution surface, not consolidation
§3.2: *"Execute the script asset per its own `run`/`setup`/`cwd` metadata — no
AI."* §6 files it under shared plumbing.
Today `run`/`setup`/`cwd` on a script asset are **display hints only**:
`resolveExecHints` (`src/output/renderers.ts:118-142`) merges indexed metadata,
`@run`/`@setup`/`@cwd` header comments, and — critically — **auto-detection from
extension + dependency files** (`detectExecHints`). Nothing executes them.
Making them executable means: a scheduled task or workflow step naming a script
from an *installed third-party bundle* runs (a) an auto-detected interpreter
line and (b) a `setup` command (typically a package install) at cron time.
`src/core/activation-policy.ts` enumerates four activation rules — dangerous env
keys, install-time key scan, task activation, write activation — **none** covers
executing a bundle-supplied script. The spec's §10 "no change to how script
assets are authored or stored — this proposal executes and embodies them" waves
past exactly the interesting part.
Recommendation: add an activation rule (fifth) for third-party script execution,
decide whether `setup` runs at all (I'd say no — it is an install-time concern),
and forbid auto-detected `run` for non-primary-bundle scripts.
### M7. Literal `env:` values will be over-redacted
§3.7: *"Values sourced from `env/` and `secrets/` assets join the dispatch's
`sensitiveValues`"* — implying literals do not.
`collectWorkflowDispatchSensitiveValues` (`native-executor.ts:1163-1185`) starts
from `new Set(Object.values(env ?? {}))` — **every** resolved env value, no
provenance distinction. `collectSensitiveValues` (`src/core/redaction.ts:193-202`)
applies no length, entropy, or dictionary floor.
So `env: [{ LOG_LEVEL: debug }]` (the spec's own example, §3.7) adds the string
`"debug"` to `sensitiveValues`, and every occurrence of `debug` in the unit's
output, journaled result, and error text is replaced. Worse,
`redactUnitOutcome` (`:1187-1193`) collapses any redacted `failureReason` to
`"reported_failure"` — so a common literal value can silently destroy the
failure taxonomy `retry.on` keys off.
Recommendation: tag env entries with provenance at resolution
(`ref` vs `literal`) and feed only ref-sourced values to `sensitiveValues`; or
add a minimum-length/entropy floor. Say which in §3.7.
### M8. The `env:` list shape is an IR change, a hash-version change, and a per-unit-env change
§3.7: *"This **extends the existing schema additively** — the workflow
`unit.env` is already a list of ref strings, so every currently-valid value stays
valid."* §10: no IR/journal changes.
- `compile.ts:46` / `ir/schema.ts` — `env?: string[]`. A list of
`string | Record<string,string>` is a **type change** through
`WorkflowUnitDraft` → `IrUnitNode` → `decodeWorkflowPlanV3` validation →
`plan_json`. Existing values stay *authorable*; the IR does not stay the same.
- `step-work.ts:353-357,387` — the input-hash preimage includes `env` and the
comment is explicit: *"`env` carries NAMES ONLY, never resolved values:
hashing a resolved secret would leak it into a durable hash oracle"*. A list
containing literal mappings puts literal values into the preimage (fine) but
mixes them with names, and `secrets/<name>` ref-valued entries must be hashed
as refs, not values. Either way `hashVersion: 4` (`:379`) has to become 5,
which invalidates every journaled `input_hash` and every replay golden.
- **Per-unit env is not supported.** `native-executor.ts:449-457` resolves env
**once per step**, before any dispatch, from `template.env`, and passes one
`Record<string,string>` to every unit (`RunUnitInput.env`, `:611`). §3.6's
`AKM_ITEM` / `AKM_ITEM_INDEX` for map+shell steps requires per-unit env
assembly, which also moves the `sensitiveValues` computation and the
"one binding error fails the whole step cleanly" property (`:446-448`).
- The cascade also requires env at layers that have no env today:
`ProgramDefaults` (`program/schema.ts:203-211`) has no `env`, and
`DEFAULTS_KEYS` (`parser.ts:104`) does not list it.
Recommendation: drop "additively" from §3.7, name the hash-version bump, and
specify per-unit env for map fan-out.
### M9. Markdown-task recognition collides with the workflow adapter; the task goldens are declared immutable
§6: *"Adapter | `.yml`-only recognize/place | markdown task recognized by
`type: task` / `tasks/` residence"*.
- `src/core/adapter/adapters/index.ts:73-90` orders `akmWorkflowAdapter`
**before** `akmTaskAdapter`.
- `akm-workflow-adapter.ts:60-76` — *"any `.md` file in this bundle IS a workflow
… UNLESS its frontmatter declares a DIFFERENT non-empty `type:`"*.
So `type: task` is **mandatory** in practice for a markdown task in a
workflow-adapter bundle; the "or `tasks/` residence" alternative in §6 and the
schedule-only examples pattern would be claimed as workflows. Note also that
`recognize()` receives a component-relative path, and `akmTaskAdapter.directoryList()`
returns `["."]` (tasks live anywhere) — residence-based recognition is not
available in this adapter's contract as written.
Separately, `akm-task-adapter.ts:24-26` declares its fixture + four goldens
(`tests/fixtures/format-family-goldens/akm-task/{recognition,placement,lint,renderer}.json`)
a *"Conformance oracle (authored, DO NOT modify)"*. §3.6's "schema and golden
churn on a pre-release format — approved" does not acknowledge that these
specific goldens are marked immutable, nor `looksLikeRoot`'s `.yml`+`schedule`
probe (`:136-155`), which would need a disjoint `.md` probe that does not fire
on workflow bundles.
Recommendation: state that `type: task` is required (drop the residence
alternative), and get explicit sign-off to re-author the akm-task conformance
oracle.
### M10. Migration is not a byte-rewrite, is not lossless, and re-creates the invisible-file failure
§8: *"Task conversion joins it as one more content step"*, *"No key survives with
a changed meaning"*, and *"The 0.7→0.8 lesson — leftover files must never be
**silently invisible** — is honored structurally."*
Four problems.
1. **Shape.** The existing task migration phase
(`scripts/akm-migrate/migrate/legacy/task-target-ref-migration.ts`) rewrites
*YAML scalar bytes in place*: `TaskTargetRefRewrite = { filePath, from, to,
before, after, mode }` (`:44-52`) with an atomic same-path write. There is no
rename in the plan structure, no journal phase for a rename, and no
crash-resume story for a half-renamed set. `.yml → .md` plus a full
frontmatter reshape plus body synthesis is a new migrator, not a new step.
2. **Read-only bundles.** `:53-66` — `readOnlyLegacyTasks` exists precisely
because *"The migration NEVER rewrites a read-only bundle … but the 0.9
runtime removed the v1 parser, so these tasks will fail after upgrade."*
Under this proposal every `.yml` task in every registry-installed bundle
becomes permanently unrunnable and unconvertible. "No key survives with a
changed meaning" is true; "lossless" is not.
3. **Invisible files.** `src/core/asset/asset-placement.ts:159-170` makes `task`
`.yml`-only for `isRelevantFile` / `toCanonicalName` / `toAssetPath`.
Flipping it to `.md` makes stray `.yml` task files invisible to
`resolveAssetPath` — which is what `akm task run` uses
(`src/tasks/runner.ts:159`). The 0.7→0.8 lesson is **not** honored
"structurally"; it requires *new* code that deliberately scans for a
no-longer-relevant extension in `task sync` / `task doctor`. That is a work
item, not a property.
(`src/tasks/task-id.ts:8,15,41-46` also hardcodes `.yml`/`.yaml`, including
in the portable-length budget.)
4. **Dropped fields.** The §8 table's `llm.*` row ends in an ellipsis. Three
`llm` sub-fields have **no home** in §3.4's nine value fields:
`supportsJsonSchema`, `contextLength`, `enableThinking`
(`src/tasks/parser.ts:331-338`; `src/tasks/schema.ts:61-68`;
`schemas/akm-task.json:46-56`; identical set on the workflow side,
`schemas/akm-workflow.json:76-87`, and all six are carried into
`FrozenLlmEngine`, `freeze.ts:231-236`). Either the vocabulary grows to
twelve fields or the migration is lossy — the spec should say which.
### M11. `defaults.on_error` is silently deleted
§3.4: *"**graph keys** (`map`, `route`, `inputs`, `output`, `gate`, `retry`,
`on_error`, `isolation`) are steps-only"*.
`on_error` is legal at document level today: `src/workflows/parser.ts:104`
(`DEFAULTS_KEYS = ["engine","model","timeout","on_error","llm"]`),
`schemas/akm-workflow.json:200`, `program/schema.ts:210`, and it is *read*:
`compile.ts:231` — `unit?.onError ?? defaults?.onError ?? "fail"`. Making
`on_error` steps-only removes a working document-level default, and §8's
migration table has no row for it.
Recommendation: either keep `on_error` (and `retry`, arguably) as a document
default, or add a migration row that lifts `defaults.on_error` onto every step.
---
## Minor
**m1 — "Trigger keys are consumed *only* by `akm task sync`" (§3.3, decision 10)
is inaccurate.** `enabled` is also read at fire time by the runner:
`src/tasks/runner.ts:169` → `shouldSkipUnactivatedTask({ enabled, scheduled })`,
which is activation-policy rule 3 (`src/core/activation-policy.ts`, doc block
rule 3: *"Task activation (`tasks/runner.ts`)"*). This is a defense-in-depth
property worth preserving; the spec's phrasing invites removing it.
**m2 — §3.5's embodiment sentence is backwards.** *"`akm agent` embodiment
honors it under an explicit `--model` override"* — `contribute-cli.ts:87-88`:
`// --model flag wins over the asset's modelHint`. The asset's model is honored
in the *absence* of `--model`. The design conclusion drawn from it still stands,
but the sentence should be fixed.
**m3 — §3.5 understates the `.yml` prompt-asset bug.** *"`prompt: agents/x`
resolves the asset to prompt text only"*. `runner.ts:582-593` does
`fs.readFileSync(assetPath, "utf8")` with **no frontmatter stripping**, so the
YAML frontmatter (`---\nmodel: opus\ntools: …\n---`) is prepended to the prompt
sent to the agent. Same for `kind: "file"` (`:576-579`). This is a live bug on
the current branch, worth fixing independently of the proposal.
**m4 — "the engine node … keeps its strict schema" overstates.** Both
`LlmEngineSchema` and `AgentEngineSchema` are `.passthrough()`
(`src/core/config/schema/engines.ts:54,82,104`) with a *blacklist* superRefine
(`:83-88`, `:105-131`). `model` legal on both, LLM fields rejected on agent
kind — that part of §3.4 is verified. But the new value fields `cwd`, `shell`,
`env`, `params` are in neither list, so an engine node would silently accept and
silently ignore them. "Strict" needs to become strict (closed schema) if the
spec's asymmetry argument is to hold.
**m5 — `status: draft` overloads the OKF lifecycle family.**
`schemas/akm-asset-envelope.json` defines `status: ["draft","stable","deprecated"]`
as *"OKF v0.2 lifecycle status"*, shared across all asset types and stamped by
`okf-support.md`'s enhancement contract. §4 makes it load-bearing for scheduler
installation (*"draft tasks are never installed by `sync`"*), creating a second
activation gate alongside `enabled:` with no stated precedence, and leaving
`status: deprecated` undefined. Recommend: keep activation on `enabled:` only,
or define the truth table.
**m6 — `cwd:` × `isolation: worktree`.** `request.cwd` is currently set *only*
by worktree isolation, and `defaultUnitDispatcher` rejects `cwd` on the llm
runner (`native-executor.ts:1063-1075`). A user-set `cwd:` on an isolated step,
or on an llm-engine step, has no defined behavior in the spec.
**m7 — `uses:` coverage.** §3.2 defines 5 of ~15 placement types
(`asset-placement.ts:89-178`: skill, command, agent, script, memory, env,
secret, lesson, task, session, fact, workflow, knowledge…). `uses: lessons/x`,
`uses: skills/x`, `uses: env/x` need a stated rule (error with a hint, or
"anything not listed is an error").
**m8 — task id vs placement inconsistency inherited.**
`asset-placement.ts:162-169` derives a task's canonical name from the *relative
path*, so `tasks/sub/x.yml` indexes as `sub/x`; `validateTaskId`
(`task-id.ts:7`, `/^[A-Za-z0-9][A-Za-z0-9._-]*$/`) rejects `/`, so
`akm task run sub/x` throws. §4's "an ordinary akm markdown asset at
`<bundle>/tasks/<id>.md`" plus the adapter's `directoryList(): ["."]`
perpetuates it. Worth resolving in the same change.
**m9 — embedded templates.** `src/tasks/embedded.ts:82` filters `.yml` and
`:97` drops any task whose target is not `command` — so the setup wizard's whole
enumeration path changes with the format (and `EmbeddedTask.command: string`
becomes `run:`). Confirmed 10 templates (`src/assets/tasks/core/` ×5,
`src/assets/tasks/improve/` ×5), all `command:` targets, so §8's "ten embedded
templates" is accurate.
---
## Verified claims (the spec gets these right)
Recorded so the above is read as targeted, not blanket:
- §2 — `akm workflow run` is the canonical orchestrator; no `start`/`next`/`complete`
subcommands; `brief`/`report` gated on `experimental.workflowEngine`
(`src/commands/workflow-cli.ts:160,324,351,522-530`;
`src/workflows/exec/workflow-engine-gate.ts:39,56`).
- §2 — scheduled workflow tasks run end-to-end (`runner.ts:199-208,352-416`
→ `runWorkflowSteps`).
- §2 — `UnitDispatchRequest` carries prompt / frozen engine / `timeoutMs` / `env`
/ `cwd` / `sensitiveValues`, and results carry `failureReason`
(`unit-dispatch.ts:9-43`).
- §2 — freeze assembles `layers = [documentDefaults, unit]`, selects engine by
nearest layer, resolves model/timeout/llm nearest-wins/deep-merged
(`freeze.ts:50,151-216`) — accurate as far as it goes (see M1).
- §3.4 — `temperature` etc. are a config validation error on an agent-kind
engine, `model` legal on both (`engines.ts:99,105-120`).
- §3.4 — command assets already read an `agent:` frontmatter field
(`renderers.ts:234`).
- §3.5 — `resolveModel` is a four-tier chain (profile aliases → global table with
`"*"` fallback → built-ins `fable`/`opus`/`sonnet`/`haiku` → verbatim), one
level, no recursion (`model-aliases.ts:45-104`).
- §3.6 — `output:` is declared on both the step and the `unit` bag in
`schemas/akm-workflow.json` (`:234`, `:351`). *Present* — but not a duplicate
(C1).
- §3.7 — `unit.env` is currently `array of string` (`akm-workflow.json:237-244`).
- §6 — `parseTaskDocument` + `TASK_KEYS` + `rejectTargetFields` +
`resolvePromptSource` = exactly 368 lines (`src/tasks/parser.ts`).
- §6 — `akm-task.json` is 87 lines and referenced only by
`tests/integration/tasks-schema.test.ts:8` (no runtime loader).
- §6 — `runCommandTask` runs raw argv only, no shell
(`runner.ts:277-289`; `parser.ts:300-306`).
- §6 — lint lives in `akm-task-adapter.ts` as `invalid-task-yaml` (`:88-98`).
- §8 — task ids and the `akm task run <id>` ABI need not change; the scheduler
argv shape is `[…akm, --scheduler-context, <path>, task, run, <id>, [--bundle
<b>], --scheduled]` and depends on the id, not the file format
(`scheduler-invocation.ts:59-78,198-227`).
- §8 — 0.9.0 already refuses un-migrated installations at the config boundary
(`src/core/common.ts:228`).
- §8 — ten embedded templates.
- §10 / §3.6 alignment with `workflow-format-unification.md` §2.3's forward
reference: *"If a non-agent unit kind (raw shell/exec) is ever added, that unit
kind reintroduces substitution as its own need"* — §3.6's `AKM_ITEM` env
approach is consistent with that ruling.
---
## Recommended changes before sign-off
1. **Withdraw §10's "no changes to IR, freeze, journal, replay."** Replace with
an explicit list: IR v4, `hashVersion` 5, a shell node kind or third engine
snapshot variant, a persona-layer freeze path, plan/journal migration for
in-flight runs.
2. **Fix C1** — keep two output spellings, or state the map-step loss.
3. **Fix C3** — move the prose append to compile time, into the hashed
`instructions`.
4. **Remove `params` from the cascade vocabulary (C4).**
5. **Rewrite §2 bullet 5 and §3.4's framing (M1)** to distinguish the two-layer
nearest-wins resolver that exists from the five-layer cascade being proposed,
and add `defaults.model` as a new config field (it is silently ignored today).
6. **Add a provenance rule for bundle-asset configuration layers (M4, M6)** —
the tool-policy ceiling and script execution both need one.
7. **Decide the placeholder grammar (M5)** and the fate of `$ARGUMENTS`.
8. **Rewrite §8** — losslessness is false for read-only bundles and for the three
`llm` sub-fields; the migrator needs rename support; the "silently invisible"
guarantee is a work item, not a structural property.
9. **Independently fix, on the current branch:** the dead `llm` guard
(`freeze.ts:67-73`), the frontmatter-in-prompt bug (`runner.ts:582-593`), and
the `{{name}}` vs `{{0}}` placeholder mismatch (`renderers.ts:178` vs
`agent-dispatch.ts:68`). All three are bugs today and the plan inherits them.