Address race-focused review: answer per ref, compare hashes, narrow the window
Commit

The reviewer found no live data race — every push, callback, and marker decision runs on the goroutine driving Execute, and the fixture state guarded last round holds under -race. What it found instead were ordering faults and one rule I got backwards.
"Silence is a refusal" was wrong, and it was mine. Last round's gitproto change synthesized a rejection for a command the target's report never mentioned. That contradicted the guard two lines above it, which says treating silence as a refusal would fail every batched bootstrap against a non-reporting target: an omitted status for a temp ref would have aborted a multi-gigabyte import over something nothing refused, and it attributed a client inference to the target in the operator-visible reason. It also had no test.
Silence is a third answer. Pusher.LastOutcome now reports applied, refused, or unknown per ref, computed from what the target actually said, and the strategy treats unknown as doubt — settled against the target's ref listing, never as a refusal and never as success. This also deletes Params.TargetReportsRefStatus: the Pusher holds the advertisement, so the capability question belongs to it rather than being hand-threaded across two packages. LastOutcome answers by value, so no caller can mutate or race with push state it does not own.
The cleanup deleted the marker whenever the branch name existed, at any hash. A concurrent sync that created the branch at an older commit C left our marker holding the only reference to C..tip; the marker was deleted anyway and those commits became unreferenced — the full re-transfer this PR exists to prevent, in the one case where the import genuinely had not landed. Cleanup now requires the branch to be at the hash this run pushed; anything else keeps the marker and says which hash it found. The test that asserted the old behaviour seeded an unrelated hash, so it was asserting the bug: it now covers both sides.
Ordering, all from the review:
- The settlement ran after the tail phase, leaving the ref listing minutes stale while tags pushed — long enough for a concurrent run to adopt the marker and start resuming from it before we deleted it. It now runs as soon as the last create has happened.
- The deferred delete was the only push in the new code not checked for refusal, so a refused cleanup was silent — the opposite of the identical case 100 lines above. Checked per marker now.
- createConfirmed could read the previous branch's answer on the one path where a create plan exists but no push carries it (branch and marker both already at the source hash). The code now tracks whether the create was pushed and treats that path as what it is: a branch the target already holds, marker stale.
- The tail phase re-derived its fetch haves from the plans, re-claiming exactly the creates the cutover withheld. It uses completedRefs, the map the run maintains as ground truth.
Smaller: targetRefsNow guards a nil target session (latent via Probe/Fetch) and documents that receive.hideRefs can hide a ref that is there — safe in this direction, since a hidden branch reads as absent and keeps a marker that was not needed; the redundant package-level listing wrapper is gone; and three pre-existing fixtures still emitted "ng "-prefixed statuses that go-git encodes a second time, which this PR's own comment warns about.
Still open, and still the user's call: the one-shot path defers marker cleanup to a prune that Bootstrap() forbids — the same permanent-marker gap, inherited from #116, now closable with the same listing.
Full suite green under -race, golangci-lint clean. Verified each new guard fails its test when removed.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_018BKBGBh3rJ6WGnZKDbBRy7 Entire-Checkpoint: 01M1C0CFNPR1W8S4GFMSEHXXZZ