Skip to content

Commit 2b0fb41

Browse files
authored
Implementation Plan: T5-P1-A1-WP1 — HookDef.mechanism Field (#4064)
## Summary Add a `mechanism` field to `HookDef` (`Literal["deny", "additionalContext", "output-rewrite"]`, default `"deny"`) so the B4 probe harness can assert expected hook outcomes without inspecting script source. Wire the field into `_canonical_registry_payload` to keep `HOOK_REGISTRY_HASH` honest. Populate `mechanism=` on all 33 `HOOK_REGISTRY` entries and add four new test methods to `test_hook_registry_codex_status.py`. Closes #3997 ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260611-063254-779098/.autoskillit/temp/make-plan/t5_p1_a1_wp1_mechanism_field_plan_2026-06-11_064800.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code) via AutoSkillit <!-- autoskillit:pipeline-signature steps=prepare_pr,run_arch_lenses,compose_pr,annotate_pr_diff,review_pr --> ## Token Usage Summary | Step | Model | count | uncached | output | cache_read | peak_ctx | turns | cache_write | time | |------|-------|-------|----------|--------|------------|----------|-------|-------------|------| | plan* | opus[1m] | 1 | 78 | 23.7k | 1.8M | 103.4k | 46 | 104.1k | 13m 39s | | verify* | sonnet | 1 | 799 | 10.1k | 596.8k | 55.8k | 33 | 34.9k | 5m 15s | | implement* | MiniMax-M3 | 1 | 1.4M | 12.1k | 532.7k | 64.8k | 86 | 0 | 9m 43s | | audit_impl* | sonnet | 1 | 190 | 9.1k | 248.7k | 45.8k | 18 | 34.9k | 4m 50s | | prepare_pr* | MiniMax-M3 | 1 | 110.2k | 2.7k | 85.0k | 44.1k | 13 | 0 | 1m 39s | | compose_pr* | MiniMax-M3 | 1 | 177.3k | 1.7k | 0 | 0 | 11 | 0 | 1m 5s | | **Total** | | | 1.7M | 59.2k | 3.2M | 103.4k | | 173.9k | 36m 13s | \* *Step used a non-Anthropic provider; caching behavior may differ.* ## Token Efficiency | Step | LoC Changed | cache_read/LoC | cache_write/LoC | output/LoC | |------|-------------|----------------|-----------------|------------| | plan | 0 | — | — | — | | verify | 0 | — | — | — | | implement | 68 | 7833.8 | 0.0 | 177.4 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **68** | 47294.5 | 2556.7 | 870.9 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 78 | 23.7k | 1.8M | 104.1k | 13m 39s | | sonnet | 2 | 989 | 19.1k | 845.5k | 69.8k | 10m 5s | | MiniMax-M3 | 3 | 1.7M | 16.4k | 617.7k | 0 | 12m 28s |
1 parent dae5f5d commit 2b0fb41

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

src/autoskillit/hook_registry.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class HookDef:
3030
codex_status: Literal["works-as-is", "degraded", "fix-required", "not-applicable"] = (
3131
"works-as-is"
3232
)
33+
mechanism: Literal["deny", "additionalContext", "output-rewrite"] = "deny"
3334

3435
def __post_init__(self) -> None:
3536
if self.event_type != "SessionStart" and not self.matcher:
@@ -94,34 +95,41 @@ def __post_init__(self) -> None:
9495
"guards/ingredient_lock_guard.py",
9596
"guards/pipeline_step_guard.py",
9697
],
98+
mechanism="deny",
9799
),
98100
HookDef(
99101
matcher="mcp__.*autoskillit.*__remove_clone",
100102
scripts=["guards/remove_clone_guard.py"],
103+
mechanism="deny",
101104
),
102105
HookDef(
103106
matcher=r"mcp__.*autoskillit.*__open_kitchen.*",
104107
scripts=["guards/open_kitchen_guard.py"],
105108
timeout_seconds=5,
109+
mechanism="deny",
106110
),
107111
HookDef(
108112
matcher="AskUserQuestion",
109113
scripts=["guards/ask_user_question_guard.py"],
110114
timeout_seconds=5,
111115
session_scope="headless_only",
112116
codex_status="not-applicable",
117+
mechanism="deny",
113118
),
114119
HookDef(
115120
matcher=r"mcp__.*autoskillit.*__merge_worktree",
116121
scripts=["guards/branch_protection_guard.py"],
122+
mechanism="deny",
117123
),
118124
HookDef(
119125
matcher=r"mcp__.*autoskillit.*__push_to_remote",
120126
scripts=["guards/branch_protection_guard.py"],
127+
mechanism="deny",
121128
),
122129
HookDef(
123130
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
124131
scripts=["guards/unsafe_install_guard.py"],
132+
mechanism="deny",
125133
),
126134
HookDef(
127135
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
@@ -140,20 +148,24 @@ def __post_init__(self) -> None:
140148
# Must stay in sync with _EXEMPT_SESSION_TYPES in guards/pr_create_guard.py —
141149
# stdlib-only boundary on hook scripts prevents a shared import.
142150
exempt_session_types=frozenset({"orchestrator"}),
151+
mechanism="deny",
143152
),
144153
HookDef(
145154
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
146155
scripts=["guards/compose_pr_body_guard.py"],
147156
session_scope="headless_only",
157+
mechanism="deny",
148158
),
149159
HookDef(
150160
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
151161
scripts=["guards/planner_gh_discovery_guard.py"],
152162
session_scope="headless_only",
163+
mechanism="deny",
153164
),
154165
HookDef(
155166
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
156167
scripts=["guards/artifact_download_guard.py"],
168+
mechanism="deny",
157169
),
158170
HookDef(
159171
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
@@ -162,6 +174,7 @@ def __post_init__(self) -> None:
162174
# Must stay in sync with _EXEMPT_SESSION_TYPES in guards/git_ops_guard.py —
163175
# stdlib-only boundary on hook scripts prevents a shared import.
164176
exempt_session_types=frozenset({"orchestrator"}),
177+
mechanism="deny",
165178
),
166179
HookDef(
167180
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
@@ -170,52 +183,62 @@ def __post_init__(self) -> None:
170183
# Must stay in sync with _EXEMPT_SKILLS in guards/test_runner_guard.py —
171184
# stdlib-only boundary on hook scripts prevents a shared import.
172185
exempt_skills=frozenset({"implement-experiment"}),
186+
mechanism="deny",
173187
),
174188
HookDef(
175189
matcher=r"Write|Edit",
176190
scripts=["guards/generated_file_write_guard.py"],
191+
mechanism="deny",
177192
),
178193
HookDef(
179194
matcher=r"Write|Edit|Bash|mcp__.*autoskillit.*__run_cmd",
180195
scripts=["guards/write_guard.py"],
181196
session_scope="headless_only",
197+
mechanism="deny",
182198
),
183199
HookDef(
184200
matcher=r"Write|Edit",
185201
scripts=["guards/planner_result_naming_guard.py"],
186202
session_scope="headless_only",
203+
mechanism="deny",
187204
),
188205
HookDef(
189206
matcher=r"Write|Edit",
190207
scripts=["guards/recipe_write_advisor.py"],
191208
session_scope="interactive_only",
209+
mechanism="deny",
192210
),
193211
HookDef(
194212
matcher=r"Grep",
195213
scripts=["guards/grep_pattern_lint_guard.py"],
196214
codex_status="not-applicable",
215+
mechanism="deny",
197216
),
198217
HookDef(
199218
matcher=r"Bash|Write|Edit|Read|Glob|Grep",
200219
scripts=["guards/mcp_health_advisor.py"],
201220
timeout_seconds=5,
202221
session_scope="interactive_only",
203222
codex_status="degraded",
223+
mechanism="deny",
204224
),
205225
HookDef(
206226
matcher=r"mcp__.*autoskillit.*__(run_skill|run_cmd|run_python).*",
207227
scripts=["guards/skill_orchestration_guard.py"],
208228
session_scope="headless_only",
229+
mechanism="deny",
209230
),
210231
HookDef(
211232
matcher=r"Bash|mcp__.*autoskillit.*__(run_cmd|run_python)",
212233
scripts=["guards/recipe_read_guard.py"],
213234
session_scope="headless_only",
235+
mechanism="deny",
214236
),
215237
HookDef(
216238
matcher=r"Bash|Agent",
217239
scripts=["guards/background_exec_guard.py"],
218240
session_scope="headless_only",
241+
mechanism="deny",
219242
),
220243
HookDef(
221244
matcher=r"(mcp__.*autoskillit.*__)?dispatch_food_truck",
@@ -225,11 +248,13 @@ def __post_init__(self) -> None:
225248
"guards/ingredient_lock_guard.py",
226249
"guards/fleet_claim_guard.py",
227250
],
251+
mechanism="deny",
228252
),
229253
HookDef(
230254
event_type="PostToolUse",
231255
matcher="mcp__.*autoskillit.*",
232256
scripts=["formatters/pretty_output_hook.py"],
257+
mechanism="output-rewrite",
233258
),
234259
HookDef(
235260
event_type="PostToolUse",
@@ -240,48 +265,57 @@ def __post_init__(self) -> None:
240265
"pipeline_step_post_hook.py",
241266
"recipe_confirmed_post_hook.py",
242267
],
268+
mechanism="output-rewrite",
243269
),
244270
HookDef(
245271
event_type="PostToolUse",
246272
matcher=r"mcp__.*autoskillit.*__(run_skill|run_python).*",
247273
scripts=["review_gate_post_hook.py"],
274+
mechanism="output-rewrite",
248275
),
249276
HookDef(
250277
event_type="PostToolUse",
251278
matcher=r"(mcp__.*autoskillit.*__)?dispatch_food_truck",
252279
scripts=["resume_gate_post_hook.py"],
280+
mechanism="output-rewrite",
253281
),
254282
HookDef(
255283
event_type="PostToolUse",
256284
matcher=r"Write|Edit",
257285
scripts=["lint_after_edit_hook.py"],
258286
session_scope="headless_only",
259287
codex_status="degraded",
288+
mechanism="output-rewrite",
260289
),
261290
HookDef(
262291
event_type="PostToolUse",
263292
matcher="Skill",
264293
scripts=["skill_load_post_hook.py"],
265294
codex_status="not-applicable",
295+
mechanism="additionalContext",
266296
),
267297
HookDef(
268298
matcher=r"Read|Write|Edit|Bash|Grep|Glob",
269299
scripts=["guards/skill_load_guard.py"],
270300
session_scope="headless_only",
271301
codex_status="fix-required",
302+
mechanism="deny",
272303
),
273304
HookDef(
274305
matcher=r"mcp__.*autoskillit.*__(wait_for_ci|enqueue_pr)",
275306
scripts=["guards/review_loop_gate.py"],
307+
mechanism="deny",
276308
),
277309
HookDef(
278310
matcher=r"(mcp__.*autoskillit.*__)?reset_dispatch",
279311
scripts=["guards/reset_resume_gate.py"],
312+
mechanism="deny",
280313
),
281314
HookDef(
282315
event_type="SessionStart",
283316
scripts=["session_start_hook.py"],
284317
session_scope="interactive_only",
318+
mechanism="additionalContext",
285319
),
286320
]
287321

