akm docs

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.

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:

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:

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:

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:

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.

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".

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:


  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.