push --dry-run overcounts commits_sent by ignoring remote have negotiation
Background
muse push --dry-run reports an inflated commits_sent count that does not match what the live push actually sends. Observed during the phase1-merge-engine merge (2026-06-12):
muse push local main --force-with-lease --dry-runreportedcommits_sent: 1271- The live push sent
commits_sent: 0
The same pattern appeared earlier in the same session:
muse push local dev --force-with-lease --dry-run(muse repo) reportedcommits_sent: 16, live sent0muse push local dev --force-with-lease --dry-run(musehub repo) reportedcommits_sent: 4, live sent0
The root cause: the dry-run walks the local DAG and counts commits reachable from the local tip but not reachable from the remote branch tip being pushed to. It does not simulate the full have negotiation that the live push performs — specifically, it does not account for other remote branch tips the server already knows about.
In the live push, step 0 fetches all remote branch heads (dev, main, task/phase1-merge-engine, etc.) and adds them all to the have set. Step 1 then computes want - have, which correctly returns 0 when the target commit is already reachable via another remote branch. The dry-run skips this and computes want - {remote_branch_tip} only, which counts the entire history between the two tips.
This is misleading and erodes trust in dry-run output. An agent or human seeing commits_sent: 1271 before a push will reasonably pause and investigate, wasting time.
Goal
muse push --dry-run produces a commits_sent count that matches what the live push would actually send, using the same have negotiation logic.
Phases
Phase 1 — Reproduce and characterise
Deliverables:
RC_01— Write a test that sets up a repo with two branches on the remote (e.g.devandmain), advancesmainlocally to matchdev, then asserts thatpush --dry-run mainreportscommits_sent: 0(matching the live push). Test must be red before the fix.
Phase 2 — Fix dry-run have negotiation
Deliverables:
FX_01— Update the dry-run path inmuse pushto fetch all remote branch heads (same as step 0 of the live push) and use the full set ashavewhen computingnew_commits.FX_02—RC_01is now green.FX_03— Existing push tests remain green.
Phase 3 — Validate the fix end-to-end
Deliverables:
VL_01— Run the exact scenario that triggered this report: localmainfast-forwarded to match remotedev;push --dry-run mainreportscommits_sent: 0.VL_02— Confirm that when there ARE genuinely new commits, dry-run still reports the correct non-zero count.
Acceptance criteria
muse push --dry-runcommits_sentalways matches what the live push sends.RC_01,FX_01–03,VL_01–02all checked off.- No regression in existing push tests.
Out of scope
- Changing the live push
havenegotiation (it is already correct). - Dry-run accuracy for
objects_sent(separate concern, lower priority).
Closed — fix landed on task/mwp-7-sibling-negotiation
Root cause
push.pydry-run block built its commit-walk have-set from only the target branch's cached tracking ref (get_remote_head(remote, push_branch)). Sibling branches (e.g. a previously pusheddev) were ignored, sowalk_commitshad no BFS boundary anchors and counted all reachable commits instead of just new ones.A second sub-bug: the
c != local_headfilter excludedlocal_headfrom the have-set. When the remote'sdevtip equalledlocal_head(up-to-date scenario), the walk returned all commits instead of 0.Fix (push.py — dry-run block)
Replace
get_remote_head-based have with all local tracking refs:This mirrors the live path's sibling-ref negotiation (push.py:819-822).
Evidence
commits_sent=18before fix →commits_sent=1after. ✅commits_sent=18before →1after. ✅commits_sent=0, objects_sent=0. ✅commits_sent=2, objects_sent=2. ✅pytest -m wire(test_mwp7_sibling_negotiation.py) green.Commit:
sha256:6ec37c93009f7c30451ea8db62a0a1b764bb79fa93231b5506bb6855251b3e5d