claude-dev-env 1.26.0 → 1.26.2

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.
Files changed (24) hide show
  1. package/hooks/blocking/code_rules_enforcer.py +5 -1
  2. package/hooks/blocking/test_code_rules_enforcer.py +61 -0
  3. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +4 -2
  4. package/hooks/notification/subagent_complete_notify.py +5 -5
  5. package/hooks/notification/test_subagent_complete_notify.py +7 -0
  6. package/hooks/validators/git_checks.py +4 -1
  7. package/hooks/validators/test_output_formatter.py +7 -2
  8. package/package.json +1 -1
  9. package/skills/bugteam/SKILL.md +143 -309
  10. package/skills/bugteam/SKILL_EVALS.md +46 -46
  11. package/skills/bugteam/reference/README.md +13 -0
  12. package/skills/bugteam/reference/audit-and-teammates.md +127 -0
  13. package/skills/bugteam/reference/design-rationale.md +28 -0
  14. package/skills/bugteam/reference/github-pr-reviews.md +86 -0
  15. package/skills/bugteam/reference/team-setup.md +51 -0
  16. package/skills/bugteam/reference/teardown-publish-permissions.md +70 -0
  17. package/skills/bugteam/scripts/README.md +4 -0
  18. package/skills/bugteam/{_claude_permissions_common.py → scripts/_claude_permissions_common.py} +37 -33
  19. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +55 -0
  20. package/skills/bugteam/{grant_project_claude_permissions.py → scripts/grant_project_claude_permissions.py} +10 -11
  21. package/skills/bugteam/{revoke_project_claude_permissions.py → scripts/revoke_project_claude_permissions.py} +13 -14
  22. package/skills/bugteam/sources.md +93 -0
  23. /package/hooks/validators/{config.py → validator_defaults.py} +0 -0
  24. /package/skills/bugteam/{test_claude_permissions_common.py → scripts/test_claude_permissions_common.py} +0 -0
