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.
Files changed (42) hide show
  1. package/_shared/pr-loop/audit-contract.md +159 -0
  2. package/_shared/pr-loop/code-rules-gate.md +64 -0
  3. package/_shared/pr-loop/fix-protocol.md +37 -0
  4. package/_shared/pr-loop/gh-payloads.md +85 -0
  5. package/_shared/pr-loop/scripts/README.md +20 -0
  6. package/_shared/pr-loop/scripts/_claude_permissions_common.py +234 -0
  7. package/_shared/pr-loop/scripts/code_rules_gate.py +975 -0
  8. package/_shared/pr-loop/scripts/config/__init__.py +0 -0
  9. package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +36 -0
  10. package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +11 -0
  11. package/_shared/pr-loop/scripts/config/code_rules_gate_constants.py +56 -0
  12. package/_shared/pr-loop/scripts/config/fix_hookspath_constants.py +25 -0
  13. package/_shared/pr-loop/scripts/config/gh_util_constants.py +31 -0
  14. package/_shared/pr-loop/scripts/config/preflight_constants.py +47 -0
  15. package/_shared/pr-loop/scripts/fix_hookspath.py +260 -0
  16. package/_shared/pr-loop/scripts/gh_util.py +193 -0
  17. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +130 -0
  18. package/_shared/pr-loop/scripts/preflight.py +227 -0
  19. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +156 -0
  20. package/_shared/pr-loop/scripts/tests/conftest.py +51 -0
  21. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +135 -0
  22. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +169 -0
  23. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +58 -0
  24. package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +50 -0
  25. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +917 -0
  26. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +102 -0
  27. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +374 -0
  28. package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +47 -0
  29. package/_shared/pr-loop/scripts/tests/test_gh_util.py +257 -0
  30. package/_shared/pr-loop/scripts/tests/test_gh_util_constants.py +61 -0
  31. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +49 -0
  32. package/_shared/pr-loop/scripts/tests/test_preflight.py +333 -0
  33. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +82 -0
  34. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +49 -0
  35. package/_shared/pr-loop/state-schema.md +81 -0
  36. package/package.json +2 -1
  37. package/skills/bugteam/SKILL.md +332 -108
  38. package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
  39. package/skills/bugteam/test_team_lifecycle.py +9 -0
  40. package/skills/pr-converge/SKILL.md +1005 -395
  41. package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
  42. 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"