claude-dev-env 8.106.1 → 8.106.3

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.
@@ -1,4 +1,4 @@
1
- """Tests for skill_loaded_reminder. Checks restoration after compaction and helper prompts."""
1
+ """Tests for skill_loaded_reminder. Checks restoration after compaction and spawn pass-through."""
2
2
 
3
3
  import json
4
4
  import sys
@@ -76,10 +76,6 @@ def _run_main(hook_payload: object) -> str:
76
76
  return captured_stdout.getvalue()
77
77
 
78
78
 
79
- def _agent_call(tool_input: dict[str, object], tool_name: str = "Agent") -> dict[str, object]:
80
- return {"hook_event_name": "PreToolUse", "tool_name": tool_name, "tool_input": tool_input}
81
-
82
-
83
79
  def _user_turn(transcript_path: Path) -> dict[str, object]:
84
80
  return {"hook_event_name": "UserPromptSubmit", "transcript_path": str(transcript_path)}
85
81
 
@@ -90,85 +86,28 @@ def _write_transcript(tmp_path: Path, all_lines: list[str]) -> Path:
90
86
  return transcript_path
91
87
 
92
88
 
93
- class TestSubagentSpawn:
94
- def test_a_plain_subagent_prompt_opens_with_the_poteto_mode_invocation(self) -> None:
95
- emitted = json.loads(
96
- _run_main(
97
- _agent_call(
98
- {
99
- "description": "Find callers",
100
- "subagent_type": "Explore",
101
- "prompt": "List every caller of main.",
102
- }
103
- )
104
- )
105
- )
106
- hook_output = emitted["hookSpecificOutput"]
107
- rewritten_input = hook_output["updatedInput"]
108
- assert hook_output["permissionDecision"] == "allow"
109
- assert rewritten_input["prompt"].startswith(
110
- "Before any other work, invoke the poteto-mode skill with the Skill tool."
111
- )
112
- assert rewritten_input["prompt"].endswith("\n\nList every caller of main.")
113
- assert rewritten_input["subagent_type"] == "Explore"
114
- assert rewritten_input["description"] == "Find callers"
115
-
116
- def test_the_legacy_task_tool_name_is_rewritten_too(self) -> None:
117
- emitted = json.loads(_run_main(_agent_call({"prompt": "Go."}, tool_name="Task")))
118
- assert "poteto-mode" in emitted["hookSpecificOutput"]["updatedInput"]["prompt"]
119
-
120
- def test_a_prompt_that_already_names_the_skill_is_left_alone(self) -> None:
121
- assert _run_main(_agent_call({"prompt": "Invoke poteto-mode first. Then go."})) == ""
89
+ class TestRegistration:
90
+ def test_a_spawn_payload_from_a_stale_registration_prints_nothing(self) -> None:
91
+ stale_spawn_payload = {
92
+ "hook_event_name": "PreToolUse",
93
+ "tool_name": "Agent",
94
+ "tool_input": {"prompt": "Reply leaf."},
95
+ }
96
+ assert _run_main(stale_spawn_payload) == ""
122
97
 
