onepipeline 0.33.0

Execute a task DAG over oneagentgraph and onevcs, merging their event streams into one.
Documentation
# onepipeline contract

Composes oneagentgraph + onevcs, owns the task DAG, merges the three event streams.

A plan is **one onetaskgraph project**, and a node is one task in it. Plan schema v3 = ai-orchestrator tracked-graph schema v7 node shapes (`agent` direct, lifecycle with `repo`, `kind: human`, nested `steps` on one branch, `expects_no_diff`, `context`, cross-DAG `run:<id>#<node>` refs, What/Why/Acceptance-criteria task prose, per-node `max_turns`), with: `repo` resolved through onevcs; new optional per-node `executor: NAME`; new optional `agent_graph: REF` overriding the default node-scope graph config; a **required** `title` on every lifecycle node and an optional `body` beside it, which are the subject and the prose its change request opens with, and an optional `draft: bool` beside those, default `false`, which leaves that change request as a draft at closeout for a person to lift; and **no `done_when`** — v7's judge-only bar is retired here, because onejudge hands that field to the judge verbatim and the criterion belongs in the onejudge document the node-scope graph's worker already points at, written once. A per-node bar is the task's own `## Acceptance criteria`, which the judge reads as the first message of the transcript it is given. A plan still carrying `done_when` is refused **by name**, and the refusal says where the bar goes instead.

`schema_version: 3` is the change that made the change request the plan's to state, and it is **additive**: this build reads **3, 2, and 1**, and what each version added is keyed to the version the *document* declares. A lifecycle node needs a `title` at 3; a plan written before it states none, and its publication takes no subject of its own — which is onevcs deriving one from the branch's own conventional commits, a better subject than anything this layer could compose about work it did not do. A plan below 3 naming `body` is refused **by that field's name**, exactly as a field no schema ever had is: the field is what its author has to act on, not the number the document was written at. `done_when` is answered the same way at every version — it is not a field of this schema, and the refusal says where the review bar goes instead. `verify_via_ci` is retired on the same terms and refused by that field's own name at every version: it was accepted here and read by nothing, and what it asked for is now observed rather than declared — a `change-auto` publication watches the host's own required checks to their conclusion, so the refusal names that policy rather than leaving its author to guess what asks for CI verification now. The **only** version refusal is a number this build has never written, and it names the versions that are read. Every optional field is omitted from what this crate writes when it is empty, so a plan round-trips as the file wrote it.

Where a plan lives: a run is launched by naming a **qualified onetaskgraph project id** — `otg:plan-store` — which reaches `onepipeline start <qualified-project-id>`. A plan file is not a launch input at any schema version. The id is qualified by **source**, so a `local-md` project id is launchable directly, with no copy into a remote system first. Nothing special-cases a remote source. That is the flow: author locally, read it back, run it, and copy it up only when it should become durable.

**A plan is one onetaskgraph project; a node is one task in that project.**

* The plan-level fields — `schema_version`, `goal`, `name`, `concurrency` — are reserved metadata keys `onepipeline.<field>` on the **project**, carrying the same names and the same JSON types the schema-3 plan document uses. The project's own `title` is the plan's name when `onepipeline.name` is absent.
* A node's **id** is the reserved metadata key `onepipeline.id` on the task: required, and unique within the project. It is a metadata key rather than the task's native id because a native id is opaque in a remote system, and a node id has to survive a copy between two sources.
* A node's **task prose** is the onetaskgraph task's `content`.
* A node's **title** is the onetaskgraph task's `title`. Every source carries one, so a title that is blank is the store's way of carrying none, and a lifecycle node whose task's title is blank is refused for the `title` schema 3 requires of it.
* A node's **deps** are real onetaskgraph dependency edges — the `blocks` edges arriving at this task — resolved to node ids through each far task's own `onepipeline.id`. An edge whose far end carries no `onepipeline.id` is refused where the plan is read, naming both ends. A cross-DAG `run:<id>#<node>` reference is the one dependency no edge can carry — it names another run's DAG, which is an item of no source — so it is the reserved key `onepipeline.deps`, which carries those references and nothing else.
* A node's **repo** is the first entry of its task's `repositories`. A `onevcs` identity that is not a normalized origin — a local checkout, named by its absolute path — is not a value that list can hold, and travels on the reserved key `onepipeline.repo`; a task naming a repository both ways is refused.
* A node's **delivers** is its task's own `delivers`: the tickets its work delivers, as qualified task ids, a bare entry naming the task's own source. It is never a metadata key — a task stating `onepipeline.delivers` is refused as not a node field — and a node whose task delivers nothing delivers nothing. An entry that is not a qualified task id, and two nodes of one plan delivering the same ticket, are refused where the plan is read and where an edit introduces them, naming the node and the entry, or both nodes and the ticket.
* Every remaining schema-3 node field — `kind`, `persona`, `max_turns`, `expects_no_diff`, `context`, `parked`, `executor`, `agent_graph`, `repo_type`, `workflow`, `merge_policy`, `base_branch`, `branch`, `body`, `draft`, `execution_checkout`, `steps`, `resume`, `adoption`, `consumes` — is a reserved `onepipeline.<field>` metadata key carrying the same JSON value the plan document carried under that name. A retired field is refused by its own name here exactly as it is in a plan document.

Every validation a plan document crossed is applied at the point a project is read, before any provider time is spent: the shape rules, the reference rules, acyclicity, the required title on a lifecycle node, a key no plan field answers to, and the named refusal for each retired field.

**onepipeline drives the binary, it does not link the crate.** That is onetaskgraph's own recorded decision for both of its SDKs, and it is what keeps a crates.io release ordering out of every onepipeline release. onepipeline resolves the binary from `ONETASKGRAPH_BIN` when that names an executable and from `onetaskgraph` on PATH otherwise. At launch, before anything is dispatched, it compares the binary's reported version against the minimum this build requires and refuses — naming the path it resolved, the version it found, the minimum it needs, and how to install one. An absent binary is refused the same way, and neither refusal creates a run root or dispatches a node.

**Status writes back; the journal does not move.** As a node's state changes, onepipeline writes that state onto its task's status category: `queued` while a driver drives the run and the node waits, `in-progress` while it is dispatched, `done` when it settles done, and `cancelled` when it is parked or dropped. Before its first dispatch, every driver — the one a launch starts and the one an adoption starts — waits for one attempt at projecting the whole plan, bounded by the store's command deadline, so every task the run has claimed reads `queued` before any of that work starts; an attempt that fails or times out is reported, and the dispatch goes ahead. At closeout, whether the run settled or was stopped, a node that never started is written `todo`, which releases the claim. Each node's `delivers` is written onto its task, and the tickets those tasks deliver move by the store's own relation between the two: onepipeline writes no ticket itself. The settlement detail goes to reserved metadata. The run journal and the run ledger stay onepipeline's, and the plan of record is still the graph the run is executing, projected from that journal under this run's ownership lock rather than re-read from the store.