@@ -1036,6 +1036,8 @@ def _resolve_enclosing_function_qname(
1036
1036
  break
1037
1037
  current_ancestor = parent_by_child_id.get(id(current_ancestor))
1038
1038
  if enclosing_function_name is None:
1039
+ if enclosing_class_name is not None:
1040
+ return f"<class:{enclosing_class_name}>"
1039
1041
  return None
1040
1042
  if enclosing_class_name is not None:
1041
1043
  return f"{enclosing_class_name}.{enclosing_function_name}"
@@ -1077,7 +1079,9 @@ def check_file_global_constants_use_count(content: str, file_path: str) -> list[
1077
1079
  if each_node.id not in callers_by_constant:
1078
1080
  continue
1079
1081
  enclosing_qname = _resolve_enclosing_function_qname(each_node, parent_by_child_id)
1080
- if enclosing_qname is not None:
1082
+ if enclosing_qname is None:
1083
+ callers_by_constant[each_node.id].add("<module-scope>")
1084
+ else:
1081
1085
  callers_by_constant[each_node.id].add(enclosing_qname)
1082
1086
 
1083
1087
  issues: list[str] = []
@@ -0,0 +1,61 @@
1
+ """Tests covering file-global constant reference resolution edge cases.
2
+
3
+ Loop2-C: class-decorator usage of a module-level constant must count as a
4
+ caller so the single-caller rule fires correctly.
5
+
6
+ Loop2-D: module-scope usages must register as a distinct caller bucket so
7
+ the "zero function references" exemption does not swallow real references.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib.util
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+
16
+
17
+ def _load_enforcer_module() -> ModuleType:
18
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
19
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
20
+ assert spec is not None
21
+ assert spec.loader is not None
22
+ module = importlib.util.module_from_spec(spec)
23
+ spec.loader.exec_module(module)
24
+ return module
25
+
26
+
27
+ code_rules_enforcer = _load_enforcer_module()
28
+
29
+
30
+ PRODUCTION_FILE_PATH = "packages/claude-dev-env/hooks/blocking/example_production.py"
31
+
32
+
33
+ def test_should_flag_constant_used_only_in_class_level_decorator() -> None:
34
+ source = (
35
+ "TIMEOUT = 5\n"
36
+ "\n"
37
+ "def register(value):\n"
38
+ " def wrap(cls):\n"
39
+ " return cls\n"
40
+ " return wrap\n"
41
+ "\n"
42
+ "@register(TIMEOUT)\n"
43
+ "class Foo:\n"
44
+ " pass\n"
45
+ )
46
+ issues = code_rules_enforcer.check_file_global_constants_use_count(
47
+ source, PRODUCTION_FILE_PATH
48
+ )
49
+ assert any(
50
+ "TIMEOUT" in issue and "only 1 function/method" in issue for issue in issues
51
+ ), f"Expected class-decorator usage to register as a caller, got: {issues}"
52
+
53
+
54
+ def test_should_flag_constant_used_once_at_module_scope_and_once_in_function() -> None:
55
+ source = "UPPER = 1\nSHADOW = UPPER\n\ndef lonely_caller():\n return UPPER\n"
56
+ issues = code_rules_enforcer.check_file_global_constants_use_count(
57
+ source, PRODUCTION_FILE_PATH
58
+ )
59
+ assert issues == [], (
60
+ f"Expected module-scope + function usage to count as 2 distinct callers, got: {issues}"
61
+ )
@@ -165,12 +165,14 @@ def test_should_flag_private_upper_snake_constant_used_by_only_one_function() ->
165
165
  ), f"Expected private UPPER_SNAKE to be flagged, got: {issues}"
166
166
 
167
167
 
168
- def test_should_accept_constant_referenced_only_at_module_scope() -> None:
168
+ def test_should_flag_constant_referenced_only_at_module_scope() -> None:
169
169
  source = "A = 1\nB = A + 1\n"
170
170
  issues = code_rules_enforcer.check_file_global_constants_use_count(
171
171
  source, PRODUCTION_FILE_PATH
172
172
  )
173
- assert issues == [], f"Expected module-scope reference to not count, got: {issues}"
173
+ assert any("A" in issue and "only 1 function/method" in issue for issue in issues), (
174
+ f"Expected single module-scope reference to be flagged, got: {issues}"
175
+ )
174
176
 
175
177
 
176
178
  def test_should_skip_non_python_files() -> None:
@@ -9,11 +9,13 @@ import subprocess
9
9
  import sys
10
10
  import platform
11
11
  import os
12
+ import time
13
+ from datetime import datetime
12
14
 
13
15
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
16
  from notification_utils import notify_discord
15
17
 
16
- NTFY_TOPIC = "claude-02633f9d93ea8794"
18
+ NTFY_TOPIC = os.environ.get("CLAUDE_NTFY_TOPIC", "")
17
19
  DEFAULT_MESSAGE = "Task completed"
18
20
  ACTIVITY_WEBHOOK_SECRET_ID = os.environ.get("BWS_DISCORD_ACTIVITY_SECRET_ID", "")
19
21
 
@@ -25,8 +27,6 @@ LOG_FILE = os.path.join(CACHE_DIR, "subagent-notify-debug.log")
25
27
  def log_debug(message: str) -> None:
26
28
  """Append debug message to log file."""
27
29
  try:
28
- from datetime import datetime
29
-
30
30
  with open(LOG_FILE, "a") as f:
31
31
  f.write(f"{datetime.now().isoformat()} - {message}\n")
32
32
  except Exception:
@@ -60,8 +60,6 @@ def get_task_info_from_stdin() -> str:
60
60
  return f"Agent {agent_id} completed" if agent_id else DEFAULT_MESSAGE
61
61
 
62
62
  # Find the Task tool call that spawned this agent (with retry for race condition)
63
- import time
64
-
65
63
  tool_use_id = None
66
64
  for attempt in range(3):
67
65
  with open(transcript_path, "r") as f:
@@ -118,6 +116,8 @@ def get_project_name() -> str:
118
116
 
119
117
  def notify_ntfy(title: str, message: str, priority: str = "default") -> None:
120
118
  """Send push notification via ntfy.sh with title and message."""
119
+ if not NTFY_TOPIC:
120
+ return
121
121
  try:
122
122
  subprocess.Popen(
123
123
  [
@@ -56,6 +56,13 @@ def test_main_forwards_activity_secret_id_to_notify_discord() -> None:
56
56
  assert call_kwargs["message"] == FIXTURE_TASK_DESCRIPTION
57
57
 
58
58
 
59
+ def test_notify_ntfy_skips_when_topic_unset() -> None:
60
+ module_under_test = load_hook_with_environment({"CLAUDE_NTFY_TOPIC": ""})
61
+ with patch.object(module_under_test.subprocess, "Popen") as popen_spy:
62
+ module_under_test.notify_ntfy(title="t", message="m")
63
+ assert popen_spy.call_count == 0
64
+
65
+
59
66
  def test_main_skips_notify_discord_when_task_description_is_empty() -> None:
60
67
  module_under_test = load_hook_with_environment(
61
68
  {"BWS_DISCORD_ACTIVITY_SECRET_ID": FIXTURE_ACTIVITY_SECRET_ID}
@@ -6,7 +6,10 @@ import sys
6
6
  from dataclasses import dataclass
7
7
  from typing import List
8
8
 
9
- from config import DEFAULT_BASE_BRANCH_WHEN_UNKNOWN
9
+ try:
10
+ from .validator_defaults import DEFAULT_BASE_BRANCH_WHEN_UNKNOWN
11
+ except ImportError:
12
+ from validator_defaults import DEFAULT_BASE_BRANCH_WHEN_UNKNOWN
10
13
 
11
14
 
12
15
  SUBPROCESS_TIMEOUT_SECONDS = 30
@@ -1,6 +1,7 @@
1
1
  """Tests for output formatting."""
2
2
 
3
3
  import os
4
+ import sys
4
5
 
5
6
  import pytest
6
7
 
@@ -95,11 +96,15 @@ class TestJsonFlag:
95
96
  import json
96
97
  import subprocess
97
98
 
99
+ validators_directory = os.path.dirname(os.path.abspath(__file__))
100
+ hooks_directory = os.path.normpath(
101
+ os.path.join(validators_directory, os.pardir)
102
+ )
98
103
  result = subprocess.run(
99
- ["python", "run_all_validators.py", "--json"],
104
+ [sys.executable, "-m", "validators.run_all_validators", "--json"],
100
105
  capture_output=True,
101
106
  text=True,
102
- cwd=os.path.dirname(os.path.abspath(__file__)),
107
+ cwd=hooks_directory,
103
108
  )
104
109
 
105
110
  output = result.stdout.strip()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.26.0",
3
+ "version": "1.26.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {