Setup & Operations Handbook · v0.2

Conveyor

A merge conveyor for teams whose PRs are written by AI agents. Agents open tens of PRs an hour; a serialized merge queue lands one per CI cycle. Conveyor replaces the lock with optimistic concurrency — speculative merge trains, a fast/full gate split, automated post-merge repair, and PRs that regenerate instead of rebase. No server, no database: state lives in git refs, logic runs in your existing GitHub Actions.

Pillar 1Fast signal

A change-scoped check gates merging in minutes; the heavy suite moves post-merge.

Pillar 2Merge trains

Queued PRs are tested on top of each other's predicted merges, in parallel. N land per cycle.

Pillar 3Auto-repair

The full suite runs continuously on main; failures bisect to a culprit and dispatch a fix-forward agent.

Pillar 4Intents, not diffs

Evicted or conflicted PRs are regenerated from a task manifest. Write-set claims prevent conflicts up front.

How it works

Conveyor treats merging the way a database treats transactions: optimistically, with validation at commit time and cheap retries. The AI context adds the thing databases never had — retries are nearly free. Re-running an agent against a new head costs cents and minutes; asking a human to rebase costs hours. Every mechanism below is built on that asymmetry.

The life of an agent PR

  1. Claim. Before work starts, the agent or scheduler reserves a write-set: conveyor claim "packages/api/src/billing/**" --task BILL-1234. Overlapping claims are refused (or warned, per config) — the collision is resolved before any code exists.
  2. Work. The agent branches from main, does the task, and writes .conveyor/intents/<task>.md — a self-contained restatement of the task. The manifest is the durable artifact; the diff is a cache of it.
  3. Enqueue. PR opened, conveyor:queued label applied. FIFO order, with a conveyor:urgent lane that jumps ahead.
  4. Speculate. The coordinator assigns a train position and builds the speculative branch. Fast checks run on every position in parallel.
  5. Land. Green positions merge in order via fast-forward — the tested SHA is byte-for-byte the landed SHA. Claims release automatically.
  6. Verify. The full suite runs post-merge. Failures bisect to a culprit and dispatch repair.

Speculative trains, in detail

A train is a chain of branches, each testing the predicted future state of main:

main ──●  (current head)
        \
         ●──────── conveyor/train/1   = main + PR#101
          \
           ●─────── conveyor/train/2  = main + PR#101 + PR#104
            \
             ●────── conveyor/train/3 = main + PR#101 + PR#104 + PR#107

All positions run concurrently. With a 10-minute fast check and batch_size: 8, steady-state throughput reaches 48 PRs/hour on one repo — a serialized queue with the identical check manages 6. Because positions are cumulative, failure attribution is built in: position 2 green and position 3 red means PR#107 is the culprit. No pre-merge bisection is ever needed.

When a position fails

  • The culprit PR is evicted — labeled, claim retained, check log attached as a PR comment.
  • Positions behind it are rebuilt without it, reusing green prefixes: one eviction costs one round of checks for the tail, not a restart.
  • If the PR's manifest allows it, Conveyor dispatches regeneration. Human PRs get a comment instead.
  • Flake shield: with train.retries: 1, a red position re-runs once before eviction.

When a merge conflict occurs

A PR that no longer merges cleanly onto its position isn't an error — it's cache invalidation. It's evicted immediately (no CI spent) and routed to regeneration.

Regeneration: replay the intent, don't rebase the diff

A regeneration job checks out fresh main on your runners (same-repo branches only, never forks), runs your configured agent.regenerate command with .conveyor/intents/<task>.md as the task, force-pushes the PR branch, and re-enqueues. The agent re-derives the change in current context instead of patching stale text.

Regeneration counts live in refs/conveyor/state. Past max_regenerations (default 3) the PR parks as conveyor:failed and a human is mentioned. A repeatedly regenerating intent is a signal: it usually means two tasks are genuinely coupled and should be one.

The post-merge gate

Pre-merge checks are a sampling strategy, not a proof. full_check.run executes against main after every landing, debounced to the latest head under load. On failure:

  1. Bisect. If the last green main was several landings back, Conveyor bisects the merge commits in between to isolate the culprit.
  2. Repair per full_check.on_failure: fix-forward (default) dispatches an agent job with the failure output plus the culprit's intent — the fix PR enters the urgent lane; after max_fix_attempts it falls back to revert. notify does neither, just pages.
  3. Contain. Landing continues while main is red by default; set full_check.block_on_red: true to pause the train instead.