**Live edits write through.** Every accepted graph edit updates the onetaskgraph project's tasks after the reconciler commits it to the journal. Write-back is best effort and retried off the reconcile loop: a slow or unavailable store is reported and never changes an edit ruling, a node settlement, or a scheduling decision.

A store is configured in onetaskgraph's own document, `onetaskgraph.yaml`, discovered from the directory a launch runs in — so which backend a plan lives in is the operator's, and this crate ships no configuration surface of its own for it. `examples/plan-store/` is one such store, holding the example plans this crate ships.

Per-node **controls** — what a node declares about how its dispatch runs, as against what it is asked to do — reach that dispatch as node-scope overrides on its agent-graph launch, applied after the run-wide `--node-set`s: `max_turns` is the worker member's own turn ceiling. A control this build accepts and cannot apply refuses the plan at validation, and refuses the launch again where it is composed; neither path may fall back to a default. A control a dispatch could not run under is not representable on the dispatch at all — `max_turns` is a `NonZeroU32` there, and the checked conversion from the plan's own `Option<u32>` happens at the trust boundary, so `max_turns: 0` is refused where the plan is read rather than by the member that could not start. A node with `steps` declares its persona, task, and turn budget on them, and a `kind: human` or `expects_no_diff` node — which has no dispatch — declares none of the three.

`resume` continues a node on the branch its previous attempt preserved: `{branch, checkpoint?, completed_steps?}`. `branch` is the preserved branch. `completed_steps` names the steps that branch already carries, and a continuation skips exactly those and re-runs the rest — an absent or empty list re-runs the whole workstream, which repeats work but never skips it. `checkpoint` must be a commit reachable on the remote; a local-only revision is not a checkpoint, because the machine that continues the node is not the machine that made it.

Cross-DAG edges: a `run:<id>#<node>` dep resolves by reading the referenced run's ledger, and only a `node-settled` of `done` satisfies it. An unknown run, a node that has not settled, and one that settled `failed` or `skipped` all leave the consumer **blocked, never failed** — the upstream may still arrive. Resolution is re-read on **every reconcile pass**, so an upstream that arrives after its consumer was blocked starts that consumer on the pass that observed it rather than parking the run. On first resolution the consumer records how far the upstream had got (`cross-dag-satisfied`, `{dependency, last_seq}`); if the upstream passes that point afterwards the consumer reports it once (`upstream-modified`, `{dependency, captured_last_seq, observed_last_seq}`) and is **reported, not re-run** — the work was correct when it was done, and repeating it is the planner's judgement. `last_seq` is the count of records in the upstream's merged store, because a run is written by several processes and no single stream's `seq` describes it.

Execution is **continuous and deterministic**. There are no rounds: `onepipeline start` runs the engine's reconcile loop itself, and a node — and each step within a lifecycle node — dispatches the moment its dependencies settle `done`, with settlement triggering integration and publication immediately. The loop drains the planner's durable command queue, re-resolves cross-DAG edges, and re-derives the frontier on every pass. **No agent is required to use onepipeline**: deterministic code drives the whole lifecycle.

The only thing that pauses anything is a **decision point**, and it pauses only the subtree that depends on it. A blocking surface — a ready `kind: human` node's attestation, a completion request, any surface declared blocking — holds its dependents back while independent branches proceed, and clearing it (`attest`, `reply`) auto-resumes the paused subtree inside the running loop, with no external driver action. Non-blocking surfaces never pause anything. `decision-pending` is emitted when a blocking surface begins holding dependents back and `decision-cleared` when it releases them.

Driver contract: `onepipeline start PROJECT [--attach|--detach] [--dag-graph off|REF] [--pr-author-graph REF] [--heartbeat-interval 1800] [--set PATH=VALUE]... [--node-set PATH=VALUE]... [--acknowledge-concurrent] [--launch-config FILE] [--filter-agentgraph SPEC] [--filter-vcs SPEC] [--filter-profile NAME=SPEC]... [--success-hook COMMAND] [--failure-hook COMMAND] [--hook-timeout SECONDS]` executes the plan the qualified onetaskgraph project id names, holding the run ownership lock (single writer) in the process that drives it — attached, this one; detached, the driver it retains. `--dag-graph` defaults to `off`; given a REF it launches that dag-scope agent graph (shipped: `monitor` member + resettable-cron `check-in` member) as an **observer and channel author** — never as the driver, and no member of it runs an engine verb. Its judge side may still be `onepipeline channel serve RUN` as a command provider. `--pr-author-graph` names the agent graph a lifecycle node's change request **body** is drafted by, and defaults to no graph at all, exactly as `--dag-graph` defaults to `off`: this crate ships the flag, not the document. Each repeatable `--set` is forwarded opaquely and in order to that dag-scope launch; each repeatable `--node-set` is forwarded opaquely and in order to every node-scope launch. Both lists — and the pr-author graph — are retained in the launch record and replayed by `adopt`. Attach returns when the run settles — the graph completed, a blocking surface waits with nothing else able to move, or exit 3 = nothing is driving the run. `awaiting-planner` means exactly that conjunction: an outstanding decision **and** nothing else able to move. An attached launch is running the loop, so it waits out every decision it can still make progress beside — independent branches keep going and a decision cleared while they do resumes its subtree inside that loop — and returns only when the run cannot advance without something arriving over the channel. A detached launch, and an attached one that has returned, hands the same state to `adopt`. What keeps driving after a launcher returns is **this executable**, never an installed sibling: a detached dag-scope launch retains `onepipeline drive GRAPH --task ... --dir ...`, which composes this build's own `oneagentgraph` so one build decides what a graph document may contain, and the detached run driver is `onepipeline drive-run RUN`, which takes the ownership lock, launches the observer graph the run declares — inside the process tree a `stop` reaps — and runs the same loop an attached launch runs in-process. Both are `hide = true`, absent from `--help` and from the surface list, and both are reached directly by `scripts/smoke-published.sh`, which is required of every hidden verb because a published artifact that cannot reach them cannot launch a detached run at all. `onepipeline adopt RUN` attaches a fresh driver to an intact ledger, resuming mid-decision from the folded state. It refuses a run something is genuinely driving; a run the liveness verdict has already called PARKED — or otherwise undriven — it may take over, ending that parked driver politely, waiting for it to go, and only then taking the lock, which it says on stderr and still has no `--force` for. Ownership: runs belong to the launching session; `runs --mine`; `stop` refuses another session's run and `--force` names the owner.

