101. Pull requests are integrated one at a time, each against a master observed a moment before
Context
On 2026-09-30 seventy pull requests were open. None was ready by any honest definition, and nothing said so. The repository had three answers to "what can land":
- the forge's
mergeableflag, which this repository cannot use. Derived files are resolved by a per-clone merge driver (merge=derived) that GitHub cannot run, so the forge calls nearly every pull request conflicting over files a generator rewrites anyway; scripts/land, which asked git the right question and then merged every clean pull request onto one integration branch. That is a batch planned from a list that its own first merge makes stale, and a batch's regressions are nobody's in particular;- integration batches built by hand (A to E), for the same reason and with the same cost: a red batch had to be bisected by a person.
Meanwhile the mechanics are fixed. Every merge moves the derived artifacts every other
branch carries, so every other pull request stops containing master the moment one lands.
A pull request that does not contain master cannot be merged without leaving master's
derived data stale. The required check (ci) takes hours.
Decision
Pull-request integration is a typed subsystem of the executable (crate::integration),
with one canonical state, one classification and one executor.
-
One canonical state. A
PullRequestAssessmentper open pull request: what the forge observed, what its head is to master, its disposition, its reasons, its evidence, its risk, its overlaps, and the master and head it was decided against, with the moment the forge was observed. The command line, the HTTP route, the MCP tool and the Cockpit render that value and nothing else. Its vocabulary is typed, so no client parses prose. A reason is aReasonCodewhose wire form is thecodeorcode:payloadstring it always was:stacked_on:#N,base_is:BRANCH,draft,label:NAME,auto_merge_armed,merge_commit_not_allowed,superseded_by:#N,successor_open:#N,successor_not_landed:#N,successor_unread:#N,head_reachable_from_master,merge_changes_nothing,patch_ids_upstream,only_derived_artifacts_differ,relation_unknown:WHY,conflicts_on:COUNT,depends_on:#N,review:STATE,review_policy_unread,required_check_failed,behind_master:COMMITS,fork_head,required_checks:STATE,no_required_checks,required_checks_unread,dependency_cycle:#N,dependency_closed_unmerged:#N,dependency_unread:#N,declarations_unread, and, on a ready one,contains_masterwithrequired_checks_passedorrequired_checks_skipped. The trail, the OpenAPI string arrays and the Cockpit read the same strings as before, and a code an older trail carries that this list does not name is kept verbatim. Evidence has a typed kind, with the wire words it had (required_checks,review,relation_to_master,dependency,label,draft,base, plusrequired_check, one per required context,auto_mergewhenever the forge has auto-merge armed,repository_settingswhen the settings allow no merge commit, andsupersession, one per declared successor, one per declaration nobody entitled made and one when the pull request's cross-references were not read whole;freshnessis reserved), and a source: the forge observation at its moment, or git on a named master and head. -
The relation to master is git's.
git merge-tree --write-treewith this clone's drivers, andgit check-attr mergefor which paths are derived, from master's own.gitattributes. A conflict on a derived path is the regeneration's, not a person's. The result is cached by the pair of SHAs, which are immutable. It is decided on exactly the head the assessment names as evaluated: a head that is not in the clone, because it moved during the refresh, isunknown, never the relation of whatever a fetched ref holds now. An attribute read that fails isunknown, never "nothing is derived". Only a pair of full commit ids is a cache key, and anunknownis never cached. Where the merge would conflict or change only derived output,git cherryis asked too: when every commit the head has and master lacks is on master already as an equal patch, the relation ispatch_ids_upstream, the change landed and master moved past it. A partial match never gives it, nor a head with a merge commit of its own in that range (a merge's resolution has no patch to compare), nor a clean merge that changes authored paths, where master lacks something the head carries, such as a landed change master reverted since. Beside the relation, from the same pair of commits and cached under the same key, is what the merge changes by kind (ChangeShape): every authored path it changes or conflicts on, the derived ones, and the version the head declares in the crate manifest when it raises the one its merge base declares. The relation's variants stay as they are; the shape is what overlaps and risk read, so a conflicting head overlaps others over its whole change and not only over the paths it conflicts on. An overlap names its kind:version_bumpwhen both raise the version,releasewhen both change a record under.ai/repo/releases/, andauthoredfor shared authored paths. Risk ishighwithpaths_unknownwhen the relation isunknown, never "documentation only"; a version bump (version_bump:V), a shared one (overlapping_version_bump:#N) and a shared release (overlapping_release:#N) arehighfactors; and.ai/repo/releases/replaces the derivedshare/version.txt, which the authored paths never contained. -
Fifteen dispositions, decided in one order. They are
ready(lane ready);needs_refresh,waiting_for_checks,waiting_for_reviewandwaiting_for_dependency(waiting);needs_repairandconflicting(repair);redundant,supersededandpossibly_redundant(cleanup); anddraft,blocked,unsafe,other_baseandunknown(held). The list is a runtime value (PullRequestDisposition::ALL), so a surface that names the dispositions of a lane derives them rather than copying them.readyis reached only when the pull request targets the base, is not a draft, carries no label that holds it, has no auto-merge armed, has no declared successor, is not on master already, the repository allows a merge commit, it contains the current master, has its declared dependencies landed, satisfies the review policy, and has every required check passed on its current head. These are gates, asked in this order:base,draft,label,auto_merge,supersession,relation_to_master,merge_method,dependency,review,no_failing_check,freshness,required_checks. The labels that hold are one table,LABEL_POLICY, each with its effect; the policy copies it and the classifier reads that copy, and no other list of labels exists. Today every effect ishold, compared case-insensitively:do-not-merge,do not merge,blocked,hold,on-hold,wip,manual-merge. No label opts a pull request out of the executor's refresh (owner decision D11). A pull request on which the forge has auto-merge armed isunsafe(auto_merge_armed): the forge would merge it on its own, outside the executor and against whatever master is then, and a refresh would only hand it a head to merge, so nothing acts on it until a person disarms it; the queue names every such pull request in a diagnostic. The executor merges only with a merge commit, because the derived-file driver resolves merges and a squash or a rebase would replay commits it never saw. The merge method is thereforemergewhen the repository's settings allow a merge commit (allow_merge_commit), and none otherwise; with none, every pull request the relation does not already decide isblocked(merge_commit_not_allowed, withrepository_settingsevidence) and the queue says why. The gate is asked afterrelation_to_masterbecause closing work that is already on master does not depend on the merge setting: aredundantpull request staysredundant, saysmerge_commit_not_allowedafter its own reason, andprs cleanup --applystill closes it; so does asupersededone. It never falls back to a squash or a rebase. Every gate is asked whatever the others answered. The assessment'sgateslists each with whether it passed, and itsreasonshold every failing gate's findings in the same order. The disposition is the first failing gate's, so a draft that also conflicts and fails its check isdraftand says all three. A failed required check is asked before freshness and a pending one after it: a head behind master needs a refresh, which runs its checks again, unless a check already failed. A gate that fails only because an earlier one did, such as freshness when the merge conflicts, fails without a reason of its own. The required checks are the base's branch protection and the rulesets that apply to it, together; if either cannot be read, nothing is ready. A check bound to an app (app_idin the protection,integration_idin a ruleset) is only that app's check run. A commit status of the same name is not it, and neither is another app's check run of that name: it neither passes the check nor fails it nor holds it pending. A context two sources bind to two apps is required of both. When a context is bound, the refresh reads each check run's app from the forge's GraphQL rollup (checkSuite.app.databaseId);gh pr listdoes not carry it. A check run of a bound context whose app was not read (the head moved between the two reads, its check suite names no app, or its contexts did not fit the pages that are read) makes that checkunknown(required_checks:unknown), never passed, and the queue says which pull requests carry one; a writer read that fails fails the refresh. The binding names the app, not the workflow: every workflow of the repository writes as the same app, one the pull request's own head defines included, so the binding keeps another app out and does not prove which workflow ran. (Amended 2026-10-07: before this, the app was never read, becausegh pr listdoes not report it, and any check run of the name passed.) Of a context's reports, one still running makes it pending, and otherwise the newest completed report is its verdict, so a failure a re-run fixed has passed. A required check that is pending, missing or unreadable, or written by anyone but the app it is bound to, is not passed, and neither is a skip, unless the policy permits that context's skip. A base that requires no check proves nothing about a head: an empty set after a successful read isunknown(no_required_checks), never ready (owner decision D5). The review policy is read from the protection and rulesets the same way (approvals, code owners, stale dismissal), and an approval counts only on the commit it was given on: approvals of an earlier head arestale, and enough of them on the head with a code owner's still owed iscode_owners_pending. The forge's review decision comes before the policy:REVIEW_REQUIREDis a pending review even where the policy requires none, because a rule this policy does not list can. A dependency is declared only by a line that opens withDepends on,Stacked on,RequiresorLand afterand a number; the same words mid-sentence are prose. It is satisfied by a merge alone: one closed without a merge holds the pull requestblocked(dependency_closed_unmerged:#N), one the forge cannot account for isunknown(dependency_unread:#N), and declared dependencies that form a cycle among open pull requests block every member (dependency_cycle:#N). A head that contains another open one's head is an inferred dependency: evidence, never a block. A pull request is stacked only on a branch of this repository, never on a fork's branch of the same name. A successor is declared the same way, by the same parser: a line of a pull request's body that opens withSuperseded by #Nnames N as its successor, and a line of N's body that opens withSupersedes #Mnames N as M's, from the other side. Thesupersessiongate is asked beforerelation_to_master, because a pull request whose successor landed usually conflicts with what the successor brought, and is superseded rather than conflicting. A declaration counts only from someone the repository lets declare one: the pull request whose body carries it is a branch of this repository and its author is anOWNER,MEMBERorCOLLABORATOR, in either direction, tested on the author of the body that speaks. Anyone else's, a fork's, or one whose association was not read is evidence (possible_supersession, naming who said it) and changes no disposition. A successor landed when git finds its head or its merge commit in master, whatever the forge calls it: one merged by a squash, and one closed after a batch carried its head in, both landed. An authorised successor still open holds the pull request (successor_open:#N,waiting_for_dependency): it must not land while its successor may, and once the successor lands it is closed rather than merged. One that landed makes itsuperseded(superseded_by:#N), and the assessment names N insuperseded_by; one closed unmerged releases it to its own relation; one merged somewhere master does not contain leaves itpossibly_redundant(successor_not_landed:#N) for a person; one that cannot be read isunknown(successor_unread:#N). A pull request whose cross-references were not read whole isunknown(declarations_unread) before any of that: an unread declaration is never taken for none. Of several successors, one that landed decides, then one still open, then one merged elsewhere, then one unread. -
One merge at a time, and no plan survives it. The executor's step takes no plan. It observes the forge, decides, observes again, and acts only if the second decision names the same master and head and still says
ready.drainis a loop over that step. There is no variable in which a candidate list could survive a merge. A merge is then proved where it landed: the first-parent successor of the decided master must be this merge, with the decided head as its second parent; anything else isunexpected_master, even when the forge calls the pull request merged. The forge-side half of that guard is the protection'srequired_status_checks.strict; the executor reports a base without it and cannot set it (D8). An unverified merge holds every later drain until a person acknowledges it (D7); an interrupted merge is verified, never repeated, before the next decision. -
Refreshing is a separate, bounded act. When nothing is ready,
drain --refreshbrings master into the first pull request that needs it: a merge commit with the derived driver and a fresh derive, pushed as a plain fast-forward while origin still serves the head that was observed, never forced and never a rewrite. This is whatproject.land-and-publishalready prescribes, andprs repair <n> --applyis the same act for one pull request a person names, under the same lease and trail; its dry run decides offline on the recorded observation. The pipeline is one deep: while a refreshed pull request waits for its checks, no other is refreshed, because merging the first would put the second behind again and waste its CI run. Only a run the executor started holds it: the required check of the head arefreshedevent recorded as pushed, while it is pending or missing. Missing counts because an aggregate check is not created until the jobs it needs finish, but it holds only for a bound after the push (four hours), so a check that never reports cannot freeze every refresh. A check on a head the author pushed holds nothing. -
Cleanup demands more than merging. Only two dispositions are closed, and only with
--apply. Aredundantpull request is closed on git's strong evidence: its head is an ancestor of master (head_reachable_from_master), its merge changes no file (merge_changes_nothing), or every commit it has and master lacks is on master as an equal patch (patch_ids_upstream). Asupersededpull request is closed because a successor an owner, member or collaborator declared for it landed; the closing comment names that successor and does not claim the closed work is on master. Weak evidence is never closed: one that differs only in derived output, or whose successor was merged somewhere master does not contain, ispossibly_redundantand is left for a person. Age, shared paths and similar titles are not evidence. The trail records the closure asclosed_redundantorclosed_superseded.Wire change (owner decision D2, shipped with the 0.13 minor). Before it, the word
supersededmeant whatredundantmeans now: the strong relation to master, closed by cleanup with the actionclosed_superseded. The strong case was renamedredundant, its closureclosed_redundant, andsupersedednow means only an explicitly declared successor that landed, withclosed_supersededas its closure. A client that matchedsupersededto find work already on master matchesredundantnow. Every older line still reads: a trail line withclosed_supersededparses, and its reasons (head_reachable_from_master,merge_changes_nothing) say which meaning it carried, since a closure of a declared successor always carriessuperseded_by:#N. The relation to master keeps its own word,superseded(merge_changes_nothing), which is git's fact, not a disposition.Amended 2026-10-07 (decisions D1 to D5 of the K2/K3 audit; proposed, the owner may override). D1 A declaration is authorised by the forge's association of the pull request that carries it (
OWNER,MEMBER,COLLABORATOR, same repository), in both directions; unknown is unauthorised. D2 An authorised declaration holds the replaced pull request until its successor resolves: landed, it issupersededand closable; closed without landing, it is released. An unauthorised one ispossible_supersessionevidence and neither holds nor closes. D3 Landed is git's fact — the successor's head or merge commit is an ancestor of the base — so a successor that reached master inside a batch counts; the forge'sMERGEDonly words the evidence. D4 Declarations about an open pull request are read from its own cross-references (paged) and its own body, never from a bounded search of closed pull requests; a read that is partial holds that pull request (declarations_unread), and one that fails fails the observation. D5 The rule is version 3 and names all four closure grounds: ancestry, a no-change merge, every patch upstream (which version 2's bullet never named, though the code closed on it), and an authorised declaration whose successor landed. Before this amendment any body'sSupersedes #Mheld and closed #M, the closed declarers came from the 200 newest search results, and the observation schema was 3; it is 4.Amendment to D3, 2026-10-07 (ruling R1; a narrowing the owner may reverse). Git's fact is trusted only about a head nobody could move to manufacture it. The attack: an owner's open #1 says
Superseded by #7, where #7 is a contributor's fork pull request; #1 is held on it, as D2 says. The fork's author force-pushes #7's branch to any old commit of master and closes #7. Its head is then an ancestor of master, and D3 as first written called that landed:prs cleanup --applyclosed #1 with none of #7's work on master. So a successor the forge does not report merged counts as landed only when its head lives in this repository and is an ancestor of the base (the batch-landed path); a cross-repository successor closed unmerged never lands, wherever its head points, and releases the hold exactly as one closed without landing. A successor the forge reports merged lands when its head or its merge commit is an ancestor of the base, fork or not. Where a resolved successor's head lives is read with it, and unread is a fork. In the same spirit a successor the forge says changes no file brought nothing to master and never lands, which closes the same trick played with a branch of this repository reset onto master.Rulings of 2026-10-07 on D4 and on what counts as a declaration. R3 A declarations read the forge will not answer fails the whole refresh (exit 12) and records no observation: the one recorded before it stays as it was. Only a truncated read is a per-pull-request hold. R4 A line inside a markdown quote (
>), a fenced code block or an HTML comment is not its author's declaration; D1 put all trust on whose body speaks, so only what that body states counts. A dependency keeps the lenient reading: it can only make a pull request wait. R5 An open declarer whose repository flag the declarations read did not state is read as cross-repository, and authorises nothing.Residuals, 2026-10-07 (known, accepted, the owner's to revisit). R2 Truncation holds, and an outsider can cause it: the page limit counts every cross-reference to a pull request, and a mention by an issue or from another repository is dropped only after it was paged through. Whoever writes more mentions of
owner/repo#Nthan are read (fifty pages of a hundred) holds #N asunknown(declarations_unread) on every refresh; no command releases it, and a person decides it. That is a block and never a closure, it costs the outsider one written mention each, and the rest of the queue keeps moving. No release mechanism is invented here. R6MEMBERis the forge's word for an organisation's member whatever their access to this repository, and it is evaluated when the observation is made, not when the body was written. A former collaborator who is still a member can edit the body of an old pull request of theirs in this repository toSupersedes #M: if that pull request is open it holds #M, and if it merged long ago #M issupersededand closable. D1 namesMEMBERand stands; a personal repository has no members and is not exposed. -
One executor per base branch. An exclusive lease under the common git directory: an exclusive
flockon its file, held for the executor's life, with the holder recorded beside it for whoever reads. The kernel decides who holds it, so executors started at once cannot both win, and a holder that ends, even by a crash, releases it at once; a live holder is never taken over, however old its record. The staleness bound is what an observer reports, not a licence to take the lease. Observers never take it.The lease guards one clone: every worktree shares its common git directory, and a second clone on another machine has its own. The repository is guarded by the mesh (ADR 0067) when this checkout's shared server runs it: before the lease, an executor takes an exclusive mesh claim on
integration/<repository>/<base>as its checkout's session, which every linked runtime admits against. A second machine's executor is refused with exit 12, naming the holding session and what it said it was for, before it takes its lease or acts. The claim's key is written in the lease record and released after the lease. A mesh that is not running here refuses nothing: the lease alone guards, and the record,prs briefand the Cockpit say so ("per-clone guard only"). A claim an executor left behind when it was killed is its checkout's own session, which the mesh never refuses to that session; the next executor there takes the lease and releases the leftover, so a crash costs the other machines a wait, never a claim nobody can release. -
The network is declared, not tolerated.
prs refresh,prs drain,prs cleanupandprs repair --applyrun the GitHub CLI andgit fetch. Every other surface renders the recorded observation with its age. SECURITY.md names the exception. Besides the open pull requests, an observation reads, with every open pull request, its author's association and the pull requests that mention it (gh api graphql, paged; a mention is a declaration only if that pull request's body saysSupersedes), and each successor or dependency an open one names that is not open (gh pr view), and fetches their heads, so that git can say whether a successor landed after it stopped being listed as open. -
Every act is recorded. Selections, stale decisions, merge attempts, merges with the master before and after, refusals, refreshes and closures go to an append-only trail that the
integration.eventscapability serves. The trail is the repository's, not a checkout's: one file under the common git directory,<git-common-dir>/majordomus/integration/events.jsonl, beside the lease, so every worktree writes and reads the same one. Each act is appended before it is taken, and an act the trail cannot record is not taken. -
Failure classes and retry. Every failed act is recorded with its class:
stale,conflict,new_failing_check,review_revoked,transient,policy_violation,verification_failedorunreadable. The first six belong to one candidate, and the drain goes on with the next; the last two belong to the repository or the executor, and the drain stops. A candidate the forge refused to merge, or whose refresh failed, is held back asneeds_repair(executor_merge_refused,executor_refresh_failed) while its head and master are the ones refused — folded from the trail, never from a plan — so one refused change does not block the queue. A transient failure holds nothing back. The forge is asked again only on an outage (crate::integration::retry, case 742), a merge never; a continuous drain waits out up toCONTINUOUS_TRANSIENT_LIMITconsecutive outages, each recorded asobserve_failed, before it ends. -
Continuous mode is a bounded loop of bounded drains.
prs drain --continuousholds the lease for its whole run and repeats one bounded drain per cycle, so every action is still preceded by a fresh observation and no plan outlives a merge. Between cycles it waits an interval insideINTERVAL_SECONDS(30 to 900 s: the floor spares the forge, the ceiling keeps a waiting worker well insideLEASE_STALE_AFTER), in one-second slices so a signal is honoured within a second of the step in progress finishing. It stops on a signal, on a merge that could not be verified, and afterCONTINUOUS_TRANSIENT_LIMITconsecutive outages. A dry run is never continuous. -
Waiting is a fact, and starvation is shown, not traded for. The trail records when a pull request becomes actionable and stops being so, and, on every selection, the actionable pull requests passed over;
crate::integration::waitfolds those into how long each has waited and how often another was chosen. A pull request passed overSTARVING_AFTERtimes is named as starving. The rank is not changed by it: its tie-break already prefers the older of two otherwise equal candidates, and a long wait is never a reason to merge something less safe sooner. -
The rollout is staged, and the trail is its record. Stage 1 is a dry run against the real repository (
prs refresh,prs status,prs drain --dry-run), which moves nothing. Stage 2 is one bounded merge (prs drain --max 1), stage 3 small bounded drains (--max 3, then--max 5). Stage 4, continuous mode, is refused (exit 10, before the lease, the base or the forge) until the trail holdsROLLOUT_MERGES_BEFORE_CONTINUOUS(five) verified merges since the last merge that could not be verified; such a merge starts the count again, acknowledged or not, because the record it ends was the evidence. The defaults are the safe ones:prs status,planandexplainread; a drain merges only when asked to; cleanup closes nothing without--apply.
Consequences
Throughput is bounded by the required check: one pull request lands per CI run of its refreshed head. That is the true cost of this repository's mechanics, and the planner makes it visible rather than hiding it inside a batch. Parallel speculative refreshes — a merge queue — are a later decision, and they need this one's evidence first.
scripts/land is deleted. scripts/force-merge-all, already dispositioned for deletion by
the automation inventory, is deleted with it. scripts/ci/backlog-check --remote measures
the backlog through majordomus prs.
Alternatives rejected
- Trust the forge's
mergeable. It is wrong in both directions here: measured CONFLICTING on clean merges and MERGEABLE on a real conflict. - Merge every green pull request in sequence from one list. Every merge invalidates the list. The second merge would be decided against a master that no longer exists.
- Batches. They trade attribution for throughput, and a red batch costs a person a bisection. They remain a person's decision, not the executor's. On 2026-10-16 the owner took that decision: the landing seat lands combined tops (ADR 0167), and the executor described here is unchanged.
- Merge with
--admin. The branch protection's refusal is the answer, not an obstacle. - Close pull requests by age or similarity. Neither is evidence that the work landed.
Amendment, 2026-10-15: a merge GitHub makes is guarded by checks, not by strict (I1409)
D8 left required_status_checks.strict to the owner and named it the forge's half of the
guard. I1409 asked the narrower question that leaves open: when the forge makes a merge onto
a master that moved after the pull request's ci ran, what refuses a stale derived tree
before it is published? Measured on 2026-10-15, the protection requires ci with
strict: false and enforce_admins: false. The answer is three checks, each on the tree it
judges, and none of them is strict.
- Before the merge, the required
ci. A pull request's run checks outrefs/pull/<n>/merge, the merge the forge would make at that moment, and itssitejob runsscripts/generate-site-data --checkand itsrustjob runsmajordomus generate --checkthere (.github/workflows/validate.yml, jobsite, stepderived site data in sync). - Between that run and the merge, the forge's own text merge. The forge runs no
custom merge driver. With
merge.derivedundefined, git falls back to the text merge, which is what.gitattributessays it does. Every derived tree records onesource_hashinsite/data/generated/source.json, and that hash covers the declared site inputs: 1497 paths, including the registry projectionsdocs/generated/{registry,graph,openapi, cli,web,changelog}.json. Two sides that each changed a canonical input each changed that line, so the forge reports the pull request as conflicting and does not merge it. A bare repository with no driver configured shows it:git merge-tree --write-treeof masterc0e3585dc8with the heads of #1159 and #1149, both CONFLICTING on GitHub, listssite/data/generated/source.jsonamong the conflicts. If only one side changed canonical inputs, the merged inputs are that side's inputs and its derived data is their projection (case 3048 holds the unrelated merge current). - After the merge, before publication, the merged commit. A push to master runs
validate.ymlon the merge commit, with the samesiteandrustchecks, andpages.yml, whosedeployjob runsscripts/pages build. That command exits 10 on stale site data (cmd_current), and the job runs it before itspublishstep. The master mergec0e3585dc8was committed by GitHub. On it, validate run 38635619021 (job 116098148462) passedderived site data in sync, and pages run 38635619039 (job 116105040387) ranbuildat 21:07:39Z andpublishat 21:09:30Z. Stale site data is refused atbuild, so gh-pages keeps the last current commit and thepages-livegate names the owed publication.
Decision. The guard on the server-side merge path is the ci-time freshness gate, at
the three points above. It is not strict required checks, and it is not a new merge-time
check. The documents claim exactly that: .githooks/pre-commit guards a merge a person
commits, and the checks above guard one the forge commits. No document may claim the hook
guards a GitHub merge.
Rejected:
strict: trueas the guard. It is a repository setting and the owner's act (D8, I1409 non-scope). It would re-runcifor every open pull request whenever master moves, which is the throughput this record already prices at one landing per run. It would also not cover an--adminmerge, since admins are not enforced.- A merge queue. It is the "later decision" of the Consequences above. It needs this ADR's evidence first and is not owed for derived data, because point 2 already makes the forge refuse the only combination a text merge could get wrong for the site.
- Re-deriving in the Pages build. I1409 non-scope: generation is not deterministic per tree, so the build would produce a third answer.
Residual, stated rather than claimed away. A derived artifact outside the declared site
inputs, such as a generated region of AGENTS.md or a skill under .claude/skills/, can be
changed by two pull requests that touch different lines, and the forge can text-merge those
changes cleanly into a result that is not a projection. Point 1 cannot see that merge, and
point 2 does not cover those files. Point 3's generate --check on the push run is the
first thing that refuses it: master goes red before any person relies on it. None of those
bytes is published by the site.