"Merge a task branch to main via the orchestrator's merge queue using the submit→poll protocol. Use this skill whenever you need to merge a completed task branch into main and the orchestrator might be running — it submits via merge_request(wait_secs=100) then polls merge_status until the outcome is terminal, preventing races without blocking indefinitely. Trigger this when an agent says 'merge to main', 'submit merge', 'merge task branch', finishes fixing a blocked task and needs to merge, or any time code on a task branch is ready to land on main. If the escalation MCP isn't reachable, the skill falls back to direct merge. Prefer this over raw git merge --no-ff whenever working in the dark-factory repo."
Resources
1Install
npx skillscat add leodearden/dark-factory/merge-queue Install via the SkillsCat registry.
Merge Queue
When the orchestrator is running, all merges to main go through the merge queue — a serial worker that rebases, verifies, and atomically advances main using compare-and-swap. This prevents races between concurrent tasks, the steward, and interactive sessions.
The escalation MCP exposes a merge_request tool that lets you submit to this queue from outside the orchestrator workflow. This skill tells you how to use it.
Core rule: every
merge_requestcall passes an explicit boundedwait_secs; completion is awaited only viamerge_statuspolling.The server default is now
0(immediate, non-blocking). Passing an explicit boundedwait_secs(use100) lets a fast or idle-queue merge resolve terminally within the submit call, avoiding a poll round-trip. Never omitwait_secs.
Why this matters
Direct git merge --no-ff into main bypasses the merge queue. If the orchestrator is also running, you'll race with its merge worker — two actors trying to advance the same ref simultaneously. The queue serializes this safely. It also runs post-merge verification and prevents .task/ directory contamination from reaching main.
Workflow
1. Prepare your branch
Before submitting, make sure your branch is in mergeable shape:
# In the task worktree
git rebase main
# Resolve any conflicts
# Run verification (tests, lint, type-check)Pre-rebasing reduces the chance of conflicts inside the merge queue. If you skip this, the queue will attempt the merge anyway but is more likely to return conflict.
2. Check if the escalation MCP is reachable
Call any lightweight escalation MCP tool to confirm the server is up:
mcp__escalation__get_pending_escalations()- If it responds: proceed to step 3 (use the merge queue).
- If it errors or times out: the orchestrator isn't running. Fall back to direct merge (step 6).
3. Submit the merge request
mcp__escalation__merge_request(
task_id="<TASK_ID>",
branch="<TASK_ID>",
worktree="<path to worktree>",
description="<brief description of what's being merged>",
wait_secs=100,
verified_green=<True|False> # decide per the verified_green rule below — do not default to True
)Parameters:
task_id— the task number (string)branch— the task ID only (e.g.,"466"), not the full branch name. The merge worker prepends thetask/prefix automatically.worktree— absolute path to the task's worktree (e.g.,/home/leo/src/dark-factory/.worktrees/42/)description— optional context for logswait_secs=100— always pass this explicitly. The value 100 equals the server's maximum bounded wait (_MAX_WAIT_SECS), so fast/idle-queue merges return their terminal outcome in the same call. The call always returns within ≤100 s.verified_green— a per-submission decision, not a default. PassTrueonly when the verification suite (tests, lint, type-check from step 1 "Prepare your branch") actually ran and passed on this branch, on top of its own base; omit it (or passFalse) if you skipped verification, or it failed and you're submitting anyway for an unrelated reason (e.g. resubmitting after aconflict/blockedfix-up you haven't re-verified). This vouches to the merge queue that the branch was seen green pre-merge — it emits aworkflow_verifyevent so a later merge failure caused by an unrelated main landing can be attributed asINTEGRATION_SKEWinstead of degrading toINDETERMINATE. Since/dolands its branches through this skill, honoring this rule here also covers/dosubmissions./unblock's step 6 D2 is this same rule, with an added D1/D2 split for its rebase/re-verify loop — an agent holding both skills is applying one rule, not two.- Caution — not retractable: the classifier's green fact is any-prior-green, keyed by task ID — once
verified_green=Truehas been emitted for a task ID, omitting it (or passingFalse) on a later resubmission for that same task ID does not undo the earlier green. If you resubmit after an unverified fix-up (e.g. a quick conflict resolution you didn't re-run the suite on), a genuineBRANCH_BUGin that fix-up can still be misattributed toINTEGRATION_SKEWon a later merge failure, because the classifier only sees "this task ID was green once," not which commit. When in doubt, re-run verification before resubmitting rather than relying on an earlierTrue.
- Caution — not retractable: the classifier's green fact is any-prior-green, keyed by task ID — once
The call returns with either a terminal status or a non-terminal status:
Terminal at submit time (
done,already_merged,done_wip_recovery,conflict,blocked,unknown_branch,unmerged_state,stash_failed,wip_halted,wip_recovery_no_advance,error,superseded): the merge resolved within the bounded wait. Jump straight to step 4.- The six worker-internal outcomes (
wip_halted,done_wip_recovery,wip_recovery_no_advance,unmerged_state,stash_failed,error) are rare;merge_statuscollapses all of them exceptdone_wip_recoverytoblockedwhen observed by polling (escalation/src/escalation/server.py::_map_terminal_state) — handle them asblocked. (failed, which this list named until task 4829, is not a value the server ever returns; the real one iserror.) already_mergedmeans the branch tip was already an ancestor of main — treat it the same asdoneonly when the response carries acommit, or the canonical ancestry check's exact-subject marker search finds the merge on main. A branch that never advanced past its creation point is an ancestor of main too, so the ancestry fact alone does not establish that anything landed: whencommitis falsy and that search comes back empty, do not stamp done — run that check and treat "nothing on main cites the task" as not landed.supersededmeans your request was replaced by a successor before it could be individually processed. The response includessuperseded_by: "<mr-* request id | coalesce-* train id>". See Follow the superseded successor below to determine how to resolve it.
- The six worker-internal outcomes (
Non-terminal (
queued,attached): the submission succeeded as durable intent — the merge worker has accepted the request and will process it. This is not a failure. Therequest_idin the response identifies your submission. Proceed to "Poll for completion" below.attachedmeans your submission was coalesced with an already-in-flight request for the same branch. Whether you share that in-flight entry'srequest_iddepends on the response's own disclosure — see "Poll for completion" below, which branches on thepoll_byfield the response carries alongsidesource,inflight_request_id,inflight_task_id, andpollable.
Poll for completion
Pick the poll handle from the submit response's poll_by field (present on attached responses since task 3148; a queued response has no in-flight entry to disclose, so it's always polled by its own request_id). Read it defensively — result.get("poll_by", "request_id") — so a response from a server predating this field degrades to today's behaviour instead of raising:
poll_by == "request_id"(or absent) — the returnedrequest_idIS the handle:mcp__escalation__merge_status(request_id="<request_id from submit>")poll_by == "task_id"— no in-flightrequest_idis known; the returnedrequest_idis your own submitting call's id, not a handle. Poll the in-flight task instead:mcp__escalation__merge_status(task_id="<inflight_task_id from submit>")Terminal states on this arm are UNCONFIRMED too, for the same reason as the
brancharm below. A task-keyed poll is submission-scoped only while the live-snapshot tier is still serving it; once that stops (e.g. a mid-flight orchestrator restart) the durable tiers resolve it to the most-recent finalized record for the task (retention ringget_by_task→ event storelatest_merge_finalized(task_id=...),escalation/server.py:2244-2287), and task_ids are reused verbatim across resubmissions exactly like branches. Apply the identical rule: adonecarryingkind: "found_on_main"is already git-authoritative — accept it directly, do not re-check ancestry. It is safe because the tier answersdoneonly when the branch advanced past its recorded creation point, and a commit on main positively cites the task, and that commit's effect is still present at main HEAD (unless the project opts out of citation checking withgit.commit_citation_pattern: "", where only the first applies andmerge_shais the branch tip; and unless the task declaresmetadata.delivered_checksthat a delivered-checks differential confirmed, which waives the effect-present leg alone —merge_shais still a commit on main, but its effect may since have been reverted) — a re-check would only repeat a weaker version of what the tier already did. The ancestry re-check applies only to adoneserved from the durable retention-ring/event-store tiers (nokind, nomerge_sha), which you confirm with the canonical ancestry check below. Re-checkget_merge_queue()before acting on aconflict/blocked/abandoned. Because a rejecteddonewould otherwise re-serve forever, this arm shares thebrancharm's 20-minute ceiling and the same stop-and-report exit.poll_by == "branch"(equivalentlypollable == false) — neither handle is known: a foreign or pre-restart merger owns the worktree, so there's no in-process entry, no retention alias, and no waiter. The returnedrequest_idwas never enqueued — polling it directly resolvesstate: "unknown", which here is expected, NOT a lost record. Poll by branch instead, which benefits frommerge_status's git-authority tier (self-resolves a landed merge tostate: "done"/kind: "found_on_main"):mcp__escalation__merge_status(branch="task/<TASK_ID>")Terminal states on this arm are UNCONFIRMED. A branch-keyed poll resolves the most-recent finalized record for that branch (retention ring
get_by_branch→ event storelatest_merge_finalized(branch=...),escalation/server.py:2251-2286) — nothing ties that record to your submission, and the event store survives restarts. Task branches are reused verbatim across resubmissions, so a prior round'sdone/conflict/blocked/abandonedcan satisfy the terminal test on the very first tick. Exception — adonecarryingkind: "found_on_main"is NOT subject to this: that shape comes from the git-authority tier, a live probe of main reached only because the durable tiers missed, so it structurally cannot be a stale prior-round record — and it cannot be a branch that merely looks landed either: the tier answersdoneonly when the branch advanced past its recorded creation point, and a commit on main positively cites the task, and that commit's effect is still present at main HEAD (unless the project opts out of citation checking withgit.commit_citation_pattern: "", where only the first applies andmerge_shais the branch tip; and unless the task declaresmetadata.delivered_checksthat a delivered-checks differential confirmed, which waives the effect-present leg alone —merge_shais still a commit on main, but its effect may since have been reverted). Accept it directly. For adoneserved from the durable tiers (nokind, nomerge_sha), confirm with the canonical ancestry check below — not-landed means stale, so keep polling. On aconflict/blocked/abandonedfrom this arm, treat it as possibly predating this submission: re-checkget_merge_queue()and who owns the worktree before resolving or resubmitting.Cross-check
get_merge_queue()for queue-wide context if useful. On this armunknownis the live state, not a first-tick fluke — it persists for as long as the foreign merger is still working, since the git-authority tier can only resolve once the merge is provably on main — by either of its two paths:is_ancestor(tip, main)while the branch ref still exists, orfind_merge_markeronce the ref is gone (theelif tip is Nonedeleted-branch path,escalation/server.py:2448-2459, which makes nois_ancestorcall at all). Do NOT read it as a terminal failure or as licence to direct-merge, and do NOT resubmit on it (thestate: "unknown"resubmit rule below is scoped to the other two arms). Exception to "same for all three arms" below: this arm and thetask_idarm are bounded by a 20-minute wall-clock ceiling, starting from your first poll on that arm (not from the original submit) — matchingskills/unblock/SKILL.mdandskills/unblock-low-risk/SKILL.md:177-189. At expiry, in order: (1) run the canonical ancestry check one final time; (2) landed (rc=0, or rc=128 with a merge-marker hit) → treat asdone/found_on_mainper thestate: "unknown"handling below; (3) only if it comes back genuinely not-landed → stop and report to the human — never resubmit, never fall back to a direct merge. The ladder's landing-unattributable verdict takes the same exit but is reported as landing-unattributable, never as not-landed, and is never resubmitted.
Whichever handle you poll, the cadence and state handling below are the same for all three arms:
Backoff: start at 15 s, cap at 60 s. When the response contains an eta_seconds field whose value is a positive number, use that value as the sleep duration (capped at 60 s); if eta_seconds is absent or null, fall back to the 15 s→60 s backoff schedule.
Waiting between polls. The backoff says how long to wait; this says how to carry each wait out. Never carry out its 60 s end as a bare sleep: on 2026-09-27 and again on 2026-10-07 the harness blocked a standalone sleep 60 with a message steering to Monitor, while a bounded loop ran. Run each wait as a bounded foreground loop that also wakes early when git shows the landing:
timeout <WAIT_SECS> bash -c 'while git merge-base --is-ancestor task/<TASK_ID> main 2>/dev/null; [ $? -eq 1 ]; do sleep 5; done'; echo "wait rc=$?"<WAIT_SECS> is the backoff value; pass the Bash tool an explicit timeout above that bound (e.g. timeout: 120000 for a 60 s wait). The loop keeps waiting only while the probe answers rc=1, so it wakes on rc=0 and on rc=128 (branch ref gone): the merge lane deletes a landed task branch on cleanup (the canonical ancestry check's own note), so an until <probe> loop could sleep through a landing whose ref vanished between ticks. Measured 2026-10-07: an rc=1 probe gave wait rc=124; a missing ref and a landed probe each gave wait rc=0.
- Reading it.
wait rc=0: the probe left rc=1, so poll now.wait rc=124: the bound elapsed, so poll on schedule. The loop decides only when to poll;merge_statusand the canonical ancestry check still decide what happened. rc=124 is never evidence of "not landed": a landing that rewrote the commits while this branch's ref stayed put keeps the probe at rc=1 forever on work that did land — see Follow the superseded successor rule 3 for the non-tipcoalesce-*train member. Likewise rc=128 alone is not a landed verdict, and a failed merge never moves the probe off rc=1. - Probe already off rc=1. If the canonical check answers anything but rc=1 before the wait — the ladder's phantom-branch rc=0 (a branch that never advanced past its creation point), or a ref that is already gone — the probe cannot wake you. Use
until false; do sleep 5; doneas that wait's loop, so it still waits the full bound. - Long waits, interactive sessions only (deep in the queue, or verifying with no
eta_seconds). Don't spend a turn per minute: run the same loop with a larger bound as ONErun_in_backgroundBash command, its Bashtimeoutat least 60 s above the bound —skills/escalation-watcher/SKILL.md's "Bash-tool timeout contract", which also governs the ceiling. Every wake — the loop exiting, its bound elapsing, or a "was stopped" notification — is the designed re-arm point, not a failure: pollmerge_statusonce, and re-arm only while the state is live. Expect to re-arm: reify task/7182's merge took about 2h40m and three watch rounds. A headless or unattended session keeps to the foreground form above — ending the turn there exits the session with the wait still pending. - Monitor is a per-event stream, not a better tool for this one-outcome wait; the loaded Monitor schema's own description says the same. If you use it anyway: load its schema with
ToolSearch("select:Monitor")before the first call (it is deferred, and a call with guessed parameters is rejected); take its harness-cappedtimeout_mslimit from that schema, not from memory; and expect to re-arm at every expiry.
Live states (queued, verifying, gate, finalizing) — keep polling.
Terminal states (done, conflict, blocked, abandoned, superseded) — proceed to step 4. (Unlike the other four, superseded doesn't tell you the actual outcome by itself — it names a successor you still need to resolve; see Follow the superseded successor below.)
state: "superseded" — Your request was superseded by a successor. The response includes superseded_by: "<mr-* request id | coalesce-* train id>". Never fall back to direct merge, and never resubmit, while that successor is unresolved — it may already be in flight, and either would race it. The successor isn't always pollable the same way your own request was; see below for the shape branch.
Follow the superseded successor. superseded_by names one of two shapes, and only one of them is a request id you can poll by request_id:
mr-*id (generation-advance path) — a real request id.gen_nextwas enqueued the normal way throughenqueue_merge_request, and this outcome resolves asMergeOutcome("superseded", superseded_by=gen_next.request_id, ...)(orchestrator/src/orchestrator/merge_lane/worker.py::_maybe_auto_chain_generation). Poll it:mcp__escalation__merge_status(request_id="<superseded_by value>")with the same 15 s→60 s backoff, bounded by its own 20-minute wall-clock ceiling (same as the
task_id/brancharms above). Handle its eventual terminal state per step 4. If this poll returnsstate: "unknown"on any tick — not only at the ceiling — do not fall through to thestate: "unknown"handling below (it ends in a resubmit for therequest_id/task_idarms, which risks racing a successor that may still be in flight); go straight to the escape below instead.coalesce-*id (coalesce-train path) — this names the train, not a request._COALESCE_TRAIN_ID_PREFIX = 'coalesce-'(orchestrator/src/orchestrator/merge_lane/worker.py);train_id = f'{_COALESCE_TRAIN_ID_PREFIX}{tip_id}-{uuid.uuid4().hex[:8]}'(orchestrator/src/orchestrator/merge_lane/worker.py::SpeculativeMergeWorker._maybe_coalesce_waiting_singles) is what every absorbed single receives as itssuperseded_by(orchestrator/src/orchestrator/merge_lane/worker.py::SpeculativeMergeWorker._maybe_coalesce_waiting_singles). Do not poll it byrequest_id. It resolves through none ofmerge_status's tiers: no retention-ring alias is ever recorded for a train id (the ring is keyed onreq.request_id;orchestrator/src/orchestrator/merge_lane/worker.py::enqueue_merge_request, its_on_finalizedcallback), no event-storemerge_finalizedrow is keyed on one, and the git-authority Tier 3.5 probe is skipped whenever onlyrequest_idis passed (key = branch if branch is not None else task_id; if key is not None:,escalation/server.py:2619-2622). Polling it byrequest_idreturns an honeststate: "unknown"that will never resolve to anything else. (TheGroupMergeRequestcarrying the train does get its ownmr-*request_id, auto-generated at construction —orchestrator/src/orchestrator/merge_lane/types.py::MergeRequest.request_id— but the caller never sees it; the train id is not a stand-in for it.)
Escape — for the coalesce-* case, or an mr-* poll that came back unknown or is still unresolved at its ceiling: stop polling by request_id. Fall back to the branch handle plus the canonical ancestry check on every tick:
mcp__escalation__merge_status(branch="task/<TASK_ID>")Four rules make this fallback loop terminate correctly. On the coalesce-* arm these four rules are the normative copy, and the poll["state"] == "superseded" bullet under Polled terminal failures in skills/unblock/SKILL.md mirrors them — the two files agree on this arm, so a reader who arrives at either one reads the same rules. The mechanism both rest on: the tip is rebased before the merge and the member's own ref is never advanced to those rewritten commits, so _delete_branch_if_on_main retains it — making rc=1 the common outcome for a non-tip member and rc=128 the rare one. skills/unblock/SKILL.md remains the reference for the mechanics this file does not restate — its #resumed-poll terminal set and its 20-minute wall-clock ceiling.
- Drop
supersededfrom this resumed loop's terminal set. The branch handle re-serves the identical frozensupersededrecord on tick 1, and for a coalesce-absorbed member that record never changes — polling to a plain terminal set would just bounce you back into this same bullet. Use("done", "conflict", "blocked", "abandoned")plus asupersededwhosesuperseded_bydiffers from the one you just disregarded (a genuinely new absorption — re-enter this subsection against it). - rc=128-with-empty-marker does not mean "not landed" here — and on the
coalesce-*arm, neither does rc=1 (see rule 3 for the mechanism: why a non-tip member's rc=1 is the common, permanent case there, not a reason to wait it out). A train merges only the tip branch, so a non-tip absorbed member's commits land under the tip's marker, not its own. Check two signals under either rc=128-with-empty-marker or rc=1 on thecoalesce-*arm; this widened trigger does not extend to themr-*arm, where rule 3's original rc=1 restriction still stands: (a) the tip's merge marker on main — run step 1's selecting search against the tip's subject:S="Merge task/<TIP_ID> into main" git log main --fixed-strings --grep="$S" --format='%H%x09%s' \ | awk -F'\t' -v s="$S" '$2==s && !seen {print $1; seen=1}'<TIP_ID>is parsed off thecoalesce-<TIP_ID>-<hex>id by stripping thecoalesce-prefix and
the trailing-plus 8 hex chars (uuid.uuid4().hex[:8]) — not a naive split on-. Theawk
half is the subject selection and is not optional:--grepmatches commit bodies too,
and on this arm a body match would be stamped as the tip merge sha. Do not add--max-count=1— it stops at the newest message match, so a body match shadowing the real
tip merge would make this signal read not-landed on a train that landed, and this arm has no
step-3 ladder to fall through to.
`skills/_shared/deriving-landed-sha.md`
is the normative statement, with the measured shadowing population. An empty result here means no
subject-matching tip marker, which is not on its own a not-landed verdict for the train. And (b)
whether this task's own scheduler status has already flipped todone— read withget_task(id="<TASK_ID>", project_root="<PROJECT_ROOT>"). The orchestrator flips it
automatically once the train lands (mark_member_doneinorchestrator/src/orchestrator/harness.py, which callsscheduler.mark_done(..., kind="merged", ...)); if it already readsdone, that flip already happened and no further write is needed.
The landing verdict then splits by rc: 2v states the veto that governs both arms once, and 2a and 2b give only what differs between them.
2v. The veto (both arms). This is the single statement of the veto for 2a (rc=128-with-empty-marker) and 2b (rc=1) alike; each arm's own trigger below says when a landing signal has fired. Once it has, the work landed — but there is no self-stamp on either arm: signal (b) is a veto, not a corroborator, and the two outcomes it leaves are exhaustive — neither licenses a write. Re-read signal (b) fresh and take exactly one of them:
done— the automatic flip already happened: nothing to write, conclude landed and proceed to cleanup (the cleanup recipe is the arm's own; see 2a and 2b).Any other status —
pending,merge-deferred, anything else, including a status you cannot read — means do not write, ever:pendingis exactly what_revert_withheld_memberleaves behind oncemark_member_done's_delivered_checks_withholdblocks the flip because the member's declared capability is
not verifiably on main — the member's ONLY recovery edge, documented as such in-source.merge-deferredis either the normal pre-merge train park (every member must already bemerge-deferredbefore_do_train_mergeadvances the tip) or a FAILED revert, which
in-source logging flags as needing operator attention rather than silently retrying._do_train_merge'sdone_wip_recoverypath is an explicit "merge landed on main but
members were NOT flipped" split-brain that escalates rather than returning done.
Stamping
doneover any of these overrides a deliberate orchestrator decision: it destroys
the member's only recovery edge, marks done a task whose declared capability is not
verifiably on main (which unblocks its dependents through the delivered-check dep-gate), and
races a scheduler re-dispatch of the same task. These are the veto's three harms, and the branch ref's existence or absence never changes them.A content proof cannot discharge the veto.
git cherry main task/<TASK_ID>(2b's recipe) proves this branch's commits are patch-equivalent on main, while the withhold gates on the member's declared capability against the committed main tree — a different predicate an all--git cherryis fully compatible with, so a passing content check next to a non-donestatus is exactly the split-brain, not a reason to stamp over it.Exit under veto: keep polling under rule 3's loop — the flip is asynchronous, so the normal landed case reaches
donewithin a tick or two and exits clean. If a non-donestatus still holds at rule 3's existing 20-minute wall-clock ceiling, stop and report to the human — report it as landed-but-not-credited, citing the tip merge sha and the current status (and, on 2b, thegit cherryoutput), never as "not landed" — and never resubmit, never direct-merge, never self-stamp.
2a. Under rc=128-with-empty-marker — the trigger is either signal saying landed. The veto (2v) applies unchanged; this arm adds only:
- Cleanup on
done: proceed to cleanup; the branch ref is already gone under rc=128, so there is no ref to delete. - The exit's citation omits
git cherry: the branch ref is gone, so 2b'sgit cherry main task/<TASK_ID>content proof is unavailable here and the veto rests on the scheduler status alone. That is sound, because 2v already establishes that the content proof cannot discharge the veto. Losing a proof that was never able to license a write costs this arm nothing. - Why this arm carries the veto — it is rare here but reachable, and reachable by following
this very subsection's advice. Orchestrator cleanup will not produce rc=128 for a
stale-by-rebase non-tip member:GitOps._delete_branch_if_on_maindeletes only whenGitOps._branch_has_commits_beyond_mainis False, and that predicate countsmain..<branch>
by sha (git rev-list --count), not by patch id, so the member's pre-rebase shas are
unreachable from main, the count is non-zero, and the ref is RETAINED. But 2b below tells yougit branch -D task/<TASK_ID>is safe oncegit cherryis non-empty and all--; an agent
who takes that advice and then re-enters this ladder arrives here at rc=128 with an empty
own-marker. The withhold is orthogonal to whether any ref survives:mark_member_done→_delivered_checks_withhold→_revert_withheld_member
(orchestrator/src/orchestrator/harness.py) fires when the train lands and sets the
member topendingregardless of ref existence, andDeliveredChecksConfig.enabled
(orchestrator/src/orchestrator/config.py) defaults True — so the two conditions compose.
2b. Under rc=1 specifically — the trigger is signal (a) showing the tip landed; signal (b) is then the veto (2v) and not a corroborator. This arm adds only:
- Cleanup on
done: step 4's cleanup recipe:git branch -d task/<TASK_ID>will refuse here
— "not fully merged" — since this ref is stale-by-rebase and not an ancestor of main;git branch -D task/<TASK_ID>is safe once thegit cherrycheck below is non-empty and every
line is-, and leaving the ref in place is also fine. - Confirm by content anyway — it is what makes the eventual report accurate, not a licence
to write:git cherry main task/<TASK_ID>— the same patch-id testGitOps.rebase_preserving_task_commitsinorchestrator/src/orchestrator/git_ops.pyuses to
tell a legitimate post-rebase dedup from a genuine commit wipe — prints-for each of this
branch's commits already patch-equivalent on main and+for one genuinely absent; treat
this member as landed-by-content only when the output is non-empty and every line starts
with-. An empty output is NOT landed — a branch that never advanced past its
creation point also prints nothing, sogit cherry main task/<TASK_ID> | grep -q '^+' || echo landedbelongs to the same unsound-idiom family this file already bans elsewhere.
- rc=1 does not mean resubmit here. Right after absorption the successor is typically still in flight, so ancestry rc=1 (branch ref exists; its commits are not ancestors of main — which, for a non-tip train member, is fully compatible with the work having landed patch-equivalently) is not evidence that the
supersededrecord is a stale prior round, and not a reason to give up. The canonical ancestry check's rc=1 resubmit line does not apply in this loop — whether you arrived here via thecoalesce-*arm or via anmr-*poll that came backunknown/unresolved, a still-in-flight successor makes rc=1 expected either way. On thecoalesce-*arm, rc=1 is not only a not-landed-yet signal — for a non-tip absorbed member it is the normal and permanent post-landing state, so rule 2's two landing signals are consultable under rc=1 too, on every tick, not only under rc=128-with-empty-marker. A train merges only the tip branch (tip_branch=tip_req.branch), which by stacking already carries every member's commits, and that tip is rebased onto current main before the merge, rewriting each stacked commit's SHA — this member's owntask/<TASK_ID>ref is never advanced to those rewritten commits, so it can never become an ancestor of main, and ancestry alone will never resolve this arm. rc=128 does not reliably arrive instead: a stale-by-rebase ref still carries commits beyond main, soGitOps._delete_branch_if_on_mainretains it rather than deleting it — rc=128 is the rare outcome for a non-tip member and rc=1 is the common one, so do not re-narrow rule 2's signals back to rc=128 alone. On themr-*arm, the rc=128-with-empty-marker restriction stands as before: the generation-advance successor merges the same branch at a newer SHA, so a merge-commit landing makes the ref rc=0 (or rc=128 after cleanup) and a persistent rc=1 there means not-landed unless the ladder's rebased-solo-landing arm resolves it, because that successor is a solo request and can itself land rebased — rule 2's signals do not apply there (signal (a) isn't even derivable on that arm: there is nocoalesce-<TIP_ID>-<hex>id to parse a TIP_ID from). Keep polling the branch handle under rule 1's resumed terminal set, re-running the ancestry check each tick — it re-derives the real answer from git rather than from the frozensupersededrecord, and on thecoalesce-*arm also re-derives whether rule 2's signals have resolved it. Stop-and-report only once rc=1 still holds — with rule 2's signals, where applicable, still unresolved, or resolved-landed but vetoed by a non-donescheduler status (in which case take rule 2v's landed-but-not-credited exit instead of this one) — at this resumed loop's own 20-minute wall-clock ceiling (same ceiling and stop-and-report exit already used by thetask_id/brancharms above and themr-*arm above) — never resubmit, never direct-merge, even at expiry. get_merge_queue()no longer showing the train is not a landing signal. It means only "stop waiting on the train" — and is equally consistent with a derail: on any non-donetrain outcome the orchestrator re-pends the still-unlanded members for solo re-merge (_redrive_coalesce_members,orchestrator/src/orchestrator/merge_lane/worker.py), which also removes the train from the queue with nothing of yours on main. This skill elsewhere directs you to consultget_merge_queue()for context (§"Poll for completion" above) — do not read its absence as evidence the train landed. On queue-absence with neither of rule 2's two signals saying landed, keep polling the branch handle under rule 1's resumed terminal set to the ceiling; never flip the task on queue-absence alone.
Whichever shape superseded_by takes, never direct-merge and never resubmit while a successor is unresolved.
state: "unknown" — for the request_id/task_id poll arms, this means the orchestrator restarted and the retention ring no longer holds this request (a record that was enqueued). merge_status now self-resolves a landed merge via its git-authority tier: if the branch is provably on main it returns state: "done" with kind: "found_on_main" and merge_sha directly. unknown does not mean "not landed" — the tier is deliberately silent whenever it cannot attribute a landing, which now includes a branch that never advanced past its creation point and a landing that no commit on main cites. If merge_status still returns unknown, confirm deterministically:
The canonical ancestry check — three outcomes, not two. Every "is it on main?" confirmation in this skill means this check. Never use the two-way idiom git merge-base --is-ancestor ... && echo "on main" || echo "not on main": a deleted branch ref exits 128, which that idiom silently reports as "not on main" — inverting the truth for the normal post-merge state, since the merge lane deletes task branches on cleanup (_delete_branch_if_on_main, orchestrator/src/orchestrator/git_ops.py), and on the branch arm a foreign merger's cleanup deletes it out from under you.
git merge-base --is-ancestor task/<TASK_ID> main; rc=$?; echo "ancestry rc=$rc"
# The trailing `echo` is REQUIRED, not decoration. `--is-ancestor` prints
# nothing on rc=0 OR rc=1, and the `rc=$?` assignment itself exits 0, so
# without it the tool reports exit 0 and identical empty output for "on
# main" and "NOT on main" -- silence you would have to guess at. Echoing the
# numeric rc is NOT the two-outcome `&& echo` idiom banned above: it prints
# on every path and keeps all three outcomes distinguishable. Do not "tidy"
# it away.The three outcomes, and the sha derivation behind rc=0 and rc=128, are `skills/_shared/deriving-landed-sha.md` — the single normative copy of the ladder (exact-subject marker search, ref-existence gate, containment, the group-merge candidate, the phantom-branch citation gate, and the DoneProvenance contract). Run it in full; do not improvise a shorter version. This skill enters it ancestry-first, so the rc above already answers its ref-existence question and its rev-parse probe is not needed here — see Two entry points.
This skill's dispositions on the ladder's verdicts:
- A verified sha (on rc=0, or on rc=128 with a non-empty marker) → treat as done/
found_on_mainand stamp exactly what that arm specifies —commitandnote, both required. - rc=1 → not an ancestor of main. Outside the coalesce arm and with the queue healthy: resubmit (go back to step 3) — with two exceptions from the ladder's rebased-solo-landing arm:
- That arm's pass — a
request_id-armdonewhoselast_outcome.merge_shapasses the arm's three checks — is a landing: stamp the ladder's{"kind": "merged", "commit": ..., "note": ...}payload. - Landing-unattributable → stamp nothing, never resubmit, never direct-merge; report it as landing-unattributable, with the
git cherryoutput, rather than as not-landed.
- That arm's pass — a
- rc=128 with an empty marker on the
coalesce-*arm → not one of the ladder's genuine not-landed outcomes below; Follow the superseded successor's rule 2a governs it (tip's merge marker + this task's scheduler status; either landing signal means it landed, and there is no self-stamp on this arm — adonestatus means the automatic flip already happened and nothing is left to write, any other status means never write, and the exit at the ceiling is landed-but-not-credited). The shared ladder carries this carve-out explicitly, so running it "in full" no longer contradicts rule 2a. - rc=1 on the
coalesce-*arm → that resubmit line does not apply; Follow the superseded successor's rules 2/2v/2b/3 govern instead. Rule 2b'sgit cherry main task/<TASK_ID>content proof is correct there — the branch is not an ancestor of main, so the test actually discriminates — and is unaffected by the shared doc's ban on substituting it on the rc=0 arm. Honour rule 2v's veto (both arms): there is no self-stamp on this arm either, so the scheduler status decides only which exit applies —donemeans the flip already happened and nothing is left to write; any other status means keep polling, and at the ceiling take its landed-but-not-credited exit (citing the tip merge sha, thegit cherryoutput and the current status) rather than reporting not-landed. - The ladder's genuine not-landed outcomes, a citation gate that is un-evaluable (
git.commit_citation_pattern: ""), and landing-unattributable → stamp nothing. Un-evaluable and landing-unattributable each prove neither verdict, so never read either as not-landed.
Never fall back to a direct merge in response to unknown — unknown means the server lost its record, NOT that the merge failed. The direct-merge fallback (step 6) is ONLY for orchestrator down/congested. This block's resubmit line does not apply to the poll_by == "branch" arm — there nothing was ever enqueued, so unknown is that arm's expected live state, not a lost record; see its 20-minute ceiling above for the bounded exit instead. Nor does it apply when this check is reached from Follow the superseded successor's Escape — that subsection's rule 3 governs rc=1 there instead; do not resubmit.
4. Handle the outcome
The outcome arrives from either the submit call (terminal at submit time) or the poll loop. Handle each status:
done — Merge succeeded. Main has been advanced atomically.
- Update the task:
set_task_status(id="<TASK_ID>", status="done", project_root="<PROJECT_ROOT>", done_provenance={"kind": "merged", "commit": "<merge-commit-sha>"})Use
{"kind": "merged", "commit": "<sha>"}when this branch's merge commit landed on main (the normal case). The server backstops withgit merge-base --is-ancestor <sha> main.Where the SHA comes from depends on which call produced the
done, and not every source is
safe to record askind: "merged". A terminal-at-submit-timemerge_requestresponse carries
the true merge SHA incommit— safe to use directly. A polleddonedoes not, unless the
response carriesmerge_sha(the git-authority tier'skind: "found_on_main"shape). Arequest_id-arm event-storedonecarriessourceandlast_outcomebut no top-levelmerge_sha(escalation/src/escalation/server.py::merge_status), and itsoutcomeis the
raw state string"done", not a commit hash. Itslast_outcome.merge_sha, whenlast_outcome.stateisdoneandlast_outcome.branchnamestask/<TASK_ID>, is the
request's recorded landing sha. It is not safe to record bare: it is the input to the
ladder's rebased-solo-landing arm
when the marker search below is empty and ancestry is rc=1.A
merge_shais always a commit ON main, on both of the tier's resolution paths — the
citing commit discovered on main on the live-branch path, the merge commit itself on the
deleted-branch (find_merge_marker) path — and on both it is checked to still be present at
main HEAD before being returned
(escalation/src/escalation/git_authority.py::found_on_main_response), so it is safe to
record exactly as returned. Never substitute the branch tip or agit merge-baseresult for
it. Two exceptions weaken that guarantee:- Citation gate opted out — on a project that sets
git.commit_citation_pattern: ""(an
explicit per-project opt-out for repos with no citation convention) the live-branch path
skips the citation gate andmerge_shais the raw branch tip — not a commit on main, and
not effect-present-checked, so a reverted landing is indistinguishable from a live one. If
you are on such a project, confirm with the marker search below — including its subject
check — before recording it. - Delivered-checks differential — if the task declares a non-empty
metadata.delivered_checksand the differential confirmed the capability, the tier accepts
even though the commit's effect is ABSENT at main HEAD.merge_shais still a commit on
main, but only the effect-present check was waived — it may name a landing that was later
reverted, so confirm the same way before recording it.
- Citation gate opted out — on a project that sets
So: record a bare
merge_shaas{"kind": "found_on_main", "commit": "<merge_sha>", "note": "<explanation>"}, notkind: "merged"— only re-derive and use the actual merge
commit ifkind: "merged"provenance is specifically wanted — and re-derive it with step 1's selecting merge-marker search (git log main --fixed-strings --grep="$S" --format='%H%x09%s' | awk -F'\t' -v s="$S" '$2==s && !seen {print $1; seen=1}',S="Merge task/<TASK_ID> into main"), which yields only a commit whose
subject is exactly the marker.- Do not collapse it to a bare
--grep ... --max-count=1: that stops at the newest
message match, which may be a body match shadowing the real merge. The subject selection
is not optional here —--grepmatches the whole commit message, so a docs commit or revert
merely quoting the subject is returned too, and recording it askind: "merged"is the
strongest provenance shape there is while the server's only backstop (git merge-base --is-ancestor <sha> main) passes for every commit on main.
`skills/_shared/deriving-landed-sha.md`
is the normative statement of the guard and of why the bare search cannot supply it; do not
restate the argument, link it. - Never re-derive by eyeballing
git log main --oneline | head -5— unscoped to this
task, so any SHA picked from it is likely an unrelated task's merge, and the server's only
backstop —git merge-base --is-ancestor <sha> main— passes for every recent commit on
main and would not catch it.
- Do not collapse it to a bare
If the response has neither
commitnormerge_sha— including on thepoll_by == "branch"arm, where a durable record can resolvedonewith nomerge_sha— re-derive the
merge commit the same way. Never fabricate a SHA; and an empty search is not a not-landed
verdict — resolve it with
`skills/_shared/deriving-landed-sha.md` and stamp
the sha whichever arm actually yields one (found_on_mainneeds acommitand anote),
rather than picking a SHA you merely believe is on main. On rc=1 that includes the
rebased-solo-landing arm, fed by
thelast_outcome.merge_shaabove.
Use
{"kind": "found_on_main", "commit": "<landing sha>", "note": "<one-sentence explanation>"}when the implementation is already on main from a sibling task / prior orchestrator run; the server runs the same git merge-base --is-ancestor backstop as for kind="merged".
- Clean up worktree and branch:
git worktree remove .worktrees/<TASK_ID> git branch -d task/<TASK_ID> # After a rebased solo landing `-d` refuses ("not fully merged"); `-D` is safe per rule 2b's cleanup wording.
already_merged — The branch was already an ancestor of main. Usually another merge or a manual push landed it, but a branch that never advanced past its creation point emits the identical signal with nothing landed.
- Same as
done— update task status and clean up — subject to thecommit-or-marker-search condition under "Terminal at submit time" above.
conflict — Merge conflicts detected. The conflict_details field has the specifics.
- Resolve conflicts in your worktree.
- Rebase onto current main again (main may have moved).
- Resubmit to the merge queue (go back to step 3).
blocked — Post-merge verification failed, or CAS retries exhausted. The reason field explains why.
- Read the reason carefully. Common causes:
- Verification failure (tests/lint broke after merge) — fix in your worktree, resubmit.
- CAS retry limit — main was moving too fast (rare at normal concurrency). Wait a moment and retry.
.task/contamination detected — check that.task/isn't committed on your branch.
failed — The merge worker encountered an unexpected error (surfaces from the submit call only; merge_status collapses this into blocked). Treat it the same as blocked: read any failure_diagnostic or reason field, fix the underlying problem, and resubmit.
unknown_branch — The branch ref does not exist in the target repository (surfaces from the submit call only). Likely causes: the branch name is wrong, the branch was deleted before submission, or the request was routed to the wrong repo's escalation MCP. Verify the branch exists locally (git branch -a) and that you're submitting to the correct escalation server.
abandoned — The submission was cancelled via merge_cancel before it finished (surfaces from the poll loop). If the merge is still wanted, resubmit (go back to step 3); otherwise, no further action is needed.
needs_rebase — Your branch was bounced at conflict-graph time (disk-free, before any verify slot was consumed) because it has a textual conflict with another branch already in the queue. The merge worker attempted a mechanical speculative rebase:
- Clean auto-rebase: your branch was re-queued automatically with work preserved; no agent was dispatched, and
merge_first_enqueued_at(your aging priority) was unchanged. No action required — the queue will re-process it. - Real conflict: the rebase failed; the merge was bounced with a conflict that requires resolution. Fix the conflict in your worktree and resubmit.
- Bounce cap reached (
MERGE_BOUNCE_CAP=3): the 1688 thrash-backstop triggered — the task is blocked without further rebase. Read the reason, resolve the underlying conflict, and unblock manually.
superseded — Your request was superseded (surfaces from either the submit call or the poll loop). The response includes superseded_by: "<mr-* request id | coalesce-* train id>".
Critical: do NOT fall back to a direct merge or resubmit on superseded — a successor may already be in flight, and either would race it. Follow the successor instead:
- Take the
superseded_byvalue from the response. - Determine how — and whether — to poll it per Follow the superseded successor above; the handle depends on whether the value is an
mr-*request id or acoalesce-*train id. - When the successor reaches a terminal state, handle it exactly as you would handle that status for your own request (e.g.,
done→ update task status;conflict→ resolve and resubmit your branch).
Your branch lands when the successor lands — but for a coalesce-* absorption of a non-tip member, that is confirmed via the tip's merge marker and this task's own scheduler status, not via merge_status, and that confirmation holds even though the branch's own ancestry check stays rc=1 permanently (see the linked subsection).
5. Abandoning a submission (merge_cancel)
To abandon a submitted merge — for example, the work was superseded, the wrong branch was submitted, or the queued entry is redundant — call:
mcp__escalation__merge_cancel(request_id="<request_id from submit>")The response is { cancelled, state, reason }:
cancelled: truewithstate: "abandoned"— a pending waiter was dropped. The merge will not proceed.cancelled: false— the request was already finalized (terminal), unknown, or already cancelled. Thestateandreasonfields explain why.
Important: merge_cancel is the only explicit-cancellation path. An MCP client disconnect no longer cancels the merge (durable intent), so an abandoned submission must be cancelled deliberately.
If your submit returned status: "attached", whether the returned request_id cancels the shared in-flight entry depends on that response's poll_by:
poll_by == "request_id"— the returnedrequest_idIS the in-flight entry's id; cancel it as shown above.poll_by == "task_id"orpoll_by == "branch"— the returnedrequest_idnames your own coalesced submission, not the in-flight entry. Treat the cancel as best-effort:cancelled: false/state: "unknown"is the expected outcome here, not evidence of a problem. On"branch"specifically, a foreign or pre-restart merger owns the worktree — reaching the real entry isn't this caller's cancel to make. Re-check the actual state first (merge_status(task_id=...)/merge_status(branch="task/<TASK_ID>"), which self-resolves a landed merge via the git-authority tier), then thegit merge-base --is-ancestorconfirmation described understate: "unknown"above, before deciding anything further.
Separately — if you're holding a coalesce-* train id from a superseded response's superseded_by (not from an attached submit's poll_by), it is not cancellable via merge_cancel either: merge_cancel looks the id up in _waiters, which is keyed on MergeRequest.request_id (escalation/server.py:2716), but a coalesce train is registered only through _register_item (orchestrator/src/orchestrator/merge_lane/worker.py::SpeculativeMergeWorker._register_item) and never as a waiter — so the call resolves to cancelled: false / state: "unknown" for the same reason it can't be polled by request_id. (The nearby docstring at escalation/server.py:2679-2681 describes a different mechanism — door/attach-coalesced submissions — not trains; its "use the in-flight request_id" remediation does not apply here.) There is no cancel path for an in-flight train — see Follow the superseded successor instead.
6. Fallback: direct merge
This fallback is ONLY for orchestrator down/congested — NEVER a response to state: "unknown".
If the escalation MCP is down (orchestrator not running), merge directly. There's no queue to race with.
cd <worktree>
git rebase main
# resolve conflicts if any, run verification
git checkout main
git merge --no-ff task/<TASK_ID> -m "Merge task/<TASK_ID>: <description>"
# run verification on main
git checkout task/<TASK_ID> # return to worktree branchAfter a successful direct merge:
- Update task status via fused-memory MCP (if available)
- Clean up worktree and branch
Quick reference
| Situation | Action |
|---|---|
| Orchestrator running | Submit via merge_request(wait_secs=100, verified_green=...) — True only when verification actually passed, False otherwise — then poll merge_status |
| Orchestrator not running | Direct git merge --no-ff |
Submit returns queued or attached |
Durable intent confirmed; poll the handle poll_by names (request_id for queued and the poll_by=="request_id" arm; task_id/branch when attached discloses them) with 15 s→60 s backoff (realise each wait per Waiting between polls) |
merge_status returns state: "unknown" |
request_id/task_id arms: run the canonical ancestry check, then derive the sha with `skills/_shared/deriving-landed-sha.md` — run it in full rather than shortcutting it from this row. rc=0 → landed, but the sha comes from the ladder's group-merge / citation arms, never from HEAD; rc=128 → branch ref gone, decided by the ladder's exact-subject marker search (never an unfiltered --merges listing) — and an empty marker search is not by itself a not-landed verdict (step 3), so on the coalesce-* arm apply Follow the superseded successor rule 2 before concluding anything; rc=1 + healthy queue, outside the coalesce arm → resubmit. poll_by == "branch" arm: unknown is that arm's live state — keep polling to the 20-min ceiling, then stop and report, never resubmit. Never direct-merge in response to unknown. |
Terminal state arrived on an unscoped arm (poll_by "branch" or "task_id") |
Unconfirmed — a branch- or task-keyed poll can match a prior round's record on the same reused branch/task_id. A done with kind: "found_on_main" is git-authoritative — accept directly. Otherwise accept done only when the canonical ancestry check says landed (rc=0, or rc=128 with a merge-marker hit); re-check get_merge_queue() before acting on a conflict/blocked/abandoned. |
Outcome conflict |
Fix in worktree, resubmit |
Outcome blocked |
Read reason, fix, resubmit |
Outcome done or already_merged |
Update task status, clean up |
Outcome superseded |
mr-* → poll by request_id (rc=1 there still means not-landed). coalesce-* → not pollable; use the branch handle + ancestry check — rc=128-with-empty-marker or rc=1 license the two landing signals (tip's merge marker; scheduler status), which establish that it landed but license no write — never self-stamp on either arm; report landed-but-not-credited at the 20-min ceiling. Never direct-merge, never resubmit. See Follow the superseded successor. |
Outcome needs_rebase — auto-rebase succeeded |
Queue re-processed automatically; no action needed |
Outcome needs_rebase — real conflict or cap |
Fix conflict in worktree, resubmit (or unblock if cap reached) |
| Abandon a queued submission | merge_cancel(request_id) — the only explicit-cancellation path |
| Unsure if orchestrator is running | Probe get_pending_escalations() — if it responds, use the queue |
The two-layer merge queue
The orchestrator's merge queue uses a two-layer pipeline to separate fast conflict detection (disk-free, reorderable) from in-flight verification (immutable, frozen).
Layer 1: Speculative merge graph (suffix)
The unfrozen suffix (_lane_buffers) holds items that are queued but not yet verifying. The conflict graph (suffix_conflict_graph) tracks footprint overlaps between suffix items. At each recompute cycle (recompute_suffix_conflict_graph()), the queue detects textual conflicts and:
- Bounces the younger conflicting suffix item (
_bounce_conflicting_suffix_items) — graph-time, disk-free, before any verify slot is consumed or_merge-*worktree is created. - Attempts a mechanical speculative rebase first: if clean, the item is re-queued with work preserved and
merge_first_enqueued_atunchanged; if a real conflict is found, the bounce escalates; if the cap (MERGE_BOUNCE_CAP=3) is hit, the 1688 thrash-backstop triggers → blocked without further rebase.
Reordering within the suffix is always disk-free (only recomputes the merge-tree for the affected suffix items, never touches in-flight verify state).
Layer 2: Frozen verify frontier (prefix)
The frozen prefix = {verifying} ∪ {landed}. Items in the frozen prefix are immutable:
- A verify is always dispatched against the tip of the frozen prefix (
frozen_prefix_tip). - No reorder or re-base ever touches an in-flight verify item.
- The suffix recompute only touches
_lane_buffers; in-flight_inflightorder andbase_shaare unchanged.
Health check: two_layer_invariants(main_sha) → [] when all §5.3 invariants hold.
Conflict-clique aging order and disjoint throughput bypass
Within a footprint conflict clique, items are ordered by age of first submission (_aging_key = (merge_first_enqueued_at, request_id)). The oldest first-submission wins — preserving the most expensive work. merge_first_enqueued_at is persisted write-once in task metadata and survives orchestrator restarts (falls back to enqueued_at for legacy entries).
Items whose footprint is disjoint from everything ahead bypass out-of-order for throughput — a disjoint item never waits behind a blocked clique.
No-landings circuit-breaker
When the landing rate ≈ 0 over a window and warm-lane free bytes are falling, NoLandingsCircuitBreaker automatically:
- Calls
force_halt_schedulerto stop dispatch. - Files an L2-INFO escalation (role
orchestrator-no-landings-breaker). - Auto-resumes when a clean landing occurs (
landings_totalrises) or disk recovers.
Operator-observable heartbeat keys
The snapshot() call exposes these additive, backward-compatible keys:
| Key | What it shows |
|---|---|
suffix_conflict_graph |
Current conflict-graph edges for suffix items |
frozen_prefix |
{request_ids, tip_merge_commit, verify_depth} |
metrics |
{retries_per_landing, drift_at_detection, landings_total} |
two_layer_invariants |
[] when healthy; list of violation strings otherwise |
hosts |
Per-verify-host {name, is_local, slot_state, quarantined, quarantine_class, unavailable_since, unavailable_secs, streak, reason} — tells RU-quarantine (ru) from divergence-quarantine (divergence) from a leaked slot (busy/parked, no occupant) from free-and-never-asked-for. An RU-tracked host with no allocator slot is appended with slot_state: null |
When any host is quarantined the heartbeat line also carries an inline | DEGRADED <n>/<m> hosts quarantined: <name>=<class> segment, and themerge_heartbeat event carries the same hosts block structurally.
For the full architectural companion, see references/two-layer-model.md.
Related work
- Warm-lane Δp space-safety batch (1859–1861 / reify 4716–4719): attacks Δp on the task-dispatch path (warm-lane disk-space gates). Complementary to the merge-queue path targeted here; no shared seam.
- Merge-verify ENOSPC fail-soft (workflow.py transient-infra block → re-queue): handles ENOSPC at the individual verify step. This is a separate symptom task and is out of scope for the two-layer merge queue (§10 of the PRD). Referenced here for orientation only.