gate
v0.5.0 · MIT · Python 3.11+

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

  1. 26agent sessions submitted
  2. 19merged, each tested
  3. 3broken by tests, rejected
  4. 2conflicts bounced
  5. 1secret blocked
  6. 1too large, sent back
One block per sessionin order
merged 19rejected 6too large 1

Broken code ever on mainNone

01 / The run

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 main green, and every landed commit carries provenance trailers (session, model, tool, spec, risk tier, approver).
ResultSessionCommit or reason
mergedrace-x1b61d7c7d09 [lib]
mergedrace-y1b326305e47 [lib]
rejectedrace-x2conflict: lib/shared_x.py
rejectedrace-y2conflict: lib/shared_y.py
mergedfeat-0140876582a6 [default]
mergedfeat-02219971d87f [default]
mergedfeat-034c0fb0ae11 [default]
mergedfeat-0464e96af062 [default]
mergedfeat-05aa86b90cda [default]
mergedfeat-06d04189dd1e [default]
mergedfeat-077db0d8504f [default]
mergedfeat-08c537c60a7e [default]
mergedfeat-0973ce976d6b [default]
mergedfeat-10b6e5ab9e49 [default]
mergedfeat-115bcc2b8363 [default]
mergedfeat-12188d48c64c [default]
mergedfeat-13b865d623ce [default]
mergedfeat-14ece7db7d27 [default]
mergedfeat-15f7642458c2 [default]
mergedfeat-16cb4a50800f [default]
rejectedbroken-1tests: FAILED (failures=1)
rejectedbroken-2tests: FAILED (failures=1)
rejectedbroken-3tests: FAILED (failures=1)
approvedauthzapproved by juwen, queued at 22600b2011
mergedauthz33c9df7c9a [default]
abandoneddead-enddiff archived to <repo>/.git/gate/attempts/dead-end.json.gz
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>/.

02 / Audit

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.

03 / Throughput

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.

--parallelwallmerges/day (extrapolated)commuted publishespublish-lock hold
1109.5 s126k0 / 2487 ms
845.9 s301k22 / 241.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.

04 / Install

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 8

Standard library only. Python 3.11+, Git 2.40+. MIT licence.

Still out of scope

  • Semantic conflicts are caught after the fact (gate verify bisects 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.