The merge queue — continuous merge, serialized, tiered¶
The rule, in one line: work lands on
maincontinuously, one candidate stream at a time, through a gate that tests it as merged and prunes its worktree and branch on the way out. Merging is not deploying.Rules summary:
AGENTS.md→ Concurrent development — lanes, arbitration classes. Mechanism:agent_utilities/governance/merge_queue.py. Sibling design: lane concurrency.
CONCEPT:AU-OS.governance.serialized-merge-queue · CONCEPT:AU-OS.governance.tiered-merge-gate · CONCEPT:AU-OS.governance.merged-tree-verification · CONCEPT:AU-OS.governance.cross-branch-duplicate-symbol · CONCEPT:AU-OS.governance.merge-deploy-decoupling
Why this replaced bulk reconciliation¶
Work used to sit on long-lived branches and land in a bulk reconciliation gate at the end of a wave. The batching was never the value — the adversarial review inside it was. Batching was how this repository afforded that review. Once the review is automated, continuous merge is strictly better, because long-lived branches have a cost that no amount of end-of-wave care pays back.
Measured cost of long-lived branches, on this repository:
| Cost | What actually happened |
|---|---|
| Duplicate work that only existed because branches were invisible to each other | two CandidateClaim classes, two Fragment shapes, two independent fixes for one tenancy bug. Every lane surveyed correctly and each was right at the moment it looked — the premise it surveyed was main, and main was stale. |
| Stale premises | ~8 deferred items worked against a main that no longer existed. A premise rots in proportion to how long the branch lives. |
| Prune blast radius | 79 worktrees at peak, and a prune bug whose blast radius scaled with the long-lived branch count. |
And what the bulk review caught that naive continuous merge will miss — each is now a mechanical check rather than a human reading diffs:
| The bulk gate caught | Now caught by |
|---|---|
| an add/add duplicate where git silently dropped one of two new node classes | cross-branch duplicate-symbol scan |
D-OB-17: two branches git auto-merges cleanly into a tree that ImportErrors — no conflict markers, broken result |
testing the candidate as merged |
| five collisions the gate combined rather than letting git pick a side | the same duplicate scan — a rejection names both call sites, which is the input needed to combine them |
a fix on main silently undone by a branch that merges without a conflict |
repo-invariant contract checks on the merged tree — because zero conflicts is not a safety property |
The scan is not merely a re-implementation of the human check: run against the
branches in flight on the day it was written, it immediately reported Fragment,
Artifact, FragmentLike and _split_row each being added independently on two
different branches — including the exact Fragment collision cited above, still
un-landed and still invisible to both lanes.
The shape¶
flowchart TD
subgraph lane["Each lane (its own worktree, its own branch)"]
W[work + commit] --> P[pre-commit green] --> E["agent-utilities merge-queue enqueue"]
end
E -->|"one APPEND-ONLY fragment per lane<br/>in the shared --git-common-dir"| Q[(merge queue)]
Q --> R{"runner holds the<br/>reconciliation-merge LEASE"}
R -->|lease busy| D[exit 75 — defer]
R --> B["build a rolling trial commit<br/>merge-tree --write-tree → commit-tree<br/>(objects only, no working tree)"]
B -->|conflicts| RJ1["reject that ONE candidate<br/>batch continues"]
B --> M["materialize the merged commit<br/>in a throwaway detached worktree"]
M --> G["FAST GATE<br/>duplicate scan · contract checks · import smoke · targeted tests"]
G -->|fail, batch| BI["bisect the batch"]
BI --> B
G -->|fail, single| RJ2["reject with the failing checks"]
G -->|pass| L["git merge --ff-only into main<br/>guarded_tree_mutation"]
L --> PR["prune worktree + branch<br/>repository-manager prune_guard"]
L --> S["SLOW TIER, off the queue:<br/>full suite + guardrail gates on main"]
S --> PROM{"explicit promotion"}
PROM --> DEP["fast-forward refs/heads/deployed<br/>+ rollout restart"]
Nothing here is a new arbitration mechanism. Serialization is the existing
reconciliation-merge LEASE; the queue is an existing APPEND-ONLY
FragmentStore; scratch and pytest basetemp are existing PARTITION-class
paths. Both rows live in
lane_resources.yaml.
The latency budget — and why the gate must be cheap¶
This is the constraint everything else is derived from. With hundreds of agents
the queue is the bottleneck, and a gate whose cost exceeds its value gets
bypassed. That is this codebase's single most repeated failure: D-OP-4 (a
pre-commit chain too slow to run per-commit, so work went uncommitted), D-KCI-6
(a generator whose safe output looked destructive, so nobody ran it). A merge queue
gated on the 43-minute full suite will be bypassed within a day, and a bypassed
queue is worse than no queue because it also carries the illusion of a gate.
Published budget: 180 s from "my candidate is at the head of the queue" to
"landed, or rejected with a reason". It is a constant in the module
(FAST_GATE_BUDGET_SECONDS) and it is reported by merge-queue status, because a
budget nobody can read is a budget nobody can hold the queue to.
What runs inside the budget, and what does not¶
| Check | Cost (measured, this repo) | Tier |
|---|---|---|
merge-cleanliness (merge-tree --write-tree) |
~25 ms per candidate — no checkout, no index, no ref | fast |
materialize the merged commit (worktree add --detach) |
0.5–0.8 s | fast |
| cross-branch duplicate-symbol scan | 0.3–2.3 s for 40–70 changed files | fast |
| repo-invariant contract checks (merged tree) | ~31 s for the 11 discovered scripts, concurrent (was ~73 s sequential; 0.72–0.81 s when only 2 scripts were discoverable, before D-MW-9) | fast |
| import smoke over changed modules | 1.9 s (3 modules) → 14.3 s (36 modules) | fast |
| targeted tests over changed paths | 34 s (36 test files) | fast |
| whole gate, 1 candidate | 14.6 s and 36.4 s on two real branches | fast |
| whole gate, 4-candidate batch, 43 changed files | 58.3 s | fast |
| full unit suite, guardrail gates, backend-parity matrix | ~43 min | slow — outside the queue |
Two escape valves keep the fast tier honest rather than letting it quietly become the slow one:
- above
MAX_TARGETED_TEST_FILES(40) the selection has stopped being targeted, so it defers to the post-merge suite and says so; - the targeted-test step has a hard
TARGETED_TEST_BUDGET_SECONDS(120) ceiling, and exceeding it is a deferral, not a failure. A gate that hangs is a gate that gets bypassed; the queue's value is that it always returns.
Why the full suite is deliberately after the merge¶
Because it cannot fit, at any concurrency:
- one 43-minute gate run = 1.4 merges/hour. Ten agents merging once an hour already offer 10/hour. Utilisation ρ = 7. The queue diverges — depth grows without bound and every lane is blocked forever.
So the full suite runs on main after landing, batched. The trade is explicit:
a regression the fast tier does not catch is on main for one suite-cycle. That is
acceptable only because merge is decoupled from deploy — see
below. Nothing ships from an untested main.
Why optimistic batching, with the arithmetic¶
A serialized queue's throughput is batch_size / gate_duration. Take a
p90 gate of 60 s:
| throughput | 10 agents (≈10 merges/hr) | 100 agents (≈200 merges/hr) | |
|---|---|---|---|
| no batching (N=1) | 60/hr | ρ = 0.17 — fine | ρ = 3.3 — diverges |
| batch N=8 | 480/hr | ρ = 0.02, batch is usually 1–2 anyway | ρ = 0.42, wait ≈ 60/(1−0.42) ≈ 103 s ✅ |
So batching is not an optimisation here; it is the difference between a queue that works at 100 agents and one that does not. It is the industry merge-train pattern and this repository adopted it for exactly that reason.
The cost of batching is mis-attribution — a failing batch does not say which
candidate failed — and that is paid off by bisection (integrate_batch): split
the batch, re-gate each half, recurse. A clean batch is 1 gate run. A batch of 8
with exactly one bad candidate is at most 7 gate runs (≈7 min), and the good
halves land as the bisect proceeds rather than waiting for attribution to finish.
Since candidates arrive from lanes whose own pre-commit was already green, the
common case is the clean one.
Two failure modes are handled before bisection is ever needed, so they never cost a
run: a candidate that conflicts with the rolling head is identified exactly (it
is the one whose merge-tree exited 1) and dropped from the batch, and a candidate
already contained in the head is a no-op. One lane's conflict never rejects seven
innocent ones.
Tested as merged, not as it sat on the branch¶
CONCEPT:AU-OS.governance.merged-tree-verification
D-OB-17 is the whole argument: two branches, no conflict markers, a merge git is
perfectly happy with, and a tree that raises ImportError. Both branches import
fine individually. Nothing that looks at a branch — not its CI, not its
pre-commit, not a diff review — can see it. Only building the merged tree and
running it can.
So the gate never runs against a candidate's branch. It runs in a throwaway detached worktree checked out at the trial merge commit. The sequence is entirely object-level until the very last step:
git merge-tree --write-tree <head> <branch>— a real three-way merge that writes a tree into the object database. No working tree is touched, no ref moves, no index is read. ~25 ms.git commit-tree <tree> -p <head> -p <branch>— seals it into a commit. Still just an object; still no ref.git worktree add --detachat that commit, inside this lane's partitioned scratch dir. ~0.6 s.- Gate runs there. The worktree is removed in a
finally. git merge --ff-only <commit>in the canonical checkout.
Step 5 is a fast-forward by construction (the commit's first parent is the current tip), so git updates the ref and the working tree in one atomic operation. There is no window in which the canonical checkout holds a half-applied merge.
Verdicts name their interpreter. Every gate result reports the interpreter it
ran under, resolved explicitly to the repo .venv rather than inherited from
PATH — ambient-python3 inheritance produced ~80 false "environment-blocked"
verdicts here in a single day, and a green/red result is only ever a claim about
the interpreter that produced it.
Zero conflicts is not a safety property¶
It is the expected signal for a whole defect class, not evidence against one. Git's conflict answer is about text: "did two people edit the same lines". Nothing in that answer is about whether the resulting tree still upholds an invariant. A queue that treats "merged cleanly" as "merged safely" has confused the two.
What was claimed, and what the merged trees actually said¶
The concern raised was concrete: main carries the D-WD-1 fix that removed an
engine round-trip (resolve_placement) from the identity minter; 22 of 25
unmerged branch tips still contain that line, and most merge without conflict — so
landing them will silently re-arm an engine dependency on the authentication path.
Measured directly, by materialising all 23 mergeable candidates and reading the file off disk in each merged tree:
| count | |
|---|---|
branch tips carrying placement = resolve_placement( |
22 |
| merged trees carrying it | 0 of 23 |
check_tenant_identity_contract.py verdict on the merged trees |
23 of 23 pass |
Not one branch modifies request_identity.py relative to its own merge base.
main changed that hunk; every branch left it alone; three-way merge therefore takes
main's side, universally. "The tip has the old line" is true of every branch that
has not synced, and is not a revert. The instrument was the mismatch: git grep on
a branch tip cannot answer a question about a merge result — which is the same
lesson as the ~80 false "environment-blocked" verdicts, one level up.
But the failure mode is real — it just has a different shape¶
Reproduced in a controlled repository (branchB):
mainlands the fix.- A lane forks after the fix — so its merge base already contains it.
- The lane puts the line back.
mainmoves on elsewhere.
→ git merge-tree reports no conflict, and the merged tree carries the revert.
This is the honest version of the concern, and it is invisible to conflict detection by construction: the merge base has the fix, only one side changed it, so git applies that side. It is also invisible to the duplicate-symbol scan, which looks at what candidates add, not what they remove.
So the gate now runs the invariant itself against the merged tree — see contract checks.
Why the "removed line" heuristic was measured and then not built¶
The proposal was to flag lines the merge removes that main has and the candidate's
merge base did not. Both halves were measured rather than argued:
| predicate | catches the real revert? | findings on 23 live candidates |
|---|---|---|
in main, not in merge base, absent from merged |
No — misses it | 0 (silent) |
in main and in merge base, absent from merged |
Yes | 17 of 23 fire; median 8 lines, max 667, across up to 60 files |
The first predicate cannot catch it: a lane can only cleanly revert a fix it
already has, so the fix line is at the merge base — never "added since". Confirmed
on branchB above: it reports nothing.
The second predicate does catch it, but it is definitionally "every line this branch
removes". Firing on 17 of 23 real candidates with a 667-line worst case is a gate
lanes will learn to scroll past inside a day — the exact D-OP-4 / D-KCI-6
failure this design is built to avoid. A file-deletion arm fared no better: its one
finding across 23 candidates (tests/test_backend_tiered_migration.py on
fix/otr4-heterogeneous-tail) was a false positive — a deliberate test-file move
in a refactor.
So it was not built. A line-diff heuristic cannot distinguish a lane doing its job from a lane undoing someone else's; a contract states the invariant once and stays silent until it is actually violated. Cost of being wrong differs too: a missed heuristic finding is a bug, a noisy one is a disabled gate.
Repo-invariant contract checks on the merged tree¶
scripts/security/check_tenant_identity_contract.py already mutation-tests the exact
reverted line (" placement = resolve_placement(graph, [], None)\n return
GraphSession(") and passes on main with {"ok": true, "selfCheck": true} — but it
was wired into no pre-commit hook, so nothing ran it at merge time. It does now.
Two design choices make this generalize rather than special-case D-WD-1:
- Discovered, not enumerated. The gate globs
scripts/security/check_*.pyin the merged tree. A new invariant is a new script — no change tomerge_queue.py. A candidate that adds a contract has that contract enforced against itself in the same gate run. - Compared against the base. A candidate whose merged tree has fewer contracts than the base is refused: removing the check that guards an invariant is not a way to satisfy it. A repository that genuinely has none passes, and says so — "nothing found" and "everything passed" must not share a value, but neither may an honest absence be treated as a fault.
Measured cost, updated (D-MW-9). The discovered set grew from 2 scripts to 11:
check_swallowed_errors.py and 6 boundary/contract gates
(check_native_change_envelope_boundary.py, check_context_compiler_boundary.py,
check_http_egress_boundary.py, check_current_only_contract.py,
check_public_graph_boundary.py, check_external_graph_contract.py) had been
living one directory up, at scripts/check_*.py — outside CONTRACT_CHECK_GLOB
entirely, so the queue never ran them (they were wired only into
.github/workflows/guardrails.yml, and that workflow only fires on a push/PR this
polyrepo's CI actually receives — a real gap when a checkout is ahead of its
remote). Moved into scripts/security/ where discovery already looked.
Sequential execution of all 11 measured ~73 s — ~40% of the 180 s
FAST_GATE_BUDGET_SECONDS lease on its own, dominated by
check_context_compiler_boundary.py's full-repo test-file scan (~30 s). Contract
checks are independent, read-only scripts against the same tree, so they now run
concurrently (ThreadPoolExecutor, CONTRACT_CHECK_MAX_WORKERS = 8) — wall
time tracks the slowest single script, ~31 s measured. Proven end-to-end (pre-move):
injecting the reverted line into an otherwise-clean merged tree flips
check_tenant_identity_contract.py from {"ok": true} / exit 0 to
{"error": "TenantIdentityContractError"} / exit 1 in 146 ms.
Differential contract gating (D-MW-9). Three of the newly-discovered gates are
genuinely red on main right now — real, pre-existing feature debt, not a defect
in the gate: check_current_only_contract.py (~490 retired-surface references),
check_context_compiler_boundary.py (23 raw-Agent-construction sites),
check_native_change_envelope_boundary.py (1 legacy direct-write seam). Judging
every discovered script by exit code alone — the original shape of this step —
will refuse every candidate the moment discovery widened, recreating the exact
absolute-green deadlock this document's differential-test section already escaped.
compute_contract_baseline (mirroring compute_test_baseline below) now diffs
each non-clean script's output lines against the same script's output on
base_ref: a line present on the merged tree but absent from the base run is a NEW
violation and blocks; a line present on both is pre-existing debt, reported
explicitly, never silent, never blocking. Scripts that fail via a static message
or a bare sys.exit(1) with no itemized output (check_sbom_licenses.py's
{"error": "LicenseAuditError"} shape) fall back to script-level
clean/not-clean — the finest signal available for that class of script, and a
known, documented limitation: a second, different violation in an
already-red non-itemized script will not be caught until the script itself
reports its findings one-per-line. Cached per (base_sha, script), same
per-item granularity as the test-baseline cache below.
On the gate's interpreter. The gate resolves and reports the repo
.venvexplicitly and never invokesuv run. A bareuv run pytestfalls back to the system interpreter (pytest is not a base dependency), yielding fastmcp 3.3.1 and a flood of phantom failures — and concurrentuvinvocations re-sync a shared venv underneath a running gate. Neither may happen inside a merge gate, whose entire value is that its verdict is trustworthy.
Differential (regression) gating for targeted tests¶
main itself is not always green. Judging targeted-tests against the merged tree
in isolation — the original shape of this step — meant a candidate that touched an
already-red module was rejected for main's own failures, even when it strictly
improved them: measured on a real branch, the merged tree failed only 9 of the
same two test files where main already failed 30, fixing 21, and it was still
rejected. contract_baseline (above) already established the fix for contract
checks — compare the merged tree against the base, not against a fixed target —
and compute_test_baseline applies the identical idea one level down, at the
individual failing-test-id.
- Id-level only, never by file/module/pattern/count. A failing test id is
permitted exactly when that same id already fails, identically, on the base ref.
This is the only shape that cannot be gamed into masking a real regression — see
the module's own
AU-OS.governance.test-regression-baselineblock for the reasoning against each looser alternative. - Fail-closed on a degraded read; pass on an honest absence. If the base run
cannot be produced — it times out, crashes, or does not even collect
(
_PYTEST_READABLE_EXIT_CODES = {0, 1, 5}; a collection/usage/internal-error exit is not in that set) — the candidate is refused, never treated as "the base has no pre-existing failures." A base that genuinely produces zero failures for the selection is the other case, and that means any merged-tree failure is unambiguously new. - Cached, per-file (D-MW-10). Originally keyed by
(base_sha, sorted selection, interpreter)under the same shared, unversioned arbitration directory the queue itself uses — but during an active merge wave (mainmoving dozens of times) almost every batch computes a different selection at a differentbase_sha, so that key almost never hit: one real branch was REFUSED after its baseline run exceeded the (then-shared) 120 s budget under concurrent-lane contention. The cache is now keyed per(base_sha, FILE)instead of per(base_sha, whole selection): a selection that is a pure SUBSET of one already baselined at the samebase_sha— exactly whatintegrate_batch's bisection produces on every retry, since a sub-batch's changed-path union is by construction a subset of its parent's — now costs nothing at all, no subprocess, no worktree checkout. A partially-overlapping selection only pays for the genuinely new files. The base run also has its own, larger, separate budget (BASELINE_TEST_BUDGET_SECONDS = 240, decoupled from the merged-tree run'sTARGETED_TEST_BUDGET_SECONDS = 120) because it is not on a candidate's critical path once its cache is warm. Pure content-hash keying (hash the selected files' bytes, dropbase_shafrom the key entirely) was considered and rejected: most merges touch source files the selected tests import, so a content-only key will reuse a baseline computed against a different, stale tree — violating "never permit a failure that isn't provably present on the base ref." Only a readable result is ever cached: an unreadable run may be a transient fluke, and caching that will turn one bad run into a standing refusal. - Measured cost. Against this repo's own real, pre-existing red
(
tests/integration/knowledge_graph/test_engine_helpers.py+test_knowledge_tools.py, 30 failing / 1 passing onmainat the time of writing): a cold baseline run took ~34 s (well inside even the original 120 s ceiling) and a cache hit took ~4 ms. The baseline is only ever computed after the merged-tree run is itself readable, so a candidate whose merged tree is fully green pays nothing extra beyond a cache lookup when a baseline already exists for that exact selection, and a candidate whose merged tree fails pays the cold cost at most once per distinct selection per base commit.
The cross-branch duplicate-symbol scan¶
CONCEPT:AU-OS.governance.cross-branch-duplicate-symbol
Two lanes that never saw each other's branch each wrote a CandidateClaim. They
were in different files, so git merged both without a word and every test
passed. This is not a merge conflict and no per-branch check can find it: the
duplicate only exists in the relation between two candidates, and the merge queue
is the one place both are visible at once.
duplicate_definitions() parses each candidate's changed .py files with ast,
collects the module-level class/def names it adds relative to its merge
base, and reports any name added by more than one candidate — with every call
site named, because a rejection has to hand back what a human or agent needs in
order to combine the two implementations rather than pick one.
Three deliberate scoping decisions:
- module-level only. A method named
runon two different classes is not a duplicate; a module-levelclass Fragmentdefined twice is. - added, not present. A name already on
mainis shared ancestry, not a collision. Reporting it will bury the signal. - a file that does not parse contributes nothing — but that is not a fail-open read, because import-smoke and the targeted tests report a syntax error loudly, with a usable traceback. This check declines to guess where a better-placed check already speaks.
Prune on merge¶
Worktrees and branches are removed as part of landing, so they never accumulate
to 79 again. The prune is delegated to repository-manager's guarded prune
(CONCEPT:RM-PRUNE-GUARD) and is not re-implemented here, because that
implementation already gets three things right that a naive prune does not:
refs/lane-backup/<branch>is anchored immediately before the remove — one ref write, taken at the moment of deletion, so it cannot go stale the way an anchor laid down at lane start does;git merge-base --is-ancestoris re-asked at remove time, not trusted from an earlier scan;- deletion goes through
git branch -d, never-D, so git re-decides reachability under its own ref lock, atomically with the remove. Correctness comes from git's refusal, not from this repository's scan agreeing with itself. - occupancy is read from the lane protocol (merge in progress, uncommitted work, a
live lease), and
mergedrequiresbehind > 0— a worktree sitting exactly at base is the start of a lane, not the end of one, and is classifiedactive.
When repository-manager is not importable in the running interpreter, the prune
fails closed: it reports pruned: false with the reason and leaves the branch
alone. An un-pruned branch is untidy; a wrongly-pruned one loses work.
Merge is not deploy¶
CONCEPT:AU-OS.governance.merge-deploy-decoupling
The hazard, precisely. graph-os runs source-over-site-packages: the pod
NFS-mounts the canonical checkout read-only at /au with PYTHONPATH=/au:/skills
(services/graph-os/k8s/graph-os.deployment.yaml, volume au-src →
10.0.0.12:${WORKSPACE_ROOT}/agent-packages/agent-utilities). That NFS path is
the literal canonical working tree, not a staged copy, so import agent_utilities
resolves to whatever is on main right now, shadowing the baked wheel in the pinned
image.
Nothing hot-reloads — Python caches sys.modules, so a running pod keeps serving
the code it started with. But nothing pins, either. So the accurate statement is
not "merge deploys"; it is worse:
A merge arms a deploy that fires at a moment nobody chose — the next node drain, eviction, OOM kill, reschedule, or
rollout restart.
At one merge a day that is survivable. At a hundred agents merging continuously it is not, and it is why a finished fix currently sits blocked on an operator decision: merging it will also ship it.
The decoupling. Point au-src at a checkout of refs/heads/deployed instead of
the canonical main tree:
# services/graph-os/k8s/graph-os.deployment.yaml
volumes:
- name: au-src
nfs:
server: 10.0.0.12
readOnly: true
# was: ${WORKSPACE_ROOT}/agent-packages/agent-utilities (the `main` tree)
path: ${DEPLOYED_ROOT}/agent-utilities # a worktree of `deployed`
# one-time: create the promotion worktree at today's main
git -C agent-packages/agent-utilities worktree add \
${DEPLOYED_ROOT}/agent-utilities -b deployed main
# promote — explicit, deliberate, and only ever a fast-forward
git -C ${DEPLOYED_ROOT}/agent-utilities merge --ff-only <a main SHA the slow tier passed>
kubectl rollout restart deployment/graph-os -n platform
Then the two facts separate cleanly:
- merge — the queue fast-forwards
main. The fleet does not see it. Nothing is armed. This is the operation that must be cheap, continuous, and unremarkable. - promote — an explicit fast-forward of
deployedto a namedmainSHA the slow tier has since gone green on, plus a rollout restart. This is the operation that must be deliberate.
That is one NFS path change plus one extra worktree. No new service, no new
arbitration class, and the same fast-forward-only discipline the queue already uses
for main. It also gives rollback a meaning it does not currently have:
deployed can be moved back to a known-good SHA without touching main at all.
agent-utilities merge-queue promotion reports the state, and says undecoupled
in plain words while the ref does not exist — because a fleet that is one eviction
away from an unplanned deploy should not look healthy.
What is now impossible, rather than discouraged¶
The distinction matters here: eleven documented rules were violated in one day by competent actors with the rule in front of them. These are structural.
| Previously a documented rule | Now |
|---|---|
"resolve conflicts on the operator's branch, never against the shared main tree" |
No code path checks a candidate out in the canonical tree. The merge is built from objects; land() is --ff-only. There is nothing to resolve there. |
| "don't leave a reconciliation on a detached HEAD" | There is no detached HEAD. Trial commits are objects reachable from their branch until main fast-forwards; a crash strands nothing. |
| "run the tests before the operator merge" | The tests run in a worktree checked out at the merge commit. The branch tree is never the thing tested — the operator cannot accidentally test the wrong tree. |
"only one merge into main at a time" |
The reconciliation-merge lease. A second runner gets exit 75. |
| "don't clobber another lane's queue entry" | Two lanes write two different files. There is no shared mutable queue file to clobber. |
"never git branch -D a lane's branch" |
Prune delegates to branch -d + an anchor ref taken at remove time. -D is not reachable from this path. |
| "don't merge into a dirty canonical tree" | guarded_tree_mutation refuses, the candidates stay queued, and the runner defers. |
Still discouraged, not impossible — stated plainly:
- The queue can be bypassed by hand.
git mergein the canonical checkout still works:lane-guard's carve-out for an in-progress merge (MERGE_HEAD) is exactly the shape a hand-merge has. Closing it means teachinglane-guardto require a livereconciliation-mergelease held by a queue runner. Do not record this as solved. - Duplicate symbols are detected, not prevented. Two lanes can still each write a
Fragment; the queue catches it at landing time rather than at authoring time. Preventing it needs the concept/symbol reservation ledger to cover symbols, not just concept ids. - The fast tier is a sample, not a proof. Targeted tests over changed paths miss a regression in a module nothing changed. That is the deliberate trade for a queue that returns in under three minutes, and it is why the slow tier exists and why promotion waits for it.
Using it¶
# in your lane, after pre-commit is green and everything is committed
agent-utilities merge-queue enqueue # offers this branch; returns immediately
agent-utilities merge-queue status # depth, order, budget, recent outcomes
agent-utilities merge-queue run # drain a batch (holds the lease; exit 75 = defer)
agent-utilities merge-queue withdraw --branch <b> --reason "…"
agent-utilities merge-queue promotion # how far `deployed` lags `main`
Exit codes match lane lease: 75 = the lease is held, defer and do not proceed;
1 = a candidate was rejected, with the failing checks in the JSON. Enqueue is
deliberately non-blocking and checks nothing — verification happens once, at
the head of the queue, against the main that actually exists then. Checking at
enqueue time will re-create the stale-premise problem the whole design exists to
kill.
Source liveness and unresolved expiry¶
The normal commit hook freezes Git's actual staged index, including a partial
commit's alternate index. The native push adapter checks every outgoing ref from Git's input, including
release tags already reachable on the remote, before delegating unchanged to
pre-commit. Bootstrap installs this adapter into pre-commit's generated hook;
re-run normal bootstrap to update an existing installation. The queue checks the materialized merged commit.
Each invokes scripts/check_liveness_source.py against freshly checked remote
main; missing objects, missing push metadata, analyzer failure, a moving main,
or a date rollover fail closed. Fetch and rebase before retrying stale evidence.
Both snapshots use the same interpreter and analyzer on the same evaluation
date. The checker rejects added census identities and content hashes from the
existing fake-code detector, so replacing a finding at the same symbol cannot
hide a new defect. It retains the absolute never_executed invariant. Available
working-tree coverage must not silently disappear into an uncovered comparison;
unpaired coverage.json requires strict enforcement.
Unchanged expired obligations remain visibly reported as UNRESOLVED EXPIRY.
A source differential pass permits source delivery, not a clean liveness or
release claim. No dates, obligations, accepted-finding files, or persistent
waivers are changed. Normal tests, lint, types, security, and native checks retain
their own authority.
Changes to deferred API paths (including deletion or rename), deferral records,
the strict checker and its analyzer/reconciler dependencies, service registry,
or release tooling use strict enforcement against the exact candidate.
Release-tag pushes also use strict enforcement. The existing release test suite
still includes the absolute expiry regression test; no release workflow or
native gate is weakened. The manual guardrail-liveness-strict hook and direct
scripts/check_liveness.py command remain strict.
The reviewed source-policy implementation itself follows the approved source comparison policy; it does not disable the normal hook to bootstrap publication. Policy changes require independent review before publication. The paired census has a 300-second queue ceiling and fails closed on timeout rather than deferring missing evidence to a post-merge run. This adds an explicit liveness check to the existing fast checks; the original tests, contract checks, dirty-checkout guards, and serialized landing lease remain in place.