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.
- package/hooks/blocking/code_rules_enforcer.py +5 -1
- package/hooks/blocking/test_code_rules_enforcer.py +61 -0
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +4 -2
- package/hooks/notification/subagent_complete_notify.py +5 -5
- package/hooks/notification/test_subagent_complete_notify.py +7 -0
- package/hooks/validators/git_checks.py +4 -1
- package/hooks/validators/test_output_formatter.py +7 -2
- package/package.json +1 -1
- package/skills/bugteam/SKILL.md +143 -309
- package/skills/bugteam/SKILL_EVALS.md +46 -46
- package/skills/bugteam/reference/README.md +13 -0
- package/skills/bugteam/reference/audit-and-teammates.md +127 -0
- package/skills/bugteam/reference/design-rationale.md +28 -0
- package/skills/bugteam/reference/github-pr-reviews.md +86 -0
- package/skills/bugteam/reference/team-setup.md +51 -0
- package/skills/bugteam/reference/teardown-publish-permissions.md +70 -0
- package/skills/bugteam/scripts/README.md +4 -0
- package/skills/bugteam/{_claude_permissions_common.py → scripts/_claude_permissions_common.py} +37 -33
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +55 -0
- package/skills/bugteam/{grant_project_claude_permissions.py → scripts/grant_project_claude_permissions.py} +10 -11
- package/skills/bugteam/{revoke_project_claude_permissions.py → scripts/revoke_project_claude_permissions.py} +13 -14
- package/skills/bugteam/sources.md +93 -0
- /package/hooks/validators/{config.py → validator_defaults.py} +0 -0
- /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
|
|
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
|
|
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
|
|
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 = "
|
|
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
|
-
|
|
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
|
-
["
|
|
104
|
+
[sys.executable, "-m", "validators.run_all_validators", "--json"],
|
|
100
105
|
capture_output=True,
|
|
101
106
|
text=True,
|
|
102
|
-
cwd=
|
|
107
|
+
cwd=hooks_directory,
|
|
103
108
|
)
|
|
104
109
|
|
|
105
110
|
output = result.stdout.strip()
|