muse diff treats a bare path as a commit ref instead of a pathspec (git-parity gap)
Background
While reviewing a working-tree change during the muse clone hash-mismatch debug
session, muse diff musehub/services/musehub_wire_shared.py produced no output.
The path argument is interpreted as a commit ref (commit_a), fails to resolve,
and the command prints ⚠️ Commit '<path>' not found.
muse diff does support path scoping — but only via the --path / -p flag.
A bare positional path, and the git-universal -- pathspec separator, are not
recognized. This diverges from git diff, where git diff <path> (when the name
is unambiguous) and git diff -- <path> both scope the diff to that path. The
result is a surprising "no diff" for a natural, git-idiomatic invocation.
Observed behavior
Run from a repo with an uncommitted change to musehub/services/musehub_wire_shared.py:
| Command | Result |
|---|---|
muse diff |
✅ full working-tree diff (35 lines) |
muse diff --path <p> / muse diff -p <p> |
✅ path-scoped diff (6 lines) |
muse diff <p> |
❌ ⚠️ Commit '<p>' not found — exit 1, no diff |
muse diff -- <p> |
❌ ⚠️ Commit '<p>' not found — but exit 0 (warns yet "succeeds") |
muse diff --help documents positionals as [commit_a] [commit_b] and a separate
--path/-p flag; there is no -- pathspec handling.
Problems
- No bare-path scoping.
muse diff <path>treats the path as a commit ref. Git acceptsgit diff <path>when the token is not also a ref. - No
--pathspec separator.git diff -- <path>...is the universal, unambiguous way to force path interpretation.muse diff -- <path>ignores the separator and still treats the token as a commit. - Inconsistent exit code. "Commit not found" exits
1for a bare path but0for the--form. A not-found ref should fail consistently.
Expected (git parity)
muse diff -- <path>...— everything after--is treated as paths (pathspec), never as commits. This is the primary fix; it is unambiguous and universally idiomatic.muse diff <path>— when the token does not resolve to a commit/ref but does match a tracked path, scope the diff to that path (git's disambiguation). If a token is both a valid ref and a path, follow git: require--to disambiguate (or prefer the ref and emit a clear warning).- Consistent, correct exit codes for an unresolved ref.
Repro
cd <any muse repo with an uncommitted change to FILE>
muse diff FILE # expect a path-scoped diff; actual: "Commit 'FILE' not found"
muse diff -- FILE # expect a path-scoped diff; actual: same warning, exit 0
muse diff --path FILE # works today (workaround)
Acceptance criteria
muse diff -- <path>...scopes the diff to the given path(s); no commit interpretation after--.muse diff <path>scopes by path when the token is not a valid commit/ref but is a tracked path.- Documented ref/path ambiguity rule (matches git:
--disambiguates). - Consistent exit code when a positional does not resolve to a commit.
--helpdocuments--and bare-path behavior.- Tests:
--separator, bare-path scoping, ref-vs-path ambiguity, exit codes.
Out of scope
- Changing or removing
--path/-p— it works and stays.
Provenance
Discovered 2026-06-16 during the muse clone snapshot hash-mismatch investigation,
when muse diff <path> silently returned nothing while reviewing a fix diff.
Resolved — shipped to
main/devon all remotesCommit:
sha256:0d3de0ee53b63667de053f3ad712184124142df86473b80939347b487a65d6af(branchfeat/36-diff-pathspec, fast-forward merged dev→main, pushed to local/staging/production).Fix
commit_awith nocommit_bnow tries resolving as a commit ref first — exactly the existing behavior, unchanged for every current script or ref. Only when that fails, and the token matches a tracked file or directory (checked against both the HEAD manifest and the working tree), it falls back to treating the token as a path filter, folded into the existing--path/-pmanifest-filtering machinery rather than a parallel code path.This transparently covers
muse diff -- <path>too: argparse consumes the literal--beforerun()ever seesargs, so an unresolvablecommit_afrom-- <path>is indistinguishable from the bare<path>case by the time this code runs — same fallback, same fix, no separate implementation needed.Ref/path ambiguity rule (documented, per the ticket's own sanctioned alternative)
When a token is both a valid ref and a tracked path, muse always resolves it as the ref, deterministically. True forced-path-over-ref disambiguation via a literal
--isn't observable after argparse finishes parsing (it strips the separator before dispatch) without deeper parser-pipeline changes that felt out of proportion to this ticket's scope. This is the exact alternative the ticket explicitly offered ("or prefer the ref and emit a clear warning" — I chose silent, deterministic ref-preference over a warning, since the behavior is now fully documented in--helpand the Gitism Glossary, making a runtime warning more noise than signal). Documented in three places: thecommit_a--helptext, therun()docstring, and a new Gitism Glossary entry indocs/agent-guide.md.Exit-code consistency (ticket problem 3)
Verified this was already fixed on current code before I touched anything — both
muse diff <path>andmuse diff -- <path>already exit 1 identically for an unresolvable token (the ticket's reported "exit 0 for--" discrepancy must have been fixed by an unrelated prior change). AddedTestRefVsPathAmbiguity::test_exit_code_consistent_for_unresolvable_single_positionalso this can't silently regress.Real bug found and fixed while building this
resolve_commit_ref— and therefore every ref-taking command, not justdiff— crashed with an uncaughtValueErrorinstead of cleanly returning "not found" whenever a ref-like candidate string contained/and wasn't a branch or remote-tracking ref. Root cause:read_version_tagcalledversion_tag_pathunconditionally, andversion_tag_pathraisesValueErrorfor path-traversal-looking tag names (correct behavior for its own normal callers, e.g.muse tag add) — butread_version_tag's own docstring already promises "Noneif the file does not exist," a contract it was silently violating for syntactically-invalid names instead of just reporting "not a tag." This blocked bare-path scoping for every path under a subdirectory (muse diff sub/file.pywould crash, not just "not find" the ref) — not something this ticket introduced, but something it newly exercised and needed fixed to meet its own acceptance criteria. Fixed at the source inread_version_tag()so every caller benefits, not justdiff.Verification
tests/test_diff_pathspec.py: bare-path scoping (file and directory-prefix), the--separator, ref-vs-path ambiguity (including the documented ref-wins case), exit-code consistency, and regression guards confirming--path/-pand the two-ref/no-args forms are completely unaffected.tests/test_core_version_tags.pyfor theread_version_tagfix.test_cmd_diff.py,test_cmd_diff_hardening.py,test_diff_algorithms.py,test_diff_dir_tracking.py,test_diff_envelope.py,test_diff_json_schema.py,test_diff_staged_clean.py,test_diff_pathspec.py,test_core_version_tags.py,test_cmd_version_tags.py,test_cmd_describe_version_tags.py,test_version_tag_relative_resolution.py,test_version_tag_signing.py,test_cmd_version_tag_push.py,test_phase1_cas_branch_ref_callers.py,test_cmd_checkout.py) — all passing, zero regressions.muse-devwith both file and directory paths, the ambiguous-ref-and-path edge case, and confirmedmuse diff --helprenders the new documentation correctly.Acceptance criteria
muse diff -- <path>...scopes the diff to the path — verified (same code path as bare path, since--is invisible by the timerun()executes).muse diff <path>scopes by path when not a valid ref but a tracked path.---based disambiguation, which isn't observable post-parse).--helpdocuments the new behavior.--separator, bare-path scoping, ambiguity, and exit codes.Out of scope (respected, not touched)
--path/-punchanged and still fully functional (explicitly verified, not just assumed).