MWP-7 [muse] — Verify & close #55 (sibling-ref push negotiation); align --dry-run to the live push path (#32)
Master tracker: muse#58 — Muse Wire Protocol MVP hardening. Repo target:
muse(the push client). No musehub server changes. Closes on completion: muse#55, muse#32. Predecessors (all done): MWP-1..MWP-6. The wire regression suite is green (pytest -m wire) and the MWP6_00 anti-skip gate is live.
This ticket is fully spec'd for another agent to execute. Every deliverable carries
a test ID and a concrete code anchor (file.py::Symbol or file.py:line). Read the
Code Intelligence Appendix at the bottom first — it lists every symbol you will touch
and the exact muse code command to read it.
IMPORTANT — this project uses Muse (not git) for version control.
- Never run
git,gh, or any git subcommand. Repo paths:~/ecosystem/muse,~/ecosystem/musehub,~/ecosystem/agentception.- Use
muse code grep / symbols / cat / impact / depsbefore reading files directly. Every command accepts--json/-j.- Use
muse -C ~/ecosystem/muse <command>when CWD differs from the target repo.- Commit with full provenance:
muse commit -m "..." --agent-id claude-code --model-id claude-sonnet-4-6 --sign.- Branch first; never commit to
dev/main. Never merge todev/mainwithout gabriel's explicit permission.
Background
The two open bugs this ticket owns
#55 — push have-negotiation ignores sibling refs.
Observed: after pushing dev to staging (17 commits, 46 blobs), pushing main (which is
dev + one merge commit) attempted to upload 1,375 commits / 379 MB and died with
[Errno 32] Broken pipe. The merge-base of local main and local dev is exactly dev's
tip — every object is already on the remote via dev. Only the single merge commit needed to
go over the wire. The have set was scoped to the target branch's remote tip, so objects
reachable via sibling refs (dev) were treated as missing.
#32 — push --dry-run overcounts commits_sent.
Observed: push local main --force-with-lease --dry-run reported commits_sent: 1271; the
live push sent commits_sent: 0. The dry-run walks want − {remote_branch_tip} only — it
does not simulate the full have negotiation the live push performs over all sibling refs.
An agent or human seeing 1271 before a push will reasonably stop and investigate — eroding
trust in dry-run output.
What the live push path looks like today (the RC-7 claim)
The master tracker's RC-7 asserts the live push path was already repaired for #55, and that MWP-7 is therefore "verify + close + align dry-run." The code does look intentional:
Commit-walk boundary —
push.py:819-822(insidepush.py::run) buildsbranch_havefrom allremote_branch_heads.values():branch_have: list[str] = [ h for h in remote_branch_heads.values() if _is_valid_commit_id(h) ]passed into
_push_mpack(..., branch_have=branch_have)→walk_commits(root, [local_head], have=branch_have).Blob dedup —
mpack.py::walk_commits(mpack.py:330-336) unions every have-commit's full snapshot manifest intohave_blobs, thenblobs_to_send = manifest_blobs − have_blobs(mpack.py:362). Because Muse snapshots are flat full-repo manifests, a sibling tip's manifest contains every blob reachable at that tip — so subtracting it removes everything already on the remote via that sibling.havefor objects —push.py:749-752is likewise the union of all remote heads.
So someone wrote code intending to close #55. But #55 is still open. We do not actually know whether (a) the fix is complete and was simply never verified+closed, or (b) the fix is partial (e.g. a residual over-count in a force-with-lease or diverged-history edge). This ticket does not assume RC-7 is correct — it establishes ground truth empirically first, then branches.
What is NOT yet aligned — the dry-run path
The dry-run block in push.py::run (push.py:663-692) is inconsistent with the live path
and with itself:
if dry_run:
have: list[str] = [ # ← objects: ALL tracking refs
c for c in _all_known_have_anchors(root)
if c != local_head and commit_exists(root, c)
]
dry_branch_have: list[str] = [] # ← commits: ONLY target branch tip
_cached = get_remote_head(remote, push_branch, root)
if _cached and commit_exists(root, _cached):
dry_branch_have = [_cached]
dry_walk = walk_commits(root, [local_head], have=dry_branch_have)
dry_commits = len(dry_walk["commits"]) # ← over-counts (the #32 bug)
dry_objects = len(collect_blob_ids(root, [local_head], have=have))
dry_commitswalks back to onlyget_remote_head(remote, push_branch)— the target branch's cached tip. Sibling refs are ignored → the #32 over-count.dry_objectsalready uses all tracking refs (_all_known_have_anchors). So commits and objects in the same dry-run use different have sets. The object count is already sibling-aware; the commit count is not.
The fix is to make dry_commits use the same all-sibling-refs anchor set as dry_objects,
so dry-run mirrors the live path's negotiation logic.
Architecture recap — the push negotiation, end to end
muse push (push.py::run, entry registered by push.py::register) negotiates in two
phases. Read these symbols before touching anything (see Appendix):
| Step | Live path (push.py::run) |
Anchor | Purpose |
|---|---|---|---|
| 0 | _fetch_remote_info_safe(transport, url, token) → remote_branch_heads |
push.py:697-752 |
Discover all remote branch heads via GET /refs. |
| 1 | have = union of all remote heads; remote_head = target-branch tip |
push.py:749-757 |
Compute the negotiation anchor set. |
| — | up-to-date short-circuit when remote_head == local_head |
push.py:789-808 |
status="up_to_date", commits_sent=0. |
| 2 | branch_have = all remote heads |
push.py:819-822 |
Commit-walk stop set. |
| 3 | _push_mpack(..., have, branch_have=branch_have) |
push.py:825-828 |
Build + upload the delta mpack. |
push.py::_push_mpack (push.py:192):
commit_walk = walk_commits(root, [local_head], have=branch_have) # commit boundary
... blobs_to_send = commit_walk["all_blob_ids"] # already net of have_blobs
returns (PushResult, commits_sent, objects_sent)
mpack.py::walk_commits (mpack.py:303) is the single source of truth for what enters a
push mpack:
iter_ancestors(repo_root, commit_ids, prune=lambda cid: cid in have_set)— BFS stops at any have-commit.have_blobs= union of every have-commit's full flat manifest (mpack.py:330-336).blobs_to_send = manifest_blobs − have_blobs(mpack.py:362).
Key invariant for this ticket: walk_commits takes a single flat have list and uses it
for both the commit-prune boundary and the blob subtraction. There is no separate
"commit-have" vs "blob-have". This is why passing all sibling heads as have fixes both
dimensions of #55 at once.
Goal — definition of MVP done for MWP-7
- #55 is empirically resolved and closed with evidence. Pushing
mainwhen it lags an already-pushed siblingdevon the same remote sends only the delta — the merge commit and zero (or near-zero) net-new blobs — never the full history. --dry-runmirrors the live negotiation. For any branch,push --dry-run'scommits_sent/objects_sentequal what the live push actually sends, using the same all-sibling-refshavelogic. #32 is closed.- Both are codified as non-skipped regression tests wired into the suite (and, where the
wire marker applies, into
pytest -m wire). - No regression in the existing push test corpus (≈20
test_push_*/test_*push*files).
Phases — TDD-first, load-bearing, branch-on-result
Execution rule: each phase must be fully green before the next begins. Write the test first (red), then implement to green. Test IDs appear in both this plan and the test file. Phase 1 is the gate that decides Phase 2's shape — do not pre-judge it.
Phase 1 — Reproduce & establish ground truth (the gate)
Build the #55 scenario as a deterministic, in-process reproduction and find out, with evidence, whether the current live path already sends only the delta. This phase writes no production code — it only adds tests and records the verdict.
Scenario under test (the #55 shape):
remote has: dev → tip D (a linear chain root..D, full manifests on remote)
local has: dev → D AND main → M, where M = merge commit with parent D (main = dev + 1)
push main → remote_branch_heads = {"dev": D} (main absent on remote, or at an old tip)
expected: commits_sent == 1 (just M), objects_sent == (only M's net-new blobs, ~0)
New test file: tests/test_mwp7_sibling_negotiation.py (marker: pytest.mark.wire).
Deliverables:
- [x]
GT_01— Helper builders in the test module:_chain(root, n)(linear commit chain returning ids),_merge_commit(root, parent, content)(a commit whose manifest = parent's manifest + 1 changed/added file). Reuse the patterns intests/test_push_branch_have.py::_commit/_build_linear_chainandtests/test_push_have_filter.py::_make_commitrather than inventing new ones. - [x]
GT_02— Commit-boundary truth. Assertlen(walk_commits(root, [M], have=[D])["commits"]) == 1. Proves the commit walk stops at the sibling tipD. (Mirrorstest_push_branch_have.py::test_BH2, but framed as the #55 merge-commit case.) - [x]
GT_03— Blob-dedup truth. Assertwalk_commits(root, [M], have=[D])["all_blob_ids"]contains only M's net-new blob(s) and none of D's manifest blobs. Directly exerciseshave_blobssubtraction (mpack.py:330-362) for the flat-manifest sibling case. - [x]
GT_04—run()-level live path truth. Invokepush.py::runagainst a mocked transport whose_fetch_remote_info_safereturnsbranch_heads = {"dev": D}(main absent). Capture thebranch_haveandhaveactually passed into_push_mpack(spy pattern fromtest_push_branch_have.py::test_BH5). Assert both containD, and assert the reportedcommits_sent == 1. This is the in-process proxy for the staging E2E. - [x]
GT_05— Diverged / force edge. RepeatGT_04withmainpresent on the remote at an older, diverged tipM_old(not an ancestor ofM). Record actualcommits_sent. This is the most likely place a residual #55 bug hides (the up-to-date short-circuit atpush.py:789and thehave/remote_headsplit atpush.py:749-757). - [x]
GT_06— Verdict record. Add a module docstring sectionGROUND TRUTH VERDICT:that states, based onGT_02..GT_05outcomes, whether the live path is already correct (all green) or partially broken (which assertion is red and why). This verdict selects Phase 2's mode.
Phase 1 exit gate: GT_01..GT_06 committed; verdict recorded in the issue (post a comment,
do not edit this body) and in the test module docstring.
Phase 2 — Complete the live-path fix (conditional on Phase 1's verdict)
This phase has two modes. Phase 1 decides which one you execute. Document the chosen mode in the commit message and an issue comment.
Mode A — verdict "already correct" (expected, per RC-7)
No production change to the live path. Convert the Phase-1 proxies into the permanent contract:
- [x]
LF_01— PromoteGT_04to an explicit regression assertion namedtest_LF_push_main_after_dev_sends_only_merge_commit, with a docstring citing #55 and the observed1375 → 1reduction. Keep it intests/test_mwp7_sibling_negotiation.py. - [x]
LF_02— Addtest_LF_no_net_new_blobs_when_sibling_has_all_objectsassertingobjects_sent == 0for the pure-merge case (merge commit references only blobs already in D's manifest). If a merge legitimately adds a blob, parametrize for== K. - [x]
LF_03— Run the adjacent corpus green:tests/test_push_branch_have.py,tests/test_push_have_filter.py,tests/test_push_step1_dag_walk.py,tests/test_cmd_push_hardening.py(one file at a time,python3 -m pytest <file> -q --tb=short). Zero regressions.
Mode B — verdict "partially broken" (only if a GT_* is red)
Implement the minimal fix at the exact failing anchor. Most-likely sites, in priority order:
- [ ]
LF_10— Ifbranch_haveis wrong: fixpush.py:819-822so the commit-walk boundary is the validated union of allremote_branch_heads.values()(not just the target tip). - [ ]
LF_11— Ifhave(blob dedup) is wrong: fixpush.py:749-752so the blob-have anchor is the same validated union. (Bothhaveandbranch_havemust reference the same set — see thewalk_commitssingle-flat-haveinvariant above.) - [ ]
LF_12— If the up-to-date / diverged short-circuit mis-fires (push.py:782-808): correct theremote_headselection so a sibling-covered push is not mis-reported. - [ ]
LF_13— Make every redGT_*green. Then run the Mode-ALF_01..LF_03deliverables (the regression assertions apply regardless of mode). - [ ]
LF_14—muse code impact "muse/cli/commands/push.py::run" --jsonandmuse code impact "muse/core/mpack.py::walk_commits" --jsonto confirm the blast radius; run any additional impacted tests surfaced.
Phase 2 exit gate: all GT_* green; LF_* (chosen mode) green; adjacent push corpus green.
Phase 3 — Verify against staging & close #55 with evidence
The in-process tests prove the logic; #55 is a staging bug, so it closes on staging
evidence. This phase runs a real push to https://staging.musehub.ai.
Use a throwaway repo so we never disturb a real one.
muse hub repo create --no-initso the remote starts empty and the first push is a clean ff-from-null (the same--no-inittrick MWP-6 used for its E2E fixtures).
Procedure (record exact commits_sent / objects_sent from each push):
- [x]
VS_01— Creategabriel/mwp7-sibling-verifyon staging viamuse -C ~/ecosystem/muse hub repo create --name mwp7-sibling-verify --no-init --hub https://staging.musehub.ai --json. Add the remote; build a local repo withdev= linear chain (e.g. 17 commits) andmain=dev+ one merge commit (mirror the #55 report's shape). - [x]
VS_02—muse push staging dev→ record counts (expect full chain on first push). - [x]
VS_03—muse push staging main→ assert from the[PUSH step 1] new_commits/[PUSH step 3]stderr telemetry and the JSON envelope thatcommits_sent == 1andobjects_sentis ~0 (only the merge commit). This is the literal #55 acceptance:1375 → 1. Capture the full stderr[PUSH step *]trace as evidence. - [x]
VS_04— Negative control: add a genuinely new commit with a new blob onmain, push, confirm counts reflect exactly that one commit + one blob (proves we didn't over-correct into under-sending). - [x]
VS_05— Clone the staging repo into a fresh dir andmuse verify --json→ the working tree is complete and correct (no missing blobs from the delta-only push). Ties #55 back to the MWP-1/2 clone-correctness guarantees. - [x]
VS_06— Tear down:muse hub repo deletethe throwaway repo. - [x]
VS_07— Close #55: post a comment on #55 with the before/after counts and the[PUSH step *]evidence, thenmuse hub issue update 55 --status closed --hub https://staging.musehub.ai. (Comment separately; never edit the original description.)
Phase 3 exit gate: staging evidence captured; #55 closed with a comment citing the numbers.
Phase 4 — Align --dry-run to the verified live path (#32)
Now that the live path is the confirmed reference, make dry-run mirror it. Write the failing test first.
New tests: add to tests/test_mwp7_sibling_negotiation.py (or a sibling
tests/test_mwp7_dry_run_alignment.py — pick one and note it).
- [x]
DR_01— Red repro of #32. Build the samedev/mainrepo with tracking refs written for the remote (.muse/remotes/<remote>/dev = D). Invokepush <remote> main --dry-run --json. Assertcommits_sent == 1(matching the live path). This must be RED against current code (today it walks back to onlymain's cached tip and over-counts). Mirror the #32 report: assert the current wrong value in a comment, the correct value in the assertion. - [x]
DR_02— Red repro, object dimension. Assert dry-runobjects_sentequals the live net-new blob count for the same scenario. (Objects already use_all_known_have_anchors, so this may pass today — if so, document it as already-aligned and keep the assertion as a guard.) - [x]
DR_03— Fix. Inpush.py:663-692, replace the target-branch-onlydry_branch_havewith the same all-tracking-refs anchor set already computed forhave(_all_known_have_anchors(root)), sodry_commitsanddry_objectsnegotiate against the identical set:python if dry_run: have = [c for c in _all_known_have_anchors(root) if c != local_head and commit_exists(root, c)] dry_walk = walk_commits(root, [local_head], have=have) # ← same set as objects dry_commits = len(dry_walk["commits"]) dry_objects = len(collect_blob_ids(root, [local_head], have=have))Delete the now-deaddry_branch_have/_cached/get_remote_headblock. - [x]
DR_04—DR_01/DR_02now green. - [x]
DR_05— Preserve the offline contract.tests/test_cmd_push_hardening.py:: ...test_dry_run_makes_no_http_calls(≈L544) must stay green — the recommended fix keeps dry-run offline (uses local tracking refs, noGET /refs). Run it explicitly. - [x]
DR_06— Update the dry-run docstring/comment inpush.py::run(the "no network calls" comment atpush.py:662) to state precisely: dry-run negotiates against the local tracking-ref mirror of remote heads — it equals the live push when tracking refs are fresh (i.e. after amuse fetch); stale tracking refs may still drift. (See Design Decision D1.)
Phase 4 exit gate: DR_01..DR_06 green; test_dry_run_makes_no_http_calls green.
Phase 5 — Close #32, wire regression into the suite, finalize
- [ ]
RG_01— End-to-end #32 scenario (the literal report). A test that fast-forwards localmainto match a remotedevalready in the tracking refs and assertspush --dry-run mainreportscommits_sent: 0— the exact1271 → 0case from #32. Map to #32'sVL_01. - [ ]
RG_02— Non-zero control. Genuinely new commits → dry-run reports the correct non-zero count (maps to #32VL_02). - [ ]
RG_03— Suite wiring. Confirmtests/test_mwp7_sibling_negotiation.py(and any dry-run sibling file) carrypytest.mark.wireand appear underpytest -m wire. Confirm the MWP6_00 gate (tests/test_mwp6_wire_suite_enabled.py) still passes (no flux-skip introduced). - [ ]
RG_04—muse code test --jsonon the changed files; full Phase-2 adjacent corpus green one more time. - [ ]
RG_05— Close #32: comment on #32 with the dry-run-vs-live parity evidence, thenmuse hub issue update 32 --status closed --hub https://staging.musehub.ai. - [ ]
RG_06— Update master tracker muse#58: tick the two open acceptance criteria — "Pushingmainwhen it lags an already-pusheddevsends only the delta; #55 closed with evidence (RC-7)" and "--dry-runcommit/object counts equal the real push counts (#32)" — and mark MWP-7 ✅ COMPLETE in the sub-tickets list. (Read the local source file first, edit locally, push--body-file— never cowboy-push.) - [ ]
RG_07— Merge prep: ensure all work is on the feature branchtask/mwp-7-sibling-negotiation; stop there and hand back to gabriel for the dev/main merge (do not self-merge).
Phase 5 exit gate: #55 and #32 closed; muse#58 updated; all MWP-7 tests green and wired.
Branching & commits
muse -C ~/ecosystem/muse checkout dev
muse -C ~/ecosystem/muse checkout -b task/mwp-7-sibling-negotiation \
--intent "verify+close #55 sibling-ref push negotiation; align --dry-run to live (#32)" \
--resumable
# ... per-phase commits ...
muse -C ~/ecosystem/muse commit -m "test(mwp7/phase1): ground-truth #55 sibling negotiation (GT_01..GT_06)" \
--agent-id claude-code --model-id claude-sonnet-4-6 --sign
One commit per phase minimum; test-first within each. Do not merge to dev/main — that
is gabriel's call.
Acceptance criteria (the whole-ticket gate)
- [ ]
GT_01..GT_06codify the #55 scenario and record an explicit ground-truth verdict. - [ ] Live path proven (Mode A) or fixed (Mode B): pushing
mainthat lags siblingdevsends only the merge commit and ~zero net-new blobs (LF_*). - [ ] Staging evidence:
VS_03showscommits_sent == 1(the literal1375 → 1);VS_05clone verifies complete. #55 closed with a comment citing the numbers. - [ ]
--dry-runcommits_sent/objects_sentequal the live push for the sibling-lag scenario (DR_*,RG_01..RG_02), with the offline contract preserved (DR_05). #32 closed with parity evidence. - [ ] All MWP-7 tests carry
pytest.mark.wire, run green underpytest -m wire, and the MWP6_00 anti-skip gate stays green (RG_03). - [ ] Zero regressions across the existing
test_push_*/test_*push*corpus. - [ ] muse#58's two open RC-7/#32 acceptance criteria ticked; MWP-7 marked ✅ COMPLETE.
Design decisions
D1 — Dry-run stays offline. DECIDED (gabriel, 2026-06-29).
#32's stated fix (FX_01) says dry-run should fetch all remote heads (GET /refs, like live
step 0). We deliberately do not do that. Dry-run is a local prediction: it negotiates
against the local tracking-ref mirror of remote state (_all_known_have_anchors) — the same
model the live push's --force-with-lease safety check already trusts, and the same mental
model as git push --dry-run (which also never fetches). Making dry-run silently hit the
network would break the documented "no network calls" contract, the existing contract test
test_cmd_push_hardening.py::...test_dry_run_makes_no_http_calls, and the user expectation that
dry-run is cheap and local.
The fix therefore aligns the negotiation logic (use all sibling refs, not just the target
branch) while keeping dry-run offline. Consequence — stated plainly in the UX via DR_06:
dry-run counts equal the live push when tracking refs are fresh (i.e. after a muse fetch);
stale tracking refs may drift, exactly as the live --force-with-lease lease can be stale. This
is the correct, principled behaviour — not a compromise. The network-fetch variant is
explicitly rejected and out of scope.
D2 — One flat have set, not two. walk_commits uses a single have list for both the
commit-prune boundary and the blob subtraction. Do not introduce a separate "blob-have" — the
flat-manifest model makes the single set correct for both. The live path's two variables
(have, branch_have) currently hold the same logical set; keep them consistent (or unify)
rather than letting them diverge.
Out of scope
- Any change to the mpack binary format, the presigned-upload model, or the server (musehub) negotiation. MWP-7 is client-side only.
- The clone-staleness / generation pipeline (RC-1..RC-5) — done in MWP-1..MWP-5.
- Cache-invalidation audit (RC-8 / MWP-8) — separate ticket.
- Shallow-clone (
--depth) negotiation. - Re-architecting
--dry-runinto a live-parity network call — explicitly rejected (see D1).
Code Intelligence Appendix — symbol anchors
Read these before editing. Every anchor is a muse code target.
Primary edit sites
| Symbol / anchor | Command | Role |
|---|---|---|
muse/cli/commands/push.py::run |
muse code cat "muse/cli/commands/push.py::run" --json |
Houses both the dry-run block (:663-692) and the live path (:697-828). |
muse/cli/commands/push.py::_push_mpack |
muse code cat "muse/cli/commands/push.py::_push_mpack" --json |
Builds + uploads the delta mpack; returns (PushResult, commits_sent, objects_sent). |
muse/cli/commands/push.py::_all_known_have_anchors |
muse code cat "muse/cli/commands/push.py::_all_known_have_anchors" --json |
All local tracking-ref heads — the dry-run anchor set. |
muse/cli/commands/push.py::_is_valid_commit_id |
muse code cat "muse/cli/commands/push.py::_is_valid_commit_id" --json |
Validates have anchors (used at :751, :821). |
muse/cli/commands/push.py::_fetch_remote_info_safe |
muse code cat "muse/cli/commands/push.py::_fetch_remote_info_safe" --json |
Live GET /refs → remote_branch_heads (step 0). |
muse/core/mpack.py::walk_commits |
muse code cat "muse/core/mpack.py::walk_commits" --json |
Single source of truth for the push delta: commit prune + have_blobs subtraction (:303-382). |
muse/core/mpack.py::collect_blob_ids |
muse code cat "muse/core/mpack.py::collect_blob_ids" --json |
Object-count walk used by dry-run (:840). |
Reference / dependency reads
| Symbol | Command | Why |
|---|---|---|
muse/core/mpack.py::collect_blob_ids_from_deltas |
muse code cat "muse/core/mpack.py::collect_blob_ids_from_deltas" --json |
How manifest_blobs is derived from snapshot deltas. |
muse/core/mpack.py::_build_snapshot_deltas |
muse code cat "muse/core/mpack.py::_build_snapshot_deltas" --json |
Delta construction (flat-manifest proof). |
muse/core/mpack.py::build_mpack_from_walk |
muse code cat "muse/core/mpack.py::build_mpack_from_walk" --json |
Pack assembly from a _WalkResult. |
Blast-radius checks (run before/after editing)
muse code impact "muse/cli/commands/push.py::run" --json
muse code impact "muse/core/mpack.py::walk_commits" --json
muse code deps "muse/cli/commands/push.py" --json
Existing tests to read (don't reinvent fixtures)
| File | Reuse |
|---|---|
tests/test_push_branch_have.py |
_commit, _build_linear_chain, the test_BH5 _spy_push_mpack capture pattern (→ GT_04). |
tests/test_push_have_filter.py |
_make_commit, _bare_repo, TestE2EPushTwiceVII, TestIXCrossRemoteContamination (→ GT_*, LF_*). |
tests/test_cmd_push_hardening.py |
dry-run schema + test_dry_run_makes_no_http_calls (→ DR_05 offline guard). |
tests/test_push_step1_dag_walk.py |
step-1 want − have walk semantics. |
tests/test_mwp6_wire_suite_enabled.py |
MWP6_00 anti-skip gate — must stay green (RG_03). |
Test run discipline
muse -C ~/ecosystem/muse code test --json # only tests for changed files
python3 -m pytest tests/test_mwp7_sibling_negotiation.py -q --tb=short
python3 -m pytest -m wire -q --tb=short # the wire suite gate
# NEVER: python3 -m pytest tests/ (whole-suite run is forbidden)