gabriel / muse public
feat patch #192 feat/192-musehooks-phase2-install #1 / 1
AI Agent gabriel · 10 hours ago · Sep 11, 2026 · Diff

feat(#192): Phase 2 — muse hooks install/uninstall/status (local activation)

Hooks declared in .musehooks.toml (Phase 1) are still never executed automatically -- this phase adds the explicit, local, per-clone activation step. muse commit integration (only running installed hooks) lands in Phase 3.

Added: - muse/core/paths.py: hooks_installed_toml_path() -> .muse/hooks-installed.toml. Deliberately the one place .muse/ IS the correct home for state -- whether *this specific clone* has installed hooks must never propagate via clone/push/pull, unlike .musehooks.toml itself. A cloned repo gaining silent hook activation would be a supply-chain risk; git avoids this by never auto-installing hooks from a fresh checkout either. - muse/core/hooks.py: HooksStatus dataclass + install_hooks() / uninstall_hooks() / is_installed() / get_status(). Three-state status model: "not_defined" (no hook point declares anything, wins even over a stale local marker), "defined_not_installed", "installed". Both install and uninstall are idempotent by design (calling twice is a no-op, not an error) -- matches how activation state should behave. - muse/cli/commands/hooks.py: `muse hooks install`, `muse hooks uninstall`, `muse hooks status` subcommands, following the same envelope/exit-code conventions as `muse hooks list` from Phase 1.

Tests (TDD, red confirmed before implementation): - tests/test_core_hooks_install.py (13 tests, HK_10-13): install/uninstall writing and clearing the marker, install-without-file-yet succeeding, the three status states including the "empty commands list still counts as defined" boundary and the "stale marker + deleted .musehooks.toml still resolves to not_defined" edge case, and idempotency for both install and uninstall. - tests/test_cmd_hooks_install.py (10 tests): install/uninstall/status via the real CLI (CliRunner), JSON schemas, not-a-repo exit 2, malformed-TOML exit 1, text-mode nudge naming the exact install command. - Manually smoke-tested against a real disposable repo via muse-dev: status before/after install/uninstall all correct.

43/43 tests pass across all four hooks test files. tests/test_app.py (27 tests) still green -- no regressions.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:d4df453a03f8b54af479f32103d9023556b28a0e38292215446959f238f3e418 sha
+61 ~2 symbols
5 changed · 1180 in snapshot files
sha256:b1695937bdb4b6c2ab07bbd6d1ee6ce0bfb52f55ac8bc03a79f2c2c8eb449fb6 snapshot
+61
symbols added
~2
symbols modified
5
files changed
1180
files in snapshot
0
dead code introduced
Semantic Changes 63 symbols
~ tests/test_cmd_hooks_install.py .py 21 symbols added
+ TestHooksInstall class class TestHooksInstall L35–55
+ test_install_exits_0_and_marks_installed method method test_install_exits_0_and_marks_installed L36–45
+ test_install_not_a_repo_exits_2 method method test_install_not_a_repo_exits_2 L53–55
+ test_install_twice_is_still_success method method test_install_twice_is_still_success L47–51
+ TestHooksStatus class class TestHooksStatus L77–105
+ test_defined_not_installed_state method method test_defined_not_installed_state L84–88
+ test_malformed_toml_exits_1 method method test_malformed_toml_exits_1 L97–101
+ test_not_defined_state method method test_not_defined_state L78–82
+ test_status_not_a_repo_exits_2 method method test_status_not_a_repo_exits_2 L103–105
+ test_text_mode_mentions_install_command_when_not_installed method method test_text_mode_mentions_install_command_when_not_installed L90–95
+ TestHooksUninstall class class TestHooksUninstall L58–74
+ test_uninstall_exits_0_and_clears_state method method test_uninstall_exits_0_and_clears_state L59–69
+ test_uninstall_when_never_installed_is_still_success method method test_uninstall_when_never_installed_is_still_success L71–74
+ _init_repo function function _init_repo L26–28
+ _invoke function function _invoke L16–23
+ _write_hooks function function _write_hooks L31–32
+ CliRunner import import CliRunner L11–11
+ annotations import import annotations L6–6
+ json import import json L8–8
+ pathlib import import pathlib L9–9
+ runner variable variable runner L13–13
~ tests/test_core_hooks_install.py .py 25 symbols added
+ TestGetStatus class class TestGetStatus L75–111
+ test_defined_not_installed method method test_defined_not_installed L88–91
+ test_installed method method test_installed L93–97
+ test_not_defined_when_musehooks_file_has_no_commands method method test_not_defined_when_musehooks_file_has_no_commands L81–86
+ test_not_defined_when_no_musehooks_file method method test_not_defined_when_no_musehooks_file L76–79
+ test_not_defined_wins_even_if_marker_present method method test_not_defined_wins_even_if_marker_present L99–106
+ test_status_exposes_parsed_hooks_file method method test_status_exposes_parsed_hooks_file L108–111
+ TestIdempotency class class TestIdempotency L119–131
+ test_install_twice_is_a_no_op_not_an_error method method test_install_twice_is_a_no_op_not_an_error L120–124
+ test_uninstall_twice_is_a_no_op_not_an_error method method test_uninstall_twice_is_a_no_op_not_an_error L126–131
+ TestInstallHooks class class TestInstallHooks L30–47
+ test_install_makes_is_installed_true method method test_install_makes_is_installed_true L36–40
+ test_install_without_musehooks_file_still_succeeds method method test_install_without_musehooks_file_still_succeeds L42–47
+ test_install_writes_local_marker method method test_install_writes_local_marker L31–34
+ TestUninstallHooks class class TestUninstallHooks L55–67
+ test_uninstall_clears_marker method method test_uninstall_clears_marker L56–61
+ test_uninstall_when_never_installed_is_a_no_op method method test_uninstall_when_never_installed_is_a_no_op L63–67
+ _setup_repo_with_hooks function function _setup_repo_with_hooks L19–22
+ get_status import import get_status L10–10
+ hooks_installed_toml_path import import hooks_installed_toml_path L16–16
+ install_hooks import import install_hooks L10–10
+ is_installed import import is_installed L10–10
+ muse_dir import import muse_dir L16–16
+ pathlib import import pathlib L8–8
+ uninstall_hooks import import uninstall_hooks L10–10
~ muse/cli/commands/hooks.py .py 8 symbols added, 2 symbols modified
+ _InstallJson class class _InstallJson L83–86
+ _StatusJson class class _StatusJson L89–93
+ get_status import import get_status L72–72
+ install_hooks import import install_hooks L72–72
+ uninstall_hooks import import uninstall_hooks L72–72
+ run_install function function run_install L276–303
+ run_status function function run_status L336–380
+ run_uninstall function function run_uninstall L306–333
~ muse/core/hooks.py .py 6 symbols added
+ HooksStatus class class HooksStatus L155–171
+ get_status function function get_status L202–218
+ hooks_installed_toml_path import import hooks_installed_toml_path L61–61
+ install_hooks function function install_hooks L174–184
+ is_installed function function is_installed L197–199
+ uninstall_hooks function function uninstall_hooks L187–194
~ muse/core/paths.py .py 1 symbol added
+ hooks_installed_toml_path function function hooks_installed_toml_path L153–163
Files Changed
+2 ~3
1180 in snapshot
← Older Oldest on feat/192-musehooks-phase2-install
All commits
Newer → Latest on feat/192-musehooks-phase2-install

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:d4df453a03f8b54af479f32103d9023556b28a0e38292215446959f238f3e418 --body "your comment"