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.schemavalidates 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 atnative-executor.ts:1038,1092.outputSchemavalidates 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: FrozenEngineSnapshotis required, as isinvocation: IrInvocation.src/workflows/ir/schema.ts:48,65,76—FrozenEngineSnapshotis a closed two-member union (llm|agent), validated bydecodeWorkflowPlanV3(ir/schema.ts:171—irVersion must be 3;:341-374field allowlists;:517assertUnitEngineCompatibility).src/workflows/exec/native-executor.ts:638-645— a unit with no frozen engine snapshot hard-failsdispatch_errorbefore dispatch.src/workflows/ir/freeze.ts:49-62— every unit runsfreezeInvocation, which throwsConfigError("No workflow engine is selected…")when no engine resolves. A workflow made entirely ofrun: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-155selectedEngine,:163-191exactModel,:193-209effectiveTimeout,:211-216mergedLlmOverrides— nearest-wins / deep-merge. Verified.- The engine node is not a layer:
engine.modelis consulted only as a fallback after the layers produce nothing (freeze.ts:171), andengine.timeoutMslikewise (: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.tsloads 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.modeldoes not exist.src/core/config/config-schema.ts:102-108:DefaultsSchema = { engine?, llmEngine?, improveStrategy? }.passthrough().schemas/akm-config.jsonproperties.defaultshas the same three keys withadditionalProperties: 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:
// 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 byakm agent(contribute-cli.ts:72-90→AgentDispatchRequest). - The workflow path deliberately does not carry them:
native-executor.ts:1026— "systemPrompt/tools/cwdcome from the profile/asset, not the workflow unit";unit-dispatch.ts:16documentssystemPromptas "used by frozen workflow gate judges" only;buildAgentDispatchRequest(:1029-1039) passes only prompt/model/schema.FrozenEngineSnapshothas 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. Makingagent: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 ofstring | Record<string,string>is a type change throughWorkflowUnitDraft→IrUnitNode→decodeWorkflowPlanV3validation →plan_json. Existing values stay authorable; the IR does not stay the same.step-work.ts:353-357,387— the input-hash preimage includesenvand the comment is explicit: "envcarries 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, andsecrets/<name>ref-valued entries must be hashed as refs, not values. Either wayhashVersion: 4(:379) has to become 5, which invalidates every journaledinput_hashand every replay golden.- Per-unit env is not supported.
native-executor.ts:449-457resolves env once per step, before any dispatch, fromtemplate.env, and passes oneRecord<string,string>to every unit (RunUnitInput.env,:611). §3.6'sAKM_ITEM/AKM_ITEM_INDEXfor map+shell steps requires per-unit env assembly, which also moves thesensitiveValuescomputation 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 noenv, andDEFAULTS_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-90ordersakmWorkflowAdapterbeforeakmTaskAdapter.akm-workflow-adapter.ts:60-76— "any.mdfile in this bundle IS a workflow … UNLESS its frontmatter declares a DIFFERENT non-emptytype:".
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.
- 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 → .mdplus a full frontmatter reshape plus body synthesis is a new migrator, not a new step. - Read-only bundles.
:53-66—readOnlyLegacyTasksexists 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.ymltask in every registry-installed bundle becomes permanently unrunnable and unconvertible. "No key survives with a changed meaning" is true; "lossless" is not. - Invisible files.
src/core/asset/asset-placement.ts:159-170makestask.yml-only forisRelevantFile/toCanonicalName/toAssetPath. Flipping it to.mdmakes stray.ymltask files invisible toresolveAssetPath— which is whatakm task runuses (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 intask sync/task doctor. That is a work item, not a property. (src/tasks/task-id.ts:8,15,41-46also hardcodes.yml/.yaml, including in the portable-length budget.) - Dropped fields. The §8 table's
llm.*row ends in an ellipsis. Threellmsub-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 intoFrozenLlmEngine,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 runis the canonical orchestrator; nostart/next/completesubcommands;brief/reportgated onexperimental.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 —
UnitDispatchRequestcarries prompt / frozen engine /timeoutMs/env/cwd/sensitiveValues, and results carryfailureReason(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 —
temperatureetc. are a config validation error on an agent-kind engine,modellegal on both (engines.ts:99,105-120). - §3.4 — command assets already read an
agent:frontmatter field (renderers.ts:234). - §3.5 —
resolveModelis a four-tier chain (profile aliases → global table with"*"fallback → built-insfable/opus/sonnet/haiku→ verbatim), one level, no recursion (model-aliases.ts:45-104). - §3.6 —
output:is declared on both the step and theunitbag inschemas/akm-workflow.json(:234,:351). Present — but not a duplicate (C1). - §3.7 —
unit.envis currentlyarray of string(akm-workflow.json:237-244). - §6 —
parseTaskDocument+TASK_KEYS+rejectTargetFields+resolvePromptSource= exactly 368 lines (src/tasks/parser.ts). - §6 —
akm-task.jsonis 87 lines and referenced only bytests/integration/tasks-schema.test.ts:8(no runtime loader). - §6 —
runCommandTaskruns raw argv only, no shell (runner.ts:277-289;parser.ts:300-306). - §6 — lint lives in
akm-task-adapter.tsasinvalid-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'sAKM_ITEMenv approach is consistent with that ruling.
Recommended changes before sign-off
- Withdraw §10's "no changes to IR, freeze, journal, replay." Replace with
an explicit list: IR v4,
hashVersion5, a shell node kind or third engine snapshot variant, a persona-layer freeze path, plan/journal migration for in-flight runs. - Fix C1 — keep two output spellings, or state the map-step loss.
- Fix C3 — move the prose append to compile time, into the hashed
instructions. - Remove
paramsfrom the cascade vocabulary (C4). - 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.modelas a new config field (it is silently ignored today). - Add a provenance rule for bundle-asset configuration layers (M4, M6) — the tool-policy ceiling and script execution both need one.
- Decide the placeholder grammar (M5) and the fate of
$ARGUMENTS. - Rewrite §8 — losslessness is false for read-only bundles and for the three
llmsub-fields; the migrator needs rename support; the "silently invisible" guarantee is a work item, not a structural property. - Independently fix, on the current branch: the dead
llmguard (freeze.ts:67-73), the frontmatter-in-prompt bug (runner.ts:582-593), and the{{name}}vs{{0}}placeholder mismatch (renderers.ts:178vsagent-dispatch.ts:68). All three are bugs today and the plan inherits them.