Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/autoskillit/hook_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class HookDef:
codex_status: Literal["works-as-is", "degraded", "fix-required", "not-applicable"] = (
"works-as-is"
)
mechanism: Literal["deny", "additionalContext", "output-rewrite"] = "deny"

def __post_init__(self) -> None:
if self.event_type != "SessionStart" and not self.matcher:
Expand Down Expand Up @@ -94,34 +95,41 @@ def __post_init__(self) -> None:
"guards/ingredient_lock_guard.py",
"guards/pipeline_step_guard.py",
],
mechanism="deny",
),
HookDef(
matcher="mcp__.*autoskillit.*__remove_clone",
scripts=["guards/remove_clone_guard.py"],
mechanism="deny",
),
HookDef(
matcher=r"mcp__.*autoskillit.*__open_kitchen.*",
scripts=["guards/open_kitchen_guard.py"],
timeout_seconds=5,
mechanism="deny",
),
HookDef(
matcher="AskUserQuestion",
scripts=["guards/ask_user_question_guard.py"],
timeout_seconds=5,
session_scope="headless_only",
codex_status="not-applicable",
mechanism="deny",
),
HookDef(
matcher=r"mcp__.*autoskillit.*__merge_worktree",
scripts=["guards/branch_protection_guard.py"],
mechanism="deny",
),
HookDef(
matcher=r"mcp__.*autoskillit.*__push_to_remote",
scripts=["guards/branch_protection_guard.py"],
mechanism="deny",
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/unsafe_install_guard.py"],
mechanism="deny",
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
Expand All @@ -140,20 +148,24 @@ def __post_init__(self) -> None:
# Must stay in sync with _EXEMPT_SESSION_TYPES in guards/pr_create_guard.py β€”
# stdlib-only boundary on hook scripts prevents a shared import.
exempt_session_types=frozenset({"orchestrator"}),
mechanism="deny",
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/compose_pr_body_guard.py"],
session_scope="headless_only",
mechanism="deny",
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/planner_gh_discovery_guard.py"],
session_scope="headless_only",
mechanism="deny",
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/artifact_download_guard.py"],
mechanism="deny",
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
Expand All @@ -162,6 +174,7 @@ def __post_init__(self) -> None:
# Must stay in sync with _EXEMPT_SESSION_TYPES in guards/git_ops_guard.py β€”
# stdlib-only boundary on hook scripts prevents a shared import.
exempt_session_types=frozenset({"orchestrator"}),
mechanism="deny",
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
Expand All @@ -170,52 +183,62 @@ def __post_init__(self) -> None:
# Must stay in sync with _EXEMPT_SKILLS in guards/test_runner_guard.py β€”
# stdlib-only boundary on hook scripts prevents a shared import.
exempt_skills=frozenset({"implement-experiment"}),
mechanism="deny",
),
HookDef(
matcher=r"Write|Edit",
scripts=["guards/generated_file_write_guard.py"],
mechanism="deny",
),
HookDef(
matcher=r"Write|Edit|Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/write_guard.py"],
session_scope="headless_only",
mechanism="deny",
),
HookDef(
matcher=r"Write|Edit",
scripts=["guards/planner_result_naming_guard.py"],
session_scope="headless_only",
mechanism="deny",
),
HookDef(
matcher=r"Write|Edit",
scripts=["guards/recipe_write_advisor.py"],
session_scope="interactive_only",
mechanism="deny",
),
HookDef(
matcher=r"Grep",
scripts=["guards/grep_pattern_lint_guard.py"],
codex_status="not-applicable",
mechanism="deny",
),
HookDef(
matcher=r"Bash|Write|Edit|Read|Glob|Grep",
scripts=["guards/mcp_health_advisor.py"],
timeout_seconds=5,
session_scope="interactive_only",
codex_status="degraded",
mechanism="deny",
),
HookDef(
matcher=r"mcp__.*autoskillit.*__(run_skill|run_cmd|run_python).*",
scripts=["guards/skill_orchestration_guard.py"],
session_scope="headless_only",
mechanism="deny",
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__(run_cmd|run_python)",
scripts=["guards/recipe_read_guard.py"],
session_scope="headless_only",
mechanism="deny",
),
HookDef(
matcher=r"Bash|Agent",
scripts=["guards/background_exec_guard.py"],
session_scope="headless_only",
mechanism="deny",
),
HookDef(
matcher=r"(mcp__.*autoskillit.*__)?dispatch_food_truck",
Expand All @@ -225,11 +248,13 @@ def __post_init__(self) -> None:
"guards/ingredient_lock_guard.py",
"guards/fleet_claim_guard.py",
],
mechanism="deny",
),
HookDef(
event_type="PostToolUse",
matcher="mcp__.*autoskillit.*",
scripts=["formatters/pretty_output_hook.py"],
mechanism="output-rewrite",
),
HookDef(
event_type="PostToolUse",
Expand All @@ -240,48 +265,57 @@ def __post_init__(self) -> None:
"pipeline_step_post_hook.py",
"recipe_confirmed_post_hook.py",
],
mechanism="output-rewrite",
),
HookDef(
event_type="PostToolUse",
matcher=r"mcp__.*autoskillit.*__(run_skill|run_python).*",
scripts=["review_gate_post_hook.py"],
mechanism="output-rewrite",
),
HookDef(
event_type="PostToolUse",
matcher=r"(mcp__.*autoskillit.*__)?dispatch_food_truck",
scripts=["resume_gate_post_hook.py"],
mechanism="output-rewrite",
),
HookDef(
event_type="PostToolUse",
matcher=r"Write|Edit",
scripts=["lint_after_edit_hook.py"],
session_scope="headless_only",
codex_status="degraded",
mechanism="output-rewrite",
),
HookDef(
event_type="PostToolUse",
matcher="Skill",
scripts=["skill_load_post_hook.py"],
codex_status="not-applicable",
mechanism="additionalContext",
),
HookDef(
matcher=r"Read|Write|Edit|Bash|Grep|Glob",
scripts=["guards/skill_load_guard.py"],
session_scope="headless_only",
codex_status="fix-required",
mechanism="deny",
),
HookDef(
matcher=r"mcp__.*autoskillit.*__(wait_for_ci|enqueue_pr)",
scripts=["guards/review_loop_gate.py"],
mechanism="deny",
),
HookDef(
matcher=r"(mcp__.*autoskillit.*__)?reset_dispatch",
scripts=["guards/reset_resume_gate.py"],
mechanism="deny",
),
HookDef(
event_type="SessionStart",
scripts=["session_start_hook.py"],
session_scope="interactive_only",
mechanism="additionalContext",
),
]

Expand Down Expand Up @@ -388,6 +422,7 @@ def _canonical_registry_payload(
"exempt_session_types": sorted(h.exempt_session_types),
"exempt_skills": sorted(h.exempt_skills),
"matcher": h.matcher,
"mechanism": h.mechanism,
"scripts": list(h.scripts),
"session_scope": h.session_scope,
"timeout_seconds": h.timeout_seconds,
Expand Down
2 changes: 1 addition & 1 deletion src/autoskillit/hooks/registry.sha256
Original file line number Diff line number Diff line change
@@ -1 +1 @@
d2267bd693aa5e4c6cea0d0575fe80c9201b0aa601b4efe1ebe10211ef5eeed8
411b87300515eb326df7815545e7443098873c00da08df3adb556b45487ccf50
31 changes: 31 additions & 0 deletions tests/hooks/test_hook_registry_codex_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,34 @@ def test_generate_codex_hooks_config_excludes_not_applicable(self):
f"not-applicable hook with matcher={entry.get('matcher')!r} "
"must not appear in Codex config"
)

def test_hookdef_has_mechanism_field(self):
field_names = {f.name for f in dataclasses.fields(HookDef)}
assert "mechanism" in field_names

def test_all_registry_entries_have_valid_mechanism(self):
for hook_def in HOOK_REGISTRY:
assert hook_def.mechanism in (
"deny",
"additionalContext",
"output-rewrite",
), f"Hook {hook_def.scripts} has invalid mechanism: {hook_def.mechanism!r}"

def test_pretooluse_deny_mechanism_is_set(self):
for hook_def in HOOK_REGISTRY:
if hook_def.event_type == "PreToolUse" and hook_def.codex_status != "not-applicable":
assert hook_def.mechanism == "deny", (
f"Hook {hook_def.scripts} has mechanism={hook_def.mechanism!r}, "
"expected 'deny' for PreToolUse hooks"
)

def test_posttooluse_pretty_output_is_output_rewrite(self):
for hook_def in HOOK_REGISTRY:
if (
hook_def.event_type == "PostToolUse"
and "formatters/pretty_output_hook.py" in hook_def.scripts
):
assert hook_def.mechanism == "output-rewrite", (
f"Hook {hook_def.scripts} has mechanism={hook_def.mechanism!r}, "
"expected 'output-rewrite' for pretty_output_hook"
)
Loading