gabriel / muse public
feat patch #192 feat/192-musehooks-phase1-format #1 / 1
AI Agent gabriel · 2 days ago · Sep 11, 2026 · Diff

feat(#192): Phase 1 — .musehooks.toml parser + `muse hooks list`

musehub#192: muse has no commit-lifecycle hook mechanism today, confirmed by direct source audit (no .muse/hooks/, no --no-verify, `muse commit`'s own documented algorithm has no hook step). This gap let two real incidents through in one session: musehub#182's un-guarded stale merge, and this workspace's own .museagent.md/CLAUDE.md drift going undetected because `muse agent-config status` never ran automatically.

Phase 1 of the MVP plan: a new tracked file format, `.musehooks.toml`, at repo root -- same naming family as .museignore/.museattributes/.museagent.md and deliberately NOT inside .muse/, since .muse/ is unconditionally excluded from every snapshot (muse/core/paths.py) and a file placed there can never be committed, pushed, or survive a clone -- the exact mistake that made `.muse/agent.md` dead on arrival.

Added: - muse/core/hooks.py: HooksFile dataclass + load_hooks() parser, following muse/core/attributes.py's established conventions (1 MiB size cap, explicit isinstance checks, missing file = valid empty state, malformed TOML/unknown hook point = ValueError with a clear message). MVP scope is deliberately just one hook point, VALID_HOOK_POINTS = {"pre-commit"} -- no arbitrary shell scripts, no other lifecycle points yet (see #192 "Out of Scope"). - muse/cli/commands/hooks.py: `muse hooks list` -- read-only, prints declared hook points and commands (table/JSON). This command only displays hook definitions, it never executes anything -- local activation (`muse hooks install`) lands in Phase 2, and only an installed hook ever runs, in Phase 3's `muse commit` integration. - Registered in muse/cli/app.py alphabetically between hash-object and hub.

Tests (TDD, red confirmed before implementation): - tests/test_core_hooks.py (13 tests): parsing (missing/empty/comment-only file, single/multiple commands, empty commands list, section with no commands key) and validation (malformed TOML, unknown hook point, non-string command, commands not a list, oversized file, VALID_HOOK_POINTS scope assertion). - tests/test_cmd_hooks.py (7 tests): `muse hooks list` with/without a .musehooks.toml file, JSON and text output, malformed-TOML and unknown-hook-point error paths (exit 1, message on stderr), not-a-repo (exit 2). - Manually smoke-tested against a real disposable repo via muse-dev (init, hooks list with/without file, --help) -- all correct.

20/20 new tests pass. tests/test_app.py (27 tests, covers CLI registration) still green.

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

sha256:4802c0281407b5de70ce1b34bbe1d1cd055533cbd4b8be35861a6a3eb545c1dc sha
+60 ~126 symbols
5 changed · 1178 in snapshot files
sha256:5fbcb814e1172175a1d47e58ce40069fd75b22fc427e489a53033dbff539305d snapshot
+60
symbols added
~126
symbols modified
5
files changed
1178
files in snapshot
0
dead code introduced
Semantic Changes 186 symbols
~ muse/cli/commands/hooks.py .py 12 symbols added
+ _ListJson class class _ListJson L57–60
+ EnvelopeJson import import EnvelopeJson L50–50
+ ExitCode import import ExitCode L51–51
+ argparse import import argparse L46–46
+ json import import json L47–47
+ load_hooks import import load_hooks L52–52
+ make_envelope import import make_envelope L50–50
+ require_repo import import require_repo L53–53
+ start_timer import import start_timer L54–54
+ sys import import sys L48–48
+ register function function register L63–105
+ run_list function function run_list L108–151
~ muse/core/hooks.py .py 8 symbols added
+ HooksFile class class HooksFile L55–66
+ VALID_HOOK_POINTS variable variable VALID_HOOK_POINTS L47–47
+ _FILENAME variable variable _FILENAME L44–44
+ _MAX_HOOKS_BYTES variable variable _MAX_HOOKS_BYTES L51–51
+ dataclass class import dataclass L42–42
+ field import import field L42–42
+ tomllib import import tomllib L41–41
+ load_hooks function function load_hooks L69–132
~ tests/test_cmd_hooks.py .py 18 symbols added
+ TestHooksListErrors class class TestHooksListErrors L76–93
+ test_malformed_toml_exits_1_with_clear_message method method test_malformed_toml_exits_1_with_clear_message L77–82
+ test_not_a_repo_exits_2 method method test_not_a_repo_exits_2 L91–93
+ test_unknown_hook_point_exits_1_with_clear_message method method test_unknown_hook_point_exits_1_with_clear_message L84–89
+ TestHooksListNoFile class class TestHooksListNoFile L37–49
+ test_no_musehooks_file_json_reports_empty method method test_no_musehooks_file_json_reports_empty L38–43
+ test_no_musehooks_file_text_says_none_defined method method test_no_musehooks_file_text_says_none_defined L45–49
+ TestHooksListWithFile class class TestHooksListWithFile L52–73
+ test_json_reports_pre_commit_commands method method test_json_reports_pre_commit_commands L53–62
+ test_text_mode_shows_hook_point_and_commands method method test_text_mode_shows_hook_point_and_commands L64–73
+ _init_repo function function _init_repo L28–30
+ _invoke function function _invoke L18–25
+ _write_hooks function function _write_hooks L33–34
+ CliRunner import import CliRunner L13–13
+ annotations import import annotations L8–8
+ json import import json L10–10
+ pathlib import import pathlib L11–11
+ runner variable variable runner L15–15
~ tests/test_core_hooks.py .py 21 symbols added
+ TestLoadHooksParsing class class TestLoadHooksParsing L26–67
+ test_comment_only_returns_empty method method test_comment_only_returns_empty L35–37
+ test_empty_commands_list_is_valid method method test_empty_commands_list_is_valid L59–62
+ test_empty_file_returns_empty_hooks_file method method test_empty_file_returns_empty_hooks_file L31–33
+ test_missing_file_returns_empty_hooks_file method method test_missing_file_returns_empty_hooks_file L27–29
+ test_parses_multiple_commands_preserving_order method method test_parses_multiple_commands_preserving_order L47–57
+ test_parses_single_pre_commit_command method method test_parses_single_pre_commit_command L39–45
+ test_section_with_no_commands_key_is_valid method method test_section_with_no_commands_key_is_valid L64–67
+ TestLoadHooksValidation class class TestLoadHooksValidation L75–105
+ test_commands_not_a_list_raises_value_error method method test_commands_not_a_list_raises_value_error L91–94
+ test_malformed_toml_raises_value_error method method test_malformed_toml_raises_value_error L76–79
+ test_non_string_command_raises_value_error method method test_non_string_command_raises_value_error L86–89
+ test_oversized_file_raises_value_error method method test_oversized_file_raises_value_error L96–101
+ test_unknown_hook_point_raises_value_error method method test_unknown_hook_point_raises_value_error L81–84
+ test_valid_hook_points_constant_matches_mvp_scope method method test_valid_hook_points_constant_matches_mvp_scope L103–105
+ _write_hooks function function _write_hooks L17–18
+ HooksFile import import HooksFile L10–10
+ VALID_HOOK_POINTS import import VALID_HOOK_POINTS L10–10
+ load_hooks import import load_hooks L10–10
+ pathlib import import pathlib L6–6
+ pytest import import pytest L8–8
Files Changed
+4 ~1
1178 in snapshot
← Older Oldest on feat/192-musehooks-phase1-format
All commits
Newer → Latest on feat/192-musehooks-phase1-format

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:4802c0281407b5de70ce1b34bbe1d1cd055533cbd4b8be35861a6a3eb545c1dc --body "your comment"