**Run-end hooks.** `onepipeline start` takes `--success-hook COMMAND`, `--failure-hook COMMAND` and `--hook-timeout SECONDS`, and the launch config takes `success_hook`, `failure_hook` and `hook_timeout` — keys of launch-config `schema_version: 6`, refused by name in a document declaring an earlier one — with the precedence every other launch-config field has: a flag beats the config. Naming no hook is the shipped default and does exactly what a launch did before these existed; a blank value, flag or key, is this launch saying it has none. `--hook-timeout` is a positive whole number of seconds, `600` when unnamed, and zero is refused wherever it is read, the way a zero `--writeback-item-budget` is. All three are retained in the launch record — the timeout beside the hook it bounds — omitted when absent, so a record written by an earlier build reads as naming no hooks, and replayed by `adopt`, which takes none of them. A run whose record names no hook fires none, whichever build adopts it, and an ending whose hook the record does not name fires nothing and journals nothing.

A hook is judged at the moment a driver **lets go of the run** — once it has released the ownership lock — on the graph as it then stands; a node a `retry` or `drop` superseded has left the graph and does not count. **The two hooks are disjoint, and together they cover every way a run ends**: complete, failed or skipped, unfinished with no decision outstanding, and stopped. **success** fires when every node in the graph is `done`, which covers every outcome that settles done, `no-changes` and `change-draft` among them. **failure** fires with reason kind `nodes` when the graph holds a `failed` or `skipped` node; with reason kind `unfinished` when it holds no failed or skipped node, holds any node that is not `done` — parked, cancelled, blocked or still pending — and no decision is outstanding (no unanswered blocking surface, no ready human action); and with reason kind `stopped` when `onepipeline stop RUN` established a clean teardown, which the stop verb fires itself, after it journals `run-stopped`. A stop whose teardown was refused fires nothing. **A run that has not ended fires neither hook.** A driver that lets go with a decision outstanding — `awaiting-planner` — has paused the run, not ended it: it journals `run-hook-withheld` with `{"settlement": "awaiting-planner"}` and says on stderr that no hook fired because the run is paused on a decision, and a driver that later adopts the run fires whichever hook the run then reaches. A driver that dies fires nothing, a launcher that detaches fires nothing — its retained driver judges instead — and a run holding a `complete-but-draft` node has not ended. That is the whole of what "every way a driver stops" means, and it is read no wider: hooks fire **at most once per run**, so a hook fired on a paused run would leave success unreachable for every run that ever asks a blocking question or waits on a human node. `run-hook-fired` is journaled before the command starts, and it is the marker: once a run carries one, no later driver, `adopt` or `stop` fires either hook again — a run that fired failure, was requeued and then completed included — which is what makes the two disjoint by construction. Attached and detached drivers judge and fire identically.

A hook is spawned the way a `--node-validator` command is — the command itself, with no shell and no arguments — with the launch record's `dir` as its working directory, and `ONEPIPELINE_HOOK` (`success` or `failure`), `ONEPIPELINE_RUN_ID` and `ONEPIPELINE_RUN_ROOT` (the run's own directory, absolute) in its environment. `ONEPIPELINE_LAUNCHER` and `ONEPIPELINE_LAUNCHER_SESSION` are set to the owner the launch record names when the hook fires, and removed from its environment where the record names no owner, so anything a hook launches belongs to the run's owner whichever process fired it. Its stdin carries one JSON document, then EOF: `{"version": 1, "hook": "success"|"failure", "run_id": string, "run_root": string, "reason": null | {"kind": "nodes"|"unfinished"|"stopped", "nodes": [{"id": string, "status": string, "outcome": string|null}]}}`, where `reason` is `null` for success and, for failure, `nodes` lists every node not `done` when the hook was judged, in plan order. The hook is **awaited** for up to the hook timeout, and a hook still running then has its process tree ended. Its stdout and stderr are kept, together, in `hooks/success.log` or `hooks/failure.log` under the run's own directory, and an attached driver also relays that output to its own stderr. The hook starts only after the driver has let go of the ownership lock, so while it runs the run reads as not driven — as a run whose driver has gone does — and a hook may launch or read runs, including with a verb that refuses a driven run; its ending is recorded without re-claiming the run. `run-hook-fired` carries `{"hook", "command", "reason"}` and `run-hook-finished` carries `{"hook", "exit": int|null, "ending": "succeeded"|"failed"|"could-not-start"|"timed-out", "log": string}`. **A hook never changes settlement.** Whatever its ending, these are exactly what they would be without the hook: the exit status of `start`, `adopt` and `stop`; the `{"run_id", "settlement"}` line; the run's `result.json`; every node status; and the write-back.

Run-end hooks, as a launch names them and as a hook and a reader meet them:

```json
{
  "run_end_hooks": {
    "flags": {"success": "--success-hook", "failure": "--failure-hook", "timeout": "--hook-timeout"},
    "config_keys": {"success": "success_hook", "failure": "failure_hook", "timeout": "hook_timeout"},
    "config_schema_version": 6,
    "default_timeout_seconds": 600,
    "environment": ["ONEPIPELINE_HOOK", "ONEPIPELINE_RUN_ID", "ONEPIPELINE_RUN_ROOT", "ONEPIPELINE_LAUNCHER", "ONEPIPELINE_LAUNCHER_SESSION"],
    "log": "hooks/<hook>.log",
    "results_output_lines": 20,
    "reason_kinds": ["nodes", "unfinished", "stopped"],
    "endings": ["succeeded", "failed", "could-not-start", "timed-out"],
    "stdin": {
      "success": {"version": 1, "hook": "success", "run_id": "demo", "run_root": "/runs/demo", "reason": null},
      "failure": {"version": 1, "hook": "failure", "run_id": "demo", "run_root": "/runs/demo", "reason": {"kind": "nodes", "nodes": [{"id": "build", "status": "failed", "outcome": "task-failed"}, {"id": "ship", "status": "skipped", "outcome": null}]}}
    },
    "payloads": {
      "run-hook-fired": {"hook": "failure", "command": "./scripts/report-failure.sh", "reason": {"kind": "nodes", "nodes": [{"id": "build", "status": "failed", "outcome": "task-failed"}, {"id": "ship", "status": "skipped", "outcome": null}]}},
      "run-hook-finished": {"hook": "failure", "exit": 1, "ending": "failed", "log": "/runs/demo/hooks/failure.log"},
      "run-hook-withheld": {"settlement": "awaiting-planner"}
    }
  }
}
```

