claude-dev-env 1.26.4 → 1.26.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,9 @@ import os
11
11
  import re
12
12
  import sys
13
13
 
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "config"))
15
+ from messages import USER_FACING_NOTICE
16
+
14
17
  PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15
18
 
16
19
  RESEARCH_MODE_SKILL_SEARCH_PATHS = [
@@ -99,14 +102,19 @@ def main() -> None:
99
102
 
100
103
  formatted_term_list = ", ".join(f'"{term}"' for term in found_hedging_terms)
101
104
 
102
- research_mode_content = "(Could not load research-mode skill file)"
105
+ resolved_skill_path: str | None = None
103
106
  for each_skill_path in RESEARCH_MODE_SKILL_SEARCH_PATHS:
104
- try:
105
- with open(each_skill_path, encoding="utf-8") as skill_file:
106
- research_mode_content = skill_file.read()
107
- break
108
- except OSError:
109
- continue
107
+ if os.path.exists(each_skill_path):
108
+ resolved_skill_path = each_skill_path
109
+ break
110
+
111
+ if resolved_skill_path is not None:
112
+ skill_reference = f"under the research-mode constraints defined in:\n\n{resolved_skill_path}"
113
+ else:
114
+ skill_reference = (
115
+ "under research-mode constraints "
116
+ "(no research-mode skill installed; verify with sources or reply 'I don't know')"
117
+ )
110
118
 
111
119
  block_response = {
112
120
  "decision": "block",
@@ -114,12 +122,13 @@ def main() -> None:
114
122
  f"ANTI-HALLUCINATION GUARDRAIL: Your response contains hedging language: "
115
123
  f"{formatted_term_list}. "
116
124
  f"These words signal unverified claims. You MUST rewrite your response "
117
- f"with these constraints active:\n\n"
118
- f"{research_mode_content}\n\n"
125
+ f"{skill_reference}\n\n"
119
126
  f"Do NOT simply remove the hedging word and keep the unverified claim. "
120
127
  f"Either VERIFY it with a source or replace it with 'I don't know'.\n\n"
121
128
  f"You MUST re-output the complete, revised response with the corrections applied."
122
129
  ),
130
+ "systemMessage": USER_FACING_NOTICE,
131
+ "suppressOutput": True,
123
132
  }
124
133
 
125
134
  print(json.dumps(block_response))
