Skip to content

silent revert: a branch built on an old main deletes work main just merged, while CI stays green #914

Closed

Description

@Birfy

Reported 2026-10-24 by the lead, after the fourth occurrence in a day: "this is the third time this class has happened (#864, #876, #887, and today #907) — stop adding reminders, add a mechanical gate."

Each time, a branch built from an old copy of main re-uploads whole files, and work that main merged minutes earlier disappears. CI stays green because the deletion is part of the pull request. mergeable: true, tests-ok green, and the previous fix is gone.

The occurrences (all real, all on this repository)

PR What it silently deleted
#887 #869's 84 lines of tests, merged the same hour (+34/−84 instead of +19/−0)
#876 same shape
#864 same shape
#907 @ d6e143a8 #909's whole fix, merged 40 minutes earlier: verifiedAuthor, refused: 'not-inviter', refused: 'unknown-room', the pure-transport fromHandle/fromHome — and #909's three test cases; #909 appears 0 times in that branch's core/test, 4 times on main

The rule, stated so it can be wrong

Delimit with the merge base. For a pull request, let base = merge-base(origin/main, HEAD).

  1. A = the lines that origin/main added after base — per file, the added lines of git diff <base> origin/main -- <file>.
  2. D = the lines the pull request deletes from origin/main — per file, the removed lines of git diff origin/main HEAD -- <file>.
  3. A ∩ D non-empty = silent revert. Report each file with the intersecting line text, the count, and the commit that introduced it (git log -S over <base>..origin/main for that file, or the first commit whose diff adds that line).

Set intersection, no blame walk needed for the hit itself — the introducing commit is only for the message.

Why this is not "the PR deletes lines". #910 replaced 18 lines of the inbound trust path with verifiedAuthor and merged; its deletions are its own, and at its base A was empty, so A ∩ D = ∅ — zero report. That is the negative control, and it must stay zero. A branch that re-uploads a stale file has A ∩ D ≠ ∅ by construction, because A is exactly the work it never saw.

Where it goes

Report-only, in the same shape as .github/scripts/agent-audience.mjs (#912) and .github/scripts/review-gate.mjs: read-only token, continue-on-error: true, writes $GITHUB_STEP_SUMMARY and one ::warning per file, never labels, comments, reviews or merges, and is not added to branch protection or to tests-ok's needs.

Concretely: a step in .github/workflows/merge-gate.yml's pull_request job (that job already never fails on pull_request, by its own design), running .github/scripts/silent-revert.mjs checked out from the base commit — the same rule the rest of that workflow follows, because this step must not run code a pull request supplies. It needs full history: the job has to fetch origin/main and the merge base (fetch-depth: 0 or an explicit git fetch origin main), so say how you did it in the PR.

Line shape, so the round driver can read it without opening the run: #<pr> @<head-sha7>: <file>: <n> line(s) main added after <base-sha7> are deleted here (from <commit-sha7>). Print a one-line total, and print silent-revert: 0 finding(s) on <n> file(s) when clean — a run that says nothing is indistinguishable from a run that did not execute, which is the same trap #912 hit on its first live run.

Scope

  1. The rule above, per file the pull request touches, skipping files it does not touch (that keeps it cheap).
  2. Only origin/main counts as truth. Not HEAD~1, not the branch's base.
  3. A file the pull request adds cannot revert anything; a file it deletes whole is reported as a deletion with the count, not as N line reports.
  4. No bypass trailer and no allow-list. This is report-only and it does not block; a legitimate revert simply carries the report, and a Reviewer decides. Adding a silence switch is how a gate becomes decoration.
  5. Mark each finding test or source by path, and print the two totals separately, so one noisy file cannot hide the other class. If the first week shows mostly test-file noise, the fix is to refine the rule, not to widen it by hand.

Acceptance

  • Replay #907 @ d6e143a8: red, naming core/src/host/room-membership.js and pointing at #910's verifiedAuthor / refused lines and the three #909 cases. Fixture: the real two trees, or the recorded diff, in silent-revert.test.mjs.
  • Replay #887's old tip: red, naming the 84 restored test lines.
  • Negative control #910 @ 07dee97d: zero findings. Its 18 deleted lines were its own replacement, and A was empty at its base. If this one reports, the rule is wrong and the PR is not ready.
  • A pull request that only adds or edits lines reports nothing (no deletions, empty D).
  • A pull request rebased onto the current main reports nothing: with base = origin/main, A is empty by definition. This is the case every Dev should land in, and the test pins it.
  • The workflow keeps its existing contract: merge-gate still does not fail on pull_request, and the new step's failure cannot turn the job red.
  • The run prints the zero line when clean (no silent success).
  • scripts/test-workflow.test.mjs passes with the edited merge-gate.yml.

Not in scope

  • Deciding whether a revert was intended. That is a human read of the report.
  • Enforcing anything. If this proves reliable over a week, making it a required check is a separate owner decision and a separate issue.
  • #907's own fix — that stays with #907 and its rebase; this issue only builds the thing that would have caught it.

Related

#911 / #912 — the other report-only check, and the shape this one copies; #909 / #910 — the work #907 silently reverted, and the negative control; #887, #876, #864 — earlier occurrences; #689 — audit-merges.mjs, which audits queue bypass (a different question: that script asks whether a merge went through the queue, not whether it kept main's work).

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-prPull request opened by an agentagent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a templateagent-workingAn agent has claimed this issue (the claim comment names the branch)area:automationScheduled tasks and automationenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions