Generated from cat-harness/skills/sdlc/sdlc-core/merge-queue.md — do not edit here.

✎ Edit this page’s source

Merge queue — what lands next, and as what

processes/sdlc/merge-train.bpmn is the process. The ORDER is computed by processes/sdlc/decisions/merge-priority.dmn; the facts it reads are derived by scripts/merge-queue.ts; what the steward decided is recorded against schemas/merge-queue.ts. This page says what each piece is FOR. Bean hfag.

The rule everything else follows from

The queue stores decisions, never GitHub’s facts. CI status, mergeability, labels and head SHA change without the queue being told, so a stored copy is stale the moment it is written and a reader cannot tell which. They are read live at each step. What a queue entry holds is what the steward DECIDED — the placement, a hold, an ejection — with who decided it, when, and the evidence URL it acted on. FORBIDDEN_FACT_KEYS in the schema refuses an entry that carries a fact.

Handing a PR to the queue — the sibling session’s side (STRICT)

merge-train.bpmn has two lanes that matter here:

  • sibling-session (“Sibling session”): whoever submits a PR to the queue and fixes it when it is handed back.
  • merge-steward (“Merge steward”): whoever orders and lands the queue.

Both are ROLES, not sessions. Owner, 2026-10-04: “Merge Manager may change”. So nothing in this section depends on which session holds either lane.

The PR is the message

The steward reads open PRs labelled ready-to-merge, and nothing else. A comment posted anywhere else reaches nobody: not on a PR you believe is the steward’s, and not on an issue such as #1800. Measured 2026-10-03/04 (bean j5iq): a merge-order change was posted to #1959 three hours after it merged, and the pokes on #1800 changed none of the three facts that kept #1968 out of the queue.

Ask the user before submitting

When you finish your work and are ready to merge, ask the user before submitting to the merge queue. Do not apply ready-to-merge or post the ready: marker autonomously without asking the user if you should submit. Submitting to the queue requests landing the code on main; the user or owner confirms the submission.

Submitting to the queue