**A plan is checkable without launching it, and the check is the engine's own.** `onepipeline plan check <SOURCE:PROJECT> [--check <PATH>]... [--json]` runs, in this order: first the engine's own plan loader — every refusal `onepipeline start` would make before dispatching anything, and no other rule — and then each registered consumer check, in the order its `--check` flags were given. A consumer registers a check with a repeatable `--check <PATH>` flag naming an executable, resolved against the working directory `plan check` was run from; naming none runs the loader alone. Each is spawned with the loaded plan as a single JSON document on its **stdin**, and with `ONEPIPELINE_PLAN_CHECK_SCHEMA=1` in its environment; its working directory is the one `plan check` was run from. The document is `{"schema_version": <int>, "name": <string|null>, "goal": <object|null>, "concurrency": <int>, "tasks": [<node>, ...]}`, where each node is the engine's own loaded node with every default already resolved — so a check sees exactly what the engine loaded and not the store's raw text. Each node additionally carries **`metadata`: the store's own metadata map for that task, verbatim**, including the keys outside the reserved `onepipeline.<field>` namespace, because a consumer's checks read keys the engine does not. On **stdout** a check answers with one JSON object, `{"refusals": [{"node": <string|null>, "field": <string|null>, "reason": <string>}, ...]}`; an empty list is an accept, `node` and `field` are always present and may be null, and `reason` is always a non-empty string. A check that ran answers with exit status **0**, whatever its `refusals` list holds; any **non-zero** exit status, an unparseable answer, or a missing or non-executable path means the check **could not be run**, which is reported separately from a refusal — naming the path, the exit status and the check's own stderr — and is never read as an accept. A check is handed the *loaded* plan, so when the loader itself refuses there is no loaded plan to hand it: the registered checks do **not** run, and each is reported as not run for that reason rather than as accepting. Engine refusals come first and carry `"source": "engine"`; each check's refusals follow in flag order and carry `"source": "<the path as given>"`, and the human output prints them in that order, one per line, each naming its source, its node and field where it has them, and its reason. `--json` prints exactly one object — **always**, a project this build could not read included, because a consumer parses this verb's stdout without first asking which failure it met, and the diagnosis for that one goes to stderr where every other refusal this binary makes goes: `{"project": <string>, "accepted": <bool>, "refusals": [{"source","node","field","reason"}, ...], "unrunnable": [{"check": <string>, "exit_code": <int|null>, "stderr": <string>}, ...]}`. The exit codes are chosen to match the consuming wrapper's own convention so it forwards rather than translates: **0** — the loader and every check accepted; **1** — at least one refusal, from either source; **2** — the project could not be read at all, or a registered check could not be run. `plan check` is a new verb: `onepipeline start` is unchanged, and a consumer built against an engine that does not have it keeps working.

Before launch, every targeted repository is checked through `onevcs session holders REPO --json`. A live holder refuses the launch unless `--acknowledge-concurrent` is passed; that override remains visible on stderr and emits `concurrent-acknowledged` with the shared identities and runs. A stale holder is reported and does not refuse.

Channel (public contract): `onepipeline next RUN [--filter NAME|SPEC] [--all]`, `reply RUN [FILE] [--correlation C]`, `surface RUN --kind check-in --message TEXT`, `attest RUN REF`, `stop RUN`. Surfaces and events are emitted as they occur. Reply envelope: legacy verdicts plus `{"version": 1, "commands": [...]}` with ops `add | drop | reparent | retry | cancel | requeue | attest | complete | context` — required fields and validation semantics exactly as ai-orchestrator's live-edit protocol (docs/orchestration.md#live-graph-edits): applied-or-rejected-with-reason, durable command queue, reply exit 0 = applied, 1 = accepted-not-yet-reconciled, 2 = refused/malformed. **A reply is routed by the halves it carries, never by which reader reaches the queue first.** It answers a pending surface only when it carries a **verdict half** — `completion`, `message`, or `reason` — and a **commands-only** envelope (a version and commands, no verdict) belongs to the command path alone: it leaves the pending surface, and any reader waiting there for a verdict, untouched. One carrying both is delivered to both — its commands to the command path, and the envelope itself to the pending surface, out of which that reader reads the verdict. Neither reader advances the other's cursor: the command path has its own durable queue behind its own cursor, so an envelope one reader passes over is still waiting, exactly once, for the reader it was addressed to. **A verdict is addressed to one question.** A question `channel serve` raises carries a correlation, and a verdict is bound to one question and carries that question's correlation: `--correlation C` binds it to the question `C` names and refuses, naming it, a correlation no pending question holds; one naming none is bound to the question whose `ask-manager-token:` its `message` echoes, else the question the pending slot holds, else the oldest outstanding question a live listener waits on, and with none of those it is queued carrying no correlation, for the next listener to read. A listener reads its own question's answer, a verdict on any other question its asker raised, and a verdict bound to no question — never a verdict on another asker's question. A reply carries an `author`, `planner` (the default) or `monitor`, and a **per-author op allowlist is enforced at reply validation**: `planner` may issue every op, `monitor` may issue `retry | requeue | cancel | context | add` only, and `complete`, `attest`, and `drop` are refused for the monitor with a reason — as is a commandless verdict declaring completion, which says what `complete` says in a field rather than an op. Every accepted monitor edit is additionally surfaced non-blocking to the planner. A `retry` naming neither `branch` nor `resume` **inherits both** from the node it supersedes — a node that ran, committed, and stopped leaves work on a branch, and its own settlement pins that branch to it, so the replacement continues it rather than cutting a fresh one beside committed work nothing points at; a `retry` that names either is answered with what it named. The superseded node leaves the graph in the **same edit**, which emits `node-dropped` — a cancelled node left in a continuous graph holds the run in `waiting` for ever — and what became of it stays in the run's record as its own `node-settled` and the `edit-committed` that replaced it. A `context` note carries exactly **one dispatch**: it attaches to the node's next dispatch and is consumed on delivery. It carries one further optional field, `deliver: auto|live|next`, defaulting to `auto`: `auto` delivers the note into the node's running turn when it has a controllable one and otherwise attaches it to the next dispatch, `live` refuses with a reason when it cannot deliver into the running turn, and `next` is the next-dispatch behaviour explicitly. Anything else is refused. Live delivery is `oneagentgraph interrupt RUN MEMBER --input`, addressed by the graph run and member the dispatch's own relayed envelopes stamp; that verb's exit 3 — no controllable turn in flight — is the `auto` fall-through and the `live` refusal and is not an error, while a delivery that was attempted and failed is refused under both. A note the running turn took is not also owed to the next dispatch, and `edit-committed` records which happened as `delivery: live | deferred` on the `context-added` operation it compiled. Surface consumption triggers `oneagentgraph reset-timer RUN check-in` — the whole pacemaker-reset contract.