@@ -388,6 +422,7 @@ def _canonical_registry_payload(
388422
"exempt_session_types": sorted(h.exempt_session_types),
389423
"exempt_skills": sorted(h.exempt_skills),
390424
"matcher": h.matcher,
425+
"mechanism": h.mechanism,
391426
"scripts": list(h.scripts),
392427
"session_scope": h.session_scope,
393428
"timeout_seconds": h.timeout_seconds,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d2267bd693aa5e4c6cea0d0575fe80c9201b0aa601b4efe1ebe10211ef5eeed8
1+
411b87300515eb326df7815545e7443098873c00da08df3adb556b45487ccf50

tests/hooks/test_hook_registry_codex_status.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,34 @@ def test_generate_codex_hooks_config_excludes_not_applicable(self):
5050
f"not-applicable hook with matcher={entry.get('matcher')!r} "
5151
"must not appear in Codex config"
5252
)
53+
54+
def test_hookdef_has_mechanism_field(self):
55+
field_names = {f.name for f in dataclasses.fields(HookDef)}
56+
assert "mechanism" in field_names
57+
58+
def test_all_registry_entries_have_valid_mechanism(self):
59+
for hook_def in HOOK_REGISTRY:
60+
assert hook_def.mechanism in (
61+
"deny",
62+
"additionalContext",
63+
"output-rewrite",
64+
), f"Hook {hook_def.scripts} has invalid mechanism: {hook_def.mechanism!r}"
65+
66+
def test_pretooluse_deny_mechanism_is_set(self):
67+
for hook_def in HOOK_REGISTRY:
68+
if hook_def.event_type == "PreToolUse" and hook_def.codex_status != "not-applicable":
69+
assert hook_def.mechanism == "deny", (
70+
f"Hook {hook_def.scripts} has mechanism={hook_def.mechanism!r}, "
71+
"expected 'deny' for PreToolUse hooks"
72+
)
73+
74+
def test_posttooluse_pretty_output_is_output_rewrite(self):
75+
for hook_def in HOOK_REGISTRY:
76+
if (
77+
hook_def.event_type == "PostToolUse"
78+
and "formatters/pretty_output_hook.py" in hook_def.scripts
79+
):
80+
assert hook_def.mechanism == "output-rewrite", (
81+
f"Hook {hook_def.scripts} has mechanism={hook_def.mechanism!r}, "
82+
"expected 'output-rewrite' for pretty_output_hook"
83+
)

0 commit comments

Comments
 (0)