Repository navigation
Description
The condition #37's table does not name
scripts/check_main_checkout.sh (shipped in #37, merged in f1e92dc) refuses a non-main
or detached HEAD and warns on three softer conditions. One of those three was not in
#37's verdict table at all, and by the issue's own reasoning it is the strongest of them:
| Condition | #37's verdict | Shipped |
|---|---|---|
HEAD is not main |
refuse | refuse |
| detached HEAD | refuse | refuse |
main behind origin/main |
warn | warn |
main ahead of origin/main |
(not named) | warn |
| dirty working tree | warn | warn |
#37 justifies warn-not-refuse on behind as "a stale-but-shared commit is a different
problem from an unshared one". But local commits on main that were never pushed are
precisely the unshared case — the same "code that is on no shared branch" the whole
issue objects to. git checkout main && git merge my-feature, then systemctl restart,
passes the guard as shipped.
This is not hypothetical. It fired for real during the Batch A merge, before the push:
check_main_checkout: main is 6 commit(s) ahead of origin/main (unpushed) — starting anyway
check_main_checkout: on main at f1e92dc
Why it shipped as warn
origin/main is a cached ref, only as fresh as the last git fetch, and the guard
deliberately never fetches — an ExecStartPre must not make a network call the start can
wait on. So ahead inherits exactly the staleness objection that makes behind a warn:
an operator who simply has not fetched would be refused a start.
The implementing agent flagged the tension rather than deciding it unilaterally, and
documented it in the script and in docs/DEPLOYMENT.md. This issue is that decision.
The options
- Keep it a warn (status quo) — and say so in the table as a decision rather than an
omission. Cheapest, and the ambiguity is real. - Refuse on ahead — treat unpushed commits as unshared code, which is what they are.
Costs: a fetch-less guard cannot distinguish "unpushed" from "not fetched recently", so
an operator behind a network partition who restarts gets refused for a stale ref. The
escape hatch (REPLICATOR_ALLOW_ANY_CHECKOUT=1) covers it, but reaching for the
override during an incident is exactly the pattern that gets guards deleted. - Refuse only when the ref is demonstrably fresh — e.g. compare
.git/FETCH_HEADmtime against a threshold and refuse on ahead only if the last fetch
is recent. More faithful, and more machinery than the condition may warrant.
Recommend deciding between 1 and 2 explicitly. Whichever wins, the table in
docs/DEPLOYMENT.md and the two-tier comment in the script should read as a decision.
Refs
- replicator.service should refuse to start on any checkout but main #37 — the guard; worker hardening: prioritized backlog clearance (3 batches, 5 issues) #47 — the batch that shipped it
scripts/check_main_checkout.sh,tests/test_check_main_checkout.py::test_being_ahead_of_origin_main_only_warnsdocs/DEPLOYMENT.md§ The checkout guard
Activity
gregoryfoster commentedon Aug 18, 2026
Recommendation: option 2 — refuse on ahead
The staleness objection that makes behind a warn does not transfer to ahead. It
looks symmetric and it is not.
origin/mainis updated by fetch and by a successful push from this repo. So anever-fetched ref can hide behind-ness — remote commits you cannot see, hence refusing
would fail an operator for not having network — but it cannot manufacture ahead-ness
for commits this checkout pushed, because the push would have moved the ref. "Ahead" is
therefore local evidence: these commits exist here, and nothing here ever published
them. No network call is needed for that claim to be sound, which is exactly what the
fetch-less guard can afford to assert.
The only false positive is someone publishing the identical SHAs from another clone.
Through the normal path — PR merge, squash or rebase — the SHAs differ, so the tree reads
as diverged rather than merely ahead. Still unshared, still correctly refused.
The cost is one command, in a flow that already intends it. Ahead-of-origin only
arises when someone merges locally on the VM (Batch A, Batch C). The remedy is
git push, orgit reset --hard origin/mainif the commits were unwanted — neitherneeds the override. The genuine partition case (a hotfix committed while GitHub is
unreachable) keeps
REPLICATOR_ALLOW_ANY_CHECKOUT=1, anddocs/DEPLOYMENT.mdshouldname that as the override's legitimate use so reaching for it there is a documented path
rather than the guard-erosion pattern the issue worries about.
Option 3 is rejected.
.git/FETCH_HEADmtime measures the freshness of inboundknowledge — the behind axis. It says nothing about whether the local commits were
pushed, which is the actual question. Machinery on the wrong axis, plus a threshold
nobody can tune from evidence.
A sub-decision to settle in the same change
If no
origin/mainref exists at all, the comparison block is skipped entirely andthe guard starts. Under option 2 that makes
git remote remove origina silent bypass ofthe new refusal — the same shape the "absence is hard" stance rejects for git itself.
Proposed: keep it non-fatal but warn loudly ("no
origin/main— cannot tell whethermainis shared"), and write it into the table as a decision, or it comes back as its ownissue.
Note before this lands
The live checkout is 1 commit ahead of
origin/mainright now (4e19c5b, unpushed).Once this ships, the next
systemctl restartrefuses until it is pushed. Ordering alsowants to be explicit in AGENTS.md § Server Lifecycle, which currently reads "Deploy
committed code with
uv sync --frozen && sudo systemctl restart" — it should be push,then restart.
Restart=on-failurewithStartLimitBurst=3/StartLimitIntervalSec=3600means arefusal costs three attempts and then a clean failed state — no restart loop. Same as the
existing branch refusal.
Change set
scripts/check_main_checkout.sh— ahead moves to the refuse tier; the message namesboth hypotheses (
git push, orgit fetchif you believe they are already pushed) andthe override. The two-tier comment is rewritten as the decision with the asymmetry
argument, not as a deferral.
tests/test_check_main_checkout.py—test_being_ahead_of_origin_main_only_warnsbecomes a refusal; add a diverged case (ahead and behind ⇒ refuse) and a
missing-
origin/maincase (starts, warns).test_being_behind_origin_main_only_warnsstands unchanged — that is the point.
docs/DEPLOYMENT.md§ The checkout guard — the row becomes refuse, carrying theref-is-updated-by-push reasoning so the asymmetry does not read as inconsistency; the
REPLICATOR_ALLOW_ANY_CHECKOUTentry gains the partition case.AGENTS.md§ Server Lifecycle — push, then restart.deploy/replicator.serviceunchanged, so nocpto/etc/systemd/system/is needed.Proceeding with implementation on a worktree branch.
#48 fix: apply review findings (CR #17-#23)
gregoryfoster commentedon Aug 18, 2026
Shipped in #49 — merged as
8f93d17, deployed and running (BUILD_ID=8f93d17).Decision: option 2, refuse on ahead.
origin/mainis moved bygit fetchand by a successfulgit pushfrom this repository, so a never-fetched ref can hide behind-ness but cannot manufacture ahead-ness for commits this checkout pushed. Ahead is local evidence, assertable without the network call anExecStartPremust not make — which is the asymmetry the issue was unsure about. Option 3 rejected:FETCH_HEADmtime measures the behind axis.7349fdc— the refusal, its message set (git push/git reset --hard origin/main/git fetchif you believe they are already pushed / the override during a partition), and a missing-origin/mainwarn sogit remote remove origincannot become a silent bypass.2867cbb— review findings CR A body-less 304 dead-letters — make "not modified" a first-class outcome before conditional GET is usable #17–#22 docs: curate the context surface to the 6,000-token budget (wave A, pair 3) #23: the unit's own comment,docs/STYLE.md's journald inventory, the manifest's two descriptions, and every warn moved above the refusal so one failed start names every condition.docs/DEPLOYMENT.md,docs/COMMANDS.md:git pull --ff-onlywhen the merge happened on GitHub,git pushwhen it happened here. The old single line front-loaded a push that routinely printedEverything up-to-date.deploy/replicator.servicecomment changed ⇒cpto/etc/systemd/system/done; installed unit and repo copy verified identical.codebase_context_searchverified to answer "refuse", not the old "warn".Tests: 777 passed; CI green on the PR. The escape hatch keeps the one case the refusal legitimately costs — a hotfix committed while the remote is unreachable — and
docs/DEPLOYMENT.mdnames it as such rather than leaving it to improvisation.