**What the monitor meets when it is refused.** The per-author allowlist is the `planner-channel` profile's, from `onemessagebus-agent`, and every op it does not grant the monitor is refused with that op's own reason. A refused reply's error is `refused: ` followed by the text below, word for word, for each op and for a verdict declaring completion:

```json
[
  {"author": "monitor", "command": {"op": "complete", "reason": "every node landed"}, "refused": "'complete' is not an op the monitor may issue: whether the run is finished is the planner's verdict, not an observation. Surface it to the planner instead"},
  {"author": "monitor", "command": {"op": "attest", "ref": "sign-off"}, "refused": "'attest' is not an op the monitor may issue: a human action is attested by the person who took it, never by a watcher. Surface it to the planner instead"},
  {"author": "monitor", "command": {"op": "drop", "id": "build", "dependents": "detach"}, "refused": "'drop' is not an op the monitor may issue: removing work from the graph is a decomposition decision the planner owns. Surface it to the planner instead"},
  {"author": "monitor", "command": {"op": "reparent", "id": "build", "deps": []}, "refused": "'reparent' is not an op the monitor may issue: rewiring dependencies is a decomposition decision the planner owns. Surface it to the planner instead"},
  {"author": "monitor", "command": {"op": "amend", "id": "build", "text": "the gate is green"}, "refused": "'amend' is not an op the monitor may issue: what a node is judged against is a decomposition decision the planner owns. Surface it to the planner instead"},
  {"author": "monitor", "command": {"op": "note", "id": "build", "addressee": "worker", "text": "stop re-running the judged tier"}, "refused": "'note' is not an op the monitor may issue: a note may bind a criterion the node's judge decides against, which is the planner's decision rather than an observation. Surface it to the planner instead"},
  {"author": "monitor", "command": {"op": "settle", "id": "build", "outcome": "done", "evidence": "the change is on the base"}, "refused": "'settle' is not an op the monitor may issue: settling a node from evidence declares an outcome this run never observed, which is the planner's decision rather than an observation. Surface it to the planner instead"},
  {"author": "monitor", "verdict": {"completion": true}, "refused": "declaring the run complete is not something the monitor may do: whether the run is finished is the planner's verdict, not an observation. Surface it to the planner instead"}
]
```

**The planner channel is `onemessagebus`'s.** A run's channel directory is kept by the `onemessagebus` core's local transport under `onemessagebus-agent`'s `planner-channel` layout. Its queues, cursors, projection and exclusive sections, the asker a question is raised under and the wait for its answer, the per-author allowlist above, the command validators that judge what is offered to a queue, and the carry store a note no running turn took waits in are that crate's; `onemessagebus`'s own `docs/contract.md` and `docs/transport.md` are the one source of how they behave. What stays this crate's is its vocabulary and its policy: the channel verbs and their exits, the reply envelope and its ops, which question a verdict is addressed to, the surface kinds, and the bytes of every record the channel directory holds. `start --bus-config PATH`, or a launch config's `bus_config`, names an `onemessagebus` configuration the run's channel is kept under, and the launch record keeps it. The launch refuses it, naming the key and its value and creating no run, when it names a transport other than `local`, a `transport.dir`, a profile other than `planner-channel`, a `queues` block, a codec other than `onejudge` or an `onejudge` codec asking on a queue other than `surfaces`, or an `authors` block that widens what an author may issue. `bus_config` and `envelope_reviewer_bar` are the keys launch-config `schema_version: 7` adds, refused by name in a document declaring an earlier one.

**The envelope and the filter are `onemessagebus-agent`'s.** The envelope, its labels, sources and phases, the artifact reference, the kind, the filter and its matcher are the agent profile's types over the `onemessagebus` core, re-exported at `onepipeline::event` and `onepipeline::filter` — the same types `oneagentgraph` and `onevcs` re-export, so a relayed envelope is one value on every side of a relay. **`onemessagebus`'s own `docs/contract.md` is the one source of their shape**, with its `docs/wire.md` beside it; what this document says about the envelope and the filter grammar is a marked copy of it, decided against `onemessagebus` 0.4.0, and `tests/contract.rs` drives this document's fixtures through the re-exported types. What stays this crate's is its policy and its vocabulary: the launch config and its `filters:` block, the shipped profiles, the `PipelineKind` set, and each of its payloads, every one registered as the bus message `agent.pipeline.<kind>@2` beside the envelope that carries it.

Merged stream: envelope NDJSON, one store per run, interleaving the three sources `pipeline`, `agentgraph`, `vcs`. A relayed envelope keeps its producer's own `stream`, `seq`, `source`, and kind, so a sibling's kind is a wire string this library never rejects. A lifecycle node's `onevcs` session is **followed** — `onevcs events TOKEN --follow` — from the moment there is a token until the session closes, so the gate run, the push, the change request, the description written onto it, the draft lifted, the check polling, and the merge reach the store while they happen rather than in one batch at settlement; each session envelope is stamped with the node it belongs to, which its producer cannot know, and an enricher never rewrites a key the producer stamped. A follow that never started, or that neither ended cleanly nor relayed a record, falls back to reading the stream once. This library's **own** kinds are a closed set — the `PipelineKind` enum, which is what emits them — and exactly these: `run-started`, `node-ready`, `node-dispatched`, `node-settled`, `edit-committed`, `edit-rejected`, `planner-surface-queued`, `planner-surfaced`, `planner-replied`, `human-attested`, `driver-adopted`, `run-stopped`, `quiet-worker`, `decision-pending`, `decision-cleared`, `cross-dag-satisfied`, `upstream-modified`, `completion-requested`, `concurrent-acknowledged`, `body-not-drafted`, `run-hook-fired`, `run-hook-finished`, `run-hook-withheld`. `edit-committed` carries the `author` that submitted the edit. A *dispatch* that produced **nothing** and failed is re-asked, and only that one — an attempt that answered has already answered — and each attempt is recorded as its own `node-dispatched` carrying `attempt`, `attempts`, and the bounded reason the previous attempt gave, so a reader counts dispatches per node to see a retry while the settlement still distinguishes `no-agent-progress` from `task-failed`. A dispatch that ended for a reason that is **not the agent's verdict on its task** settles `dispatch-died` and not `task-failed`, chosen by classifying the failure's own detail rather than by inspecting the branch — so it covers both a dispatch that died holding finished work and one that produced nothing at all. It carries `cause`, the producer's own classification (`rate_limit`, `quota`, `auth`, `spawn-error`, and whatever else that layer spells) carried as the producer spelled it, and `head`, the commit the node's branch was left at; both are omitted where there is none, `branch` carries the branch a node that had one left, and the per-node views name the cause and — where a branch and a head are present — say the branch may carry finished work and name that commit. It is deliberately not `infrastructure-failure`, which is the dispatch layer refusing **before any work began** — an executor that could not start anything, or a session the sibling refused to open — and which is retried for exactly that reason: none of it is the agent's and the failure carries no work to lose. `dispatch-died` is the opposite case, the dispatch started and the agent worked, and it is not retried. A whole *lifecycle node* is re-dispatched under those same fields where its publication failed in a way that left the work on its branch, which the publication-failure paragraph below decides; the two are one kind and one payload shape, and neither is inferred from the other. The envelope's reserved `round` label is **deprecated and no longer stamped by anything this library writes**; it survives on the envelope because the agent profile's `Labels` still declares it.

