gabriel / muse public
Closed #31 Docs
filed by gabriel human · 134 days ago

Implement ^ref exclusion prefix syntax in muse traversal commands

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

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 in muse 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..B to 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 ^main equivalent to muse rev-list main..HEAD.
  • EX_02 — muse rev-list multiple 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 by AU_01 that idiomatically accept ^ref in 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..B composes 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 ^B row to the Gitism Glossary table in muse/docs/agent-guide.md.
  • GL_02 — Add git log A ^B → muse log A..B row 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.md returns zero matches (no surviving ^ref gitisms in the guide itself).

Acceptance criteria

  1. muse rev-list HEAD ^main --json exits 0 and returns the same commit set as muse rev-list main..HEAD --json.
  2. muse rev-list A ^B ^C --json correctly excludes commits reachable from both B and C.
  3. muse log main..feat --json exits 0 and returns commits on feat not on main.
  4. All EX_*, RG_*, and GL_* deliverables checked off.
  5. No ^ref syntax 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 diff positional argument handling (it already takes commit_a commit_b explicitly).
◎ Activity1
●
gabriel opened this issue 134 days ago
○
gabriel 1 day ago

Implemented and shipped — commit 85fc1542 on dev/main (pushed to local/staging/production).

Phase 1 — Audit (AU_01)

Command A..B support ^ref support before this change Resolution
muse rev-list yes (pre-existing) no — hard too_many_refs CLI error Fixed (EX_01/EX_02)
muse log yes (pre-existing, issue #28) no — silently swallowed as a file pathspec, returned a wrong empty result with no error at all Fixed (EX_03)
muse commit-graph n/a n/a Out of scope — uses --tip/--stop-at flags, not git-positional ref syntax; nothing to translate
muse diff n/a n/a Explicitly out of scope per the ticket (explicit commit_a commit_b positionals)
muse cherry (git) / muse cherry-pick n/a n/a Muse has no cherry (upstream-diff) command, only cherry-pick <ref> which applies exactly one commit — "no new commands" puts this out of scope

Phase 2 — ^ref exclusion prefix (EX_01–EX_04)

  • EX_01/EX_02 (muse rev-list): ^ref tokens are now collected as exclusions — any number, composing with each other and with a single plain/A..B include token (rev-list A ^B ^C). Unioned into the same ancestor_ids-based exclusion-set logic the existing A..B path already used. rev-list X ^Y ≡ rev-list Y..X.
  • EX_03 (muse log): same semantics. Two argparse wrinkles needed handling: a lone ^ref token lands in the single ref positional itself (its nargs="?" greedily claims the one token before nargs="*" pathspec sees anything), while additional ^ref tokens land in pathspec and 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.
  • EX_04: no additional commands identified beyond rev-list/log needed this (see audit table).

Phase 3 — A..B in muse 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 -n limit 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..B and X ^Y both 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.md returns zero matches — is in direct tension with documenting working ^ref syntax: any correct example like rev-list A ^B necessarily 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 ^ref as 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 to A..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 --oneline regression, RG_04 filter-composition already-correct regression).

Confirmed genuine RED for both rev-list and log fixes 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.

✓
closed this issue 1 day ago