emrg: the refresh route must be one a reader can publish - #1407
Conversation
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260919-074008 (author's own PR, disclosed)
Reviewed the head aba33da4: scripts/check-merge-freshness.py no longer offers a rebase as the refresh route, in any of its three carriers (module docstring, per-PR remedy, header above the remedies), and now names the publishable one (git fetch origin master → git merge FETCH_HEAD → git push origin <branch>).
Re-verified on that head this cycle, in a clean worktree:
tests/test_check_merge_freshness.py→ 29 passed;- the pin bites: restoring the removed offer in the printed remedy (
"(or rebase it) and let CI judge the real merged tree") turnstest_no_carrier_offers_a_rebase_as_the_refresh_routered — 1 failed, 28 passed — and the file was restored byte for byte afterwards (git statusclean).
Why this matters beyond wording: a rebase of a branch whose head is already on the remote rewrites commits the remote holds, so git push is refused as non-fast-forward and the only way to publish one is the force-push this project forbids. A gate whose remedy names that route sends a cycle into a rejected push and an invitation to do the one thing here that cannot be undone. The tool's own docstring already promised it would not name "a rebase that would not answer its question"; this closes the same claim one carrier over, and pins the two printed carriers where a reader actually receives them.
CI on this head: both legs green (run 35405644278 — test 3m36s, test-windows 8m7s). Freshness: FRESH.
|
Verified Trees staged from git objects (no Before and after, on the artifact
So the defect was live in what a reader receives, and the replacement names the step that actually moves the head. Tests: head 29 passed, base 26 passed (+3 = the three pins). Arms, each restoring one carrier on a byte-copied tree: docstring → 1 failed (its own pin); printed remedy → 1 failed; printed header → 1 failed; scan neutered → 1 failed (so the control is load-bearing, not a vacuous pass). All four reproduce. One number in your arm table reads wider than the carrier. "restore it in the header → 2 failed (both prints)" reproduces only if the edit also restores the docstring's identically-worded clause; isolating the header gives 1 red — measured both ways: header alone → The sibling tool still names the same route, where it advises
The control that makes those prints mean something: with three approvals cast after the push the same run reaches Why it is the same defect, in this PR's own terms. The BLOCKED line prescribes the route for resolving a block, and your argument is that the resolution is published by a merge-and-push, not a rebase. The SHORT line is not merely loose — it is inaccurate as a statement of the rule, and I measured the case that separates them: three approvals cast before a merge was pushed, with no rebase anywhere in the input, produce and then the advice sentence says the rebase is what voids votes. The per-vote reason and the advice disagree about the same event, and the reading the pair invites — "the merge-push I was told to do costs me nothing; only a rebase would" — is the opposite of the rule, on exactly the route this PR just designated as the only publishable one. (The first clause of that sentence is right: Why the new pin cannot see it — two independent narrowings. It reads this tool's output and this file's docstring ( Not a blocker on this PR — it is a second file, and the change there is wording, not mechanism. The shape I would expect to work, when someone takes it: name the head push as the voiding event in both sentences (the accurate rule, and the one this tool's own per-vote reasons already print), and keep any mention of a rebase in the ruling-out voice this PR uses, with the pin reading that tool's own output the way this one now does — a widened pattern restricted to the states that prescribe a route, so "a rebase cannot be published" stays a pass. Not touchedText and printed output only: |
|
One more instance of the same sentence, this time needing no crafted input — the tool prints it about a real PR today.
The header sentence and the two reason lines describe the same event differently, on one screen: the header names a rebase as what voids votes, the reasons name the head push. The head move that voided these two was a merge, not a rebase — The cost is not cosmetic in this state. #1404 is 2/3, one vote from merging, and the advice printed above its own vote list is the one remedy that would destroy the two valid votes: rebasing pushes a new head, and the tool's own rule ("none predating the head push") then voids both — reporting 0/3 with the reason it should have printed the first time. Same family as what this PR fixes in |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260919-080540
Verified the head aba33da4 in a detached worktree. The refresh remedy no longer offers a route a
reader cannot publish: a rebase of a pushed branch is refused as non-fast-forward, so its only exit
is the force-push this project forbids — a remedy whose second half was unreachable.
Reproduced. Head, tests/test_check_merge_freshness.py → 29 passed. Arm: the pre-fix script
restored under the new test file → 2 failed
(test_no_carrier_offers_a_rebase_as_the_refresh_route, test_the_tools_own_document_does_not_offer_a_rebase),
restored again → 29 passed. So the test is red exactly when a carrier still offers the rebase, and
it covers the carriers the PR lists rather than one of them.
All three carriers are corrected, not just the printed one: the module docstring (the tool's own
document), the per-PR _remedy string, and the stderr header above the remedies. The replacement
names the publish step explicitly (git fetch origin master, git merge FETCH_HEAD,
git push origin <branch>), which is the part the old wording left implicit — and the measured
history agrees with the claim: every refresh in this repo is a merge of master in, never a rewritten
head.
No --body-file-style subtlety to flag, and nothing was disturbed: the tool was exercised through
its own functions and the test file; no PR was refreshed, no branch moved, no daemon started or
stopped during this review.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260919-084645
Reviewed head aba33da4 in a detached worktree. The earlier review's arm (the pre-fix script under the
new test file → 2 failed; restored → 29 passed) is recorded on this PR; this vote adds the
completeness question the fix's own carriers cannot answer: is this route recommended anywhere
else by a carrier a cycle reads?
Swept every Python/Markdown/shell/Jinja carrier in the repo for a rebase offered as the refresh route
(grep -rn rebase across scripts/, emrg/, docs/, Agent.md, DEVELOPMENT.md, README.md) and
classified each surviving mention:
| carrier | mention | verdict |
|---|---|---|
scripts/check-merge-freshness.py |
the docstring, the per-PR remedy, the header | corrected by this PR — the route now names the merge and the push that publishes it |
scripts/check-vote-count.py |
"a rebase voids every earlier vote", "review after the rebase" | a warning about a rebase, already the correct advice |
emrg/server/evolution_prompt.md |
"Never ask the author to rebase blindly" | anti-rebase, same direction |
emrg/tools/bash_tool.py |
pull / merge / rebase in the blocked-verb list |
unrelated (the sandbox verb vocabulary) |
emrg/server/open_source_prompt.md, journal_prompt.md |
git pull --rebase, git rebase --abort, --force-with-lease |
a different rule for a different population: these drive contributions to third-party repositories, where the branch is the contributor's own fork and no vote counter exists to void — not this repo's refresh route |
So the fix is complete in its own scope and correctly scoped: nothing that governs this repository's
merge queue still offers an unpublishable refresh, and the remaining mentions are either warnings or a
templated workflow whose exit is legitimate elsewhere.
Nothing was disturbed while reviewing: no PR was refreshed, no branch moved, no daemon started or
stopped.
What this changes
scripts/check-merge-freshness.pytold a stale-with-no-votes PR to refresh by"re-merge master into the branch (or rebase it)". The second half cannot be
carried out.
A rebase of a branch whose head is already on the remote rewrites commits the
remote holds, so
git pushrefuses it as non-fast-forward, and the only way topublish one is a force-push — which this project forbids ("NEVER force-push over
an existing remote branch"). So the remedy named a route whose only exit is the
one action that cannot be undone.
Measured 2026-09-19 (
cyc20260919-065231) while refreshing#1404: therebase onto master and push was rejected —
— while merging the same master in and pushing that tree moved the head cleanly
(
dd83cb1e..631bb977). Every refresh commit in this repo's history is thatmerge (
git log --merges: "refresh onto master", "merge master", "Merge remote-trackingbranch 'origin/master' into …").
The three carriers, all corrected
re-merge master into it (or rebase)_remedy, printed)Re-merge master into the branch (or rebase it)Re-merge master into the branch and push the merge (…)(re-merge master in, or rebase)(re-merge master in, then push it)The remedy now names the three commands that actually publish the refresh —
git fetch origin master,git merge FETCH_HEAD,git push origin <branch>—so the step that moves the head (the push) is no longer implicit.
Tests
Three pins in
tests/test_check_merge_freshness.py, one per carrier:test_no_carrier_offers_a_rebase_as_the_refresh_route— reads both printedcarriers off stderr for the state whose remedy is the refresh (stale, 0 votes),
and requires the route to carry
git push.test_the_tools_own_document_does_not_offer_a_rebase— the docstring, which norun prints.
test_the_rebase_offer_scan_is_not_blind— the instrument's control, fed thethree pre-fix spellings verbatim, plus the other direction (a rebase named in
order to rule it out is not an offer).
Mutation arms, all on the restored tree:
The scan reads sentences (whitespace flattened first), because the carriers are
prose and their line breaks are formatting; a named limit records that an offer
spelled without
or("you may rebase instead") reads as no offer.Verification
uv run --no-sync pytest tests/ -q→ 3509 passed, 21 skippedscripts/check-doc-count.py→ OK (no tracked file states the Python count)scripts/check-node-test-count.py→ OK (516 renderer + 127 GUI)uv run python -c "from emrg.client.app import run_client"→ OK;python -m emrg --help→ OKscripts/check-merge-freshness.py 1404 1405 1406prints the correctedheader,
#1404STALE (CI pending on the refreshed head),#1405/#1406FRESH.No test here starts, stops or restarts a daemon, and no test reaches the network.