Implement ^ref exclusion prefix syntax in muse traversal commands
Background
Git supports two syntactically different but semantically equivalent ways to express commit exclusion in traversal commands:
- Range syntax —
A..B: commits reachable from B but not from A. Already implemented inmuse rev-list. - Exclusion prefix —
^ref: exclude all commits reachable from ref. More flexible because multiple exclusions compose:git rev-list A ^B ^C ^D.
Git uses ^ref idiomatically in rev-list, log, cherry, and anywhere commit traversal is involved. Agents have deep ^ref muscle memory from git training data. When they write muse rev-list task/feature ^dev --json they get a hard CLI error with no hint about the correct form.
Additionally, muse log currently accepts only a single [ref] — it has no range or exclusion syntax at all. This means the idiomatic git log main..feat (show what's on feat that isn't on main) has no muse equivalent, forcing agents to reach for rev-list as a workaround or fall back to the broken ^ syntax.
Discovered during phase1-merge-engine merge prep (2026-06-12).
Goal
Every command in muse that idiomatically accepts commit ranges or exclusions in git supports both A..B range syntax and ^ref exclusion prefix. Agents can write natural commit traversal expressions without hitting CLI errors or unexpected output.
Phases
Phase 1 — Audit: where does git use ^ref and A..B?
Deliverable:
AU_01— Produce a table mapping git commands that accept^ref/A..Bto their current muse equivalents, noting which already support range syntax and which do not. At minimum cover:rev-list,log,diff,commit-graph,cherry.
This table drives Phases 2 and 3. Scope is limited to commands already implemented in muse — no new commands.
Phase 2 — ^ref exclusion prefix support
Add ^ref as a first-class exclusion prefix to every traversal command identified in Phase 1.
Deliverables:
EX_01—muse rev-list ^ref— single exclusion:muse rev-list HEAD ^mainequivalent tomuse rev-list main..HEAD.EX_02—muse rev-listmultiple exclusions:muse rev-list A ^B ^C— commits reachable from A but not from B or C.EX_03—muse log ^ref— same semantics as rev-list, applied to log output.EX_04— Any additional commands surfaced byAU_01that idiomatically accept^refin git.
Each deliverable requires a failing test written first (TDD), named test_EX_NN_*.
Phase 3 — A..B range syntax in muse log
muse log currently accepts only a single [ref]. Add range support so muse log main..feat works.
Deliverables:
RG_01—muse log A..B— shows commits on B not on A, newest-first, in standard log format.RG_02—muse log A..B --json— same, structured output.RG_03—muse log A..B --oneline— compact form.RG_04—muse log A..Bcomposes with existing filters:--author,--since,--touches,--max-count.
Each deliverable requires a failing test written first (TDD), named test_RG_NN_*.
Phase 4 — Glossary and agent-guide
Deliverables:
GL_01— Add^ref→muse rev-list A..B/muse rev-list A ^Brow to the Gitism Glossary table inmuse/docs/agent-guide.md.GL_02— Addgit log A ^B→muse log A..Brow to the same table.GL_03— Add a note explaining direction:A..B= "reachable from B but not A" (easy to mix up).GL_04— grep gate:grep -n " \^[a-zA-Z]" docs/agent-guide.mdreturns zero matches (no surviving^refgitisms in the guide itself).
Acceptance criteria
muse rev-list HEAD ^main --jsonexits 0 and returns the same commit set asmuse rev-list main..HEAD --json.muse rev-list A ^B ^C --jsoncorrectly excludes commits reachable from both B and C.muse log main..feat --jsonexits 0 and returns commits on feat not on main.- All
EX_*,RG_*, andGL_*deliverables checked off. - No
^refsyntax survives in agent-guide.md.
Out of scope
- Three-dot (
A...B) symmetric difference — separate ticket if needed. - New traversal commands not already in muse.
- Changing
muse diffpositional argument handling (it already takescommit_a commit_bexplicitly).
Implemented and shipped — commit
85fc1542ondev/main(pushed to local/staging/production).Phase 1 — Audit (AU_01)
A..Bsupport^refsupport before this changemuse rev-listtoo_many_refsCLI errormuse logmuse commit-graph--tip/--stop-atflags, not git-positional ref syntax; nothing to translatemuse diffcommit_a commit_bpositionals)muse cherry(git) /muse cherry-pickcherry(upstream-diff) command, onlycherry-pick <ref>which applies exactly one commit — "no new commands" puts this out of scopePhase 2 —
^refexclusion prefix (EX_01–EX_04)muse rev-list):^reftokens are now collected as exclusions — any number, composing with each other and with a single plain/A..Binclude token (rev-list A ^B ^C). Unioned into the sameancestor_ids-based exclusion-set logic the existingA..Bpath already used.rev-list X ^Y≡rev-list Y..X.muse log): same semantics. Two argparse wrinkles needed handling: a lone^reftoken lands in the singlerefpositional itself (itsnargs="?"greedily claims the one token beforenargs="*"pathspec sees anything), while additional^reftokens land inpathspecand would otherwise be silently treated as file paths — the worse of the two pre-existing bugs, since it produced a wrong answer with no error rather than a loud one.Phase 3 —
A..Binmuse log(RG_01–RG_04)Investigation found this was already implemented in a prior ticket (issue #28,
tests/test_log_dotdot_range.py) for plain text/JSON output and-nlimit composition (RG_01/RG_02). Verified live that--oneline(RG_03) and composition with--author/pathspec/--max-count(RG_04) already worked correctly through the shared filter pipeline — added regression tests for both; no code changes were needed for this phase.Phase 4 — Docs (GL_01–GL_04)
Added a new "Commit exclusion syntax" subsection to the Gitism Glossary in
docs/agent-guide.md, framed the same way as the existing refspec-syntax entry — "now works identically to git, not a gotcha" — rather than as a pattern to translate away from. Includes the direction-disambiguation note (GL_03):A..BandX ^Yboth mean "reachable from the non-excluded side," which is easy to get backwards.GL_04 note: the ticket's literal gate —
grep -n " \^[a-zA-Z]" docs/agent-guide.mdreturns zero matches — is in direct tension with documenting working^refsyntax: any correct example likerev-list A ^Bnecessarily contains a space immediately before a caret-prefixed letter. Confirmed the file had zero such matches before this change. Resolved by following the gate's evident intent (no surviving entry frames^refas a broken/unsupported gitism needing translation) rather than gaming the literal grep command by, e.g., obscuring the syntax with non-breaking spaces.Tests
tests/test_cmd_rev_list.py: 4 new (EX_01/EX_02 equivalence toA..B, multi-exclusion composition, JSON output, bad-ref error).tests/test_log_dotdot_range.py: 4 new (EX_03 equivalence, caret-alone defaulting to current branch, multi-exclusion composition, bad-ref error) + 2 new (RG_03--onelineregression, RG_04 filter-composition already-correct regression).Confirmed genuine RED for both
rev-listandlogfixes by reverting each and re-running — failures landed exactly where the removed logic was supposed to act (rev-list hard-errored as before; log silently returned the wrong empty result, exactly matching the bug described in the ticket) — then GREEN with the fixes restored.Regression:
test_log_dotdot_range.py,test_cmd_rev_list.py,test_log_collect_all_commits.py,test_log_json_schema.py,test_log_graph.py,test_log_supercharge.py,test_cli_log.py,test_cmd_log.py,test_log_branch_flag.py— 327 tests, all green, no regressions.