Skip to content

A pull request that edits a projected plan record is not refused because GitHub does not show the edit yet, and the trunk is refused for it once a declared window has passed

GitHub's issues and milestones are a projection of the plan on the trunk. A pull request

declared statusdeclared status: guaranteed Declared deterministic and blocking, implemented, and naming the behavioural test that is to prove it. Whether a recorded run supports it is the evidence verdict, shown beside it.

recorded evidencerecorded evidence: not run Declared guaranteed, and not supported by a recorded run: the test is named, and no passing run of the current inputs is recorded. how evidence is recorded

GitHub shows the trunk's plan and nothing else, so a record a pull request edits is behind there until the pull request has merged and the landing has applied it (.github/workflows/github.yml). The gate reports behind, state, milestone and closed as PENDING for the records a head changed since its merge base, and on the trunk for the records that changed in a landing younger than apply.window in .ai/repo/ci/github.yaml. A record has changed when its file or its rendering differs. Drift on any other record is refused as before, and so is every record when the change cannot be measured.

What it means

GitHub's issues and milestones are a projection of the plan on the trunk. A pull request that edits a plan record, closes one, or moves one to another milestone changes what GitHub should say, and GitHub cannot say it before the pull request has merged. The gate that compares the two does not hold that against the pull request. It reports the records the pull request changed as PENDING and refuses only drift the pull request did not cause.

After the merge the same records are PENDING on the trunk for a declared window, which is the time the landing has to apply the projection. Past the window the trunk is refused and the refusal names the command to run.

How it works

scripts/ci/github-check runs scripts/github-sync --check and reads its drift lines. Four states are what an apply writes: behind, state, milestone, and the closed the adapter reports beside state when GitHub is the closed side. For those four the gate asks which records changed since a reference commit.

runreference commitwords on the PENDING line
a pull request, or a tree given MJ_GH_BASE=<ref>the merge base with the base branchprojected after merge
a tree the default branch does not containthe merge base with the remote's default branchprojected after merge
the default branchthe youngest first-parent commit older than the windowthe landing applies

A record has changed when its file differs from the reference, or when its rendering does. The second half matters because a status is derived: closing one issue moves the body of every issue that waited for it, and can close its milestone, without touching their files. The gate renders the plan at the reference and at the head with scripts/github-sync --plan and compares each record's body hash and status line. The reference is rendered by its own adapter when it carries one, so a change to the renderer counts as the head's change too.

The window is apply.window in .ai/repo/ci/github.yaml, in seconds. The gate reads it from the tree under test and carries no copy. A landing's age is the committer date of its first-parent commit on the trunk, so a commit written last week and merged a minute ago landed a minute ago.

PENDING lines are counted apart from findings. The last line reads github-check: 0 finding(s), 2 pending, and the exit status follows the findings alone.

How to see it

scripts/ci/github-check                            # this tree, judged as CI would judge it
MJ_GH_BASE=origin/master scripts/ci/github-check   # as a head that merges into origin/master
scripts/github-sync --plan                         # offline: the rendering the gate compares
bash test/run.sh 1038_a_head_is_judged_against_what_it_changes

What it does not cover

The gate applies nothing. After a landing the projection is applied by .github/workflows/github.yml, which every push to master starts (Pull-request integration, project.the-projection-is-applied-at-landing), and the gate only bounds how late that may be. When that run fails, a person's scripts/github-sync --apply from master is the fallback.

Only the four states an apply writes can be pending. A hand-edited region (edited, conflict) and a remote issue claiming an unknown record (unmanaged) are refused on every run, whoever changed the record.

A pull request is not excused for drift the trunk left behind. If a landing is still inside its window and unapplied, another pull request that did not change those records is refused for them until somebody applies.

A new record is missing, not behind, and missing is ratcheted against .ai/repo/ci/github-drift-baseline.txt and is never pending (the neighbouring claim).

What cannot be measured is not assumed. With no base that resolves, no merge base, a shallow clone or no declared window, the gate prints a NOTE line saying which and refuses everything it refused before this claim existed.

Why it exists

On 2026-10-10 pull request #855 changed one line of issue I1900 and failed with DRIFT behind issue I1900 (#753) body is behind the canonical record. Pull request #820 closed eight records and failed with 8 record(s) state. Neither could go green on its own head: the only way to apply the change was to merge it, and the gate refused the merge for the change not being applied. The way out was to not edit the record, which meant the plan could not be corrected by a pull request. Had either change landed anyway, the trunk's own run would have been red until somebody applied by hand, and a release tag waits for that run.

Evidence

test/cases/1038_a_head_is_judged_against_what_it_changes.sh drives the gate over a fixture repository with a base, a head and two landings, against a scripted gh that serves the remote and fails on any call that is not a read. It proves that a record the head changed is pending and the run exits 0, that a record it did not change is refused beside it, that a record whose rendering moved with an untouched file is pending, that a landing inside the window is pending on the trunk and the same landing two hours old is refused with the remedy named, that the window is the declared one, and that a base or a window that cannot be read refuses. The case names the mutation that breaks each assertion.

The rule is project.github-projection-gated@1.

Detail rendered from docs/claims/projected-record-edit-is-landable.md.

From the statement to the proof

  1. 1 · the public statement

    A pull request that edits a projected plan record is not refused because GitHub does not show the edit yet, and the trunk is refused for it once a declared window has passed

    Made on Plan.

  2. 2 · the canonical claim

    projected-record-edit-is-landabledeclared status: guaranteed in docs/CLAIMS.yaml

    Defined in read it on this site · docs/PLANNING.md

  3. 3 · the implementation

    scripts/ci/github-check

  4. 4 · the test and the scenarios

    Test: test/cases/1038_a_head_is_judged_against_what_it_changes.sh

  5. 5 · the recorded runs

    recorded evidence: not run how evidence is recorded

Verify it yourself

The test runs in a disposable temporary repository and asserts the behaviour, not a string in the source.

from a clone of the repository
bash test/run.sh 1038_a_head_is_judged_against_what_it_changes

Where this claim is used

responsibilities it covers

Related claims same implementation

Searches the titles and descriptions of every page this site publishes.