@@ -0,0 +1,135 @@
1
+ """Tests for hedging_language_blocker hook response shape."""
2
+
3
+ import importlib.util
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+
10
+ HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "hedging_language_blocker.py")
11
+ _HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
12
+ _CONFIG_DIR = os.path.join(_HOOKS_DIR, "..", "config")
13
+ if _HOOKS_DIR not in sys.path:
14
+ sys.path.insert(0, _HOOKS_DIR)
15
+ if _CONFIG_DIR not in sys.path:
16
+ sys.path.insert(0, _CONFIG_DIR)
17
+ import hedging_language_blocker
18
+ from messages import USER_FACING_NOTICE
19
+
20
+ RESEARCH_MODE_SKILL_BODY_MARKER = "Three anti-hallucination constraints are ALWAYS active."
21
+ HEDGING_MESSAGE = "This is likely correct."
22
+ CLEAN_MESSAGE = "This is verified by the source document."
23
+ EMPTY_MESSAGE = ""
24
+
25
+
26
+ def run_hook_with_message(assistant_message: str) -> subprocess.CompletedProcess:
27
+ hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
28
+ return subprocess.run(
29
+ [sys.executable, HOOK_SCRIPT_PATH],
30
+ input=hook_input_payload,
31
+ capture_output=True,
32
+ text=True,
33
+ check=False,
34
+ )
35
+
36
+
37
+ def run_hook_with_patched_search_paths(
38
+ assistant_message: str,
39
+ search_paths: list[str],
40
+ ) -> subprocess.CompletedProcess:
41
+ """Run the hook with RESEARCH_MODE_SKILL_SEARCH_PATHS overridden via a wrapper script."""
42
+ wrapper_script = (
43
+ "import sys, json, os\n"
44
+ f"sys.path.insert(0, {repr(os.path.dirname(HOOK_SCRIPT_PATH))})\n"
45
+ "import hedging_language_blocker as blocker\n"
46
+ f"blocker.RESEARCH_MODE_SKILL_SEARCH_PATHS = {repr(search_paths)}\n"
47
+ "blocker.main()\n"
48
+ )
49
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as wrapper_file:
50
+ wrapper_file.write(wrapper_script)
51
+ wrapper_file_path = wrapper_file.name
52
+
53
+ hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
54
+ try:
55
+ completed_process = subprocess.run(
56
+ [sys.executable, wrapper_file_path],
57
+ input=hook_input_payload,
58
+ capture_output=True,
59
+ text=True,
60
+ check=False,
61
+ )
62
+ finally:
63
+ os.unlink(wrapper_file_path)
64
+ return completed_process
65
+
66
+
67
+ def test_user_facing_notice_importable_from_config_messages():
68
+ config_messages_path = os.path.join(_CONFIG_DIR, "messages.py")
69
+ specification = importlib.util.spec_from_file_location("messages", config_messages_path)
70
+ module = importlib.util.module_from_spec(specification)
71
+ specification.loader.exec_module(module)
72
+
73
+ assert module.USER_FACING_NOTICE == USER_FACING_NOTICE
74
+
75
+
76
+ def test_hedging_message_emits_block_with_short_user_notice():
77
+ completed_process = run_hook_with_message(HEDGING_MESSAGE)
78
+
79
+ assert completed_process.returncode == 0
80
+ parsed_response = json.loads(completed_process.stdout)
81
+
82
+ assert parsed_response["decision"] == "block"
83
+ assert parsed_response["systemMessage"] == USER_FACING_NOTICE
84
+ assert parsed_response["suppressOutput"] is True
85
+ assert "likely" in parsed_response["reason"]
86
+
87
+
88
+ def test_hedging_reason_contains_not_installed_notice_when_skill_absent():
89
+ completed_process = run_hook_with_patched_search_paths(
90
+ HEDGING_MESSAGE,
91
+ ["/nonexistent/path/one/SKILL.md", "/nonexistent/path/two/SKILL.md"],
92
+ )
93
+
94
+ assert completed_process.returncode == 0
95
+ parsed_response = json.loads(completed_process.stdout)
96
+
97
+ assert parsed_response["decision"] == "block"
98
+ assert "no research-mode skill installed" in parsed_response["reason"]
99
+ assert "verify with sources or reply" in parsed_response["reason"]
100
+ assert "SKILL.md" not in parsed_response["reason"]
101
+ assert RESEARCH_MODE_SKILL_BODY_MARKER not in parsed_response["reason"]
102
+
103
+
104
+ def test_hedging_reason_contains_skill_path_when_skill_present():
105
+ with tempfile.TemporaryDirectory() as skill_dir:
106
+ skill_file_path = os.path.join(skill_dir, "SKILL.md")
107
+ with open(skill_file_path, "w") as skill_file:
108
+ skill_file.write("# Research Mode Skill\n")
109
+
110
+ completed_process = run_hook_with_patched_search_paths(
111
+ HEDGING_MESSAGE,
112
+ ["/nonexistent/path/SKILL.md", skill_file_path],
113
+ )
114
+
115
+ assert completed_process.returncode == 0
116
+ parsed_response = json.loads(completed_process.stdout)
117
+
118
+ assert parsed_response["decision"] == "block"
119
+ assert "SKILL.md" in parsed_response["reason"]
120
+ assert "no research-mode skill installed" not in parsed_response["reason"]
121
+ assert RESEARCH_MODE_SKILL_BODY_MARKER not in parsed_response["reason"]
122
+
123
+
124
+ def test_clean_message_passes_through_with_no_output():
125
+ completed_process = run_hook_with_message(CLEAN_MESSAGE)
126
+
127
+ assert completed_process.returncode == 0
128
+ assert completed_process.stdout == ""
129
+
130
+
131
+ def test_empty_message_passes_through_with_no_output():
132
+ completed_process = run_hook_with_message(EMPTY_MESSAGE)
133
+
134
+ assert completed_process.returncode == 0
135
+ assert completed_process.stdout == ""
@@ -0,0 +1 @@
1
+ # pragma: no-tdd-gate
@@ -0,0 +1,4 @@
1
+ # pragma: no-tdd-gate
2
+ """User-facing notice messages for blocking hooks."""
3
+
4
+ USER_FACING_NOTICE = "Agent was found guessing - sourcing opinions..."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.26.4",
3
+ "version": "1.26.5",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {