Metadata-Version: 2.4
Name: onepipeline-cli
Version: 0.17.1
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Rust
Classifier: Topic :: Software Development :: Build Tools
License-File: LICENSE
Summary: Execute a task DAG over oneagentgraph and onevcs, merging their event streams into one.
Keywords: orchestration,dag,agents,cli,ndjson
Home-Page: https://github.com/nickderobertis/onepipeline
Author: Nick DeRobertis
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/nickderobertis/onepipeline
Project-URL: Repository, https://github.com/nickderobertis/onepipeline

# onepipeline

Execute a task DAG over [`oneagentgraph`](https://github.com/nickderobertis/oneagentgraph)
and [`onevcs`](https://github.com/nickderobertis/onevcs), merging their event
streams into one.

`onepipeline` is the composition layer. It executes a plan — a dependency graph
mixing direct agent nodes, repository lifecycle nodes, and explicit human actions
— continuously, dispatching each node the moment its dependencies
settle, through a pluggable **executor seam**, and keeps a live channel open to
the planner supervising the run. The plan itself lives in
[`onetaskgraph`](https://github.com/nickderobertis/onetaskgraph): a run is
launched by naming a project of whichever backend you already track work in, so
a plan is something you can open, edit and share without this harness in the
loop. The
agents come from `oneagentgraph`; the clones, worktrees, publications, and
change requests come from `onevcs`. Nothing here verifies a change: that is the
repository's own merge path — the host's required checks where a change
publishes remotely, and the repository's `pre-push` hook at the publishing push
where it publishes locally. Dependency direction is one-way: neither sibling
depends on this crate.

The public types, traits, config schemas, and CLI surface are the approved
contract in [`docs/contract.md`](docs/contract.md), compiled — and implemented
behind it. `onevcs` is **linked and called**: sessions, publication, and a
session's event stream are library calls, so what a publication did is a typed
value rather than a line of prose to parse. `oneagentgraph` is still run as a
CLI, so a build of it that refuses will make the dispatches this crate starts
refuse too; the composition layer itself is complete.

## Install

```bash
pip install onepipeline-cli      # prebuilt binary, no Rust toolchain
npm install -g onepipeline-cli   # the same binary, via npm
cargo install onepipeline        # from crates.io, compiled locally
```

To install a revision that has not been released yet — which today is every
revision, since the crate depends on its siblings by git and a git dependency
cannot be published — build it from the repository:

```bash
cargo install --git https://github.com/nickderobertis/onepipeline onepipeline --locked
```

The package name is not optional: `cargo install --git` searches the whole
repository, and this one also carries the `onepipeline-testfakes` test harness,
so an unqualified command fails with `multiple packages with binaries found`.

Prebuilt archives for Linux (x86-64, arm64), macOS (Intel, Apple silicon), and
Windows (x86-64) are attached to every release, with `sha256` checksums.

## Where a plan lives

A plan is one **onetaskgraph project**, and a node is one task in it. A run is
launched by naming that project's qualified id:

```bash
onepipeline start plans:tracked-release --heartbeat-interval 1800
```

Which backend that store is — a folder of Markdown, Linear, GitHub Projects — is
onetaskgraph's own configuration, discovered from the directory you launch in:

```yaml
# onetaskgraph.yaml
sources:
  plans:
    plugin: local-md
    config: { root: ./plans }
```

`examples/plan-store/` is a complete store of that shape, holding the two example
plans this repository ships. Nothing here special-cases a remote source, so a
`local-md` project runs directly — author locally, run it, and copy it up only
when it should become durable.

The mapping is [`docs/contract.md`](docs/contract.md)'s, and it is one rule per
field: the plan-level settings (`schema_version`, `goal`, `name`, `concurrency`)
are reserved `onepipeline.<field>` metadata keys on the **project**; a node's id
is `onepipeline.id` on its task; its prose is the task's `content`, its title the
task's `title`, and its repository the first of the task's `repositories`; its
dependencies are real onetaskgraph dependency edges; and every other node field
is `onepipeline.<field>` carrying the same JSON value a plan document carried
under that name. One task of the example store:

```markdown
---
title: "feat: implement approved release"
project: "tracked-release"
repositories:
  - "github.com/nickderobertis/some-service"
depends_on:
  - "tracked-release/design-approval"
metadata:
  "onepipeline.id": "service"
  "onepipeline.persona": "engineer"
  "onepipeline.max_turns": 24
---
## What
Implement the approved API and rollout behaviour.
```

onepipeline **drives the onetaskgraph binary** rather than linking the crate, so
one has to be installed: from `ONETASKGRAPH_BIN` when that names one, and from
`onetaskgraph` on the `PATH` otherwise. Its version is checked before anything is
dispatched, and an absent, unusable, or too-old install refuses the launch —
naming the path, the version, the minimum, and how to install one — rather than
becoming a run that fails on its first node.

The run's own record does not move: the journal, the ledger, and the graph a run
is executing are still this crate's, projected from that journal under the run's
ownership lock. Node status, settlement metadata, and accepted live graph edits
are projected back onto the onetaskgraph project in the background. A failed
write is reported and retried; it never changes execution or an edit ruling.

## What it does

`start` **drives the run itself**: a node — and each step within a lifecycle
node — dispatches the moment its dependencies settle, and settlement triggers
integration and publication immediately. No agent is required. The only pauses
are decision points: a ready `kind: human` node, or any surface declared
blocking, holds back the subtree that depends on it while every other branch
carries on, and clearing it with `attest` or `reply` resumes that subtree inside
the running loop.

`--dag-graph REF` attaches an agent graph as an **observer** — the shipped one is
a `monitor` member that watches the stream and raises what does not line up, plus
a resettable-cron `check-in` member that surfaces a status when nobody has
reported one for a while. It never drives the engine. Attached, `start` returns
when the run settles; exit `3` means nothing is driving the run, and
`onepipeline adopt RUN` attaches a fresh driver to the intact ledger, and takes
the same `--attach`/`--detach` pair `start` does: detached, it prints the launch
record and returns once the driver it retained has the run, so recovering one run
does not hold the session supervising the others.

A lifecycle node states the `title` its change request opens under, and may state
its `body` too. `--pr-author-graph REF` names an agent graph that drafts that body
instead, from the branch's own diff, once the branch is verified and before the
change request is opened; naming none is the default, and a drafting dispatch that
does not get there costs the change request its body and nothing else. It costs no
visibility either: a drafting dispatch that was configured, attempted, and produced
no body is recorded against the node under one of three endings — `dispatch-failed`
for one that could not be run or ran without succeeding, `schema-refused` for one
whose every answer the schema rejected, and `no-body` for one that answered inside
the schema and put nothing in it, which are three different fixes — and the node's
own settlement says the same thing, so `results` shows it. Naming no graph and
writing the `body` yourself spend no dispatch and are not reported.

A publication that fails does not always finish the node. `onevcs` says which
failure it was, and five of them settle under a word of their own — `checks-failed`
for a required check the host reports concluded red, `checks-unsettled` for a bound
that elapsed with the change still outstanding, `push-rejected` for a push the merge
path refused, `sync-conflict` for a base that moved under the publication, and
`pushed-unverified` for a push that reached the remote with the merge path unreadable
behind it. The first four leave the rejected tree on the branch the session handed
back, so the node is **dispatched again on that branch**, with no step
recorded as completed and
with the failure's reason and the id of every artifact its publication recorded
delivered as that dispatch's own context — the worker meets the diagnosis, on the
tree that has to change. `pushed-unverified` is answered differently, because
nothing about its tree was rejected: the work is already on the origin, so the
**merge path is read again** — bounded by `ONEPIPELINE_MERGE_PATH_READS`, three by
default — rather than the agent re-dispatched for a fresh clone and a fresh gate to
re-push what the remote already carries. A verdict that arrives during those reads
settles the node; reads that never get one settle it `failed` saying where the work
is, what commit it is at, and what stopped the read. Everything else settles
`publication-failed` as it always
did and is not retried: the repository's own gate, a request refused at a trust
boundary, and a seam with no implementation behind it all answer the same way
however many times they are asked. The loop is bounded by
`ONEPIPELINE_PUBLICATION_ATTEMPTS`, three by default, and a node that spends it
settles `failed` under the last failure's word, saying how many attempts were made
and what each one ended with.

A dispatch that ends for a reason that is **not the agent's verdict on its task**
settles `dispatch-died` rather than `task-failed`: a rate limit twenty seconds after
the final report, a harness that lost its credential, a run root deleted underneath
a live turn. The word is chosen by classifying the failure's own detail and never by
inspecting the branch, so a dispatch that died holding finished work and one that
produced nothing at all reach the same word. The settlement carries `cause` — the
producer's own classification, `rate_limit`, `quota`, `auth`, `spawn-error` — and
`head`, the commit the node's branch was left at, and `results` and `status` say in
one sentence that the branch may carry finished work and name that commit. It is not
`infrastructure-failure`, which is the dispatch layer refusing **before any work
began** and is retried for exactly that reason.

The planner supervises over the channel:

```bash
onepipeline next run-1                                   # read the next surface
onepipeline reply run-1 <<<'{"version":1,"commands":[    # edit the live graph
  {"op":"retry","id":"failed","node":{"id":"retry","task":"..."}}]}'
onepipeline attest run-1 design-approval                 # complete a human action
```

Every edit is applied or rejected with a reason: `reply` exits `0` when the
reconciler applied it, `1` when it is queued but not yet reconciled, and `2` when
it was refused.

Two of those ops reach a node that is already running, and they are deliberately
not the same lever:

```bash
onepipeline reply run-1 <<<'{"version":1,"commands":[    # steer the worker
  {"op":"context","id":"build","note":"the fixture moved to tests/data"}]}'
onepipeline reply run-1 <<<'{"version":1,"commands":[    # move the bar
  {"op":"amend","id":"build","text":"The comment lines are out of scope: leave them."}]}'
```

A `context` note **steers the worker only**. It is rendered under
`## Planner context` saying of itself that it reports observed state and adds no
acceptance criteria, it carries exactly one dispatch, and it does not change what
the node is judged against. An `amend` **does** change that: its text becomes part
of the node's effective task, rendered under `## Amendment` above the task's
operational notes and claiming precedence over them, so the worker and the judge
reviewing it read the same ruling — on the dispatch that follows it and on every
later one, until another `amend` replaces it. A turn already in flight is not
reached: its task was composed before the ruling existed, and so was the one its
judge reads. A node's current amendment is readable from
`status` and from `results` before anything replaces it. Without the second lever
a manager's mid-dispatch ruling reaches the worker and not its judge, and the
node's own judge can tell it to undo what the manager decided.

`amend` is the planner's; an observing monitor may not issue one, because moving a
bar is a decomposition decision rather than an observation.

A launch may also name a **node validator** — a command of the host's own, which
every op that introduces or changes a node's task (`add`, `retry`, a `requeue`
whose amendment touches `task`, and `amend`) is offered the resulting node to, as
JSON on its stdin. Exit `0` accepts the edit; a non-zero exit refuses it with the
command's own stderr as the reason. It is named by `--node-validator COMMAND`, by
`ONEPIPELINE_NODE_VALIDATOR`, or by a launch config's `node_validator`, in that
order of precedence; naming none is the default and runs no validator at all.

A launch may also name an **envelope reviewer** — a second command of the host's
own, offered the whole reply envelope once, after every command in it has passed
this crate's own validation and the node validator above and before any of its
edits is committed. One JSON document crosses its stdin: the run's goal, every
node the envelope introduces or changes with the op that produced each, and the
plan they are being edited into, as the envelope leaves it. That is the review no
per-node check can make — two added nodes that duplicate each other, a contract
seam between two nodes of one edit, the dependency edges the edit introduces, or
whether the edited graph still delivers the goal. Exit `0` accepts it; a non-zero
exit **refuses the whole envelope**, so no command of it half-applies, with the
command's own stderr as the reason and every op and node the envelope carried
named beside it. The refusal also names the node the reviewer **objected to**,
which is not the same set: the reviewer declares it on a line of its stderr
reading `objection: cover` — one line per node, repeatable — and a reviewer that
declares none is reported as having declared none rather than as objecting to
everything. A reviewer that cannot be started refuses the envelope rather than
letting it through **reviewed by nothing**. An accepted envelope is offered
**once per envelope** — unlike the node validator's two offers, because this hook
exists for a review a host plausibly answers with an agent, and because the
submission check is the only place a refusal is still whole. It is named by
`--envelope-reviewer COMMAND`, by `ONEPIPELINE_ENVELOPE_REVIEWER`, or by a launch
config's `envelope_reviewer`, in that order of precedence; naming none is the
default and runs no reviewer at all.

Read-only views — `runs`, `status`, `host`, `monitor`, `results`, `goals`,
`transcript`, `telemetry` — report unread surfaces, driver liveness, and
provider health without touching a run. `status` says what each in-flight node
is doing right now, with an event count and an age; `transcript RUN [NODE]`
renders a dispatched turn's tools and its words; `telemetry` reports what each
party spent and where the wall clock went, in eight buckets that sum exactly.
Anything nothing in the stack measures is reported absent, never as a zero.

## Where a dispatch runs

The [executor seam](docs/contract.md) decides. v1 ships the local executor only;
the trait and the rules grammar are shaped so a dispatch-server or Kubernetes
executor is a config change rather than a code change.

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

Ordered: the first rule whose `when` holds decides, and a rule with no `when` is
the fallback. A `when` tests an executor's capacity, the node's own labels
(`when: {node_label: {persona: reviewer}}`), or both — several conditions in one
`when` all have to hold.

## Development

```bash
just bootstrap   # from a clean clone
just check       # the deterministic gate
just gate        # check + the diff-scoped llmlint tier
```

`just --list` is the full command surface.
[`docs/contract-divergences.md`](docs/contract-divergences.md) records every place
the code could not compile the contract exactly as written, and what the planner
who owns the contract ruled on each.

## License

MIT.

