onepipeline 0.17.5

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 **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.
* 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`, `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: `todo` while it waits, `in-progress` while it is dispatched, `done` when it settles done, and `cancelled` when it is parked or dropped. 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]...` 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.

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]`, `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 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.

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 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`. `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 duplicated cross-repo envelope type because the sibling libraries still declare 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.

<!-- llmlint: ignore[contracts_have_one_source_or_a_drift_gate] the duplication is the
approved contract's own mechanism rather than a missing gate, and it cannot be closed
from inside one of the three repositories. The envelope this paragraph names is under the
same decision and for the same reason — three independently-released tools would have to
co-version to share a crate — and the mechanism that holds them together is the one named
here: the grammar committed in *this* document is the source, and `tests/contract.rs`
drives the `filters:` fixture below through `src/filter.rs`'s own types, extracted from
this file rather than restated beside it, so a copy that stops matching this text fails
`just check`. `oneagentgraph` and `onevcs` carry the same text and each run the same gate
against it; the cross-repository half is the contract owner reading one committed
grammar. A shared artifact here would be exactly the shared source the contract refuses. -->

**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), 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 duplicated per repository by design and held together the same way: the grammar committed in this document is the one source, and each producer's own contract test drives the example below through its types. 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. `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 opened, in the node's **own** worktree, which is where the diff is, and carrying none of the node's own turn budget. 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. 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. 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.