akm docs

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/. 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, okf-support.md, docs/reference/workflows.md

2026-08-18 owner clarification: Agent, Command, Engine, and Model Resolution 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: 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

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

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:

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.

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

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:

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:

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:

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

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:

- 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):

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)

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