**Every publication failure is named, and a preserving one is re-dispatched.** `onevcs` reports a `FailureKind` on a publication that ran and did not land, and each one settles its node under a word of its own: `checks-failed` — a required check the host reports concluded red; `checks-unsettled` — the bound on watching the host elapsed with the change still outstanding; `push-rejected` — the publishing push was refused by the merge path; `sync-conflict` — the base moved under the publication and the bounded resolve-and-requeue did not converge; and `pushed-unverified` — the publishing push reached the remote and the merge path could not then be read, so the work is on the origin and what a further attempt re-reads is that path rather than the push. `publication-failed` is the **residual** and keeps exactly the meaning it had: the gate the repository's own rules name, a request refused at a trust boundary, a seam with no implementation behind it, and a refusal of the publish call itself. Nothing that already had a word changes meaning, and a kind the sibling adds later is a routing decision this crate makes rather than a residual it falls into silently. Which of them a node can meet follows the policy its publication ran under: `change-auto` asks the host to land the change and is watched to its end, so it is where a red required check and an elapsed bound are observed at all, while `change-open` remains the **stated exception** — a person decides, so the node settles at change-request open with its change unlanded and nothing waits or polls for a merge somebody else owns.

Four of them are **preserving**: the work is still on the branch the session handed back and the *tree* is what was rejected, so the node is **re-dispatched on that branch** rather than settled `failed`. `pushed-unverified` is the exception and is recovered differently — its work is on the origin as well, so nothing about its tree is what has to change, and the recovery is a **bounded re-read of the merge path** rather than a dispatch of the agent: re-dispatching it buys a fresh clone and a fresh complete gate to re-push what the remote already carries. The re-read is bounded by `ONEPIPELINE_MERGE_PATH_READS`, three by default and the whole budget, so `1` is reading once and settling on whatever that said; it stops early on a cancellation, and a verdict that arrives on any read — merged, change request open, queued, or a check the host reports red — settles the node exactly as though the first read had answered it. Reads that never get a verdict settle the node `failed` under `pushed-unverified`, carrying the branch, the commit the push landed at, and `onevcs`'s own sentence about what stopped the read, which also names the `publish-branch` that lands it once the host answers. The re-dispatch is pinned to the preserved branch — continuing it rather than cutting a second one beside committed work — and records **no step as completed**, so every step runs again against the tree that has to change; republishing an unaltered tree meets the same refusal, which is the one ending this must not have. The failure's own reason and the id of every artifact its publication recorded — the check's log, the push's output, the conflict's hunks — reach that dispatch as its **node context**, in the same `## Planner context` section a planner's note is rendered into and disclaiming itself the same way, so a worker meets the diagnosis instead of going to look for it. The planner's own note is not carried again: a note carries exactly one dispatch and the attempt that just ran was it. A preserving failure whose branch the execution checkout did not take back is settled and not re-dispatched — there is nothing left to continue — and `invalid` and `not-implemented` stay terminal by their kind, because they answer the same way however many times they are asked.

The loop is **bounded**, by `ONEPIPELINE_PUBLICATION_ATTEMPTS` and three by default — the whole budget, so `1` is publishing once and settling on whatever that said. Each attempt after the first is recorded as its own `node-dispatched` carrying `attempt`, `attempts`, and the bounded reason the previous attempt gave, which is the same shape a dispatch that produced nothing is re-asked under. A node that spends the budget settles `failed` under the **last** failure's word, with a detail naming how many attempts were made, the branch they were made on, and what each one ended with — the difference between a check to fix and a check that is never going to pass.

**Filtering is owned by the stream's source.** Consumers have different attention budgets, so each producer takes a filter and narrows what it emits, rather than every consumer downstream re-filtering the same firehose. One grammar across the stack: `{include: [matcher, ...], exclude: [matcher, ...]}`. A matcher's fields are all optional and conjoin — every field it names must hold — and they are `source` (exact equality), `kind` (a glob over the kind's kebab-case wire string, where `*` stands for any run of characters including none and every other character is itself), `phase` (exact equality against the agent profile's one reserved top-level dimension, which this crate stamps on nothing it writes and relays as `onevcs` stamped it), and the reserved labels `run_id`, `node`, `step`, `member`, `persona` (exact equality; a matcher naming a label the envelope did not stamp does not match it). An absent or empty `include` admits everything; a match in `exclude` always rejects, whatever `include` said. `stream` is deliberately not matchable — it names a producing process rather than anything a consumer means — and neither are payload fields, nor the deprecated `round`. A **relayed** envelope's `kind` is matched as the wire string it arrived as: a sibling library's kind is not the reading library's own set, and is never rejected for being unknown. Like the envelope, the filter type is `onemessagebus-agent`'s `EventFilter`, re-exported, so every producer in the stack filters with one implementation; this paragraph is a marked copy of `onemessagebus`'s contract, and this crate's contract test drives the example below through the re-exported type, so a bus release whose grammar stops matching this text fails this crate's gate. A spec is **external input** and is refused where it is read, naming the matcher it is about — which list, and which position in it: a field that is not a matcher field is named in the refusal, as is a matcher naming no field at all (it matches every envelope, so one in `exclude` silences the stream entirely) and one whose field is empty (nothing on the stream carries an empty kind or an empty label).

