UNPKG

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

722 lines (628 loc) • 41.6 kB
# Task / Workflow Unification: One Target Vocabulary, One Prose Rule Status: PROPOSAL v9 — v8 (the post-Opus-review redesign) revised against a four-lens Sonnet panel, then re-verified claim-by-claim against the current baseline (98 claims checked, 22 corrected; a further 7 corrections from an audit of that pass). Review corpus: [`reviews/task-workflow-unification/`](./reviews/task-workflow-unification/). Baselined on `origin/main` @ `17a0dca`, which merged the 0.9 polish branch (`71bb686`). Breaking changes are approved for 0.9.0. Date: 2026-08-01 Related: [`workflow-format-unification.md`](./workflow-format-unification.md), [`okf-support.md`](./okf-support.md), [`docs/reference/workflows.md`](../../reference/workflows.md) > **2026-08-18 owner clarification:** > [Agent, Command, Engine, and Model Resolution](./agent-command-engine-model-design.md) > is authoritative wherever this proposal discusses agent personas, command > execution, command placeholders, engine/model cascading, tool policy, > capability handling, or model aliases. In particular, this proposal's > `with:`/named/positional command-template grammar, primary-bundle tool-policy > ceiling, static engine-capability declarations, and built-in wildcard alias > recommendations are superseded. The owner subsequently settled the 0.9.2 > source-format direction in the > [0.9.2 implementation plan](../../plans/0.9.2-agent-command-workflow-plan.md): > task v3 is GitHub-step-shaped, AKM Markdown and GitHub-shaped YAML are peer > workflow formats, and both compile through one internal versioned IR. The > concrete schemas and examples below are therefore non-normative. The > still-compatible concepts — tasks as scheduling wrappers, agents as > selectors rather than targets, task composition in workflow steps, live task > resolution, and frozen workflow resolution — remain design input. --- ## 1. Grounding — what tasks and workflows are in akm Owner's model; everything below derives from it. 1. **A task is a bundle asset that abstracts over OS-scheduled work** — discovered like any asset, *enabled* into a cron/launchd/schtasks entry that executes the defined work or dispatches an agent with the task's prompt. A platform-agnostic way to share repeatable processes behind one CLI. 2. **A workflow step is a task** — defined inline or referenced by ref — so the same repeatable process is composable without duplicating content or config. 3. **Fewer concepts, lower cognitive load.** akm is a simple, intuitive abstraction layer over the tools and processes autonomous agents use. Not in scope: tasks do not grow `steps:`; workflows do not grow `schedule:`. The unit of sharing is the asset, of scheduling the task, of procedure the workflow. ## 2. The format (stable since v5–v6, corrections applied) ### 2.1 Targets | Declaration | Work performed | |---|---| | *(none)* | Agent, prompted with **the nearest prose** — a task's body, or a step's `## <id>` section | | `uses: <asset-ref>` | Execute the referenced asset per its subdir-declared type (§2.2) | | `run: <shell text>` | Shell, no AI — GitHub-Actions semantics; `sh` on POSIX, `powershell` on Windows, `shell:` override | `uses:` xor `run:` — one schema rule; the double-target error class is structurally impossible. No ref-vs-inline sniffing exists. > **The prose rule.** The machine surface names the target; the nearest > prose is the instructions. With no target key, the prose *is* the work. ### 2.2 `uses:` — a closed set of four executable types | Ref | Execution | Inputs | |---|---|---| | `uses: commands/<n>` | Fill the template (§2.4), dispatch an agent | `with:` | | `uses: scripts/<n>` | Execute per the script's `run`/`setup`/`cwd` — gated by the execution-activation policy (§5.6) | `env:` | | `uses: tasks/<n>` *(steps only)* | Compose the task as this step's work (§2.3) | the task's own, overridable | | `uses: workflows/<r>` *(tasks only)* | `runWorkflowSteps()` | `with:` → declared param flags | Any other subdir (including `agents/` — a persona, not work) is an error with a targeted hint. **`with:` replaces v5–v7's call-site `params:`** — review finding M1 (3/3): `params:` already means *declarations* (name → JSON Schema) in workflow frontmatter, and one key carrying declarations, run-arguments, placeholder fills, *and* a cascading value was the same overload this spec deletes `prompt:` for. GHA's `inputs:`/`with:` split exists precisely here, and §2.4/§4 give `with:` exactly one meaning: **inputs handed to the referenced asset, never cascaded.** Two `uses:` modes differ in kind, named for authors: `tasks/<n>` **composes** — the referenced definition becomes this step's own work — while `workflows/<r>` **invokes** — a child run executes and returns. `with:` merge on a composition chain (a step referencing a task that itself targets a command or workflow) is per-key shallow merge, referencing step wins; `with.arguments` is one key, replaced whole. The merged mapping feeds the final target: placeholder fill for a command, declared param flags for a workflow. ### 2.3 Steps are tasks — composition by reference ```yaml steps: - id: lint uses: tasks/lint-check # the task's target, fields, env, and body timeout: 2m # call-site override (one cascade layer nearer) - id: fix run: bun run fix # inline task: same keys, defined here ``` - Step keys override the referenced task's keys — the task and the step are adjacent cascade layers (§4); `env:` concatenates task-then-step. - Trigger keys (`schedule`, `enabled`) are consumed **only** by `akm task sync` and, at fire time, the task runner's enabled gate (`src/tasks/runner.ts:169` — v7's "sync-only" wording corrected). Anywhere else they are no-ops with a lint notice; a referenced task cannot double-fire. - **Call-site prose is appended at freeze, not at dispatch.** Review finding (3/3): there is no "one assembled prompt seam" — prompts are built per-unit at dispatch, and the unit hash covers the *frozen template*, not the assembled string. So: when the resolved work is agent-dispatched, the step's section text is concatenated onto the frozen instruction template (blank-line separator, byte-exact) **at freeze**, which puts it inside the hash preimage automatically and keeps the append ahead of the item/inputs/gate blocks. For shell work the section is documentation, ignored at runtime. - Refs resolve at freeze (§5.4), so in-flight runs are immune to edits. - No nesting: a referenced task targeting `workflows/*` is a compile error on a step; a task referencing a task is an error. ### 2.4 Command templates — filled against the real placeholder grammar Review finding M2 (3/3): command placeholders are `$ARGUMENTS` / `$1`–`$9` (positional) and `{{name}}` per the advertised grammar (`extractParameters`, `src/output/renderers.ts:164-186`); the actual filler is `fillPlaceholders()` in `src/commands/agent/agent-dispatch.ts:67-72`, which substitutes only positional `{{0}}` forms, leniently — a live bug fixed by this work, and a second call site the new `with:` contract replaces. Three further grammars for the same placeholders exist today and diverge from each other: the indexer's recognition sniff `COMMAND_PLACEHOLDER_RE` (`src/indexer/walk/matchers.ts:126`, consumed at `:239`) matching only `$ARGUMENTS`/`$1`–`$3`; `extractCommandParameters` (`src/indexer/passes/metadata.ts:440-461`), a near-clone of the display grammar that differs from it in case-sensitivity and digit bounding; and a lenient sniff at `src/core/adapter/adapters/tool-dir-shared.ts:201-204`. **Convergence is scoped to the fill and display grammars — the two recognition sniffs keep their `$`-only forms.** Adding `{{name}}` to `COMMAND_PLACEHOLDER_RE` would be a live mis-classification hazard, not a cleanup: it returns specificity 18, outranking the directory rules (10/15), so any `knowledge/`, `facts/`, or `instructions/` `.md` containing a mustache token would be re-typed `command`. The recognition gap is narrower than it looks anyway — `classifyByDirectory` / `classifyByParentDirHint` (`matchers.ts:43-47`, `:147-156`, `:178-197`) already type any `.md` under `commands/` as a command at specificity 10/15, beating `classifyBySmartMd`'s `knowledge` (5). The sniff is only the fallback for command-shaped `.md` living *outside* `commands/` without `tools:`/`agent:` frontmatter. The contract, defined against that reality: - `with:` mapping keys fill `{{name}}` placeholders. - The reserved key `with.arguments` (string) fills `$ARGUMENTS`, and its whitespace-split words fill `$1`–`$9`. - An unmatched placeholder or unused `with:` key is a **lint warning and is left verbatim** — not a runtime hard error, so imported `$ARGUMENTS` commands remain usable as targets. - `with.arguments` is reserved: a literal `{{arguments}}` placeholder is filled by it too, with a lint warning about the shadowing. Words past `$9` remain reachable only through `$ARGUMENTS`. Filling happens at freeze (workflows) / dispatch (tasks), producing the prompt string. Bodies remain verbatim — templating stays scoped to the command asset type, whose definition is "a template with placeholders." ## 3. The task asset `<bundle>/tasks/<id>.md`: the shared envelope (`$ref akm-asset-envelope.json`, OKF v0.2 families included), trigger keys, at most one target, and any §4 fields. No `version:` key. **Recognition requires frontmatter `type: task`, honored in two subsystems** (R2 finding: v8 cited only one). At the bundle-adapter layer, the workflow adapter is ordered ahead of the task and generic akm adapters in the source-root probe list (6th of 11 — `src/core/adapter/adapters/index.ts:73-90`; that array is `looksLikeRoot` probe order, and only the adapter that claims a root ever calls `recognize()` on its files), and inside an akm-workflow bundle it claims every `.md` file whose frontmatter does not declare a contrary `type:` (`akm-workflow-adapter.ts:67-70`, `:160`). At the per-file indexer classifier — the path that actually types `tasks/*.md` during normal indexing — `src/indexer/walk/matchers.ts` gains a `type: task` branch **ordered before** its existing `"agent" in fm → command` rule (`matchers.ts:234-236`), which would otherwise misclassify any task carrying an `agent:` selector — including this section's own example. Residence is a lint *expectation*, not a recognition signal. ````markdown --- type: task description: Weekly review digest. schedule: "0 8 * * 1" agent: agents/reviewer timeout: 10m --- Review the week's completed tasks and summarize action items. ```` ````markdown --- type: task description: Full nightly quality sweep. schedule: "15 2 * * *" run: akm improve --strategy thorough --skip-if-locked --- # Nightly improve sweep Runbook prose: indexed, shown, never executed. ```` ````markdown --- type: task schedule: "@daily" uses: commands/weekly-review with: { scope: team } env: [env/prod] --- ```` Lifecycle unchanged: discover via search/show; enable via `enabled:` + `task sync`; the OS entry still invokes `akm task run <id>`. Lifecycle interactions pinned (review minors): `status: draft` → `sync` never installs, regardless of `enabled:`; `status: deprecated` → installs with a warning; a previously-installed task that becomes `draft` (or `enabled: false`) is uninstalled by the next `sync` — reconciliation is to desired state, both directions. Two adapter-policy changes with named mechanisms (panel M13 follow-up): the akm adapter's current `type: "task"` special-case — parse as pure YAML, `frontmatter: null`, so markdown base checks never fire (`akm-adapter.ts:404-412`) — is **removed with the format**, and markdown tasks get the full markdown base-check suite; and tasks are declared **improve-ineligible**. No such site exists on `BundleAdapter` (`src/core/adapter/bundle-adapter.ts:69-121` declares only adapter-level optional methods), so v8's "where the adapter already declares per-type capabilities" was wrong. Three candidate homes, cheapest first: the existing per-type refusal set `DISTILL_REFUSED_INPUT_TYPES` (`src/commands/improve/distill.ts:134-142`, already gated at `eligibility.ts:380` and documented as driving the planner "for free") gains `task`; or the per-type table `TYPE_PRESENTATION` (`src/core/type-presentation.ts:74`) gains an eligibility flag; or `eligibility.ts` gains an explicit type filter. A task body is an executable prompt, not curatable knowledge. Lint runs; improve never rewrites it. ## 4. Configuration: two selectors, one value vocabulary, one cascade — built, not "exposed" v7 claimed freeze "already implements" the cascade. Review finding C4 (3/3): freeze's layer list is exactly `[documentDefaults, unit]` (`freeze.ts:50`); `DefaultsSchema` has **no** `model` field and is `.passthrough()`, so v7's own worked example is a silent no-op today (`config-schema.ts:102-108`); `defaults.llm` is hard-rejected as retired; no persona layer exists. What exists is a two-layer resolver whose *shape* proves the pattern fits. This section specifies **building** the rest. **Selectors** (nearest wins; each picks a node whose fields join the cascade): | Selector | Node | Source | |---|---|---| | `engine:` | execution node | config `engines.<name>` | | `agent:` | persona node | bundle `agents/<name>` | **Value fields** — legal at every layer, merged per-field, nearest wins; `env:` concatenates: `model` · `temperature` · `max_tokens` · `extra_params` · `timeout` · `env` · `cwd` · `shell` · `on_error` · `retry`. (`with:` is *not* a value field — it binds to one referenced asset. `params` no longer exists as a field name outside workflow declarations. `on_error`/ `retry` join the vocabulary because `defaults.on_error` already exists at document level today — review M12 — making "graph keys are steps-only" false for them; they remain meaningless on a task and lint says so. Value fields other than `timeout`/`env` on a `uses: workflows/*` task draw the same inapplicable-field notice — they configure a unit, and a workflow invocation has none.) **Merge classes, stated once** (panel finding: a generic "nearest wins" would silently downgrade the existing deep-merge): **scalars** (`model`, `temperature`, `max_tokens`, `timeout`, `cwd`, `shell`, `on_error`) — nearest wins whole; **mappings** (`extra_params`) — deep-merged along the chain, preserving today's `deepMergeConfig` behavior (`freeze.ts:211-216`); **lists** (`env`) — concatenated, later wins; **`retry`** — replaced whole (half-merging `max`/`on` would be incoherent). `with:` merges per-key at its target (§2.2), outside the cascade. **Layers, far → near:** ``` config defaults: → engines.<selected> → agents/<selected> → document defaults: → uses: tasks/<ref> → step/task keys ``` (`document defaults:` is the workflow frontmatter's `defaults:` block and exists only there — a standalone task IS its own call site, so for tasks that layer is simply absent, not an implied new task key.) **What must be built (the honest inventory):** 1. `DefaultsSchema` gains the value fields, and stops silently swallowing unknown keys (strict with migration-safe notices). 2. A **persona snapshot**: the agent asset resolved through the provenance ceiling — 07 P1-D, today inline in the show command (`src/commands/read/show.ts:433-445`): self-declared `tools:` honored ONLY for the operator's **primary bundle**, keyed off primary-bundle identity, explicitly *not* the writable bit, failing closed. That check is **extracted into a shared persona resolver** consumed by both `show` and freeze — a named refactor in §9, since its current home is a CLI formatter. The snapshot `{systemPrompt, toolPolicy, model, …valueFields}` freezes into the plan; `toolPolicy` is consumed by agent-kind engines and draws the standard unconsumed-field notice on LLM engines. (This also fixes the live bug where `prompt: agents/x` ships raw file bytes *including frontmatter* to the model — `runner.ts:592-593`.) 3. One cascade module — a new shared module (e.g. `src/core/cascade.ts`; there is no `src/exec/` tree today, only `src/workflows/exec/` and `src/tasks/`) — the only implementation of layer merge, consumed at freeze (workflows) and dispatch (tasks). It consolidates the three engine/model resolution sites that exist today (`freeze.ts:163-209`, `tasks/runner.ts:479-514`, engine-resolution) including their subtly divergent opencode-sdk `llmEngine` fallback rules — one implementation, one fallback rule. 4. **Capability notices replace both kind-gates.** The task path's hard error and freeze's guard — which review M6 showed is *dead code* (`freeze.ts:67-73` computes `llm` only for llm engines, then guards `kind !== "llm"`), meaning workflows already silently drop these fields — are both replaced by one behavior: engines declare the fields they consume; an unconsumed field is a lint/freeze **notice**. Strictness stays only where the kind is known at authoring time: the engine node's own schema (which today is `.passthrough()` + blacklist; it becomes genuinely strict as part of this work — review minor). 5. Aliases: resolution runs once, after the cascade settles `model`, through the existing `resolveModel()` chain. Review M4 (3/3): the builtin table has only `claude`/`opencode` columns, so portability to LLM endpoints is currently *false* — the builtin entries gain a `"*"` column, and an alias that resolves nowhere for the selected platform is a freeze/lint notice, not silent pass-through. 6. The gate judge (`workflow.judgeEngine`) stays outside the cascade in 0.9.0 — frozen separately, as shipped. Noted, not changed. One sentence to teach: *set a field where it should usually apply; override it closer when a case differs; the nearest value wins.* ## 5. Execution architecture ### 5.1 One target model, two executors A shared target module resolves every declaration to a typed value: ``` Target = { kind: agent-prose } # nearest prose is the prompt | { kind: command, ref, with } # template fill → agent prompt | { kind: script, ref } # script asset, exec | { kind: shell, text, shell } # inline run: | { kind: workflow, ref, with } # tasks only | { kind: task, ref } # steps only; resolves recursively once ``` Execution is a strategy seam with exactly two implementations: - **AgentUnitExecutor** — today's paths (task runner's `executeRunner()`; the orchestrator's journaled `UnitDispatcher`), consuming an assembled prompt + frozen engine + persona snapshot. - **ShellUnitExecutor** — `Bun.spawn()` for `run:` text and script assets, used by both the task runner and the orchestrator. Staging is symmetric and named (panel: the task path's staging was unstated): **tasks** run *resolve → execute* — the same resolve stage (cascade, persona, targets, templates) with no plan persistence; **workflows** run *compile → resolve → freeze → execute* (§5.4). Both executors sit behind one interface; each surface wraps results into its own recording sink (`task_history`/`logs.db` vs the run journal) — the adapter lives at the sink, not inside the executor, which is how the `RunnerSpec`-vs-`UnitDispatchRequest` split stays out of the executors' type signatures: the resolve stage produces one dispatch plan both consume. **ShellUnitExecutor is extracted from the task runner's existing hardened command path** (`runManagedSubprocess`, `src/core/subprocess.ts`, called at `tasks/runner.ts:277-289`: process-group spawn, SIGTERM→SIGKILL kill ladder, injectable spawn/timer seams, `AKM_EVENT_SOURCE` stamping) and adopted by the orchestrator — not a new spawn wrapper. (`src/tasks/backends/exec-utils.ts` is the scheduler-backend exec seam, not part of this path.) Composition is monotemporal by rule: a referenced task's fields join the cascade from the copy resolved at freeze, under the workflow's config snapshot — a composed task never re-reads config at dispatch. Run recording stays split (cron job vs journaled plan) — that boundary is real. Everything upstream unifies: target resolution, template filling, cascade, alias resolution, env assembly. ### 5.2 Shell units in the IR — v3 → v4, not a non-goal Review C2 (3/3): `IrInvocation.engine` and `UnitDispatchRequest.engine` are required, the engine snapshot union is closed, freeze hard-errors with no engine, and the IR rejects empty instructions — v7's "no IR changes" was false. Owned properly: - `IrInvocation` becomes a discriminated union: `{ kind: "agent", engine, … }` | `{ kind: "shell", script, shell, cwd, timeoutMs }` (`timeout` is a value field for `run:`, so the shell member carries it). Shell members exclude engine/llm fields **structurally** — by union-member shape, not by decoder-validated absence. - Engine resolution happens **per unit kind**: shell units need no engine, so a shell-only workflow freezes on an engine-less machine (the current "no engine selected" error narrows to plans containing agent units). - Instructions become optional for shell units (parser + IR relaxation — review M10; agent units keep the non-empty invariant). - Shell units journal as normal `workflow_run_units` rows (status, timing, attempts, `failure_reason`; no tokens). Under the external driver protocol, shell units are **orchestrator-owned**: `brief` lists them as non-claimable and the engine executes them natively — a harness never claims shell work. - Crash/restart: shell units use the same claim/lease rows as agent units; a running shell unit whose lease expires re-dispatches on resume under its retry policy. Re-execution idempotency is the author's contract — the same contract every cron job already carries — stated in the docs, not solvable by the engine. - Plan IR version bumps to 4; decoders reject v4 plans in older binaries (existing plan-version machinery). ### 5.3 Unit identity — hashVersion 5, preimage stated Review C3 (3/3): hashVersion 4's preimage (`step-work.ts:333-392`) covers the frozen template bytes but none of the *other* new inputs — shell text, the `uses:` target kind/ref/content hash, and the persona snapshot. Without a bump, editing `run:` text, re-pointing a `uses:` ref, or editing `agents/<n>` would silently reuse completed journal rows. (Freeze-time prose appending, §2.3, already rides in via `template`.) hashVersion 5's preimage, explicitly: | Field | Notes | |---|---| | template instructions | **post** freeze-time prose append (§2.3) and **post** template fill, so `with:` values are covered via the filled bytes | | target kind + `uses:` ref + resolved content hash | a re-pointed or edited referenced asset re-dispatches; also the retained provenance for composed steps (the "single-file provenance" minor). Script bytes are **embedded at resolve**, like a command's filled text — execution never re-reads live files, keeping composition monotemporal (§5.1) | | persona snapshot hash | system prompt, tool policy, and value fields of the frozen persona — editing `agents/<n>` re-dispatches (panel critical: omitted in v8) | | shell text, `shell`, `cwd` | shell units | | item / inputs / params / dispatch / invocation / schema | as v4 | | env **names** + env **literal values** | see §5.5 — literals are plan content; ref-sourced values stay names-only | | isolation, gateFeedback | as v4 | `retry`/`on_error` stay excluded (policy, not input) — unchanged rationale. ### 5.4 Freeze becomes two stages; lint gets a dry freeze Review M11: `uses:` resolution needs asset IO inside a function documented pure, and freeze currently has exactly one caller (run start), so every composition error would escape lint. Restructure: **compile** (pure) → **resolve** (asset loader injected: personas, referenced tasks, command templates) → **freeze** (snapshot + hash). `akm lint --type workflows` runs compile+resolve with the real loader and no persistence, so dangling `uses:` refs, back-door workflow nesting, placeholder mismatches, and capability notices all surface at lint time. Composed instructions carry the existing 256 KiB cap with a lint warning at 80%. ### 5.5 `env:` — one shape, two provenance classes Format unchanged from v6 (list of env-asset refs and literal mappings, later wins, bare-mapping shorthand). Semantics corrected by review M3 (3/3), which showed "strictly additive" was false at the IR/hash/redaction layers. The clean line is **provenance, not shape**: - **Literal entries** are plan content: author-written bytes already durable in the source file — frozen into the plan, hashed (§5.3), shown by `brief`, **never redacted** (`LOG_LEVEL: debug` must not scrub "debug" from every log — the over-redaction finding). - **Ref-sourced entries** (`env/<n>` groups, `secrets/<n>` values) are runtime-resolved: hashed by **name only**, resolved at dispatch, values joined to `sensitiveValues` for redaction, never durable. (Existing contract, now stated as the rule.) - IR `env` becomes a typed entry list (ref | literal-pair) — an IR v4 change, not "additive." - Env assembly moves per-unit where fan-out requires it: a `map:` step with a shell target receives `AKM_ITEM` (JSON) and `AKM_ITEM_INDEX` per unit — set as env-object entries on the spawned process (the `runManagedSubprocess` `env:` pattern — `src/core/subprocess.ts`, as used at `tasks/runner.ts:284`), never spliced into shell text. - Literal entries run the existing secret-shape heuristic (`param-secrets.ts`'s `detectSecretShapedParams`, built for exactly this risk on params): a secret-shaped literal is a lint error pointing at `secrets/<n>`. - The split is enforced at one place: a single env-assembly resolver (§8 plumbing) is the only producer of env entries and of their `sensitiveValues` contributions, so the literal/ref line cannot silently diverge per call site. ### 5.6 Executing scripts inherits the tools: ceiling — no new trust machinery v8 proposed a per-bundle grant key. The panel killed it twice over: `activation-policy.ts` documents a standing decision **against new persisted trust machinery**, and the claimed precedent — the `tools:` provenance ceiling — has **no override at all**. v9 inherits the ceiling exactly instead of imitating it loosely: - Shell-class work (a script asset, or the `run:` text of a task) executes **only when its defining asset lives in the operator's primary bundle** — the same line 07 P1-D draws for self-declared tool grants (`show.ts:433-445`): keyed off primary-bundle identity, explicitly *not* the writable bit, failing closed. - Evaluated in the **resolve stage** (§5.1) — the shared stage both surfaces run before any execution — so there is no sync-time/fire-time gap and no standing grant for a bundle update to swap content under. - Composition chains inherit the rule from the **defining asset's own source**: a primary-bundle task that `uses:` a third-party script — or composes a third-party task whose target is shell — refuses with an error naming the ref and its source. No chain launders provenance. - The remedy is an existing verb, not a config bit: `akm clone <ref>` copies the asset into the primary bundle, where the operator owns and reviews the bytes; the refusal message says exactly that. - Scope: this ceiling governs the **new non-AI exec surface only**. Agent-dispatched third-party tasks remain governed by the existing scheduling gates (setup/sync review, `enabled:`), unchanged. Zero new config, zero persisted trust state. Loosening later (per-bundle grants) is an additive future decision that must revisit the activation-policy document — deliberately out of 0.9.0 scope. ### 5.7 Shell ergonomics - Bare `akm` in shell/script work resolves to the current installation by a **synthesized shim**, not a bare PATH prepend: `resolveAkmInvocation()` can return a multi-element launcher invocation (`bun <script>` — the case the argv-splice was built for, panel M9 follow-up), so the runner writes a platform-appropriate shim (sh script / `.cmd`) invoking the exact resolved invocation into the run's temp dir and prepends that dir to the child `PATH` (case-insensitive `Path` merge on Windows). Replaces the argv rewrite for shell work and covers scripts for free; the task runner keeps the argv rewrite only for legacy `.yml` until retirement. - `cwd:` under `isolation: worktree` is resolved relative to the worktree root; `cwd:` on the LLM runner is a capability notice (review minors). ## 6. GitHub Actions alignment — familiar, not copied Adopted: `uses:`/`run:` pair with mutual exclusion; **`with:` for inputs to the referenced thing** (returned to the adopted column by M1); step-level keys with no nesting bag; shell semantics with per-OS defaults and `shell:`; `env:` bare-mapping shorthand; `cwd:`; `on_error`/`timeout` as step keys. Kept akm: typed asset refs (the subdir says what executes — GHA's `uses:` is opaque); `params:` reserved for declarations; no step `name:`; bundles as the sharing unit. ## 7. Steps flatten to the task shape — with the schema collision fixed The `unit:` bag and `map.unit` are still deleted; targets and value fields sit on the step; `map:` keeps `over`/`concurrency`/`reducer`. But v7's "duplicate `output` wart" was **wrong** — review C1 (3/3): `unit.output` compiles to the per-dispatch structured-result `schema` (driving structured retry / `parse_error`) while `step.output` compiles to `outputSchema`, the promoted artifact the gate validates (`compile.ts:204,229` — verified). Two concepts, two homes: - **`output:`** on a step — the step's **artifact** schema, all step kinds. On a unit step it also serves as the dispatch result schema (result *is* the artifact). - **`map.output:`** — the **per-item** result schema of a map step's units; the step's `output:` continues to describe the reduced artifact. ```yaml - id: review map: { over: steps.intake.output.files, concurrency: 3, output: { type: object } } output: { type: array } # what `steps.review.output` resolves to ``` ## 8. Migration — inside the hardened 0.9.0 cutover Rebuilt against the `71bb686` migrator and review M7 (3/3): - **The converter joins the content-migration step with a vendored, frozen copy of the v2 task parser** — the current `task-target-ref-migration.ts` imports the *live* parser from `src/`, which violates the frozen-migrator principle and breaks the moment the live parser is replaced; that gets fixed as part of this work. - Mapping: `command: <shell string>` → `run:` verbatim; `command: [argv…]` → `run:` with each element shell-escaped **for POSIX `sh`, the migrated task recording `shell: sh` explicitly** so the Windows `powershell` default never reinterprets the quoting; `prompt: |` → body; `prompt: commands/x` → `uses:` + `with:`; `prompt: agents/x` → `agent:`; `prompt: ./file.md` → inlined; `workflow:`+`params` → `uses: workflows/…` + `with:`; `timeoutMs` → `timeout`; `llm.maxTokens`/`llm.temperature`/`llm.extraParams` → flat fields; **`name:` → the body's H1** when the converted body has none, else a per-task migration notice (never silently dropped); `llm.supportsJsonSchema`/`contextLength`/`enableThinking` are endpoint-capability facts that belong on the **engine node** — the migrator emits a per-task notice naming the value and the config key (lossy-with-notice, never silent). - Scheduler entries untouched: ids and the `akm task run <id>` ABI do not change. - **The `.yml` tombstone rule** replaces v7's false "structural" claim (refusals key on config/DB shape, not stray files, and placement filters by extension — a stray `.yml` today would be invisible or wrongly installed): task discovery keeps matching `tasks/*.yml` **permanently, as a diagnostic**. In **writable** bundles, `sync`, `doctor`, and `lint` hard-error naming the file and the fix; in **read-only/third-party** bundles the same diagnostic is a warning — the operator cannot edit those files, matching the migrator's existing read-only carve-out (`task-target-ref-migration.ts:253-271`). Either way a `.yml` is never installed and never silently skipped. `<id>.yml` + `<id>.md` colliding on one conceptId is the same named error. **Conversion is interruption-safe by ordering**: the migrator journals the `.md` write and verifies it before removing the `.yml`, and the collision diagnostic special-cases the byte-equivalent mid-migration pair (resume completes the removal instead of erroring on the migrator's own half-applied state — the panel's crash-window finding; the current *backup* model journals whole-DB/config artifacts only (`migration-backup.ts:54`), so per-file crash safety lives in the content-migration step's own resume ledger — `content-migration-report.json`'s `reservedRenames` (`config-migrate.ts:518-575`); the `.md`-write-then-`.yml`-delete ordering extends that existing ledger rather than introducing per-file journaling, costed in §9). - Embedded templates convert to `.md`; `listEmbeddedTasks` and `akm setup`'s task review move to markdown-aware editing (the current YAML round-trip would destroy bodies — review minor). - The id-grammar work (subdir ids, scheduler-id `/` → `--` mapping with collision lint) lands **in the same migration step, before scheduler reconciliation** — sequencing the panel flagged as unstated. - The pre-flatten workflow schema (`unit:`, `map.unit`, `defaults.llm`) converts in the same step: bag fields lift, `llm` flattens, `unit.output` → `map.output` or step `output:` per §7. ## 9. Cost inventory — what this actually changes Honest replacement for v7's §10 (review: "non-goals false ×4"). | Surface | Change | |---|---| | IR | v3 → v4: invocation union, optional instructions (shell), typed env entries, `map.output` | | Unit hash | hashVersion 4 → 5 (§5.3 preimage) | | Freeze | two-stage (compile/resolve/freeze), per-kind engine resolution, persona snapshot, prose append, capability notices | | Dispatch | executor strategy (agent/shell); persona fields reach `UnitDispatchRequest` | | Schemas | shared target+value defs; task schema rewritten; workflow schema flattened; `DefaultsSchema` extended and made strict; engine schemas made strict | | Parsers | `parseTaskDocument` retired to the migrator (vendored); unified parser gains task recognition; section-required rule relaxed per target kind; the indexer classifier (`matchers.ts`) gains a `type: task` branch ordered ahead of the `"agent" in fm` rule | | Adapters | markdown task recognition (`type: task`); the `type: "task"` pure-YAML special-case removed (markdown base checks apply); improve-ineligibility declared per-type (§3 — `DISTILL_REFUSED_INPUT_TYPES` is the cheapest of three homes); akm-task format-family goldens rewritten directly (they carry `specificationGolden: true` / `adapterStatus: NOT IMPLEMENTED`, and `DESIGNATIONS.json` does not cover that tree at all). **DESIGNATIONS re-pointing is mandatory pre-work**: six entries are touched — `cli/c-tasks-family.json` (already `re-baseline`, chunk 9) plus five goldens pinning `tasks/*.yml` paths — `recognition/all-types.json`, `placement/all-types.json`, `minting/oracle.json` (chunk 4), `lint/all-types.json` (chunk 3), `renderer/all-types.json` (chunk 8) — whose `reBaselineChunk` must be re-pointed at this change *before* it lands, per the DESIGNATIONS `$policy` surface-owner rule | | Named refactors | 07 P1-D ceiling extracted from `show.ts` into a shared persona resolver; ShellUnitExecutor extracted from the task runner's hardened command path; the three engine/model resolution sites consolidated into the cascade module; the `akm` shim mechanism; per-file create-verify-delete ordering added to the content-migration step's existing `reservedRenames` resume ledger | | CLI | `task create`; `task add` retires with `.yml`; capability notices in lint/doctor | | Storage | `task_history.target_kind` values widened (additive) | | Docs/tests | reference docs, ~10 templates, format-family goldens, parser/freeze/executor suites | Inherited bugs fixed en route: `{{0}}`-only placeholder filling; raw frontmatter shipped to models via `prompt: agents/x`; subdir task ids indexed but unrunnable (`validateTaskId` rejects `/` — the id grammar gains subdir support with the scheduler-id mapping documented); freeze's dead llm guard. ## 10. Non-goals (now true) - No new orchestration semantics: `map`/`route`/`gate` behavior, leases, check-ins, replay *logic*, and the driver protocol are unchanged (shell units are orchestrator-owned under it). - No scheduler-backend, `sync`-reconciliation, or `akm task run <id>` ABI changes. - No change to how command/script/agent/env/secret assets are authored. - No multi-file config merging; the cascade layers are config nodes, bundle assets, and document/call-site keys. - Gate judge selection stays outside the cascade in 0.9.0. ## 11. Findings cross-reference and decision log | Finding (consolidated) | Resolution | |---|---| | C1 output collision (3/3) | §7 — two schemas, two homes (`output:` / `map.output:`) | | C2 shell units unrepresentable (3/3) | §5.2 — IR v4 invocation union, per-kind engine resolution | | C3 hash preimage (3/3) | §5.3 — hashVersion 5, preimage table; §2.3 freeze-time append | | C4 cascade layers don't exist (3/3) | §4 — "built, not exposed"; build inventory | | C5 script execution security (2/3) | §5.6 — the tools: ceiling inherited exactly (primary-bundle only, no override); v8's grant key withdrawn after the panel showed it contradicted activation-policy's no-new-trust-machinery decision | | M1 params overload (3/3) | §2.2 — `with:` for inputs; `params:` declarations only; neither cascades | | M2 placeholder grammar (3/3) | §2.4 — `with:`/`with.arguments` against the real grammar; lenient + lint | | M3 env not additive (3/3) | §5.5 — literal/ref provenance split; IR v4; per-unit env | | M4 alias portability (3/3) | §4.5 — `"*"` columns + unresolved-alias notice | | M5 agent plumbing + ceiling; false corroboration | §4.2 — persona snapshot via show layer; corroboration claim retracted | | M6 dead kind-gate (2/3) | §4.4 — both gates → capability notices | | M7 migration overclaims (3/3) | §8 — vendored parser, full mapping, tombstone rule | | M8 adapter recognition (2/3) | §3 — `type: task` required | | M9 akm-bin rewrite (2/3) | §5.7 — synthesized shim (a bare PATH prepend cannot cover multi-element launcher invocations) | | M10 prose invariants (2/3) | §5.2 — per-kind relaxation, costed | | M11 lint never freezes (1/3) | §5.4 — dry freeze in lint | | M12 `defaults.on_error` (2/3) | §4 — `on_error`/`retry` join the value vocabulary | | M13 improve surface (1/3) | §3 — improve excluded from task bodies | | Minors | §3 (draft/deprecated), §5.7 (cwd), §8 (embedded/setup, collisions), §9 (bugs fixed, target_kind, goldens) | Decisions 1–14 (rounds 1–6) stand as logged in v7 except where this revision supersedes them with review evidence: call-site `params:` → `with:` (M1); "expose the cascade" → "build the cascade" (C4); "duplicate output" → two-schema design (C1); §10 non-goals → §9 cost inventory (C2/C3). v7's remaining question (cascade sign-off) is superseded by this section's cross-reference — the cascade shape survived review; its cost didn't, and is now stated. **Round 1 Sonnet panel (v8 → v9).** Four independent reviewers (coverage audit, architecture, fresh adversarial, security+migration; full reports `sonnet-r1-*.md` in the session scratchpad). Panel criticals, all applied: the §5.6 grant key contradicted activation-policy's documented no-new-trust-machinery decision → replaced by exact ceiling inheritance; the v8 hash preimage omitted the persona snapshot → added; `with:` merge on composition chains was undefined → §2.2; the tombstone hard-error was unactionable on read-only bundles → warning carve-out. Panel majors, all applied: task-path staging named (resolve → execute); executor sinks as the adapter boundary; ShellUnitExecutor extracted from the task runner's hardened path, not rebuilt; monotemporal composition rule; persona resolver extraction scoped; merge classes stated (deep-merge preserved for `extra_params`); shell invocation carries `timeoutMs`; shell-unit crash/lease semantics; toolPolicy under capability notices; improve exclusion given a named mechanism; migration crash-window ordering; POSIX-pinned migrated quoting; launcher-aware shim. One panel claim was verified false and rejected: "the tools: ceiling doesn't exist" — it does (`show.ts:433-445`), inline in the show command, which is exactly why §4 scopes its extraction. Open items: none blocking. Two implementation-time choices are delegated to the implementer with defaults: the exact capability-declaration shape per engine kind (default: a static field list per kind), and the scheduler-id mapping for subdir task ids (default: `/` → `--` with collision lint).