123
- def test_a_poteto_agent_is_left_alone_because_it_loads_the_skill_itself(self) -> None:
124
- assert (
125
- _run_main(_agent_call({"subagent_type": "pstack:poteto-agent", "prompt": "Go."})) == ""
98
+ def test_no_pre_tool_use_group_runs_the_reminder(self) -> None:
99
+ shipped_hooks = json.loads(
100
+ (Path(__file__).resolve().parents[1] / "hooks.json").read_text(encoding="utf-8")
126
101
  )
127
-
128
- def test_another_tool_is_left_alone(self) -> None:
129
- assert _run_main(_agent_call({"prompt": "Go."}, tool_name="Bash")) == ""
130
-
131
-
132
- class TestSubagentInputWithPotetoMode:
133
- def test_every_other_input_field_is_kept(self) -> None:
134
- rewritten_input = reminder.subagent_input_with_poteto_mode(
135
- "Agent", {"prompt": "Go.", "model": "sonnet", "run_in_background": True}
136
- )
137
- assert rewritten_input is not None
138
- assert rewritten_input["model"] == "sonnet"
139
- assert rewritten_input["run_in_background"] is True
140
-
141
- def test_an_input_with_no_prompt_is_left_alone(self) -> None:
142
- assert reminder.subagent_input_with_poteto_mode("Agent", {"description": "Go"}) is None
143
-
144
- def test_a_codex_spawn_input_with_only_items_is_left_alone(self) -> None:
145
- assert (
146
- reminder.subagent_input_with_poteto_mode(
147
- "spawn_agent", {"items": [{"type": "text", "text": "Go."}]}
148
- )
149
- is None
150
- )
151
-
152
-
153
- class TestCodexSpawn:
154
- def test_a_codex_spawn_message_opens_with_the_skill_mention(self) -> None:
155
- emitted = json.loads(
156
- _run_main(
157
- _agent_call(
158
- {"message": "Fix the failing test.", "model": "gpt-6-luna"},
159
- tool_name="spawn_agent",
160
- )
161
- )
162
- )
163
- rewritten_input = emitted["hookSpecificOutput"]["updatedInput"]
164
- assert rewritten_input["message"] == "$poteto-mode\n\nFix the failing test."
165
- assert rewritten_input["model"] == "gpt-6-luna"
166
- assert "prompt" not in rewritten_input
167
-
168
- def test_a_codex_spawn_message_that_already_mentions_the_skill_is_left_alone(self) -> None:
169
- assert (
170
- _run_main(_agent_call({"message": "$poteto-mode\n\nFix it."}, tool_name="spawn_agent"))
171
- == ""
102
+ all_pre_tool_use_commands = [
103
+ each_hook.get("command", "")
104
+ for each_group in shipped_hooks["hooks"]["PreToolUse"]
105
+ for each_hook in each_group["hooks"]
106
+ ]
107
+ assert all_pre_tool_use_commands
108
+ assert not any(
109
+ "skill_loaded_reminder.py" in each_command
110
+ for each_command in all_pre_tool_use_commands
172
111
  )
173
112
 
174
113
 
@@ -353,9 +292,7 @@ class TestReminderFor:
353
292
  assert hook_output["hookEventName"] == "SubagentStart"
354
293
  assert hook_output["additionalContext"] == NOT_LOADED_REMINDER
355
294
 
356
- def test_an_agent_tool_helper_starting_prints_nothing_since_its_prompt_carries_the_line(
357
- self,
358
- ) -> None:
295
+ def test_an_agent_tool_helper_starting_prints_nothing(self) -> None:
359
296
  assert (
360
297
  _run_main({"hook_event_name": "SubagentStart", "agent_type": "general-purpose"}) == ""
361
298
  )
@@ -72,6 +72,18 @@ def test_abbreviation_violation_flags_a_lowercase_abbreviation_and_a_tracker_num
72
72
  assert 'Tracker number "card 12"' in str(abbreviation_violation("See card 12.", SHIPPED_RULES))
73
73
 
74
74
 
75
+ def test_abbreviation_violation_names_every_abbreviation_and_tracker_number_on_its_own_line() -> None:
76
+ assert SHIPPED_RULES is not None
77
+ violation = str(abbreviation_violation("The UI and the API, e.g. card 12 and card 13.", SHIPPED_RULES))
78
+ all_lines = violation.splitlines()
79
+ assert len(all_lines) == 5
80
+ assert 'Abbreviation "UI"' in all_lines[0]
81
+ assert 'Abbreviation "API"' in all_lines[1]
82
+ assert 'Abbreviation "e.g."' in all_lines[2]
83
+ assert 'Tracker number "card 12"' in all_lines[3]
84
+ assert 'Tracker number "card 13"' in all_lines[4]
85
+
86
+
75
87
  def test_widget_anchor_violation_reads_only_widget_calls() -> None:
76
88
  anchor_input = {"widget_code": '<a href="https://example.com">page</a>'}
77
89
  assert widget_anchor_violation("mcp__hearthbot__post_widget", anchor_input) is not None
@@ -19,6 +19,7 @@ from pathlib import Path
19
19
  from json_file_reader import read_json_object
20
20
  from hooks_constants.visual_reply_rules_constants import (
21
21
  ABBREVIATION_MESSAGE,
22
+ VIOLATION_LINE_SEPARATOR,
22
23
  ABBREVIATIONS_RULE_KEY,
23
24
  ALL_REPLY_TOOL_NAME_SUFFIXES,
24
25
  ALL_VISUAL_TOOL_NAME_SUFFIXES,
@@ -140,19 +141,35 @@ def mode_enabled(claude_home: Path) -> bool:
140
141
 
141
142
 
142
143
  def abbreviation_violation(prose_text: str, rules: VisualReplyRules) -> str | None:
143
- """Return the deny reason for the first abbreviation in prose with links and code removed."""
144
+ """Return the deny reason naming each abbreviation and tracker number in prose with links and code removed.
145
+
146
+ Each one gets its own line, so one refusal names them all.
147
+ """
144
148
  reminder = rules.reminder(ABBREVIATIONS_RULE_KEY)
145
- for each_match in CAPITALIZED_ABBREVIATION_PATTERN.finditer(prose_text):
146
- if each_match.group(0) not in rules.all_allowed_capitalized_words:
147
- return ABBREVIATION_MESSAGE.format(abbreviation=each_match.group(0), reminder=reminder)
148
- for each_abbreviation in rules.all_lowercase_abbreviations:
149
- pattern = LOWERCASE_ABBREVIATION_TEMPLATE.format(abbreviation=re.escape(each_abbreviation))
150
- if re.search(pattern, prose_text, re.IGNORECASE):
151
- return ABBREVIATION_MESSAGE.format(abbreviation=each_abbreviation, reminder=reminder)
152
- tracker_match = TRACKER_NUMBER_PATTERN.search(prose_text)
153
- if tracker_match is not None:
154
- return TRACKER_NUMBER_MESSAGE.format(reference=tracker_match.group(0), reminder=reminder)
155
- return None
149
+ all_abbreviations = [
150
+ each_match.group(0)
151
+ for each_match in CAPITALIZED_ABBREVIATION_PATTERN.finditer(prose_text)
152
+ if each_match.group(0) not in rules.all_allowed_capitalized_words
153
+ ] + [
154
+ each_abbreviation
155
+ for each_abbreviation in rules.all_lowercase_abbreviations
156
+ if re.search(
157
+ LOWERCASE_ABBREVIATION_TEMPLATE.format(abbreviation=re.escape(each_abbreviation)),
158
+ prose_text,
159
+ re.IGNORECASE,
160
+ )
161
+ ]
162
+ all_violations = [
163
+ *(
164
+ ABBREVIATION_MESSAGE.format(abbreviation=each_abbreviation, reminder=reminder)
165
+ for each_abbreviation in dict.fromkeys(all_abbreviations)
166
+ ),
167
+ *(
168
+ TRACKER_NUMBER_MESSAGE.format(reference=each_match.group(0), reminder=reminder)
169
+ for each_match in TRACKER_NUMBER_PATTERN.finditer(prose_text)
170
+ ),
171
+ ]
172
+ return VIOLATION_LINE_SEPARATOR.join(all_violations) if all_violations else None
156
173
 
157
174
 
158
175
  def widget_anchor_violation(tool_name: str, all_tool_input: dict[str, object]) -> str | None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.106.1",
3
+ "version": "8.106.3",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PostToolUse hook — resets the serial investigation tracker when delegation occurs.
3
-
4
- Clears the investigation timestamp tracker when the lead session delegates work
5
- via Agent, Task, or TeamCreate tools. This allows the lead to resume limited
6
- diagnostic calls after properly delegating.
7
-
8
- Companion: blocking/serial-investigation-blocker.py (PreToolUse on Read|Bash|Grep)
9
- """
10
-
11
- import json
12
- import os
13
- import sys
14
-
15
- TRACKER_STATE_PATH = os.path.join(
16
- os.path.expanduser("~"), ".claude", "runtime", "investigation-tracker.json"
17
- )
18
-
19
- DELEGATION_TOOLS = frozenset({"Agent", "Task", "TeamCreate"})
20
-
21
-
22
- def clear_investigation_tracker() -> None:
23
- try:
24
- os.remove(TRACKER_STATE_PATH)
25
- except FileNotFoundError:
26
- pass
27
- except OSError:
28
- pass
29
-
30
-
31
- def main() -> None:
32
- try:
33
- hook_input = json.load(sys.stdin)
34
- except json.JSONDecodeError:
35
- sys.exit(0)
36
- if not isinstance(hook_input, dict):
37
- sys.exit(0)
38
-
39
- tool_name = hook_input.get("tool_name", "")
40
- if tool_name not in DELEGATION_TOOLS:
41
- sys.exit(0)
42
-
43
- clear_investigation_tracker()
44
- sys.exit(0)
45
-
46
-
47
- if __name__ == "__main__":
48
- main()
@@ -1,90 +0,0 @@
1
- """Behavior tests for investigation tracker reset."""
2
-
3
- from __future__ import annotations
4
-
5
- import importlib.util
6
- import io
7
- import json
8
- from pathlib import Path
9
- from unittest import mock
10
-
11
- import pytest
12
-
13
-
14
- HOOK_DIRECTORY = Path(__file__).parent
15
- HOOK_SPEC = importlib.util.spec_from_file_location(
16
- "investigation_tracker_reset",
17
- HOOK_DIRECTORY / "investigation_tracker_reset.py",
18
- )
19
- assert HOOK_SPEC is not None
20
- assert HOOK_SPEC.loader is not None
21
- HOOK_MODULE = importlib.util.module_from_spec(HOOK_SPEC)
22
- HOOK_SPEC.loader.exec_module(HOOK_MODULE)
23
-
24
-
25
- def _run_main(input_text: str) -> int | None:
26
- with mock.patch("sys.stdin", io.StringIO(input_text)):
27
- try:
28
- HOOK_MODULE.main()
29
- except SystemExit as system_exit:
30
- if system_exit.code not in (None, 0):
31
- raise
32
- return system_exit.code if isinstance(system_exit.code, int) else None
33
- return None
34
-
35
-
36
- @pytest.mark.parametrize("delegation_tool", ["Agent", "Task", "TeamCreate"])
37
- def test_main_removes_tracker_after_delegation(
38
- tmp_path: Path,
39
- delegation_tool: str,
40
- ) -> None:
41
- tracker_path = tmp_path / "investigation-tracker.json"
42
- tracker_path.write_text("{}", encoding="utf-8")
43
- HOOK_MODULE.__dict__["TRACKER_STATE_PATH"] = str(tracker_path)
44
-
45
- exit_status = _run_main(json.dumps({"tool_name": delegation_tool}))
46
-
47
- assert exit_status in (None, 0)
48
- assert not tracker_path.exists()
49
-
50
-
51
- @pytest.mark.parametrize("unrelated_tool", ["Read", "Bash", "Grep"])
52
- def test_main_keeps_tracker_for_unrelated_tool(
53
- tmp_path: Path,
54
- unrelated_tool: str,
55
- ) -> None:
56
- tracker_path = tmp_path / "investigation-tracker.json"
57
- tracker_path.write_text("{}", encoding="utf-8")
58
- HOOK_MODULE.__dict__["TRACKER_STATE_PATH"] = str(tracker_path)
59
-
60
- exit_status = _run_main(json.dumps({"tool_name": unrelated_tool}))
61
-
62
- assert exit_status in (None, 0)
63
- assert tracker_path.exists()
64
-
65
-
66
- @pytest.mark.parametrize("invalid_input", ["not json", json.dumps([])])
67
- def test_main_ignores_invalid_input(
68
- tmp_path: Path,
69
- invalid_input: str,
70
- ) -> None:
71
- tracker_path = tmp_path / "investigation-tracker.json"
72
- tracker_path.write_text("{}", encoding="utf-8")
73
- HOOK_MODULE.__dict__["TRACKER_STATE_PATH"] = str(tracker_path)
74
-
75
- exit_status = _run_main(invalid_input)
76
-
77
- assert exit_status in (None, 0)
78
- assert tracker_path.exists()
79
-
80
-
81
- def test_main_keeps_running_when_tracker_removal_fails(tmp_path: Path) -> None:
82
- tracker_path = tmp_path / "investigation-tracker.json"
83
- tracker_path.write_text("{}", encoding="utf-8")
84
- HOOK_MODULE.__dict__["TRACKER_STATE_PATH"] = str(tracker_path)
85
-
86
- with mock.patch.object(HOOK_MODULE.os, "remove", side_effect=OSError("locked")):
87
- exit_status = _run_main(json.dumps({"tool_name": "Agent"}))
88
-
89
- assert exit_status in (None, 0)
90
- assert tracker_path.exists()