A launch declares a `filters:` block, in a **launch config** — `onepipeline start PROJECT --launch-config FILE`, a YAML or JSON document of its own — and equivalently on the command line, as `--filter-agentgraph SPEC`, `--filter-vcs SPEC`, and repeatable `--filter-profile NAME=SPEC`, each `SPEC` a path to a file holding a filter or the document itself inline as JSON. The config is the base and each flag overrides the part of it that it names: the two source filters wholesale, and a profile **by name**, so a config holding a team's profiles beside a plan can have one replaced for one launch without restating the rest. The resulting block is retained in the launch record and replayed by `adopt`.

Launch config (YAML or JSON, by path):

```yaml
schema_version: 2                     # this build reads 2 and 1; another number is refused by it
pr_author_graph: ./graphs/pr-author.yaml  # a schema 2 key, the operator's own graph; --pr-author-graph overrides it
filters:                              # optional, and omitted from what this crate writes when empty
  agentgraph:                         # forwarded to every oneagentgraph launch this run starts
    exclude: [{kind: turn-activity}]
  vcs:                                # the typed value every followed onevcs session is read through
    include: [{kind: "gate-*"}, {kind: session-closed}]
  profiles:                           # named read-time views; planner and monitor ship and are overridable
    planner:
      include: [{source: pipeline}]
    monitor: {}
```

The config is **external input**: an unknown key is refused by name rather than dropped, a `schema_version` this build does not read is refused by its number — naming the ones it does — and a filter it carries is refused by the grammar's own rules above; all of it before a run is minted, so a launch that could not be honoured never cuts a session. `schema_version: 2` is `pr_author_graph`, and the bump is **additive** the way the plan schema's is: a version-1 config is a complete document this build still reads, and naming that key there is refused **by the key's own name** rather than by the number. A document declaring only its version is a launch that says nothing at all, which is what a launch naming no config already means; every optional key — the block, and the graph — is omitted from what this crate writes when it is empty, so a config round-trips as the file wrote it, a launch that names no drafting graph is written as a document a version-1 reader accepts, and a launch record from a build predating either still reads.

The filter block itself has two halves, and they are not the same thing:

**Source filters** decide what enters the merged store, and are passed through to the library that owns each stream: `filters.agentgraph` reaches every `oneagentgraph` launch this run starts — the observer graph and every node-scope dispatch — as that binary's own `--event-filter` / `events.filter`, and `filters.vcs` is the typed value the linked `onevcs` `EventStream` filtered constructor takes for every followed session, and for the read that covers that follow's tail. Declared once at launch, so a run also stops paying to relay events nobody will read. A launch naming no `filters:` block ingests exactly what it always did.

**Read-time profiles** shape what one reader is shown and **never touch the store**: `onepipeline next RUN [--filter NAME|SPEC] [--all]` and `onepipeline monitor RUN [--filter NAME|SPEC] [--all]`, where a bare name selects one of the run's profiles, a spec is read inline or from a file, `--all` is no profile at all, and naming neither is the shipped `planner` profile. `next` reports its shaped event view beside the surface it claimed. A profile that names no profile this run has is refused, naming the ones it does. Shipped: `planner` is `include: [{source: pipeline}]` — every pipeline-level event, with the detailed `agentgraph` and `vcs` activity behind them left out — and `monitor` is unfiltered; both are overridden by declaring a profile of that name. A profile shapes the **event view** and nothing else: which surfaces exist and the unread-surface accounting over them belong to the channel, so a blocking surface is delivered under every profile.

Executor seam:

```rust
pub trait Executor {
    fn name(&self) -> &str;
    fn capabilities(&self) -> Capabilities;      // { vcs_sessions: bool, ... }
    fn capacity(&self) -> CapacityReport;        // { slots_free, load1, mem_free_bytes }
    fn dispatch(&self, req: DispatchRequest) -> Result<Box<dyn DispatchHandle>>;
}
pub struct DispatchRequest {
    pub graph: ConfigRef,                        // content-addressed node-scope agent-graph config (oneagentgraph type)
    pub task: String,
    pub labels: Labels,                          // reserved: run_id, node, step, persona
    pub controls: NodeControls,                  // the node's own dispatch controls: { max_turns: Option<NonZeroU32> }
    pub workspace: WorkspaceSpec,                // Path(PathBuf) | VcsSession(SessionRequest: onevcs type)
    pub cancel: CancellationToken,
}
pub trait DispatchHandle {
    fn events(&mut self) -> EventStream;         // envelope NDJSON relayed from wherever it runs
    fn wait(&mut self) -> Result<DispatchOutcome>;
    fn cancel(&self, mode: CancelMode);          // Cooperative | Kill
}
#[non_exhaustive]
pub struct DispatchOutcome {
    pub succeeded: bool,                         // the settlement itself: a stream of turns does not carry it
    pub detail: String,
    pub session: Option<String>,                 // the executing machine opened it, so it hands the token back
    pub branch: Option<String>,
}
```

`WorkspaceSpec::VcsSession` means the machine running the dispatch opens the onevcs session there — so the request carries `onevcs::SessionRequest`, the *ask*, and never an opened `onevcs::Session`; v1 ships `LocalExecutor` only (supports both variants), the trait + rules grammar are shaped for WS dispatch-server and k8s executors. `DispatchOutcome` is `#[non_exhaustive]`: naming a further field later is additive.

Executor rules (YAML, ordered predicates over capacity + node labels):

```yaml
executors:
  - {name: local, type: local, max_load1: 8.0, min_free_mem: 2GiB}
rules:
  - when: {executor_has_capacity: local}
    use: local
  - use: local
```

`min_free_mem` is carried as the string a rules file wrote it as, so the file round-trips; the units are exactly `B`, `KiB`, `MiB`, `GiB`, `TiB`, and a bare byte count. Any other unit is refused when the rules file loads, naming the executor and the list — read leniently an unreadable limit means *no limit at all*, so the one file written to keep dispatches off an exhausted host would be the file that removed the bound.

`when` is a mapping, and there are exactly two predicate families in it. `executor_has_capacity: NAME` matches on **capacity**: the named executor's `CapacityReport` against the limits its `executors` entry declares. `node_label: {KEY: VALUE, ...}` matches on the **node's labels** by exact string equality, never a glob or a pattern. The keys it may name are the reserved ones that exist when the choice is made — `run_id`, `node`, `persona` — because an executor is chosen once per node, before any of its steps run; `step`, `round`, and a free-form extra are refused at load rather than left as a condition nothing can satisfy. Several conditions in one `when` conjoin: all of them hold or the rule does not fire. A `when` naming neither family is refused at load rather than read as an always-true rule. A rule with no `when` at all is the fallback, and the first rule that holds decides.

