claude-dev-env 1.36.0 → 1.36.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/_shared/pr-loop/audit-contract.md +159 -0
- package/_shared/pr-loop/code-rules-gate.md +64 -0
- package/_shared/pr-loop/fix-protocol.md +37 -0
- package/_shared/pr-loop/gh-payloads.md +85 -0
- package/_shared/pr-loop/scripts/README.md +20 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +234 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +975 -0
- package/_shared/pr-loop/scripts/config/__init__.py +0 -0
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +11 -0
- package/_shared/pr-loop/scripts/config/code_rules_gate_constants.py +56 -0
- package/_shared/pr-loop/scripts/config/fix_hookspath_constants.py +25 -0
- package/_shared/pr-loop/scripts/config/gh_util_constants.py +31 -0
- package/_shared/pr-loop/scripts/config/preflight_constants.py +47 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +260 -0
- package/_shared/pr-loop/scripts/gh_util.py +193 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +130 -0
- package/_shared/pr-loop/scripts/preflight.py +227 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +156 -0
- package/_shared/pr-loop/scripts/tests/conftest.py +51 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +135 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +169 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +58 -0
- package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +50 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +917 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +102 -0
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +374 -0
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +47 -0
- package/_shared/pr-loop/scripts/tests/test_gh_util.py +257 -0
- package/_shared/pr-loop/scripts/tests/test_gh_util_constants.py +61 -0
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +49 -0
- package/_shared/pr-loop/scripts/tests/test_preflight.py +333 -0
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +82 -0
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +49 -0
- package/_shared/pr-loop/state-schema.md +81 -0
- package/package.json +2 -1
- package/skills/bugteam/SKILL.md +332 -108
- package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
- package/skills/bugteam/test_team_lifecycle.py +9 -0
- package/skills/pr-converge/SKILL.md +1005 -395
- package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
- package/skills/pr-converge/test_team_lifecycle.py +9 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Tests for _shared permission helpers extracted from skills/bugteam/scripts/."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_common_module() -> ModuleType:
|
|
14
|
+
module_path = Path(__file__).parent.parent / "_claude_permissions_common.py"
|
|
15
|
+
parent_directory = str(module_path.parent.resolve())
|
|
16
|
+
if parent_directory not in sys.path:
|
|
17
|
+
sys.path.insert(0, parent_directory)
|
|
18
|
+
spec = importlib.util.spec_from_file_location(
|
|
19
|
+
"_claude_permissions_common", module_path
|
|
20
|
+
)
|
|
21
|
+
assert spec is not None
|
|
22
|
+
assert spec.loader is not None
|
|
23
|
+
module = importlib.util.module_from_spec(spec)
|
|
24
|
+
spec.loader.exec_module(module)
|
|
25
|
+
return module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
common = _load_common_module()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_return_normalized_path_when_cwd_contains_spaces(
|
|
32
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
33
|
+
) -> None:
|
|
34
|
+
directory_with_spaces = tmp_path / "dir with spaces"
|
|
35
|
+
directory_with_spaces.mkdir()
|
|
36
|
+
monkeypatch.chdir(directory_with_spaces)
|
|
37
|
+
returned_project_path = common.get_current_project_path()
|
|
38
|
+
expected_suffix = "/dir with spaces"
|
|
39
|
+
assert returned_project_path.endswith(expected_suffix)
|
|
40
|
+
assert "\\" not in returned_project_path
|
|
41
|
+
built_rule = common.build_permission_rule("Edit", returned_project_path)
|
|
42
|
+
assert built_rule.startswith("Edit(")
|
|
43
|
+
assert built_rule.endswith("/.claude/**)")
|
|
44
|
+
assert "dir with spaces" in built_rule
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_raise_when_cwd_contains_glob_metacharacters(
|
|
48
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
49
|
+
) -> None:
|
|
50
|
+
directory_with_star = tmp_path / "weird[dir]"
|
|
51
|
+
directory_with_star.mkdir()
|
|
52
|
+
monkeypatch.chdir(directory_with_star)
|
|
53
|
+
with pytest.raises(ValueError, match="glob metacharacters"):
|
|
54
|
+
common.get_current_project_path()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_flag_glob_metacharacters_in_any_position() -> None:
|
|
58
|
+
assert common.path_contains_glob_metacharacters("/home/user/[dir]/project")
|
|
59
|
+
assert common.path_contains_glob_metacharacters("/home/user/project*")
|
|
60
|
+
assert not common.path_contains_glob_metacharacters("/home/user/dir with spaces")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_text_file_encoding_remains_local_constant() -> None:
|
|
64
|
+
assert common.TEXT_FILE_ENCODING == "utf-8"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_module_no_longer_redeclares_migrated_constants() -> None:
|
|
68
|
+
assert not hasattr(common, "ALL_PERMISSION_ALLOW_TOOLS")
|
|
69
|
+
assert not hasattr(common, "AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_save_settings_uses_unique_per_call_temp_suffix(
|
|
73
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Regression for atomic-write race: each save_settings call must
|
|
76
|
+
derive its own unique temp path so concurrent writers do not collide
|
|
77
|
+
on the same `.tmp` filename.
|
|
78
|
+
"""
|
|
79
|
+
settings_path = tmp_path / "settings.json"
|
|
80
|
+
settings_path.write_text(json.dumps({}), encoding="utf-8")
|
|
81
|
+
|
|
82
|
+
captured_temp_paths: list[Path] = []
|
|
83
|
+
real_write_atomically_with_mode = common.write_atomically_with_mode
|
|
84
|
+
|
|
85
|
+
def capturing_write(
|
|
86
|
+
temporary_path: Path, serialized_content: str, file_mode: int
|
|
87
|
+
) -> None:
|
|
88
|
+
captured_temp_paths.append(Path(temporary_path))
|
|
89
|
+
real_write_atomically_with_mode(
|
|
90
|
+
temporary_path, serialized_content, file_mode
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
monkeypatch.setattr(common, "write_atomically_with_mode", capturing_write)
|
|
94
|
+
|
|
95
|
+
common.save_settings(settings_path, {"writer": "first"})
|
|
96
|
+
common.save_settings(settings_path, {"writer": "second"})
|
|
97
|
+
|
|
98
|
+
assert len(captured_temp_paths) == 2
|
|
99
|
+
assert captured_temp_paths[0] != captured_temp_paths[1]
|
|
100
|
+
for each_temp_path in captured_temp_paths:
|
|
101
|
+
assert ".tmp." in each_temp_path.name
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_save_settings_temp_suffix_includes_pid_and_random_token(
|
|
105
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
106
|
+
) -> None:
|
|
107
|
+
"""The unique temp suffix must include the process id and a random
|
|
108
|
+
hex token so two processes cannot collide on the same temp filename.
|
|
109
|
+
"""
|
|
110
|
+
settings_path = tmp_path / "settings.json"
|
|
111
|
+
settings_path.write_text(json.dumps({}), encoding="utf-8")
|
|
112
|
+
|
|
113
|
+
captured_temp_paths: list[Path] = []
|
|
114
|
+
real_write_atomically_with_mode = common.write_atomically_with_mode
|
|
115
|
+
|
|
116
|
+
def capturing_write(
|
|
117
|
+
temporary_path: Path, serialized_content: str, file_mode: int
|
|
118
|
+
) -> None:
|
|
119
|
+
captured_temp_paths.append(Path(temporary_path))
|
|
120
|
+
real_write_atomically_with_mode(
|
|
121
|
+
temporary_path, serialized_content, file_mode
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
monkeypatch.setattr(common, "write_atomically_with_mode", capturing_write)
|
|
125
|
+
|
|
126
|
+
common.save_settings(settings_path, {"writer": "only"})
|
|
127
|
+
|
|
128
|
+
assert len(captured_temp_paths) == 1
|
|
129
|
+
suffix_token = captured_temp_paths[0].name.split(".tmp.", maxsplit=1)[1]
|
|
130
|
+
pid_text, random_token = suffix_token.split(".", maxsplit=1)
|
|
131
|
+
assert pid_text.isdigit()
|
|
132
|
+
minimum_random_token_hex_chars = 4
|
|
133
|
+
assert len(random_token) >= minimum_random_token_hex_chars
|
|
134
|
+
assert all(
|
|
135
|
+
each_character in "0123456789abcdef" for each_character in random_token
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_text_file_encoding_sourced_from_config() -> None:
|
|
140
|
+
config_module_path = (
|
|
141
|
+
Path(__file__).parent.parent / "config" / "claude_permissions_constants.py"
|
|
142
|
+
)
|
|
143
|
+
specification = importlib.util.spec_from_file_location(
|
|
144
|
+
"config.claude_permissions_constants", config_module_path
|
|
145
|
+
)
|
|
146
|
+
assert specification is not None
|
|
147
|
+
assert specification.loader is not None
|
|
148
|
+
config_module = importlib.util.module_from_spec(specification)
|
|
149
|
+
specification.loader.exec_module(config_module)
|
|
150
|
+
assert common.TEXT_FILE_ENCODING == config_module.TEXT_FILE_ENCODING
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_path_contains_glob_metacharacters_local_tuple_uses_all_collection_prefix() -> None:
|
|
154
|
+
source_text = inspect.getsource(common.path_contains_glob_metacharacters)
|
|
155
|
+
assert "all_glob_metacharacters_in_path" in source_text
|
|
156
|
+
assert "glob_metacharacters_in_path:" not in source_text.replace(
|
|
157
|
+
"all_glob_metacharacters_in_path", ""
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_is_valid_project_root_uses_extracted_directory_marker_constants() -> None:
|
|
162
|
+
"""is_valid_project_root must reference extracted constants, not inline string literals."""
|
|
163
|
+
source_text = inspect.getsource(common.is_valid_project_root)
|
|
164
|
+
assert "GIT_DIRECTORY_NAME" in source_text
|
|
165
|
+
assert "CLAUDE_SETTINGS_DIRECTORY_NAME" in source_text
|
|
166
|
+
assert "'.git'" not in source_text
|
|
167
|
+
assert '".git"' not in source_text
|
|
168
|
+
assert "'.claude'" not in source_text
|
|
169
|
+
assert '".claude"' not in source_text
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Tests for shared constants powering grant/revoke claude permissions."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _load_constants_module() -> ModuleType:
|
|
9
|
+
module_path = (
|
|
10
|
+
Path(__file__).parent.parent / "config" / "claude_permissions_constants.py"
|
|
11
|
+
)
|
|
12
|
+
specification = importlib.util.spec_from_file_location(
|
|
13
|
+
"config.claude_permissions_constants", module_path
|
|
14
|
+
)
|
|
15
|
+
assert specification is not None
|
|
16
|
+
assert specification.loader is not None
|
|
17
|
+
module = importlib.util.module_from_spec(specification)
|
|
18
|
+
specification.loader.exec_module(module)
|
|
19
|
+
return module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
constants_module = _load_constants_module()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_exposes_all_permission_allow_tools_tuple() -> None:
|
|
26
|
+
assert constants_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "Write", "Read")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_auto_mode_environment_entry_template_is_format_string() -> None:
|
|
30
|
+
rendered_template_text = (
|
|
31
|
+
constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
|
|
32
|
+
project_path="/tmp/x"
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
assert "/tmp/x" in rendered_template_text
|
|
36
|
+
assert ".claude/**" in rendered_template_text
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_get_claude_user_settings_path_ends_in_settings_json() -> None:
|
|
40
|
+
resolved_settings_path = constants_module.get_claude_user_settings_path()
|
|
41
|
+
assert resolved_settings_path.name == constants_module.CLAUDE_SETTINGS_FILENAME
|
|
42
|
+
assert (
|
|
43
|
+
resolved_settings_path.parent.name
|
|
44
|
+
== constants_module.CLAUDE_SETTINGS_DIRECTORY_NAME
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_text_file_encoding_lives_in_config() -> None:
|
|
49
|
+
assert constants_module.TEXT_FILE_ENCODING == "utf-8"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_unique_temporary_suffix_byte_length_is_positive_integer() -> None:
|
|
53
|
+
assert isinstance(constants_module.UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH, int)
|
|
54
|
+
assert constants_module.UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH > 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_git_directory_name_lives_in_config() -> None:
|
|
58
|
+
assert constants_module.GIT_DIRECTORY_NAME == ".git"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Tests for claude_settings_keys_constants.py extracted constant set."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _load_constants_module() -> ModuleType:
|
|
9
|
+
module_path = (
|
|
10
|
+
Path(__file__).parent.parent / "config" / "claude_settings_keys_constants.py"
|
|
11
|
+
)
|
|
12
|
+
specification = importlib.util.spec_from_file_location(
|
|
13
|
+
"config.claude_settings_keys_constants", module_path
|
|
14
|
+
)
|
|
15
|
+
assert specification is not None
|
|
16
|
+
assert specification.loader is not None
|
|
17
|
+
module = importlib.util.module_from_spec(specification)
|
|
18
|
+
specification.loader.exec_module(module)
|
|
19
|
+
return module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
constants_module = _load_constants_module()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_permissions_key_is_typed_string() -> None:
|
|
26
|
+
assert isinstance(constants_module.CLAUDE_SETTINGS_PERMISSIONS_KEY, str)
|
|
27
|
+
assert constants_module.CLAUDE_SETTINGS_PERMISSIONS_KEY == "permissions"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_allow_key_is_typed_string() -> None:
|
|
31
|
+
assert isinstance(constants_module.CLAUDE_SETTINGS_ALLOW_KEY, str)
|
|
32
|
+
assert constants_module.CLAUDE_SETTINGS_ALLOW_KEY == "allow"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_additional_directories_key_is_typed_string() -> None:
|
|
36
|
+
assert isinstance(constants_module.CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY, str)
|
|
37
|
+
assert (
|
|
38
|
+
constants_module.CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY
|
|
39
|
+
== "additionalDirectories"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_auto_mode_key_is_typed_string() -> None:
|
|
44
|
+
assert isinstance(constants_module.CLAUDE_SETTINGS_AUTO_MODE_KEY, str)
|
|
45
|
+
assert constants_module.CLAUDE_SETTINGS_AUTO_MODE_KEY == "autoMode"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_environment_key_is_typed_string() -> None:
|
|
49
|
+
assert isinstance(constants_module.CLAUDE_SETTINGS_ENVIRONMENT_KEY, str)
|
|
50
|
+
assert constants_module.CLAUDE_SETTINGS_ENVIRONMENT_KEY == "environment"
|