To submit, do all of these on YOUR PR:

  1. Owed CI is green on the head. See §”Admission asks which runs are OWED”.
  2. The PR is ready for review, not a draft.
  3. It carries the label ready-to-merge.
  4. It has a comment ready: <full head sha>, signed with your session link (https://claude.ai/code/session_…). Every session acts with the owner’s token, so the link is the only thing that tells sessions apart. A _Generated by Claude Code_ footer alone is not a signature.
  5. It does not carry needs-merge-human. merge-main.yml adds that label when it refuses a merge and never removes it. If you have since merged main by hand, remove the label, and say why in a comment.
  6. The body has no unticked box, and there is no open question to the owner or the steward.
  7. An agent-touched PR has an adversarial review on its head, recorded on the merge-reviews state branch by a session that is not the author (adversarial-code-review). The gate is warn-only for now, so a missing verdict is reported, not refused. A train keeps each PR’s verdict, because the review is per head.

Watch status after submitting — never fire-and-forget

When you submit something to the merge queue, you need to watch what its status is. Submitting is not the end of your turn. Watch the GitHub Actions runs on the head (check:head-has-run), evaluate merge-guard, and monitor whether the PR is admitted into a train or whether it is refused, held, or failing checks. If CI fails, diagnose and repair the failure immediately rather than leaving a failing PR in the queue.

Merge order goes on the PR, and setting an order is the owner’s call.

  • Write it in the ready: comment, on every PR it concerns (“merge before #N”).
  • Quote the owner’s ruling with its date.
  • When two PRs must not land in the wrong order, keep the label OFF the later one until the earlier one lands. A label is the only hold the steward is certain to see.

After submitting: do not spend CI you do not need yet

Owner, 2026-10-04: once CI has confirmed the PR, a submitter far back in the queue should not burn CI cycles keeping up with a churning main. Every merge of main into your PR re-runs the whole suite. While other PRs are landing, each of those runs is obsolete before the steward reaches you.

  • The first green is what admits you. Your ready: marker stays valid across merge-main bot merges (see “A push makes the marker stale” below), so the steward can still see a PR that is a few merges behind.
  • Far back, stand still. If your PR is not in the next train or two (the steward’s status comment on your PR gives your position), do not merge main by hand to stay current. Take merge-main off as well: that label makes the bot merge main into your PR on every push to main (§”Your merge cadence is an input to the bot’s throughput”).
  • Near the front, catch up once. When you are in the next train, or the steward asks you to:
    1. put merge-main back on, or merge main by hand (prepare-merge §”Submodules: check the pointers BEFORE you stage the merge”);
    2. let CI go green;
    3. post a fresh signed ready:.
  • A conflict is the exception. If main now conflicts with your PR, fix it when you see it, wherever you are in the queue. A conflict is work, not churn.

A push makes the marker stale. A head newer than your ready: sha is not intake, unless every commit since is a merge-main bot merge. Post ready: again for the new head once CI is green on it. Do not post “ready soon”: it is not intake, and it costs the steward a read.

The steward’s side: reach a submitter who has fallen behind

While watching the queue, and after every reshuffle or disentangling of trains, the steward checks the PRs near the front. A PR there can be:

  • behind main and conflicted;
  • red, or with an owed run missing on its head;
  • carrying a stale ready:;
  • carrying needs-merge-human.

When the steward finds one, it tells the submitter. It never fixes the PR on the submitter’s behalf, and never waits in silence. It uses the skills it already has:

  • The status comment on the PR, edited in place. It states the position, what is missing, and by when. Its shape is the one in merge-conflict-patterns §”When a merge-train member is refused”.
  • A hand-back bean when the PR is ejected or handed back (same section). The submitting role picks it up even if the original session is gone. Every hand-back gets one, not only a train ejection. That covers Rule_Refused, Rule_HeadNotGreen, Rule_NotReady, and a merge:guard refusal on a PR the owner asked to land. A PR comment and a queue entry are not a hand-back on their own: the work plan cannot see them.
    • Shape: titled Merge refused: #<n> <cause>, type: bug, parented under the PR’s bean’s parent (or under the PR’s bean when it is an epic or feature), marked --blocking that bean.
    • Body: the refused checks, roles, the brief (link the takeover plan if there is one), where to report, Done when, and Fails if.
    • Dedupe: check first, because beans create dedupes on nothing.
    • Process: run it through processes/sdlc/merge-refusal.bpmn (workflow_start), not from memory.

    merge:steward now prints ✗ handed back with NO open hand-back bean: #… and carries handBackBean per PR in --json.

    Measured 2026-10-04: a steward handed back a dozen PRs in one session with comments and queue entries only. The owner asked “why did you have a process failure here?”. The procedure lived in another skill (merge-conflict-patterns) that the steward never loaded, and nothing checked for it. Owner: “Backfill + enforce”. The 13 beans were backfilled (mfhc and twelve more).

  • A direct message as the fast path, by the recipe below. Use it when the PR is next and waiting costs a train.
  • The queue entry records the hold or ejection, with its reason and expiry (§”The steward answers in the queue”).

A submitter told “you are next” catches up at once. That is the trade this section makes: CI is spent on the PRs about to land, not on the whole queue.

Address the ROLE, never a session

The Merge Steward is a lane in merge-train.bpmn, not a particular session, and whoever holds it today may not hold it tomorrow. So the handover must not depend on who that is:

  • no session id written anywhere, since it dies with its container;
  • no “they are active on PR #N”, since that PR may merge or the role may move;
  • no message as the channel.

Whoever takes the lane next reads the same open PRs and the same queue entries.

A direct message is a fast path on top of that, never a replacement. Owner, 2026-10-04: use one when it is the more efficient tool, and say how you used it. It saves the receiver a polling cycle, but it never counts as handing over: the PR’s own state still has to say ready. The same recipe works in both directions:

  1. Find whoever holds the other role now. Do not use a remembered session id.
    • Cloud: run list_sessions (claude-code-remote). Match the session whose title or external_metadata.current_branches shows the lane, or the PR’s head branch when you are looking for a submitter.
    • Same machine: run ListAgents.
  2. Check it can receive. For a cloud session, run get_session(id) and confirm cross_session_inbound == "available".
  3. Send one line. Use send_message (cloud) or SendMessage (same machine), naming the PR and the head sha. The PR comment stays the record; the message only points at it.
  4. If any step fails, stop. Do not post the message as a comment on some other PR or issue instead.

coordinate §”What actually reaches a sibling” has the measurements of which tool reaches which kind of session.

The steward answers in the queue, and the queue lives on the beans branch

Once the steward has decided something about your PR (a placement, a hold, an ejection), that decision is an entry under beans/queue/<pr>.json. The entry carries pr and beans, the beans and epics the PR serves; name them in your PR body so the steward can copy them. That list is the association between a PR and its beans.

Where to read it depends on the state-branch cutover (arc fs43, bean 9ofm). Bean ugxd moves the queue to its OWN branch, cat/cat-harness/merge-queue, ahead of fs43 — §”Where the queue is GOING” below — and where the two disagree, that section is the newer plan:

  • Before the cutover, main is authoritative. Entries reach it only through the steward’s own PRs, a PR cycle late, so the edited-in-place status comment on your PR is the live answer.
  • After the cutover, beans/ (with queue/ inside it) is authoritative on the orphan branch cat/cat-harness/beans. Read it with bun run cat state:mount. The steward writes an entry the moment it decides, then bun run cat state:push, which splices only the changed paths onto the tip and stops on a conflicting write instead of overwriting it. The entry is then live for everyone, and the status comment is a courtesy that points at it.

The branch’s manifest.json says which state it is in ("authoritative"). Read that rather than this paragraph.

The cutover also changes what a bean can do here. Today a bean created on your branch is invisible until your PR lands, so it cannot carry a merge request. After the cutover, a bean written through state:push is visible at once. A submitter can then note on the PR’s bean that it is submitted, and the steward can answer on the same bean. The PR state above stays the intake signal either way: the bean is where the conversation is kept.

Not GitHub’s merge queue. It is not offered on a personal-account repository (bean 1hjm). The merge-guard commit status (bean uoob) checks the six submission points mechanically; once it lands, run it against your own PR before you announce.

Where the queue is GOING, and the tool that will write it (bean najo)

Not cut over yet. The queue is still on main under beans/queue/ (owner, 2026-10-05: “Land code now, switch later”). The store and CLI below landed first; the cutover to its own branch, cat/cat-harness/merge-queue, is bean ugxd, a separate small PR during which the steward pauses queue writes. Until it lands, merge:queue:read reads the checkout and merge:queue:record refuses — keep committing entries to main as §”The steward answers in the queue” describes. The branch’s manifest.json says which state it is in.

Why the queue comes off main at all is structural: an entry on main arrives only through a pull request, and the steward does no development work — it must not open PRs of its own, which would have it setting its own priority in the queue it manages, spending the CI the queue is starved of, and judging its own head. For the two days after the graph was declared it therefore held no entry at all: the one actor whose decisions it records was the one actor that could not write to it.

After the cutover:

bun run cat state:mount                  # the queue on disk at its declared path
bun run cat merge:queue:read             # the recorded decisions
bun run cat merge:queue:record --pr <n> --class <c> --rank <r> --rule <id> \
    --reason "<why>" --by <your session URL>          # ...or --position <n> for an owner override
  • record pushes by default, as a splice onto the branch tip carrying the blob each file was read at. So the entry is live for every sibling at once, and a steward who edited the same entry since gets this write stopped as a conflict with the path named rather than overwritten. On a conflict, re-mount and decide again; nothing was pushed and the local edit is the only copy. --no-push leaves it in the mount, where nobody else can see it.
  • --reason and --by are required. A placement nobody can explain cannot be honoured or safely undone by the next steward, and every session here acts with the owner’s token, so the session link is the only thing that says which steward decided.
  • An unreachable queue is not an empty one. merge:queue:read exits 4 and readQueueEntries THROWS when the branch is not mounted, because “no decisions recorded, all clear” printed over a graph nobody read is dh4f. Exit 1 — declared-but-absent — is a different answer with a different remedy.
  • The decisions are read back. merge:steward feeds recorded overrides into the table’s ownerOverride input and their positions into the order, treats an un-cleared ejection as refused, and prints a live hold. Until 2026-10-04 it passed none of them: a decision nobody reads back is a decision nobody took.

The pairing with workflows/ is what this cost. The queue held the decisions and a finished train run next door held the evidence they met, in one store. beans/ is still on main until arc fs43 lands (beans 9ofm, p3ny), so for now the two are one state:mount apart. That trade was made knowingly: the pairing was costing the graph every one of its nodes.

Placement is computed, not chosen

GW_Placement is DMN-backed, so workflow_complete refuses a hand-supplied outcome there. The table (hit policy FIRST) answers a route (admit or hand back), a class (PRIORITY_CLASSES), a rank and whether the PR rides alone.

  • An owner override is an INPUT (ownerOverride), never an outcome. The owner places a PR by hand in Task_Override; the schema refuses an override placement without a reason.
  • Independence is decided on AUTHORED paths (authoredPaths, touchesShared). Generated paths are excluded because merge-conflict-patterns already resolves them; counting them would make every pair of PRs conflict.
  • Ties go to the oldest PR (orderQueue): overrides at their positions, then rank, then PR number.

Admission asks which runs are OWED, never whether anything is red

A member is admitted on ownCi == "green" and green means every workflow owed for the head’s event ran and succeeded. It does not mean “nothing is failing”. The two readings come apart, and the gap is the whole reason this section exists.

Measured 2026-10-02 on #1889’s head 7ab6119405:

check runs on the head9
gating job NAMES present, of 9 expected9 — a complete name set
runs in_progress with conclusion: null3
check SUITES completed with conclusion: action_required and latest_check_runs_count: 03
runs whose conclusion is failure0

So a filter for conclusion == "failure" returns nothing, and three suites completed having executed nothing at all. Counting the runs does not catch it either — nine were present and every expected name matched. Only asking which runs are owed for this event catches it, and check-head-has-run is the one thing here that asks (bean 3pqn). Do not write a second checker: it already keeps the states apart, has a test, and is already wired into pr-checks-present.bpmn.

Its five states are the five values of ownCi, and collapsing any two is the defect:

valuemeans
greenevery owed run present and successful — the only admitting value
redan owed run ran and failed
missing-requiredit HAS runs, but not the ones owed
noneGitHub answered and nothing names this head
unknowncould not ask: no network, rate limit, HTTP error

unknown is handed back, never admitted and never reported as red (bean 0qjq): telling somebody their head is unverified when you merely could not look trains them to ignore the signal, which costs more than the gap.

This is a gate, not advice. Rule_HeadNotGreen in merge-priority.dmn sits immediately after Rule_Refused under hitPolicy="FIRST", so anything but green routes to hand-back before any admitting rule can fire, and workflow_gate refuses a hand-supplied outcome.

A dispatch is not an owed run. workflow_dispatch is the designed workaround for the bot-actor push condition (bean 0qjq, and merge-main.yml dispatches the gating workflows for exactly that reason), but a dispatched run is evidence about the TREE, not evidence that the owed pull_request run exists. The session that wrote this section made that error on #1888 — read eight dispatched jobs as “green” because the names matched — and check:head-has-run caught it. Report the two facts separately or the distinction is lost.

Overlap has a KIND, and a train is the right answer to one of the four

conflictRisk answers is there a collision. A steward needs is it the kind a train is for, which is overlapKind:

kindmeanswhat it needs
noneno path shared with any live membernothing
generated-onlyshares paths, every one resolved by a declared patternregeneration
shared-declarationthe only authored paths shared are shared declarationsordering
authoredshares an authored path that is not a declarationa train

Four values rather than three, and the reason is measured rather than tidy. A shared declaration is a subset of an authored path, not a sibling: classify("package.json").strategy is refuse, so it is authored, and isSharedDeclaration("package.json") is true (both measured 2026-10-02). A three-value partition therefore files a declaration-only collision under authored.

That is not hypothetical. merge:overlap over all 36 open PRs on 2026-10-02 (459 pairs) found #1907 × #1909 independent: false with authored_overlap = 0 — colliding on package.json and generated regions only. A pipeline that treats that like a content collision builds trains nobody needed.

authored outranks shared-declaration, which outranks generated-only, because at each step the earlier kind is the one the later kind’s remedy cannot resolve. The partition is tested in scripts/tests/merge-queue.test.ts — including that a refused member cannot be the other side of a collision, since it never enters a train.

A train admits only members that STACK cleanly (STRICT)

Owner ruling, 2026-10-04 ~17:40Z: “require that a merge train has PRs that are stacked one on top of another. You’re wasting too much time resolving conflicts in the train and blocking other stuff from happening.”

A train is a stack: member n+1 merges onto main + members 1…n with no conflict at all, generated paths included, and with no semantic break (tsc and the gate set pass on the stack). Check every candidate before admitting it:

c=origin/main
for p in <members in order>; do
  t=$(git merge-tree --write-tree $c pr/$p) || { echo "#$p does not stack"; break; }
  c=$(git commit-tree $t -p $c -p pr/$p -m stack)
done

A member that does not stack is not admitted. It does not go in on the promise that merge:main will resolve it. It goes back to its own session, which merges main on its own branch, gets CI green, and re-signals. Conflict resolution belongs on the member’s branch, never in the train, and never in the steward’s time.

Why, measured on train merge-train-2026-10-04a (#2113): two members with declared conflicts took about an hour and five pushes to reach green:

  1. The train tool dropped the gitignored-but-tracked LSI files (bean u4up).
  2. That drop staled the auto-docs pages.
  3. Another session landed #2112, and the train conflicted again.
  4. Re-merging main took a 12-minute regen.
  5. A semantic conflict surfaced: #2043 imported a module that #2112 had moved.

While the steward resolved conflicts, nothing else landed. Landing the cleanly stacking PRs one at a time, as #2109, #2078 and #2102 did in the same window, took one guard run each.

The overlapKind table below still says what a collision NEEDS. The steward now provides only the none row. Every other kind goes back to its owner.

Regenerate on a clean tree, and check it

git status --porcelain is empty before a member is merged and the tree regenerated. Checked, not assumed, on Task_Admit.

Generated directory READMEs carry a per-subdirectory file count, and the writer counts the files git would commit — git ls-files --cached --others --exclude-standard (bootstrap-tools/scripts/git-files.ts:23). --exclude-standard keeps ignored junk out, which is the __pycache__ case that drove that line, but --others still counts an untracked file nobody has ignored. So a transient written by an earlier train step moves a committed count, readme:subgraphs:check goes red, and the red belongs to nobody. Train 6 hit this.

The precondition does not make the count stable, and saying so is the point: two members each adding a file still write different integers to one line. Bean y7b3 measured 76 of 300 replayed merges conflicting on beans/README.md, every one on the | defs/ | N files | line — and the class is wider than that bean states: 54 generated READMEs across 14 instances carry 207 such rows (measured 2026-10-02). merge:main already resolves them under readme-generated-regions, so the residual cost falls on merges made by plain git rather than by the pipeline.

The fix is bean ba9e: take the integer off main into the KG’s _data layer, which classifies take-base and is therefore already auto-resolved by merge-conflict-patterns.ts:171. Not -merge and not a merge driver — both are refused in .gitattributes on measured grounds, the first because it tidies conflicts without removing them and the second because it needs per-checkout git config.

A red train ejects one member, not the train

  1. Retry only a declared-flaky gate, and only once (Task_RetryFlaky). A deterministic gate retried learns nothing.
  2. Attribute from each member’s own CI first (Task_Attribute). A member whose own CI is red, or never ran on its head — GitHub runs no pull_request CI while a PR conflicts — is the prime suspect, at no cost.
  3. Bisect only when every member is green alone (Task_Bisect).
  4. Eject and hand back with the reason (Task_Eject, then merge-refusal.bpmn). The ejection is written on the member’s entry; the rest re-run without it. The owning session fixes and re-signals ready; it is never merged on its behalf.

A PR lands only through merge:guard (STRICT)

Every merge a steward makes goes through bun run cat merge:guard <pr> --merge --session <your session id>. Never gh api -X PUT …/pulls/<n>/merge, never the web button, never merge_pull_request from an MCP tool, and, while a Merge Manager is active, never by marking a PR ready or labelling it yourself first (with none active, see §”When no Merge Manager is active”). The script (cat-harness-tools/scripts/merge-guard.ts, bean uoob, child (f) of nok9) is the merge: it evaluates eight checks over GitHub’s live facts, and performs the PUT, pinned to the head it evaluated, only when every one passes.

When no Merge Manager is active, the owning session merges (owner ruling 2026-10-06)

The separation between the session that marks a PR ready and the session that merges it exists only while a Merge Manager is active. Owner, 2026-10-06: “that only applies if there is an active Merge Manager. there isnt one right now”, and “not just for this session”.

With no Merge Manager active, the PR’s own session lands it once the owner has said to merge. It still goes through the guard:

bun run cat merge:guard <pr> --merge --session <your session id> --no-merge-manager

--no-merge-manager lifts one thing: check 2’s refusal of a merging session that marked the PR ready itself. Every other check still applies, and so does check 2’s own-session attribution. In particular, the ready: <head sha> marker signed by this session, ready-to-merge, green owed CI and a clean merge are all still required. The verdict says the separation was lifted, so the landing is auditable.

Where the guard cannot run (for example a cloud sandbox whose gh has no usable token), read its verdict instead. The merge-guard workflow posts it as the merge-guard commit status on every head. Merge only when that status on the exact current head is success, and pin the merge to that sha (expectedHeadSha / sha=), which is the same pin the guard’s own PUT uses. Say on the PR that the status was read rather than re-run. Any other state (pending, failure, or a status on an older head) is a refusal, as it would be from the script.

How to tell whether a Merge Manager is active: the owner says so, or a live session holds the merge-steward lane (§”Address the ROLE, never a session”, step 1). If you cannot tell, ask the owner. Do not assume none. A stale memory of “there was one yesterday” is not an answer in either direction. “The owner said merge” is still required. Submitting to main is the owner’s call (§”Ask the user before submitting”), and this ruling does not change that.

Owner ruling 2026-10-03, after three PRs were landed unfinished by a steward calling the PUT directly on the same day:

PRlanded withcheck that refuses it
#1937base = #1764’s head branch, after #1764 had merged; head newer than its ready: (a hand merge and two claim commits since); an unsigned ready:; needs-merge-human on; its own pull_request runs failed, only a dispatch green1, 2, 3, 4, 5
#1960no ready-to-merge, no ready: comment, - [ ] CI green in the body3, 4, 6
#1957the steward itself called ready_for_review and added ready-to-merge 75 s before merging; no ready: comment2, 3

The eight checks, each named in a refusal by number and id:

  1. base — open, based on main, and the base is not the head branch of a merged PR. A stacked PR is retargeted by its owning session first.
  2. ready-for-review — not a draft, and the latest ready_for_review event is attributable to the PR’s own session, never to yours. With no Merge Manager active, --no-merge-manager lifts the “never to yours” half (§”When no Merge Manager is active”).
  3. ready-marker — a ready: <sha> comment signed with the PR’s own session link (the one in its body), naming the head, or with only merge-main bot merges after it.
  4. labels — ready-to-merge present, needs-merge-human absent.
  5. ci — every pull_request run on the head is success or skipped, and every workflow owed for that event ran. A workflow_dispatch run counts only as the stand-in for a pull_request run GitHub held for approval (action_required, bean 0qjq) on a head that is a merge-main bot merge: merge-main has already merged main into that head, so the head IS the merge, and a dispatch on it judges the tree that would land. The latest dispatch of the same workflow on the head decides — green counts, running is not-ready, red is a defect. Held with no dispatch: not-ready if merge-main.yml’s for wf in … ; do gh workflow run line names that workflow’s file (the dispatch is still owed), reported not judged if it does not (Feature Staging, preview-only), and unknown if that line cannot be parsed. A green dispatch never rescues a pull_request run that executed and went red (#1937), and anywhere else it is reported and not counted. Feature Staging is judged, never waited for (owner ruling 2026-10-05, bean gnnj): a staging run still in flight, or not started, is not a refusal — its deploy is held by the #1956 gh-pages rate limit, measured at up to 41 min with eight previews queued — and the verdict says so as not waited for. Once it finishes, red still refuses, since stage runs real checks before it deploys — unless the only failed step is the deploy itself (DEPLOY_ONLY_STEP): a push window that never opened, or three rejected pushes, is not a verdict on the tree (owner ruling 2026-10-05). The guard reads the run’s failed steps to tell; if it cannot read them, red refuses as before. The list is NOT_WAITED_FOR_WORKFLOW_FILES in merge-guard.ts.
  6. checklist — no unticked - [ ] in the body.
  7. open-question — no comment after the marker asks the owner or the Merge Manager a question (a heuristic; its limits are on openQuestions in the script — a fresh ready: after the answer moves the window).
  8. mergeable — GitHub says the head still merges cleanly into main (bean vihx). Check 5’s runs tested the head merged with main as main was when the head was pushed; once main moves, a stale green head with a valid marker passed checks 1-7 while it conflicted (#1898, and two HTTP 405 “merge conflicts” refusals on 2026-10-05). The PR API’s mergeable: false (mergeable_state: dirty) refuses not-ready — merge main into the head and re-sign ready:. mergeable: null means GitHub has not computed it yet: the guard re-asks for up to 14 s, then reports unknown, never a pass. unstable and blocked pass here, because they are about checks and reviews, which check 5 and the owner judge. The status workflow does not fire when main moves, so a posted success can go stale on exactly this check; --merge re-evaluates live.

Exit 0 is pass (or merged), 1 refused, 2 could not determine — never a pass. A refusal is handed back to the owning session through merge-refusal.bpmn with the check’s text; the steward does not fix the PR to make it pass, which is the move all three incidents made.

What the OWNING session does, so a finished PR passes: mark it ready, then post ready: <head sha> signed with its session link — https://claude.ai/code/session_… in the footer — within ten minutes of each other, and apply ready-to-merge. Every session acts with the owner’s token, so the timeline’s actor is always the owner and cannot tell sessions apart; the signature is the only thing that can. An unsigned marker, like #1937’s, is refused.

The status has four states, and red means wrong, not unfinished. A PR that is merely not ready yet (a draft, no marker, no label, an unticked box, CI still running, an open question, a conflict with main) posts pending. Only a defect posts failure: a base that is not main or is dead, needs-merge-human, red CI on the head, or a marker or ready-flip by a session other than the PR’s own. success is pass and error is could-not-determine. The status is posted on every open PR, and red on every draft would teach readers to ignore red. A required check blocks the merge in every state but success.

The status is the backstop, not the gate. .github/workflows/merge-guard.yml runs the evaluate mode on label, draft, body, comment and CI-completion events and posts a merge-guard commit status on the head. Making that context REQUIRED is a ruleset the owner adds; this skill does not, and no agent changes repository settings.

It runs only on events that can change a verdict (#2099: ~25 runs in the 6 min after #2000). A comment counts only when it carries text a check reads — ready:, a ?, a claude.ai/code/session_ footer, or merge-main’s marker; a CI completion only on a PR head; an edited only when the body or base changed. So a check that starts reading a comment for anything else must add its text to the workflow’s if:, or the status goes stale on exactly that comment. Evaluations collapse per PR. The status is a snapshot either way, and --merge re-evaluates live; never land on a status alone.

Rule_NotReady in merge-priority.dmn applies the cheap half of checks 1-3 at placement (readiness, from LivePr.draft, baseRef, readySha, readyBy), so an unfinished PR is handed back before it is put in a train rather than at the PUT.

When the authors stall: the steward dispatches unblockers

Owner, 2026-10-04, verbatim: “dispatch agent to handle where CI isnt green. make sure you dont use up all github workspace” and “if there is something small an agent can handle that unblocks, then do so”. Authors’ sessions hit usage limits together, so the whole hand-back column can go quiet for an hour while every member in it is one small fix away from admission.

This adds a second actor to step 4 above. It changes nothing else. An unblocker fixes the head. It never merges, never adds ready-to-merge, and never posts a ready: marker. The author still vouches for the PR, and the steward still lands it only after the author signals ready, or under the owner’s explicit release.

Who goes: the PRs merge:steward routes to hand-back. Leave out drafts, PRs whose author was active in the last 15 minutes, PRs another session has claimed, and PRs under an owner block (a ruling such as “this blocks the PR” is a judgement about content, and fixing the code does not lift it). Group them into a few agents, each in its own worktree. The general swarm rules are in swarm-management.

Every unblocker gets the same rules, written once and handed to all of them:

rulewhy
at most ONE push per PR, after bun run cat gates locally — or, when every conflict was generated, after the targeted checks of merge-conflict-patterns §”A merge round — run each check ONCE”each push runs full CI and deploys a staging preview into gh-pages, which has a size budget. Pushing speculatively spends both
no workflow_dispatch, re-run, empty commit or close/reopenthe same budget, and a dispatched run is not an owed run (above)
merge commits only: no rebase, amend or force-pushit is somebody else’s branch
authored conflict → resolve only dead code, or a pure addition carried over verbatim; otherwise quote both sides and stand downchoosing between two behaviours is the author’s call
git submodule update --init and bun run cat state:mount before regenwithout the submodules merge:steward cannot load. Without the mount, fsh-guts:viz:check exits non-zero (it judges that the mounted graph renders; the page itself is built at publish, bean 0b8c) and audit:coverage:strict goes red
delete the worktree’s node_modules at the endfour parallel installs run out of the container’s disk
one comment per PR: root cause, the commit it pushed or the reason it stood downthe author comes back to an explanation, not a mystery commit

missing-required is often a head that CI never started, not a slow one. The causes to check, in order:

  • the PR edits a workflow file and GitHub cannot parse it, so no run is created at all;
  • the head was pushed with GITHUB_TOKEN, which starts no pull_request run;
  • the run is held at action_required.

The first one is not visible in the check list, so look at the YAML.

After any merge of main, check for silently dropped files. Some files under */test/results/ (LSI indexes, QA and detangle sidecars) are gitignored but still tracked. Once a resolution step removes one from the index, regen rewrites it on disk and git add -A does not stage it again. Local checks still pass, but CI’s fresh checkout fails. merge:main and the merge-main bot now refuse such a merge and name the paths (droppedInMerge in merge-base.ts, see merge-conflict-patterns §”When one side deleted the file”). A hand git merge gets no such check. After one, run git diff --diff-filter=D HEAD^1 HEAD -- '*/test/results/*', and restore anything that main still tracks with git checkout HEAD^2 -- <path> (bean 8j9e).

Stalled for days, with no handover report: write it, then hand it to a takeover agent

Owner, 2026-10-04, verbatim: “several agents (~8) just stalled for week. they didnt generate handover reports. disaptach agent to review PRs and generate plan … to takeover work by anotther agent”.

An unblocker suits a short pause. When the authors are gone for days, the steward does something else:

  1. Writes the handover report each author did not. It goes as one comment per PR, headed ## Takeover plan, in the handover-report format and measured, not recalled: intent with the owner’s rulings quoted; head SHA, conflicts split into authored and generated, owed CI; done and remaining; approval and any scope drift; ordered next steps with their falsifiers; owner questions; couplings with other open PRs. This step is read-only apart from the comment.
  2. Gives the owner one paste-able brief for the takeover agent: the cold start, the work order across PRs (the couplings decide it), the merge rules, and the open owner questions. The owner starts that agent, possibly on another account, so the brief must stand alone.
  3. The takeover session becomes each PR’s owning session. merge:guard accepts a ready: marker only from the session the PR body names, and check 2 keeps the merging session from vouching for its own landing. So the takeover agent edits the body’s session line to its own session, records “taken over from (stalled)", claims the beans, and then signs. Owner, 2026-10-04, chose this over the steward signing or a one-time bypass: *"Wait for takeover agent"*.

A PR that is green but refused by checks 3 and 4 (no marker, no label) is waiting-on-author in the queue, not active, until the takeover session signs it.

Landing

Re-run merge:steward after every merge, before the next one. Each merge moves main. A member admitted against the old main can turn dirty and then cannot land at all. Measured 2026-10-04: #2059 was admitted at f8e1006d, then reported dirty once #2069 landed. Its tested head is still green, but GitHub refuses to merge a conflicted PR, so it waits for its merge:main round and fresh CI on the new head.

Only with the owner’s release, which is now the authorisation half of merge-to-main (Call_MergeToMain in merge-train.bpmn; it was Task_Release until 2026-10-09): explicit, or a standing ruling quoted verbatim with its date, and only through bun run cat merge:guard <pr> --merge --session <id> (section above). What lands is exactly the SHA CI tested — the guard pins the PUT to the head it evaluated, so GitHub refuses it if the head moved; if main moved after the train’s CI started, re-run rather than land.

A standing release covers the queue AS IT STOOD, not every PR that arrives later. Owner, 2026-10-04, verbatim: “my approcal - that counts for the PRs that are in queue. new PRs need my approval exp-licity (through you or siblign)”. So on every sweep the steward lists the PRs opened since the release, and puts them to the owner in one question, with the table’s verdict on each. One answer (“approve all 6”) may cover the whole batch.

Post-merge asymmetry: why main needs its own regeneration pipeline (bean ey1c)

.github/workflows/merge-main.yml runs bun run cat merge:main on an opted-in PR branch when main moves, resolving declared conflicts and regenerating. There is a structural asymmetry in the other direction: after a PR merges into main, nothing in that workflow regenerates main.

So any merge changing an enumerated or counted directory can leave main red on a hard gate, even when the PR merged cleanly with zero git conflicts. The only reason it usually does not is an accident: the PR branch happened to regenerate against a base that had not moved yet. When that accident does not hold, main goes red and stays red until somebody notices.

The accidental-repair pattern (observed 3 times in 2 hours)

On 2026-10-03 on main’s own runs, step 65 (“Every declared directory’s README is current”, readme:subgraphs:check) demonstrated this pattern:

5187a4df361  after #1936                               green
8688288494a  after #1938 (arXiv licences → uploads/)   RED
268c0911a06                                            RED
ea1d8b10593  after #1940                               green
70882785607  "Add files via upload" (web UI)           RED

PR #1938 added PDFs to uploads/ without updating the generated table in uploads/README.md. It broke main. Then PR #1940’s branch happened to carry a regenerated README, which repaired main incidentally; then a web-UI upload broke it again with no generator run at all.

The trap: A green on a derived gate must NOT be read as “was never broken” or “the commit before it was clean”. Unrelated PR branches that happen to carry regenerated files incidentally repair main, creating untrustworthy gate histories.

Why this halts development: While main is red on a hard gate, every open PR inherits that red, so no PR can show green, and nothing can be merged on green CI. PR sessions start debugging their own diffs for failures that belong to main.

The automated post-merge repair pipeline

To resolve this asymmetry, .github/workflows/post-merge-regen.yml triggers on push to main and runs bun run cat post-merge:regen --create-pr.

  1. Detected and ACTED on: It audits verify/write pairs on the post-merge tree. If derived artefacts are stale, it does not merely fail silently or log red; it acts by running the generators and generating a reviewable repair.
  2. Reviewable PR, never a direct push to main: The repair is pushed to an automation/post-merge-regen-<sha> branch and opened as a PR targeting main. This ensures the repair cannot bypass gates, full PR CI runs on it, and it remains auditable and reviewable.
  3. NEGATIVE CONTROL: A merge that changes nothing counted triggers 0 repairs and opens NO PR (status: "clean", exit 0), preventing churn (bean do70).

Merge steward procedure: post-merge verification

Decision on merge steward role vs automation:

  • Automation handles post-merge verification: The steward does not manually run regen sweeps after every landing. Automation via post-merge-regen.yml executes on push to main across all types of landings (steward trains, direct pushes, web uploads).
  • Steward triage when main goes red on a derived gate: If main turns red on a verify/write check (e.g. readme:subgraphs:check, check:prov-qaqc), the steward checks whether an automated repair PR (automation/post-merge-regen-...) has already been opened before dispatching unblockers or opening duplicate PRs.
  • Priority admission for repair PRs: When the automated repair PR passes CI, the steward admits and lands it with priority under merge-priority.dmn as an unblock-main repair.

ACK every PR that enters the queue, on its bean (STRICT)

Owner, 2026-10-04: “as part of merge manager skill you need to ACK a new PR in queue on its bean”. A submitter who hears nothing cannot tell “queued” from “lost”, and the bean is where the next session looks.

When a PR enters the queue (first seen on a sweep, or approved by the owner), the steward writes three things in the same change:

  1. The queue entry, beans/queue/<owner>--<repo>--<pr>.json. It holds the table’s placement, a reason that begins ACK: and says what the PR waits on, and beans, copied from the PR body. Validate it against MergeQueueEntrySchema.
  2. A note on each bean the PR serves, one file per bean per branch: beans/notes/<bean>--<date>--<branch>.md. A note, never an append to the bean def; bean-coordination says why. Then run bun run cat beans:notes, or CI’s beans:notes:check goes red.
  3. One comment on the PR naming the entry and the beans. Before the beans cutover, items 1 and 2 reach main only through the steward’s own PR, a cycle late, so the comment is the ACK the author sees now. After the cutover, write 1 and 2 with state:push; the comment then just points at them.

A PR whose body names no bean gets the entry with beans: [] and a comment asking the author to name one. The steward does not invent the association.

Every open PR has an entry: review the whole list, then triage it

Owner, 2026-10-04, verbatim: “review all open PRs…. anything not in merge queue? put in as status unknown any found. then try to figure out where they are, what’s stale, etc.” and “add to merge manager skills.”

merge:steward skips drafts, and a PR that nobody ACKed has no entry, so the queue silently shrinks to the PRs someone happened to look at. Measured on the first review: 18 of 26 open PRs had no entry, 9 of them drafts.

On a review, and at least once per working session:

  1. List every open PR, drafts included, from the API; not from the steward’s table and not from memory.
  2. Every PR without an entry gets one with status: "unknown". A draft gets the placement rule: "draft", class: "hand-back", rank: 99, and its decision names the steward, not the DMN, because the table never placed it. Claiming the table did would be a fact the steward invented.
  3. Triage each one into a status (QUEUE_STATUSES in schemas/merge-queue.ts), with the evidence in reason:

    statuswhen
    activeits author pushed or commented within 24 hours
    waiting-on-authorhanded back with a named ask, and the author is not back yet
    blockedwaits on another PR, bean or ruling, named in reason
    staleno push and no human comment for more than 24 hours. Bot pushes from merge-main do not count as activity
    approval-voidthe owner approved it, and then its scope changed
    landedmerged. The entry stays as history; it is never deleted
  4. Report the stale ones to the owner as a decision, never close them: closing a PR is the author’s or the owner’s call (deletion-requires-confirmation).

An approval is for the content that was approved. Measured on the first review: #2082 was approved as a three-bean close, and an hour later its branch carried an 11-file code change under a new title. Compare the title and the changed-file count against what was approved before landing. On a mismatch, set approval-void and ask again. The typed fix is a release bound to releasedSha (the zmdo session’s draft on #2065); until it lands, this check is the steward’s.

Your merge cadence is an input to the bot’s throughput

Pacing and concurrency are one question, not two. merge-main.yml sweeps the merge-main-labelled PRs on every push to main, so each merge you land starts a sweep — and the per-PR merge job’s concurrency group decides what happens to the sweep already running.

Measured 2026-10-03 over the last 100 runs of that workflow (bean o8s9):

triggersuccesscancelled
push2021
pull_request_target142
workflow_dispatch02

Half of every push-triggered sweep was discarded. Run 37111610566 is the worked case: twelve per-PR jobs, six cancelled inside ten seconds of each other by the next sweep, and five of those six PRs still conflicted when re-probed minutes later. Sweep wall time is 1-15 min while a steward draining the queue lands a merge every 5-10, so under cancel-in-progress: true a sweep rarely survived to finish.

The rule, now that cancel-in-progress is trigger-dependent and a push sweep QUEUES rather than kills:

  • A sweep in flight is work in progress — do not count a PR as unmergeable while its merge job is queued behind your last landing. Re-read it after the sweep settles.
  • When you are landing faster than the sweep completes, you are the reason the behind-PRs are not catching up. The concurrency fix bounds the loss at zero rather than half, but a merge still carries only main@T(n-1) -> T(n) per sweep; a burst of landings leaves a backlog of steps to carry.

A hold has an EXPIRY and a trigger, or it outlives its reason

A hold is a decision, and like every queue entry it records what the steward decided — not a fact. Facts move. So a hold is only valid while the condition that justified it is still true, and the steward re-derives that condition at every sweep rather than inheriting it.

Write a hold with three parts or do not write one:

partwhy
what it waits onthe condition, stated so it can be checked mechanically
what ends itthe observation that lifts it, not a time
what it costswhich PRs are being held, so the price is visible

Measured, 2026-10-03. A steward froze merges so a merge-main sweep could finish without being cancelled — correct at the time, and the owner confirmed the order. The sweep finished. The hold did not. It then survived three more sweeps, and the steward kept reporting “held for ordering” while four PRs sat clean and green for roughly forty minutes. The owner had to ask “what is blocker on merging?” to end it. The answer was the steward.

Two specific failures worth naming, because both look like diligence:

  • Inheriting a hold across its own justification. The premise was “a merge now cancels the in-flight job”. Once that job completed — and once bean o8s9’s fix made push sweeps queue instead of cancel — the premise was simply false, and nothing re-checked it. A hold whose reason you cannot restate from current facts is not a hold, it is a habit.
  • Holding to protect a PR that was already conflicted. The train head was rc=1 and needed a forward merge whatever happened, so holding other merges protected nothing. If the thing you are protecting already needs the work your hold is avoiding, the hold is free of benefit and not free of cost.

Sweep the whole queue; a tracked handful is not the queue

Compute the mergeable set from the open-PR list every time, never from the PRs you happen to be following. The same session that held four green PRs was also unaware of two others that had opened and gone green in the meantime — they were not in its mental list, so they were in no list at all.

The sweep is cheap and mechanical: for every open non-draft PR, read base.ref, run git merge-tree --write-tree origin/main <head> for the rc, and read the check runs on that exact head. Three columns, one pass. A PR with base == main and rc == 0 is a candidate; whether it is mergeable is still the OWED question above, not a count of failures.

no failures and nothing pending is NOT green, and this rule got that wrong on its first use. The sweep that followed it marked #1953 mergeable on ok=1 bad=0 wait=0 — one completed check, the .jsonld siblings one, against the 23 that a full suite produces here. Nothing had failed and nothing was pending because almost nothing had been asked. That is bean 1xhc inside the sweep itself: a gate that did not fire is indistinguishable from one that passed, and a tally of failures cannot tell them apart.

So the third column is the owed SET, never a count: compare the check-run names on that head against the set owed for its event, and treat a missing name exactly as a red one. A steward that cannot say why it is not merging a PR which is green on the owed set is holding it by accident; one that merges on bad=0 wait=0 alone is merging unverified.

Read base.ref BEFORE anything else: a stacked PR is not a main-queue member

merge-tree origin/main <head> answers a question nobody asked when the PR’s base is not main. Mergeability, independence and placement are all computed against the base, so a stacked PR measured against main is measured against the wrong tree — and the merge button lands it on the wrong tree too.

Measured, 2026-10-03. PR #1937 was based on claude/quirky-davinci-ixuymr, another PR’s branch. Every check the steward ran was against origin/main: rc=0, 16/16 green, all true and all irrelevant. The merge went to the stacked base, the PR closed as merged, and its nine commits were not on main. It took a second PR to re-land, and a sibling agent independently misdiagnosed the absence as a force-push eating a merge — a scarier cause than the truth, with a destructive remedy attached.

The tell is cheap and it is first: gh api repos/<o>/<r>/pulls/<n> --jq .base.ref. In one sweep of 30 open PRs here, four had a base other than main. A queue sweep that does not print the base will mis-handle roughly one PR in eight.

A stacked PR is not admitted to the main queue. Either its base lands first and the PR is re-measured against main, or it is rebased onto main by its author. The steward’s job is to notice, not to resolve it.

And git submodule status does not catch it. This is bean ygga one layer deeper, and it defeats the check that bean prescribes.

A merge resolves a submodule gitlink like any other path. git add -A then re-records every submodule at whatever the working tree happens to have checked out, overwriting the resolution. If you initialised submodules before merging, and the base advanced them, you have just reverted them to the merge base — and git submodule status shows no + and no -, because the index and the working tree agree with each other. They are simply both wrong.

Measured, 2026-10-03, on #1819. Main had moved bootstrap to ebfa406 and bootstrap-tools to 3046412; the branch never touched either, so the three-way merge correctly took main’s side, and git add -A undid it. The symptom was not a submodule error — it was regen reporting five checks as “a real defect, not staleness” (readme:sync in its four spellings plus translate-bpmn:bootstrap), all of them one module-load failure:

SyntaxError: Export named 'generatedBanner' not found in
  .../bootstrap-tools/scripts/generated-by.ts

generatedBanner exists only from 3046412. With the pins right, all five pass untouched and regen settles at 0 unrepaired. The agent had run bun install, so this is not the missing-dependencies case either.

Two rules follow:

  • Stage explicitly after a merge, never git add -A, while any submodule is in the tree.
  • Verify the pins against the ref, not against the working tree. git ls-tree <ref> bootstrap bootstrap-tools is what exposes this; git submodule status cannot, and reporting it as clean on that basis is the 1xhc failure — a check that cannot see the defect is not evidence of its absence.

A local gate run in a contended container is not evidence — and here is the ratio

Measured 2026-10-03. A full bun test shard in this container: 14,625 tests across 715 files in 1438.88 s. The workflow’s own comment puts the same shard at 2 m 07 s – 2 m 21 s on a dedicated runner. That is roughly ten times slower, at load average 11 with three concurrent bun test runs and a regen belonging to other sessions.

At that ratio the default 5 s per-test budget stops measuring the code:

14554 pass · 57 skip · 14 fail
grep -c "timed out after 5000ms"                    -> 14
grep -cE "^error:|Expected:|Received:|toBe|toEqual" ->  0

Fourteen failures, every one a timeout, not one assertion failure, at real durations of 5.0–10.9 s. The same shard was green on the dedicated runner.

So the signature is cheap to check and worth checking before you believe a red local run: all failures are timed out after Nms and the assertion-failure count is zero. That is contention. Report it as inconclusive under contention — never as green, and never as a defect — and let CI on the exact sha be the authority. What you must not do is “fix” a test that is not broken, and never skip, disable or quarantine a test to get green applies with full force here, because the temptation is strongest when the failure is not real.

Two ways this measurement was nearly got wrong, both bean 0s6w:

  • The agent first reported “exactly one failure” from a partial log, then the run finished at 14. A count read before the run ends is not a count.
  • A compound command’s exit status is the last command’s, and a test runner’s status has to be captured before anything else runs.

Agentic pre-merge review: warn-only and the would-have-blocked counterfactual

Bean h1uq. Owner ruled agentic merge review warn-only on 2026-10-02 and confirmed 2026-10-03: “warn only. proposal predates ruling, update it.”

Architectural decision: staying WARN-ONLY with measured justification

The decision to keep agentic code review warn-only is governed by empirical evidence, not caution:

  1. Literature gap: Across five ingested agentic SE papers (CodeAgent arXiv:2402.02172v5, SWE-Debate arXiv:2404.04834v4, SWE-Router arXiv:2507.23348v1, TCAndon-Router arXiv:2601.04544v1, He et al. arXiv:2607.00053v1), zero empirical false-positive rates (FPR) exist for an LLM judge.
  2. CodeAgent’s unconfirmed flag rate: CodeAgent’s own human annotation revealed that 48.6% of GPT-4’s flags were unconfirmed. A reviewer where nearly half of all flags are unconfirmed cannot be a blocking gate.
  3. SWE-Router & SWE-Debate noise boundaries: LLM-as-a-judge suffers from self-enhancement bias, verbosity bias, and positional bias.
  4. Deterministic compile checks stay blocking: G5 (Lean module compilation), G6 (SUSHI FHIR compilation), and G7 (JSON-LD schema validation) are deterministic compile gates that have exact binary outcomes and remain blocking. An agentic reviewer does not share this property.
  5. Backlog inversion: A blocking gate over an active codebase with unreviewed content nodes would fail on existing background patterns rather than PR delta, inverting the order of necessary backfill.

The counterfactual instrument: why only a warn-only phase can measure FPR

A blocking gate destroys the counterfactual: when a gate blocks, authors modify code, bypass checks, or abandon PRs. It is impossible to determine whether the flagged condition would have actually broken production or represented a false positive.

By contrast, a warn-only gate records blocking-severity findings as “would have blocked” (WouldHaveBlockedRecordSchema in schemas/merge-queue.ts), pinned to the PR’s headSha. The PR then merges to main. Subsequent monitoring observes whether any defect materialized, enabling humans to adjudicate findings:

  • true_positive: Finding correctly identified a genuine defect.
  • false_positive: Finding flagged benign, correct code.
  • unknown: Third state (inconclusive, unverified, or cannot be determined).

The third-state rule: unknown is strictly preserved and never folded into true_positive (which would artificially deflate FPR) or false_positive (which would artificially inflate FPR).

Pre-registered promotion criteria

To prevent choosing thresholds to fit data, promotion criteria are registered as numbers and windows before data collection begins (AgenticPromotionCriteriaSchema):

  • minimumPrWindow: 50 evaluated PRs.
  • maximumFalsePositiveRate: <= 0.05 (5% FPR: confirmedFalsePositives / (confirmedTruePositives + confirmedFalsePositives)).
  • minimumReviewCoverage: >= 0.80 (80% non-unknown adjudication: (confirmedTruePositives + confirmedFalsePositives) / warnedFindings).

Until all three criteria are empirically proven, the agentic review remains warn-only.

Tool-integrated agentic pre-merge review (CodeAgent / 2402.02172v5)

Evaluated under bean gs0u and codified in methodologies/agentic-pre-merge-review.md.

Multi-agent tool-integrated code review architectures (Tang et al., arXiv:2402.02172v5) structure automated pre-merge evaluation into specialized roles (Reviewer, Coder, QA-Checker, Consistency, Security) across four sequential phases.

Three operational rules govern its integration into this repository’s merge queue:

  1. The Deterministic vs. Judged Boundary (Owner ruling 2026-10-02/10-03, bean nok9): Deterministic compile gates (Lean formal proofs, SUSHI/FHIR IG AST, JSON-LD schema renders, linter and test suites) answer questions with one computable truth — these MUST BLOCK. LLM-mediated agentic code reviews answer questions with uncalibrated error rates — these MUST WARN (“would have blocked”), exiting 0 and logging structured finding sidecars.
  2. The 49% False-Positive Guard: Standalone LLMs exhibit up to 48.58% false-positive rates in code vulnerability detection (arXiv:2402.02172v5, Table 2). An automated review gate must never block merge on unverified LLM assertions, which induce severe alert fatigue. Promotion from warn to block requires measured calibration against recorded repository defects (plj1, dh4f, w4tq, 7u3g).
  3. Bounded Dialogue Budgets: Conversational QA-Checker loops must cap at a maximum of 2 adjustment iterations to avoid latency explosions (CodeAgent measured 5 min / $0.122 per review) and context window exhaustion. Deterministic formatters and linters must run directly in CLI gates (bun run cat gates) rather than consuming multi-turn LLM reasoning.

What this does not do yet

The train’s size is a fixed cap. Sizing it by risk waits for this process’s own run records, which is why each run is a committed instance under beans/workflows/. The tools named on the steps (merge:overlap, merge:train, merge:leftover) live on branch claude/merge-pipeline-tools until it lands.

Processes that run this skill

processstep(s) that name it
A merge trainRead each queued PR’s live facts; Take the next PR in queue order; Admit it to the train, record the train id; Attribute the failure: each member’s own CI first; Bisect the train; Eject the culprit, record why

folio-assistant — code under the Apache License 2.0, documentation under CC BY 3.0. Generated schema and API references are rebuilt from source on every push to main.

deployed b08c961 · 2026-10-23T23:11:09Z