Views (CLI): `runs`, `status`, `host`, `monitor RUN [--filter NAME|SPEC] [--all]`, `results`, `goals`, `transcript RUN [NODE]`, `telemetry [--breakdown]` — unread-surface accounting, driver liveness (DRIVER DEAD vs PARKED vs UNDRIVEN), provider-health block sourced from `oneagentgraph health`. No view reports a round: the read model is the continuous graph state folded from the journal, and the ledger records it as one `result.json` per run, rewritten whenever a driver closes out. `results` names each run-end hook that fired — which hook, its reason kind, its ending, its exit, its log path, and the last 20 lines of its output — and each let-go that withheld one. `status` reports, per in-flight node, **what its dispatch is doing now**, how many events it has recorded, and how long since the last one, read from the `turn-activity` summaries `oneagentgraph` emits from both member kinds. `transcript` renders a dispatched turn's tools from those same summaries and its words from the report a `member-settled` retained at `report_path` — a two-party member's conversation as onejudge's own transcript, and a single-sided member's as the answer each harness that ran gave, because that is the report each kind settles with. That report is **copied into the run's own storage as the settlement is ingested** — from a process this library started, refusing anything that is not the producing library's own plain file, of its own name, within a bounded size — and every reader afterwards opens only that copy, at a path derived from the settlement rather than taken from it. A settlement whose copy the run does not hold is named as unretained, and the path it claimed is printed and never opened. `telemetry` carries per-party `usage` — `agent`, `judge`, `llmlint`, `total`, each with `input`, `output`, `cache_read`, `cache_write`, and `cost_usd` — and eight WALL buckets that sum exactly: `agent`, `judge`, `llmlint`, `gate`, `publication_wait`, `lock_wait`, `setup`, `scheduling`. Where two nodes are doing different things across one millisecond it is named by the more specific of the two, which is what keeps gate time and lock waiting separable from agent time. A bucket or a party nothing in the stack measures is served **absent**, never as a zero that reads as measured.

**Retention and resolution are one published promise**, so a consumer writes a report through the same path it reads one back from rather than restating a name it observed. `views::RunPaths` is the nameable type — the run id `run` and the run's own directory `dir`, minted by `RunPaths::new(RUN)` under the runs root this process reads or by `RunPaths::under(ROOT, RUN)` under an explicit one; `reports_dir()` is the run's own storage, and `report_for(STREAM, SEQ)` is where this run's copy of one relayed settlement's report is kept: `reports/<sanitised stream>-<seq>.json`. The **artifact id names the stream but not the seq**, so the file is derivable from the envelope the artifact was recorded on and never from the id alone. The stream is written as a single sanitised segment, and **the sanitiser is not public**: a report path is obtained by calling `report_for` and in no other way, so the writer and every reader share one implementation instead of two that happen to agree. Those six are what the type promises; nothing else on it is contracted. `report::retain(&RunPaths, &Envelope)` is the **writer**, published with the precondition it has always been called under: **the caller holds the producing process's authority for the path the envelope names** — it is being ingested from the stdout of a process the caller started, before that line exists anywhere a stranger could have written it, which is the one moment the named path carries the producer's authority rather than the journal's. Publishing it does not widen that, and every refusal it makes it goes on making, out loud and without following: an envelope that is not `agentgraph` / `report::MEMBER_SETTLED`, a `report::REPORT_PATH` whose base name is not `report::ACCEPTED_REPORT_FILE`, anything that is not a plain file — a symlink is the case this exists for, naming one file and delivering another — and anything past `report::MAX_REPORT_BYTES`. `ACCEPTED_REPORT_FILE` is this crate's own re-export of the producing library's report file name, so a consumer proving this path needs no `oneagentgraph` dependency of its own. `onepipeline::VERSION` is this crate's own package version: a host that pins this engine and separately pins a reader of the run store it writes has nothing else to prove the two are the same release.

Shipped content: personas `monitor` (at `personas/orchestrator.yaml`, the shipped file the orchestrator persona was rewritten into), `check-in`, `pr-author`; the dag-scope agent-graph config; a default node-scope config (worker+judge); an example plan store, `examples/plan-store/`, holding one project per example plan. The `monitor` persona is an **observer**: it reads the detailed stream, compares activity against the run's goal, each node's task prose, and the graph definition, surfaces issues, opportunities, and anomalies non-blocking, self-applies clearly intent-preserving fixes as attributed live edits within the monitor op allowlist, escalates ambiguity rather than acting on it, and never authors target-project content. pr-author composition: the drafting dispatch runs under the shipped `pr-author` persona, and a plan node or step may not claim that name — the persona is what tells that dispatch from a node's own, which are composed differently, and a node claiming it would silently lose the overrides its dispatch carries. Where a lifecycle node carries no `body` and the launch named a `--pr-author-graph`, one post-verification dispatch runs that graph once — after the branch is verified and before the change request is published, in the node's **own** worktree, which is where the diff is, and carrying none of the node's own turn budget. Its task is the opening sentence, the node's rendered task, the change request the session already holds where it holds one — a draft the worker opened, or one an earlier attempt of the node opened — and how to read the worker's transcript. The body it drafted is `results[].structured.body` of the result that ran, read out of the copy **this run retained** and never by following the `report_path` a producer named. Where the session holds no change request, the change request opens with that body; where it holds one, the body is written onto it as its description under the node's `title`, and the same publication lifts the draft and lands under the resolved policy. A drafting dispatch that does not start, fails, is cancelled, or answers with nothing the schema accepted leaves the publication untouched: the change request opens with no body and the node settles on its publication as before — and a change request the session already held keeps the description the worker left, said on the settlement. Drafting is never on the publication path. **It is never silent either.** Where a drafting dispatch was configured and attempted and produced no body, the run records `body-not-drafted` against the node, carrying `ending` — `dispatch-failed` for one that could not be run or ran without succeeding (a failed turn and a cancelled one both land there, because they differ in nothing a publication carrying no body either way can act on), `schema-refused` for one whose every answer the schema it was validated against rejected, and `no-body` for one that answered inside that schema and put nothing in it — and `detail`, which the node's own settlement carries too — after the publication's own reason where that failed as well, because the publication is what settled the node — so `results` shows the ending without a reader opening the store. Nothing is emitted for the two endings that are not failures: a launch that named no pr-author graph, and a node that carried its own `body`, both spend no dispatch. The three are kept apart because they take three different fixes, and a run that had just wired a drafter could tell none of them from a launch that had wired none.