diff --git a/engine/hooks/bug-complaint-leak/README.md b/engine/hooks/bug-complaint-leak/README.md index 53d6210..1840b16 100644 --- a/engine/hooks/bug-complaint-leak/README.md +++ b/engine/hooks/bug-complaint-leak/README.md @@ -15,7 +15,7 @@ like "add a comment to Foo.ts". - `state.py` — session cache for empty/repeat Grep - `claude_prompt_submit.py` — Claude `UserPromptSubmit` inject - `claude_pretooluse_grep.py` / `claude_posttooluse.py` — Grep leak gate -- `cursor_before_submit.py` / `cursor_post_tool_use.py` — Cursor parity +- `cursor_before_submit.py` / `cursor_posttooluse.py` — Cursor parity (`beforeSubmitPrompt` cannot inject context; checklist arrives on first `postToolUse` via `additional_context`) - `install_claude_hook.py` / `install_cursor_hook.py` — merge, do not overwrite diff --git a/engine/hooks/bug-complaint-leak/cursor_before_submit.py b/engine/hooks/bug-complaint-leak/cursor_before_submit.py index 3c6e528..c72ab7f 100644 --- a/engine/hooks/bug-complaint-leak/cursor_before_submit.py +++ b/engine/hooks/bug-complaint-leak/cursor_before_submit.py @@ -2,7 +2,7 @@ """Cursor beforeSubmitPrompt: remember bug-complaint checklist for later inject. Cursor's beforeSubmitPrompt schema is continue/user_message only; injection -happens on the next postToolUse via cursor_post_tool_use.py. Fail-open. +happens on the next postToolUse via cursor_posttooluse.py. Fail-open. """ from __future__ import annotations diff --git a/engine/hooks/bug-complaint-leak/cursor_post_tool_use.py b/engine/hooks/bug-complaint-leak/cursor_posttooluse.py similarity index 100% rename from engine/hooks/bug-complaint-leak/cursor_post_tool_use.py rename to engine/hooks/bug-complaint-leak/cursor_posttooluse.py diff --git a/engine/hooks/bug-complaint-leak/install_cursor_hook.py b/engine/hooks/bug-complaint-leak/install_cursor_hook.py index 44e77bd..daeeb51 100644 --- a/engine/hooks/bug-complaint-leak/install_cursor_hook.py +++ b/engine/hooks/bug-complaint-leak/install_cursor_hook.py @@ -3,6 +3,10 @@ If hooks.json is currently a symlink into diu-stop (legacy install.sh layout), replace it with a real file so merges never rewrite the diu-stop fragment. + +Our own entries are recognised by hook directory (MARKER), not by script +filename, so a reinstall replaces an entry written under an earlier script name +instead of leaving it behind pointing at a path that no longer exists. """ from __future__ import annotations @@ -27,17 +31,13 @@ ], "postToolUse": [ { - "command": "python3 $HOME/.cursor/hooks/bug-complaint-leak/cursor_post_tool_use.py", + "command": "python3 $HOME/.cursor/hooks/bug-complaint-leak/cursor_posttooluse.py", "timeout": 5, } ], } -MARKERS = { - "beforeSubmitPrompt": "bug-complaint-leak/cursor_before_submit.py", - "preToolUse": "bug-complaint-leak/claude_pretooluse_grep.py", - "postToolUse": "bug-complaint-leak/cursor_post_tool_use.py", -} +MARKER = "bug-complaint-leak/" DIU_STOP = { "type": "prompt", @@ -99,7 +99,7 @@ def main() -> None: changed = False for key, incoming in FRAGMENT.items(): before = json.dumps(hooks.get(key, []), sort_keys=True) - hooks[key] = merge_list(list(hooks.get(key, [])), incoming, MARKERS[key]) + hooks[key] = merge_list(list(hooks.get(key, [])), incoming, MARKER) after = json.dumps(hooks[key], sort_keys=True) if before != after: changed = True diff --git a/engine/hooks/llm-judge/tests/test_post_tool_use.py b/engine/hooks/llm-judge/tests/test_post_tool_use.py index a6a5b9e..274c81b 100644 --- a/engine/hooks/llm-judge/tests/test_post_tool_use.py +++ b/engine/hooks/llm-judge/tests/test_post_tool_use.py @@ -86,6 +86,7 @@ def test_hit_is_delivered_as_additional_context_once(self): again = self.run_script(self.script, self.payload()) self.assert_empty_success(again) self.assertEqual(again.stderr, "") + self.assertEqual(json.dumps(data).count(ON_HIT) + again.stdout.count(ON_HIT), 1) def test_no_verdict_prints_nothing(self): result = self.run_script(self.script, self.payload()) @@ -111,6 +112,7 @@ def test_hit_is_delivered_as_additional_context_once(self): again = self.run_script(self.script, self.payload()) self.assert_empty_success(again) self.assertEqual(again.stderr, "") + self.assertEqual(json.dumps(data).count(ON_HIT) + again.stdout.count(ON_HIT), 1) def test_no_verdict_prints_nothing(self): result = self.run_script(self.script, self.payload()) @@ -135,6 +137,7 @@ def test_hit_is_delivered_as_additional_context_once(self): again = self.run_script(self.script, self.payload()) self.assert_empty_success(again) self.assertEqual(again.stderr, "") + self.assertEqual(json.dumps(data).count(ON_HIT) + again.stdout.count(ON_HIT), 1) def test_no_verdict_prints_nothing(self): result = self.run_script(self.script, self.payload()) diff --git a/engine/hooks/repeat-error-stop/cursor_post_tool_use.py b/engine/hooks/repeat-error-stop/cursor_posttooluse.py similarity index 100% rename from engine/hooks/repeat-error-stop/cursor_post_tool_use.py rename to engine/hooks/repeat-error-stop/cursor_posttooluse.py diff --git a/engine/hooks/repeat-error-stop/install_cursor_hook.py b/engine/hooks/repeat-error-stop/install_cursor_hook.py index 66cea22..ce30f81 100644 --- a/engine/hooks/repeat-error-stop/install_cursor_hook.py +++ b/engine/hooks/repeat-error-stop/install_cursor_hook.py @@ -1,5 +1,10 @@ #!/usr/bin/env python3 -"""Idempotently merge repeat-error-stop into Cursor hooks.json.""" +"""Idempotently merge repeat-error-stop into Cursor hooks.json. + +Our own entries are recognised by hook directory (MARKER), not by script +filename, so a reinstall replaces an entry written under an earlier script name +instead of leaving it behind pointing at a path that no longer exists. +""" from __future__ import annotations import copy @@ -19,11 +24,11 @@ }], "postToolUse": [{ "matcher": "*", - "command": "python3 $HOME/.cursor/hooks/repeat-error-stop/cursor_post_tool_use.py", + "command": "python3 $HOME/.cursor/hooks/repeat-error-stop/cursor_posttooluse.py", "timeout": 5, }], } -MARKERS = {key: entries[0]["command"].split("$HOME/.cursor/hooks/")[1] for key, entries in FRAGMENT.items()} +MARKER = "repeat-error-stop/" def merge_hooks(data: dict) -> dict: @@ -31,8 +36,7 @@ def merge_hooks(data: dict) -> dict: result.setdefault("version", 1) hooks = result.setdefault("hooks", {}) for hook_type, incoming in FRAGMENT.items(): - marker = MARKERS[hook_type] - kept = [entry for entry in hooks.get(hook_type, []) if marker not in str(entry.get("command", ""))] + kept = [entry for entry in hooks.get(hook_type, []) if MARKER not in str(entry.get("command", ""))] hooks[hook_type] = kept + copy.deepcopy(incoming) return result