ADR-0007: The pipeline driver and chain: one session, headless, in the service repo
- Status: Proposed
- Date: 2026-10-05 (originally decided 2026-09-17, revised for the chain 2026-09-22 and promoted to the scaffold 2026-09-27, re-recorded here on the second lift)
- Deciders: Process maintainer
Context
Section titled “Context”An approved plan (ADR-0005) is a list of sessions an agent can build one at a time. Something has to run a session headless, judge it by the repo’s own gate rather than by the agent’s account, write the result back where every later decision reads it, and decide whether the plan continues. That something must not be able to mark its own failure as passed, start itself without bound, or spend without a ceiling.
Decision
Section titled “Decision”- The driver is a CI workflow in the service repo (
driver.yml) plus repo-localscripts/driver/. The plan, the gate, the branch and the PR are all in the service repo, so the driver needs no credential into anything else to do its core job. - Small scripts, each with one job and its own fixtures, so no part of the driver is
testable only by running the whole thing: read the plan (
plan-read), decide whether a session may run (preflight), compose the brief verbatim from the approved plan (session-prompt), writestatusandevidenceback (plan-write), fetch the spec by digest (fetch-spec), do the spend arithmetic (spend), pick the next session (select-next), and check the workflows’ own shape (chain-check,merge-check,step-order,workflow-context,branch-check). - Dispatch-bounded. The driver starts only by dispatch, for one session id. Every bound is stated with what it does not stop: a job timeout stops a hung run but not spend already incurred; a turn cap stops a loop but not one expensive turn; a concurrency group stops two runs racing one branch; the job’s permissions stop every capability except contents and PRs, including merging.
- The workflow runs the gate after the agent, and its exit code is the status. An
agent’s summary is a claim. The gate step runs the repo’s gate script and fixture harness
with
CI=trueand every declared input supplied. - Status and evidence are written on the session branch, never on
main. A driver able to writemain’s plan is a driver able to mark its own failure passed on the artifact every later decision is taken from. On the branch, the state change lands in the PR diff beside the work that earned it, in its own commit by a named driver identity. - Refusal is fail-closed, named and distinguishable from a crash.
preflightexits 3 withREFUSED: <reason>. It refuses when the plan is not approved, when the approver or date is empty, whenautonomy_levelis out of vocabulary or unset, when the session does not exist, whenhuman_only_actions_beforeis non-empty, when a dependency has not passed, and when the session already carries a terminal status. The workflow adds two more only it can see: the ref is not the default branch, and the session branch cannot be created. - The agent holds no credential. The spec is fetched by digest in a step before the agent runs, and the push credential is restored only after it ends. The agent’s tool allowlist cannot push, open PRs, dispatch workflows, call the forge’s API or browse.
- A red gate is written, retried within the cap, then escalated. A red gate writes
status: failedwith evidence and leaves the PR open. The chain counts attempts from the spend ledger (one record per run, so the count cannot disagree with a second field), re-dispatches while the count is withinretry_cap, and on exhaustion writesstatus: escalated, opens an escalation PR, files an issue and halts. - The chain is a separate workflow (
chain.yml). The driver runs one session and stops. A driver that chained itself would be both the worker and the judge of whether to continue, and the failure where it keeps dispatching because it believes its own verdict would have no second opinion. - Selection is the lowest-numbered eligible session. Eligible means every dependency has passed and the session’s own status invites a run. Chosen over critical-path-first, which needs a graph computation and its own tie-break and cannot be pinned by one fixture.
- The chain refuses to build on a red or unjudged
main. A run still in progress is not green. - The spend ledger lives in the ops repo, and every run appends to it, failed runs
included. A failed run still spent the money, and a ledger that recorded only
successes would leave the ceiling defending a number computed from the runs that least
needed bounding. Telemetry appends to
mainwithout review because it is a record of what happened, and the only way it can be wrong is by being incomplete, which a PR cannot catch. Code never does. - The ceilings promise
ceiling + one session, neverceiling. Both are checked before the agent step, in the driver as well as the chain, so a hand dispatch is bounded too. A check before a session cannot know what that session will cost. The ceiling makes a runaway terminate rather than compound. It is not a promise about any one session. - The driver lives in the scaffold’s common tree, with its fixtures, so a seeded repo arrives with it and the scaffold’s seed-set check fails when a piece is missing. Language-specific setup sits behind one toolchain action the overlay provides, shared by the driver and CI so the two judge a commit with the same tools.
Consequences
Section titled “Consequences”- The first live runs bought driver defects, not features. One made
retry_capunreachable on every session, because preflight refused any non-empty status. Another showed CI and the driver disagreeing on the same commit because they resolved different toolchains, which is why decision 14 shares one toolchain step. - A PR opened with the workflow’s own token does not trigger CI. The driver therefore
opens the PR with a declared credential, recorded in
.xal/gate-inputswith its expiry. - The push credential is broader than its use. It appends to one ledger file and nothing structurally limits it to that. A forge app with narrower scope is the fix, and until then it is an accepted risk with an expiry date.
- Two parsers read the plan format, the critic and
plan-read, and nothing diffs them.
Rejected alternatives
Section titled “Rejected alternatives”- The driver in the ops repo, reaching into service repos. It would need a write credential into every service, the thing the per-repo driver avoids.
- Retry on the merge trigger. A failed session leaves a PR nobody merges, so the merge trigger never fires for it and the plan stalls silently.