Boundary

Runtime risk beyond tests belongs to feature flags and staged rollout, not to a longer pre-merge queue. Conveyor's contract ends when main is green — it is not your deploy system.

State model — no server, no database

LocationContents
refs/conveyor/stateQueue order, train assignments, regeneration counts, landing + bypass history (JSON, compare-and-swap updates; concurrent ticks are safe)
refs/conveyor/claimsActive write-set claims: task id, globs, owner, expiry
conveyor/train/*Speculative merge branches under test; deleted after landing or rebuild
PR labelsHuman-visible status (conveyor:queued … conveyor:failed)

The coordinator (conveyor run) is a stateless tick: read refs + GitHub API, compute the delta, apply, exit. It fires on webhook events plus a cron safety net.

What Conveyor deliberately is not

  • Not a CI system — your workflows run the checks; Conveyor decides what to test and what to merge.
  • Not a build cache — making the fast check fast is your job (Turborepo, Nx, Bazel, path filters).
  • Not an agent framework — agent.* commands are shell templates; any CLI agent works.
  • Not a deploy pipeline.

Team setup

Steps 1–6 give you a speculative merge queue for all PRs, human and agent. Steps 7–8 add the agent-specific machinery. Prerequisites: repo admin, Actions enabled, Node 20+, and a test command that finishes in minutes when scoped to a change.

1

Install and initialize

npm install -g conveyor-ci
cd your-repo
conveyor init

init writes conveyor.yml plus four workflows — coordinator (webhooks + 5-min cron), fast-check (on push to conveyor/train/**), post-merge (full check + bisect), and agent (regeneration/fix-forward, dispatch-only) — and creates the conveyor:* labels.

2

Configure the two checks

version: 1
branch: main

fast_check:
  run: pnpm turbo run test lint --filter="...[origin/main]"

full_check:
  run: pnpm test && pnpm playwright test

The fast check must be deterministic and change-scoped — affected-graph filters are the easiest route; target under 10 minutes. The full check is everything you run pre-merge today, moved post-merge wholesale. Don't trim it — continuous execution is the point. No graph tooling? Use fast_check.test_map to route by touched paths; crude beats slow.

3

Grant the coordinator write access

The coordinator creates branches, fast-forwards main, edits labels, comments. The built-in GITHUB_TOKEN covers building trains, running checks, and evicting — but not landing: GitHub refuses to let the Actions app's own token fast-forward a branch through required status checks, even when the check is green on the exact SHA (found dogfooding this repo). Store a GitHub App token or fine-grained PAT as CONVEYOR_TOKEN (contents + pull-requests write, actions write, this repo only) — it's required for landing, keeps merges auditable, and lets branch protection exempt exactly one actor.

To mint the PAT (needs a human — GitHub demands sudo-mode passkey/2FA re-auth first): github.com/settings/personal-access-tokens/new → name it, resource owner = the org (if the org isn't listed it disallows fine-grained PATs; fall back to a classic PAT with repo + workflow scopes), repository access = only this repo, permissions Contents / Pull requests / Actions all read-write → generate (orgs may hold it for approval) → gh secret set CONVEYOR_TOKEN --repo <owner>/<repo> and paste at the prompt. No "Checks" permission exists for fine-grained PATs — Actions read/write is what makes check state readable (GitHub maps reads of Actions-created check runs to the Actions permission; a token without it 403s every tick on /check-runs). Conveyor also falls back to the Actions workflow-runs API if the Checks API is denied, which requires the fast-check workflow's name: to equal train.check (the scaffolding already does).

4

Protect main

  • Require the fast-check status (or whatever train.check names) — the landed SHA is the tested train commit, so it already carries that check; no separate "conveyor" check exists.
  • Block direct pushes for everyone except the Conveyor identity.
  • Turn off GitHub's own merge queue and auto-merge — two coordinators fighting over order helps no one.

Humans stop pressing the merge button entirely; enqueueing is the only path in.

5

Commit, push, verify the loop

git add conveyor.yml .github/workflows
git commit -m "Add Conveyor merge coordination"
git push
conveyor status

status should print an empty queue and a healthy coordinator. Permission errors here mean step 3 isn't done.

6

Land your first train

conveyor merge 101 102 103    # or add the conveyor:queued label in the UI

Watch conveyor status --watch: train branches appear, fast checks run in parallel, and all three PRs land in one fast-forward. Milestone: throughput is now batch_size per fast-check cycle. Everything past here is agent-specific.

7

Wire up the agent runner

Add your agent's API key to Actions secrets (e.g. ANTHROPIC_API_KEY) and configure the commands:

agent:
  regenerate: >
    claude -p "$(cat "$CONVEYOR_INTENT")"
    --permission-mode acceptEdits --max-turns 80
  fix_forward: >
    claude -p "$(cat .conveyor/fix-task.md)"
    --permission-mode acceptEdits --max-turns 80

full_check:
  on_failure: fix-forward
  max_fix_attempts: 2

For fix-forward jobs the coordinator materializes .conveyor/fix-task.md (culprit's intent + failure log excerpt) before invoking the command.

Security boundaries — keep these

Agent jobs run only for same-repo branches; fork PRs are never regenerated and never see secrets. The agent job's token writes only the one PR branch it's repairing. Secrets are exposed only to the agent workflow, never to check workflows.

8

Turn on claims and intent enforcement

claims:
  enforce: warn          # week 1: warn → then: block
  ttl_hours: 24

enqueue:
  require_intent: false  # flip true once agent PRs all carry manifests
  require_approval: false

Then give your agents the protocol — drop the agent snippet into AGENTS.md / CLAUDE.md, and put conveyor claim into your task dispatcher if you have one.

Rollout advice

  • Week 1: train only, claims off. Confirm throughput; watch for evictions that go green on re-run — that's flaky tests, and the train amplifies flake pain (one flake evicts a real PR and rebuilds the tail).
  • Week 2: intent manifests on agent PRs, regeneration on, claims warn.
  • Week 3: claims block, require_intent: true, fix-forward on. Full design capacity.

conveyor.yml

version: 1
branch: main                    # protected target branch

train:
  batch_size: 8                 # max concurrent speculative positions
  check: fast-check             # GitHub check name required green on train branches
  timeout_minutes: 20           # evict a position whose check hangs past this
  retries: 1                    # re-run a red position once before evicting

fast_check:
  run: <command>                # REQUIRED. Change-scoped; target < 10 min
  test_map:                     # optional path-routed commands, matched in order;
    "packages/api/**": <cmd>    #   all commands whose globs match touched files run
    "**": <cmd>                 #   fallback

full_check:
  run: <command>                # REQUIRED. Full suite; runs post-merge on `branch`
  on_failure: fix-forward       # fix-forward | revert | notify
  max_fix_attempts: 2           # failed fixes before falling back to revert
  block_on_red: false           # true = pause landings while main is red
  debounce_minutes: 5           # under load, test only the latest head this often

agent:
  regenerate: <command>         # run in fresh checkout: new main + intent.md
  fix_forward: <command>        # run on fresh branch off main; fix-task.md materialized
  max_regenerations: 3          # per-PR; exceeded → conveyor:failed + human mention

claims:
  enforce: warn                 # off | warn | block
  ttl_hours: 24                 # claims expire; regeneration renews automatically

enqueue:
  label: conveyor:queued        # applying this label enqueues a PR
  require_approval: false       # true = ≥1 human approval gates enqueueing
  require_intent: false         # true = reject agent PRs without intent.md
  urgent_label: conveyor:urgent # jumps FIFO (used by fix-forward/revert PRs)

Intent manifest

YAML frontmatter plus a self-contained task body. The test of a good manifest: an agent with no other context, on a fresh checkout of a future main, can redo the work from this file alone.

---
task: BILL-1234                 # stable task id (ticket, or any unique string)
agent: claude-code              # informational: which runner authored this
write_set:                      # globs this task is allowed to touch
  - packages/api/src/billing/**
  - packages/api/test/billing/**
acceptance:                     # verifiable completion criteria
  - "pnpm --filter api test billing passes"
  - "upgrade preview shows prorated amount"
regenerate: allow               # allow | never (never = evictions go to a human)
---

# Task

Add proration support to subscription upgrades.

When a customer upgrades mid-cycle, charge the difference between
plans, prorated by remaining days. Use the existing InvoiceCalculator;
do not introduce a new billing path. Edge cases: annual→monthly is not
proration, it is a plan change at renewal — reject with UPGRADE_DEFERRED.
  • write_set is validated against the actual diff at enqueue — files changed outside it fail conveyor intent check (warn or block per claims.enforce). The write-set is checked, not trusted.
  • The body is passed verbatim to agent.regenerate — write it as a prompt, not documentation. Constraints and named edge cases are exactly what a regenerated attempt would otherwise lose.
  • Regeneration counts live in refs/conveyor/state, keeping the manifest byte-stable across regenerations.

CLI · labels · refs

CommandWhat it does
conveyor initScaffold config, workflows, and labels in the current repo
conveyor status [--watch] [--json]Queue, train positions, check states, claims, last coordinator tick
conveyor merge <pr...>Enqueue PRs (equivalent to applying the queue label)
conveyor claim <glob...> --task <id> [--ttl 24h]Reserve a write-set; fails on overlap when enforce: block
conveyor release --task <id>Release a claim early (merge releases automatically)
conveyor claims [--json]List active claims with owners and expiry
conveyor intent new --task <id>Scaffold .conveyor/intents/<task>.md
conveyor intent checkValidate manifest schema, write-set vs. actual diff, overlap vs. claims
conveyor bypass <pr> --reason "..."Direct merge outside the train; audited in state history
conveyor pause / resumeStop/restart new trains and landings; running checks finish and report
conveyor runOne coordinator tick (invoked by Actions; safe to run manually)
LabelMeaningApplied by
conveyor:queuedWaiting for a train positionyou / agent
conveyor:urgentJumps the FIFO orderyou / fix-forward / revert
conveyor:in-trainOccupying a position; checks runningcoordinator
conveyor:evictedFailed its position or hit a conflict; log attachedcoordinator
conveyor:regeneratingAgent re-deriving the change against fresh maincoordinator
conveyor:failedOut of regeneration attempts; needs a humancoordinator

Everything is inspectable with plain git:

git fetch origin 'refs/conveyor/*:refs/conveyor/*'
git cat-file -p refs/conveyor/state
The required check

Branch protection requires the train's own check (train.check, default fast-check) — Conveyor posts no separate check. Landing is a fast-forward to the tested merge commit, so tested SHA ≡ landed SHA and the landed commit already carries the green check: protection and the train share one source of truth. A landed commit means this exact SHA passed the fast check at its train position. Want linear history? Keep PRs single-commit at authoring time. Landing itself requires CONVEYOR_TOKEN — the Actions app's own GITHUB_TOKEN cannot fast-forward through required checks.

Agent guide

Conveyor is agent-agnostic: anything that can run in a shell, edit files, and push a branch can follow the protocol.

The five rules

  1. Claim before you code. If the claim is refused, the files are someone else's right now — pick another task. Never start work on files you couldn't claim.
  2. Write the intent first, as a complete, self-contained prompt. Update it if the task's shape changes while you work.
  3. Stay inside your write-set. Need more files? Widen the claim and the manifest — don't silently touch unclaimed paths.
  4. Enqueue, then walk away. Don't watch the queue, don't rebase, don't resolve conflicts. Evictions regenerate from your manifest against fresh main; that attempt supersedes yours.
  5. A fix-forward task is a normal task. Invoked with .conveyor/fix-task.md? Fix the failure, keep the original acceptance criteria true, enqueue as usual.

Drop-in snippet for AGENTS.md / CLAUDE.md

## Merging (Conveyor)

This repo merges through Conveyor, not the GitHub merge button.

- Before starting a task, reserve the files you'll touch:
  `conveyor claim "<glob>" --task <task-id>`. If refused, do not
  start — the files are claimed by another task.
- Create .conveyor/intents/<task>.md with `conveyor intent new --task <id>`.
  Write the body as a full self-contained prompt for redoing this
  task from scratch: goal, constraints, named edge cases, acceptance
  criteria. Keep the write_set accurate.
- Before opening the PR: run `conveyor intent check` and make sure
  the fast check passes locally (conveyor.yml → fast_check.run).
- Open the PR and add the conveyor:queued label. You are done.
- NEVER: rebase onto main, resolve merge conflicts, force-push after
  enqueueing, merge directly, or re-enqueue an evicted PR without new
  changes. Evictions and conflicts are handled by regeneration.
- If your PR gets conveyor:failed, a human will look at it; move on.

Worked example: dispatching with Claude Code

TASK=BILL-1234
conveyor claim "packages/api/src/billing/**" \
               "packages/api/test/billing/**" \
  --task $TASK || exit 1      # refused claim = don't dispatch

claude -p "$(cat tasks/$TASK.md)

Follow the Merging (Conveyor) section of AGENTS.md: write
.conveyor/intents/<task>.md for task $TASK, keep changes inside the claimed
write-set, run 'conveyor intent check' and the fast check before
opening the PR, then open the PR and label it conveyor:queued." \
  --permission-mode acceptEdits

Scheduler integration

If a coordinator process assigns tasks to a fleet, put the claim step in the scheduler — refusing a dispatch is cheaper than refusing a PR:

for task in ready_tasks:
    if conveyor claim <task.write_set> --task <task.id>:
        dispatch(task)     # agent inherits an already-held claim
    else:
        defer(task)        # overlapping work in flight; retry later

Predicting a write-set before code exists is approximate — that's fine. Claims are advisory collision-avoidance; the enforced boundary is diff-vs-manifest validation at enqueue. Start with directory-level globs and tighten. Tasks whose write-sets always collide are one task wearing two ticket numbers — merge them.

Operations

Watching the system

$ conveyor status
queue      4 waiting · 6 in train · throughput 31 PRs/h (24h avg)
train      1 #211 ✓   2 #214 ✓   3 #209 ●running   4 #216 ●running
           5 #217 ○queued-check  6 #219 ○queued-check
main       green · full_check 14m ago · last landing 6m ago
claims     7 active · oldest 19h (BILL-1204)
attention  #198 conveyor:failed (3 regenerations) · #201 evicted, human PR

Daily glances: the attention rows (the only things needing a human); the eviction rate (healthy ≈ zero; evictions that go green on re-run are flaky tests — fix or quarantine fast, because one flake evicts a real PR and rebuilds the tail); regeneration counts (the same intent regenerating repeatedly means coupled tasks — merge them, don't raise the limit); and claim age (claims older than a work session are usually dead agents; they expire at ttl_hours).

Common situations

  • PR stuck at conveyor:failed: read the attached logs; fix as a human PR, or sharpen the intent and re-enqueue (which resets the counter), or close it.
  • Main red, fix-forward looping: after max_fix_attempts Conveyor auto-reverts. If the revert itself won't land, that's the one case for conveyor bypass.
  • Emergency merge: conveyor bypass <pr> --reason "..." — lands directly, audited, train rebuilds on top. Bypassed commits skip the fast gate; the post-merge gate still covers them. Bypassing weekly means your fast check tests the wrong things.
  • Humans: they enqueue like everyone else — the train is strictly faster than a full pre-merge suite. No manifest means evictions come back as a comment, not a regeneration.

Tuning

SymptomAdjustment
Queue grows faster than it drainsMake fast_check faster first (it dominates); then raise batch_size
Evictions cascade into rebuildsLower batch_size — deep speculation only pays when positions usually pass; fix the failure rate
Flaky fast checkretries: 1 is a bandage — quarantine flakes into full_check
Full check red for long stretchesLower debounce_minutes; verify fix-forward dispatches; consider block_on_red while stabilizing
Agents collide despite claimsCoarsen globs to directory level; move claiming into the dispatcher
Runner queue saturatedBudget batch_size × jobs concurrent runners, or lower batch_size

Troubleshooting

  • Trains build but nothing lands — almost always permissions. If the tick log shows Required status check "fast-check" is expected on the PATCH …/refs/heads/main call even though the check is green on that exact SHA, the coordinator is running on the default GITHUB_TOKEN, which GitHub bars from fast-forwarding through required checks — set CONVEYOR_TOKEN. Otherwise check the token identity is exempt from push restrictions and the workflow has contents: write.
  • Fast checks never start on train branches — the coordinator triggers them by explicit workflow_dispatch per position (pushes from the default GITHUB_TOKEN don't fire push workflows — GitHub's recursion guard). If checks never start, the fast-check workflow is missing its workflow_dispatch: trigger or the coordinator lacks actions: write. The push: branches: ['conveyor/train/**'] trigger is belt-and-braces for PAT/App-token coordinators.
  • Regeneration jobs fail immediately — check the agent secret is set for the agent workflow, and that the branch is same-repo (forks are excluded by design).
  • State looks wrong — the refs are plain git objects; inspect them directly. Nuclear option: delete both refs; the coordinator rebuilds from labels next tick (regeneration counts reset).
  • CAS conflict between two ticks — expected and harmless; the loser retries.

Objections

Several of these shaped the design. Two are real costs accepted deliberately; the rest are either tunables or costs teams already pay in a form today's tooling hides.

"You're merging code that hasn't passed the full suite."
The pre-merge full suite was already an illusion — it tested against a main that had moved by merge time, and said nothing about the PRs landing alongside yours. Conveyor makes the sampling explicit: the fast gate samples pre-merge, the full suite runs more often than before (continuously), and every landing is attributable. Risk postures that can't tolerate a red-main window get block_on_red: true and require_approval: true — less throughput, same machinery.
"A bot with write access to main is a security hole."
It's one scoped identity with one audit trail, replacing merge rights held by every developer and agent. Agent secrets live in a single dispatch-only workflow that never runs for forks; bypasses are recorded in state history. This is attack-surface reduction.
"Force-push on regeneration breaks code review."
A real cost, accepted deliberately. The unit of review moves to the intent — task, constraints, acceptance — plus the final diff, not the churn between attempts. Gate with require_approval: true (reviews the last diff that actually merges), or opt out per-PR with regenerate: never. Human branches are never force-pushed.
"Our suite takes hours; we can't make a 10-minute fast check."
You need a fast subset, not a fast suite: build + lint + unit tests of touched packages via test_map, no build-system migration. Crude beats slow — a 7-minute check catching 85% of failures pre-merge beats a 2-hour check catching 98%, because the tail is caught minutes later by the continuous full suite and repaired automatically.
"N speculative runs — CI cost explodes."
Per-position runs are roughly the same count as testing each PR once (which you do today), just against the predicted future state, in parallel. Conflicts cost zero CI (caught at merge, before checks). Weigh the delta against engineer-hours spent rebasing and babysitting a stale queue — for agent fleets, it isn't close.
"Regeneration will loop and burn tokens."
max_regenerations (default 3) parks thrashers for a human. The loop is also a signal: an intent that keeps regenerating is two coupled tasks wearing different ticket numbers.
"Developers won't write intent manifests."
They never do — manifests are written by agents, as part of the agent's task. Human PRs ride the train with no manifest and still merge faster. Pillars 1–3 are a pure win for all-human teams.
"Claims will deadlock us."
Claims are advisory (off/warn/block) with a TTL so dead agents can't squat, and only need directory-level accuracy — the enforced boundary is diff-vs-manifest validation, which checks what actually happened. Start at warn: the refusals it logs are collisions you were paying for as merge conflicts.
"Flaky tests will make the train thrash."
Yes — the train amplifies flake pain, and retries is deliberately only a bandage. Flakes were always costing you; the train makes the cost visible and attributable within days, with eviction logs naming the test. Quarantine them into full_check.
"Merge commits pollute our linear history."
The other deliberate cost. Fast-forwarding to the tested merge commit is what makes green trustworthy — squashing would land a SHA that was never tested. Single-commit PRs keep history effectively linear.
"We already have GitHub's merge queue / Mergify / Graphite."
Those are pillar 2 for human-rate traffic. Conveyor adds the gate split with automated repair and the agent-native machinery. If all PRs are human and the suite is fast, GitHub's queue may genuinely be enough.
"What happens when Conveyor itself breaks?"
The failure mode is boring by construction: no server, so worst case nothing merges — never "wrong things merge", because landing requires a green check on the exact SHA. Escape hatch: disable the coordinator workflow and merge by hand. State rebuilds from labels.

FAQ

How is this different from GitHub's merge queue?
GitHub's queue gives you speculation. Conveyor adds the other three pillars: the fast/full gate split with post-merge culprit-finding and automated repair, plus the agent-specific machinery — intent manifests, regeneration-instead-of-rebase, and write-set claims. All-human team? GitHub's queue may be enough.
Does it work without any AI agents?
Yes — the train, gate split, and auto-revert are agent-free. You just won't use manifests, claims, or regeneration.
Why no squash merges?
Landing is a fast-forward to the tested merge commit, so tested SHA ≡ landed SHA — that identity is the correctness core of the train. Want tidy history? Author single-commit branches.
Monorepo or many repos?
One instance per repo, and the model favors monorepos — claims and trains coordinate within a repo. Two repos changing in lockstep constantly is a repo-boundary problem Conveyor can't fix.
What stops an agent from ignoring the protocol?
Branch protection stops direct merges for everyone. require_intent and claims.enforce: block reject non-conforming PRs at enqueue, and the write-set is validated against the actual diff — checked, not trusted.
What about PRs that landed after a culprit, while main was red?
They landed on a fast-check green and stay merged; repair enters the urgent lane rather than rolling anyone back. If that window is unacceptable for your risk posture, set block_on_red: true and take the throughput cost while red.
GitLab / Bitbucket?
Not yet. The design needs only branches, refs, checks, and a labels-equivalent — ports are feasible; GitHub is first.