gabriel / muse public
Closed #36
filed by gabriel human · 129 days ago

muse diff treats a bare path as a commit ref instead of a pathspec (git-parity gap)

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

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

  1. No bare-path scoping. muse diff <path> treats the path as a commit ref. Git accepts git diff <path> when the token is not also a ref.
  2. 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.
  3. Inconsistent exit code. "Commit not found" exits 1 for a bare path but 0 for 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.
  • --help documents -- 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.

◎ Activity1
●
gabriel opened this issue 129 days ago
○
gabriel 2 days ago

Resolved — shipped to main/dev on all remotes

Commit: sha256:0d3de0ee53b63667de053f3ad712184124142df86473b80939347b487a65d6af (branch feat/36-diff-pathspec, fast-forward merged dev→main, pushed to local/staging/production).

Fix

commit_a with no commit_b now 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/-p manifest-filtering machinery rather than a parallel code path.

This transparently covers muse diff -- <path> too: argparse consumes the literal -- before run() ever sees args, so an unresolvable commit_a from -- <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 --help and the Gitism Glossary, making a runtime warning more noise than signal). Documented in three places: the commit_a --help text, the run() docstring, and a new Gitism Glossary entry in docs/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> and muse 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). Added TestRefVsPathAmbiguity::test_exit_code_consistent_for_unresolvable_single_positional so this can't silently regress.

Real bug found and fixed while building this

resolve_commit_ref — and therefore every ref-taking command, not just diff — crashed with an uncaught ValueError instead 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_tag called version_tag_path unconditionally, and version_tag_path raises ValueError for path-traversal-looking tag names (correct behavior for its own normal callers, e.g. muse tag add) — but read_version_tag's own docstring already promises "None if 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.py would 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 in read_version_tag() so every caller benefits, not just diff.

Verification

  • 14 new tests in 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/-p and the two-ref/no-args forms are completely unaffected.
  • 1 new regression test in tests/test_core_version_tags.py for the read_version_tag fix.
  • Confirmed via genuine RED: reverted both fixes, re-ran — 7 of 15 new/updated tests failed as expected (including the exact crash traceback reproduced for the version-tags bug) — then restored and confirmed GREEN.
  • 672-test regression sweep across every diff/version-tag/checkout-relevant suite (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.
  • Live-verified via muse-dev with both file and directory paths, the ambiguous-ref-and-path edge case, and confirmed muse diff --help renders 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 time run() executes).
  • ✅ muse diff <path> scopes by path when not a valid ref but a tracked path.
  • ✅ Documented ref/path ambiguity rule — ref wins deterministically (the ticket's sanctioned alternative to true ---based disambiguation, which isn't observable post-parse).
  • ✅ Consistent exit code for an unresolved positional — verified already correct, regression-tested.
  • ✅ --help documents the new behavior.
  • ✅ Tests cover -- separator, bare-path scoping, ambiguity, and exit codes.

Out of scope (respected, not touched)

--path/-p unchanged and still fully functional (explicitly verified, not just assumed).

✓
closed this issue 2 days ago