fix: muse switch crashes with unhandled traceback from detached HEAD
muse#106. git allows switching away from detached HEAD with zero special ceremony -- git switch <branch> and git checkout <branch> both just work. muse switch did not: it called the strict read_current_branch() unguarded at the top of run(), which raises ValueError on detached HEAD. The raw exception propagated all the way to an unhandled Python traceback in the user's terminal, instead of any of switch.py's own structured _emit_error calls that exist for every other error path in this function. The only working recovery was muse checkout <branch>, which happens to work only because checkout.py reads HEAD via the tolerant read_head() helper instead.
Root cause confirmed by reading the source (muse/core/refs.py:312) and reproduced live before any code change: muse switch <commit> --detach, then muse switch main, crashes every time.
Fix: switch.py now reads HEAD via the same tolerant read_head() pattern checkout.py already uses, in both the main run() path and the -C force-create path (_run_force_create) -- same unguarded read, same crash, not explicitly called out in the ticket's repro but sharing the identical root cause. current_branch becomes None when detached; every downstream use already only wanted "the branch we're leaving, or None" (display, PREV_BRANCH bookkeeping, same-commit fast-path comparison) -- none of them genuinely required an active branch. Did not touch read_current_branch() itself: it has 100+ call sites across the codebase relying on it raising, exactly the ticket's own Phase 2 guidance for when to prefer matching checkout.py's pattern over widening the shared raising function.
Two latent crash-on-None bugs caught while fixing the primary one, both on code paths only reachable once current_branch could legitimately be None: _write_prev_branch(root, None) would have written the literal string "None" into PREV_BRANCH (now guarded via _maybe_write_prev_branch, skips writing when there's no branch to record); sanitize_display(None) would have raised TypeError since it requires a str (now guarded via _display_branch).
Tests: new tests/test_cmd_switch_detached_head.py, 7 tests covering the exact repro, --json mode, switch to a nonexistent branch (clean structured error, not a crash), switch -c and switch -C from detached (branches off the detached commit, matching git), switch - returning to the last real branch rather than writing "None", and confirming muse checkout's existing recovery path is unaffected. All 7 confirmed RED against the unfixed code, GREEN after. Full tests/test_cmd_switch.py + test_cmd_checkout.py + test_core_refs.py regression suite: 250 passed, zero regressions.
Verified against the real installed CLI (not just the test suite): the exact terminal repro from the ticket now exits 0 with "Switched to branch 'main'", no traceback.
Semantic Changes
23 symbols
Files Changed
+1
~1
1209 in snapshot
0 comments
muse hub commit comment sha256:dfb030d712110599faeb978c6b5eaa4534b9544477860a1e44542d41fdb37abd --body "your comment"
No comments yet. Be the first to start the discussion.