A merge gate for AI‑agent code.
Many parallel agent sessions go in; a small number of tested, risk-tiered, provenance-tagged commits come out. Humans review only what policy says needs a human.
Recorded run, 26 sessions
- 26agent sessions submitted
- 19merged, each tested
- 3broken by tests, rejected
- 2conflicts bounced
- 1secret blocked
- 1too large, sent back
Broken code ever on mainNone
What happened in the recorded run
- 26 agent sessions submitted work: features, three deliberately broken changes, two pairs of racing edits, one change to an auth path, a 900-line change and one leaking a token.
- Every feature merged after its tests passed. The broken ones were rejected by the test suite, and the losing side of each race was bounced as a conflict.
- The auth change waited for a human (
gate approve); the oversized change was sent back to be split; the leaking change was blocked without echoing the secret. - A post-merge check found
maingreen, and every landed commit carries provenance trailers (session, model, tool, spec, risk tier, approver).
| Result | Session |
|---|---|
| merged | race-x1 |
| merged | race-y1 |
| rejected | race-x2 |
| rejected | race-y2 |
| merged | feat-01 |
| merged | feat-02 |
| merged | feat-03 |
| merged | feat-04 |
| merged | feat-05 |
| merged | feat-06 |
| merged | feat-07 |
| merged | feat-08 |
| merged | feat-09 |
| merged | feat-10 |
| merged | feat-11 |
| merged | feat-12 |
| merged | feat-13 |
| merged | feat-14 |
| merged | feat-15 |
| merged | feat-16 |
| rejected | broken-1 |
| rejected | broken-2 |
| rejected | broken-3 |
| approved | authz |
| merged | authz |
| abandoned | dead-end |
Full transcript of python demo.py
== 26 agent sessions submit == feat-01: queued (tier 1, lane default, 9 lines, 2 files) feat-02: queued (tier 1, lane default, 9 lines, 2 files) feat-03: queued (tier 1, lane default, 9 lines, 2 files) feat-04: queued (tier 1, lane default, 9 lines, 2 files) feat-05: queued (tier 1, lane default, 9 lines, 2 files) feat-06: queued (tier 1, lane default, 9 lines, 2 files) feat-07: queued (tier 1, lane default, 9 lines, 2 files) feat-08: queued (tier 1, lane default, 9 lines, 2 files) feat-09: queued (tier 1, lane default, 9 lines, 2 files) feat-10: queued (tier 1, lane default, 9 lines, 2 files) feat-11: queued (tier 1, lane default, 9 lines, 2 files) feat-12: queued (tier 1, lane default, 9 lines, 2 files) feat-13: queued (tier 1, lane default, 9 lines, 2 files) feat-14: queued (tier 1, lane default, 9 lines, 2 files) feat-15: queued (tier 1, lane default, 9 lines, 2 files) feat-16: queued (tier 1, lane default, 9 lines, 2 files) broken-1: queued (tier 1, lane default, 3 lines, 2 files) broken-2: queued (tier 1, lane default, 3 lines, 2 files) broken-3: queued (tier 1, lane default, 3 lines, 2 files) race-x1: queued (tier 1, lane lib, 1 lines, 1 files) race-x2: queued (tier 1, lane lib, 1 lines, 1 files) race-y1: queued (tier 1, lane lib, 1 lines, 1 files) race-y2: queued (tier 1, lane lib, 1 lines, 1 files) authz: needs_approval (tier 2, lane default, 1 lines, 1 files) megachange: too_large (tier 1, lane lib, 900 lines, 1 files) - 900 changed lines > 400; split the change leaky: rejected (tier 1, lane lib, 1 lines, 1 files) - secret possible secrets on added lines (values not shown): lib/client.py: github-token lib/client.py: hardcoded-credential dead-end: trace stored (11200 B -> 151 B gz, 200 redactions) == review queue (what a human actually looks at) == SESSION STATUS TIER LANE LINES FILES MODEL REASON authz needs_approval 2 default 1 1 model-alpha megachange too_large 1 lib 900 1 model-beta 900 changed lines > 400; split the change leaky rejected 1 lib 1 1 model-beta secret feat-01 queued 1 default 9 2 model-beta feat-02 queued 1 default 9 2 model-alpha feat-03 queued 1 default 9 2 model-beta feat-04 queued 1 default 9 2 model-alpha feat-05 queued 1 default 9 2 model-beta feat-06 queued 1 default 9 2 model-alpha feat-07 queued 1 default 9 2 model-beta feat-08 queued 1 default 9 2 model-alpha feat-09 queued 1 default 9 2 model-beta feat-10 queued 1 default 9 2 model-alpha feat-11 queued 1 default 9 2 model-beta feat-12 queued 1 default 9 2 model-alpha feat-13 queued 1 default 9 2 model-beta feat-14 queued 1 default 9 2 model-alpha feat-15 queued 1 default 9 2 model-beta feat-16 queued 1 default 9 2 model-alpha broken-1 queued 1 default 3 2 model-beta broken-2 queued 1 default 3 2 model-alpha broken-3 queued 1 default 3 2 model-beta race-x1 queued 1 lib 1 1 model-alpha race-x2 queued 1 lib 1 1 model-beta race-y1 queued 1 lib 1 1 model-alpha race-y2 queued 1 lib 1 1 model-beta dead-end active - - - 0 - == gate runs == merged race-x1 b61d7c7d09 [lib] merged race-y1 b326305e47 [lib] rejected race-x2 conflict: lib/shared_x.py rejected race-y2 conflict: lib/shared_y.py merged feat-01 40876582a6 [default] merged feat-02 219971d87f [default] merged feat-03 4c0fb0ae11 [default] merged feat-04 64e96af062 [default] merged feat-05 aa86b90cda [default] merged feat-06 d04189dd1e [default] merged feat-07 7db0d8504f [default] merged feat-08 c537c60a7e [default] merged feat-09 73ce976d6b [default] merged feat-10 b6e5ab9e49 [default] merged feat-11 5bcc2b8363 [default] merged feat-12 188d48c64c [default] merged feat-13 b865d623ce [default] merged feat-14 ece7db7d27 [default] merged feat-15 f7642458c2 [default] merged feat-16 cb4a50800f [default] rejected broken-1 tests: FAILED (failures=1) rejected broken-2 tests: FAILED (failures=1) rejected broken-3 tests: FAILED (failures=1) authz: approved by juwen, queued at 22600b2011 merged authz 33c9df7c9a [default] dead-end: abandoned, diff archived to <repo>/.git/gate/attempts/dead-end.json.gz == post-merge verification == green main @ 33c9df7c9a == remaining work for humans/agents == SESSION STATUS TIER LANE LINES FILES MODEL REASON megachange too_large 1 lib 900 1 model-beta 900 changed lines > 400; split the change broken-1 rejected 1 default 3 2 model-beta tests broken-2 rejected 1 default 3 2 model-alpha tests broken-3 rejected 1 default 3 2 model-beta tests race-x2 rejected 1 lib 1 1 model-beta conflict race-y2 rejected 1 lib 1 1 model-beta conflict leaky rejected 1 lib 1 1 model-beta secret == stats == sessions by status: abandoned=1, merged=19, rejected=6, too_large=1 not-merged reasons: conflict=2, secret=1, tests=3, too_large=1 model model-alpha merged 11 / 12 (92%) model model-beta merged 8 / 14 (57%) CI runs: 10 (merged sessions per CI run: 1.90) suite runs: 9 suite cache hits: 1 publishes: 6 commuted without re-test: 1 mean lock hold: 145 ms (incl. wait: 146 ms) submit->merge median: 0.5 min live agent branches: 7 attempt archives: 491 B traces: 151 B == last landed commit == Agent Bot <agent@example.com> / committer gate authz: auth/policy.py Agent-Session: authz Agent-Model: model-alpha Agent-Tool: demo-agent/1.0 Spec: TASK-authz Risk-Tier: 2 Approved-By: juwen Agent-Lane: default Agent-Base: a19eebf26ad988c2b8e41bbf20fe1cbbe660223c Agent-Head: 22600b2011effe55052a77060ce19ffce37fd899 == broken code ever on main? == NO
Recorded locally with gate v0.3.0; later versions print the same outcomes (only commit ids and timings differ). Only a local file path was shortened to <repo>/.
Evidence an auditor can re-run
gate audit rebuilds a provenance report from the target's own history: every change that reached it, which model and tool made it, under which spec and risk tier, who approved it, every automatic revert, every attempt the gate refused, and every commit that reached the target without the gate.
gate audit --since 2026-10-01 --until 2026-11-01 --out audit-2026-10.md gate --json audit # same data as one JSON document gate audit --strict # exit 2 on any finding
Anyone with the repository can re-run it and get the same table. Run gate audit --strict in CI to fail the build on a direct push or an unapproved high-risk change.
Throughput, measured
From python bench.py --sessions 160 --lanes 8 --test-seconds 2 --batch 8 on one Windows laptop (Git 2.55, Python 3.13), with a 2 s sleep standing in for CI.
--parallel | wall | merges/day (extrapolated) | commuted publishes | publish-lock hold |
|---|---|---|---|---|
| 1 | 109.5 s | 126k | 0 / 24 | 87 ms |
| 8 | 45.9 s | 301k | 22 / 24 | 1.4 s per 6.7 merges |
Against github.com through a corporate HTTP proxy, each publish held the lock about 9 s, a ceiling of roughly 77k merges/day at batch 8. A weekly workflow re-runs batch, crash-recovery and push-race scenarios against a real GitHub remote.
Try it
pip install git+https://github.com/juw12/gate
cd your-repo
gate init # detects your test command
gate spawn fix-1234
gate submit fix-1234 --model <model-id>
gate queue --drain --parallel 8Standard library only. Python 3.11+, Git 2.40+. MIT licence.
Still out of scope
- Semantic conflicts are caught after the fact (
gate verifybisects and reverts), not prevented. - Secret redaction is best-effort; plug in an external scanner for fail-closed coverage.
- State is files, not a database. Sharing across hosts is tested by simulation, not on a real NFS cluster.