-
Notifications
You must be signed in to change notification settings - Fork 1
[Hook architecture](17) Put the diu-stop hook onto the shared hook code #732
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
027cc35
bbebd6e
5c0096b
eccbe83
7757396
13ec88c
163d9e8
87798db
49b348f
fcfbe21
0cc8306
2b76f0a
c67ac17
60c89ff
b9e01d7
63cf3dd
f60edf6
bec4b7a
9883c5e
e7e865b
0343ab7
5920777
281e4f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,7 +45,6 @@ | |
| Every block names every flagged sentence, so one rewrite that fixes them | ||
| all gets through. | ||
| """ | ||
| import json | ||
| import os | ||
| import re | ||
| import sys | ||
|
|
@@ -55,8 +54,18 @@ | |
|
|
||
| sys.path.insert(0, os.path.join( | ||
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_markers")) | ||
| sys.path.insert(0, os.path.join( | ||
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_sdk")) | ||
|
|
||
| import markers # noqa: E402 | ||
| from finding import Finding # noqa: E402 | ||
| from runtime import run_hook # noqa: E402 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Installed hooks cannot import shared SDKHigh Severity The new scripts import Additional Locations (2)Reviewed by Cursor Bugbot for commit 281e4f2. Configure here. |
||
|
|
||
| RULE_WORD_LIMIT = "diu-stop.word-limit" | ||
| RULE_UNVERIFIED_CLAIM = "diu-stop.unverified-claim" | ||
| RULE_MALFORMED_MARKER_TAG = "diu-stop.malformed-marker-tag" | ||
| RULE_LEGACY_MARKER = "diu-stop.legacy-marker" | ||
| RULE_PLAIN_WORDS = "diu-stop.plain-words" | ||
|
|
||
| # Phrases banned outright (from this user's global CLAUDE.md evidence | ||
| # rules) -- rarely legitimate even mid-sentence, so no opener restriction. | ||
|
|
@@ -219,31 +228,28 @@ def find_unverified_claim(message): | |
| return claims[0][0] if claims else None | ||
|
|
||
|
|
||
| def main(): | ||
| try: | ||
| data = json.load(sys.stdin) | ||
| except json.JSONDecodeError: | ||
| return | ||
|
|
||
| if data.get("agent_id"): | ||
| return | ||
| retry = bool(data.get("stop_hook_active")) | ||
| def detect(event): | ||
| if event.get("agent_id"): | ||
| return [] | ||
| retry = bool(event.get("stop_hook_active")) | ||
|
|
||
| message = data.get("last_assistant_message") or "" | ||
| message = event.get("last_assistant_message") or "" | ||
|
|
||
| plain_words_note = try_check_reply(data) | ||
| plain_words_note = try_check_reply(event) | ||
|
|
||
| word_count = counted_words(message) | ||
| over_limit = word_count > WORD_LIMIT and not retry | ||
| claims = find_unverified_claims(message) | ||
| marker_problems = find_marker_problems(message) | ||
|
|
||
| if not over_limit and not claims and not marker_problems and not plain_words_note: | ||
| return | ||
|
|
||
| parts = [] | ||
| findings = [] | ||
| if plain_words_note: | ||
| parts.append(plain_words_note) | ||
| findings.append(Finding( | ||
| rule_id=RULE_PLAIN_WORDS, | ||
| subject=message, | ||
| message=plain_words_note, | ||
| evidence=plain_words_note, | ||
| )) | ||
| if claims: | ||
| lines = [ | ||
| "This message makes an unverified-shaped claim with no adjacent " | ||
|
|
@@ -259,18 +265,30 @@ def main(): | |
| "of what was actually run/checked in its paragraph, or -- only if " | ||
| "the check cannot run -- tag the claim there and say why." | ||
| ) | ||
| parts.append("\n".join(lines)) | ||
| parts.extend(marker_problems) | ||
| claim_message = "\n".join(lines) | ||
| findings.append(Finding( | ||
| rule_id=RULE_UNVERIFIED_CLAIM, | ||
| subject=claim_message, | ||
| message=claim_message, | ||
| evidence=claim_message, | ||
| )) | ||
| for problem in marker_problems: | ||
| rule_id = RULE_MALFORMED_MARKER_TAG if problem == markers.MALFORMED_TAG_MESSAGE else RULE_LEGACY_MARKER | ||
| findings.append(Finding(rule_id=rule_id, subject=message, message=problem, evidence=problem)) | ||
| if over_limit: | ||
| parts.append( | ||
| over_message = ( | ||
| f"Apply diu: {word_count} words, over the {WORD_LIMIT}-word " | ||
| f"guideline. Cut at least {word_count - WORD_LIMIT} words by " | ||
| "dropping a whole section or list, not by trimming words. " | ||
| "Unless this turn genuinely asked for full technical detail " | ||
| "or a specific long format." | ||
| ) | ||
| sys.stderr.write("\n".join(parts) + "\n") | ||
| sys.exit(2) | ||
| findings.append(Finding(rule_id=RULE_WORD_LIMIT, subject=message, message=over_message, evidence=over_message)) | ||
| return findings | ||
|
|
||
|
|
||
| def main(): | ||
| run_hook("diu-stop", "claude", detect, "Stop") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| import unittest | ||
| from pathlib import Path | ||
|
|
||
| HERE = os.path.dirname(os.path.abspath(__file__)) | ||
| HOOK_DIR = os.path.dirname(HERE) | ||
| ENTRYPOINT = os.path.join(HOOK_DIR, "claude_stop_check.py") | ||
|
|
||
| LONG_MESSAGE = " ".join(["word"] * 200) | ||
|
|
||
|
|
||
| def run_entrypoint(payload: dict[str, object], env: dict[str, str]) -> subprocess.CompletedProcess[str]: | ||
| merged_env = os.environ.copy() | ||
| merged_env.update(env) | ||
| return subprocess.run( | ||
| [sys.executable, ENTRYPOINT], | ||
| input=json.dumps(payload), | ||
| capture_output=True, | ||
| text=True, | ||
| env=merged_env, | ||
| ) | ||
|
|
||
|
|
||
| class SdkModeTest(unittest.TestCase): | ||
| def test_mode_override_warn_changes_block_to_warning(self) -> None: | ||
| result = run_entrypoint( | ||
| {"last_assistant_message": LONG_MESSAGE}, | ||
| {"CATSTACK_HOOK_MODE_DIU_STOP": "warn"}, | ||
| ) | ||
|
|
||
| self.assertEqual(0, result.returncode, result.stderr) | ||
| self.assertEqual("", result.stderr) | ||
| rendered = json.loads(result.stdout) | ||
| self.assertIn( | ||
| "Apply diu", | ||
| rendered["hookSpecificOutput"]["additionalContext"], | ||
| ) | ||
|
|
||
| def test_mode_stop_still_blocks_by_default(self) -> None: | ||
| result = run_entrypoint({"last_assistant_message": LONG_MESSAGE}, {}) | ||
|
|
||
| self.assertEqual(2, result.returncode) | ||
| self.assertIn("Apply diu", result.stderr) | ||
|
|
||
| def test_each_finding_writes_one_event_row_with_rule_id(self) -> None: | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| result = run_entrypoint( | ||
| { | ||
| "session_id": "diu-stop-sdk-mode", | ||
| "last_assistant_message": LONG_MESSAGE, | ||
| }, | ||
| { | ||
| "CATSTACK_HOOK_METRICS_DIR": tmp, | ||
| "CATSTACK_HOOK_MODE_DIU_STOP": "warn", | ||
| }, | ||
| ) | ||
| rows = [ | ||
| json.loads(line) | ||
| for file in Path(tmp).glob("events-*.jsonl") | ||
| for line in file.read_text(encoding="utf-8").splitlines() | ||
| ] | ||
|
|
||
| self.assertEqual(0, result.returncode, result.stderr) | ||
| self.assertEqual(1, len(rows)) | ||
| self.assertEqual("diu-stop", rows[0]["hook"]) | ||
| self.assertEqual("diu-stop.word-limit", rows[0]["rule_id"]) | ||
| self.assertEqual("warn", rows[0]["mode"]) | ||
| self.assertEqual("override", rows[0]["mode_source"]) | ||
| self.assertEqual("warned", rows[0]["action"]) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |


Uh oh!
There was an error while loading. Please reload this page.