Decompose muse/cli/commands/bridge.py (3138 lines) into focused modules
Summary
`muse/cli/commands/bridge.py` is 3138 lines and owns eight distinct responsibilities. Like `store.py` before it, it is a god object — every bridge operation (import, export, status, hooks, state I/O, git subprocess wrappers, harmony/shelf migration, drift computation) runs through it. This issue tracks a phased decomposition into focused, single-responsibility modules.
Goal: zero functional change, zero test regressions, cleaner architecture.
The destination is a `muse/core/bridge/` package that owns all domain logic, with `muse/cli/commands/bridge.py` reduced to a thin argparse registration shell (≤ 200 lines).
Responsibility map (current)
| Lines | Symbols | Responsibility |
|---|---|---|
| 67–390 | `_LastImportState`, `_LastExportState`, `BridgeState`, `_SidecarData`, `DriftInfo`, `read_bridge_state`, `write_bridge_state`, `_dict_to_toml` | TOML state persistence |
| 167–267 | `BridgeHook`, `BridgeHooks`, `load_bridge_hooks`, `run_hook`, `run_hooks` | Lifecycle hook dispatch |
| 455–802 | `AttributionMapper`, `_CatFile`, `_git`, `_batch_commit_log`, `_batch_diff_tree`, `_is_lfs_pointer`, `_strip_ansi`, `_parse_sem_ver_bump` | Low-level git subprocess wrappers |
| 804–1456 | `_replay_commit`, `_replay_branch`, `_list_git_branches`, `_list_git_tags`, `_import_tags`, `run_git_import` | Git → Muse import engine |
| 1463–1915 | `import_rerere_to_harmony`, `export_harmony_to_rerere`, `import_stashes_to_shelf`, `export_shelves_to_stash` | Harmony / shelf cross-system migration |
| 1929–2619 | `_validate_git_sha`, `_validate_git_branch_name`, `GitExporter`, `_ensure_git_branch`, `_watch_loop`, `run_git_export` | Muse → Git export engine |
| 2621–2824 | `run_git_status`, `_compute_drift`, `_count_muse_commits_since`, `_print_bridge_status_text` | Status and drift computation |
| 2830–3138 | `register`, `_register_git_import`, `_register_git_export`, `_register_git_status` | CLI argparse registration |
Target layout
``` muse/core/bridge/ init.py — re-exports public API (BridgeState, run_git_import, …) state.py — TOML state types + read/write hooks.py — hook loading and dispatch git_primitives.py — subprocess wrappers (git, _CatFile, _batch*, …) importer.py — Git → Muse import engine harmony_shelf.py — rerere↔harmony and stash↔shelf migration exporter.py — Muse → Git export engine + watch loop status.py — drift computation and status formatting muse/cli/commands/bridge.py — thin shell: argparse registration only (≤ 200 lines) ```
Phase 1 — Create `muse/core/bridge/state.py`
Scope: TOML state types and persistence.
Extract:
- `_LastImportState`, `_LastExportState`, `BridgeState`, `_SidecarData`, `DriftInfo` (TypedDicts / dataclasses)
- `read_bridge_state`, `write_bridge_state`, `_dict_to_toml`
Acceptance: `muse/core/bridge/state.py` exists, all callers in `bridge.py` updated to import from it, all bridge tests pass.
Phase 2 — Create `muse/core/bridge/hooks.py`
Scope: lifecycle hook loading and dispatch.
Extract:
- `BridgeHook`, `BridgeHooks` (TypedDicts)
- `load_bridge_hooks`, `run_hook`, `run_hooks`
Acceptance: hook logic fully decoupled from import/export engines, all bridge tests pass.
Phase 3 — Create `muse/core/bridge/git_primitives.py`
Scope: low-level git subprocess I/O. No Muse domain knowledge.
Extract:
- `AttributionMapper` — author/committer → Muse identity mapping
- `_CatFile` — git cat-file --batch process wrapper
- `_git` — subprocess runner
- `_batch_commit_log` — bulk commit log reader
- `_batch_diff_tree` — bulk diff-tree reader
- `_is_lfs_pointer`, `_strip_ansi`, `_parse_sem_ver_bump`
Acceptance: no git subprocess logic remains in `bridge.py`, all bridge tests pass.
Phase 4 — Create `muse/core/bridge/importer.py`
Scope: Git → Muse commit replay engine.
Extract:
- `_replay_commit` (~140 lines)
- `_replay_branch` (~188 lines)
- `_list_git_branches`, `_list_git_tags`, `_import_tags`
- `run_git_import` (~213 lines)
Internal dependencies: imports from `git_primitives`, `state`, `hooks`.
Acceptance: `run_git_import` callable from `muse.core.bridge.importer`, no import logic in `bridge.py`, `test_bridge_git_import.py` fully green.
Phase 5 — Create `muse/core/bridge/harmony_shelf.py`
Scope: cross-system data migration (rerere ↔ harmony, stash ↔ shelf).
Extract:
- `import_rerere_to_harmony` (~136 lines)
- `export_harmony_to_rerere` (~63 lines)
- `import_stashes_to_shelf` (~135 lines)
- `export_shelves_to_stash` (~112 lines)
Acceptance: migration logic isolated, `test_bridge_harmony_shelf.py` fully green.
Phase 6 — Create `muse/core/bridge/exporter.py`
Scope: Muse → Git export engine and watch loop.
Extract:
- `_validate_git_sha`, `_validate_git_branch_name`
- `GitExporter` class (~360 lines)
- `_ensure_git_branch`, `_watch_loop`
- `run_git_export` (~188 lines)
Internal dependencies: imports from `git_primitives`, `state`, `hooks`.
Acceptance: `run_git_export` callable from `muse.core.bridge.exporter`, `test_bridge_git_export.py` and `test_bridge_watch.py` fully green.
Phase 7 — Create `muse/core/bridge/status.py`
Scope: drift computation and status display.
Extract:
- `run_git_status`
- `_compute_drift`, `_count_muse_commits_since`
- `_print_bridge_status_text`
Acceptance: `test_bridge_git_status.py` fully green.
Phase 8 — Create `muse/core/bridge/init.py`, thin the CLI shell, delete dead code
Scope: final wiring and cleanup.
- Create `muse/core/bridge/init.py` exporting the public API
- Reduce `muse/cli/commands/bridge.py` to argparse registration only (≤ 200 lines: `register`, `_register_git_import`, `_register_git_export`, `_register_git_status`)
- Run `muse code dead --high-confidence-only` and delete anything unreachable
- Update any doc references to `muse/cli/commands/bridge.py` internals
- Update `test_bridge_phase1.py` and `test_bridge_security.py` if they reference internal paths
Acceptance:
- `muse/cli/commands/bridge.py` ≤ 200 lines, contains only argparse wiring
- `muse code breakage` — 0 issues
- `muse code test` — clean
- All `test_bridge_*.py` files pass
- No stale doc references
Cross-cutting rules (all phases)
- No functional changes in any phase — pure mechanical extraction
- Each phase is its own commit on a task branch
- Each phase merges independently to dev before the next starts
- No backward-compat shims, no re-exports, no deprecated annotations
- Delete everything that is unused after extraction
- `muse code test --json` must pass before each phase merges
- Circular import guard: `git_primitives` has no knowledge of Muse domain types; `importer`/`exporter` import from `git_primitives` and `state` but never from each other
Test files to keep green throughout
- `tests/test_bridge_git_import.py`
- `tests/test_bridge_git_export.py`
- `tests/test_bridge_git_status.py`
- `tests/test_bridge_hooks.py`
- `tests/test_bridge_harmony_shelf.py`
- `tests/test_bridge_watch.py`
- `tests/test_bridge_roundtrip.py`
- `tests/test_bridge_phase1.py`
- `tests/test_bridge_security.py`
Implementation complete — all spec items verified
Final state
muse/cli/commands/bridge.py: 3138 → 74 lines — argparse wiring only, zero re-exports, zero domain logic.Acceptance criteria check
bridge.py≤ 200 lines, argparse wiring only (74 lines)muse.core.bridge.__init__exports full public APImuse code dead --high-confidence-only→ 0 dead symbolsmuse code breakage→ 0 issuesmuse.core.bridge.*pathsgit_primitivescarries no Muse domain types (FileDiffTree moved there from state.py)test_bridge_*.pyfiles