claude-dev-env 1.28.0 → 1.29.0

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 (54) hide show
  1. package/agents/caveman.md +74 -0
  2. package/hooks/blocking/code_rules_enforcer.py +82 -7
  3. package/hooks/blocking/code_rules_path_utils.py +31 -0
  4. package/hooks/blocking/es_exe_path_rewriter.py +159 -0
  5. package/hooks/blocking/hedging_language_blocker.py +12 -2
  6. package/hooks/blocking/test_code_rules_enforcer.py +148 -0
  7. package/hooks/blocking/test_code_rules_enforcer_config_path.py +123 -0
  8. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  9. package/hooks/blocking/test_code_rules_path_utils.py +52 -0
  10. package/hooks/blocking/test_es_exe_path_rewriter.py +369 -0
  11. package/hooks/blocking/test_hedging_language_blocker.py +7 -6
  12. package/hooks/config/dynamic_stderr_handler.py +22 -0
  13. package/hooks/config/path_rewriter_constants.py +13 -0
  14. package/hooks/config/project_paths_reader.py +78 -0
  15. package/hooks/config/setup_project_paths_constants.py +41 -0
  16. package/hooks/config/test_dynamic_stderr_handler.py +48 -0
  17. package/hooks/config/test_messages.py +5 -1
  18. package/hooks/config/test_path_rewriter_constants.py +57 -0
  19. package/hooks/config/test_project_paths_reader.py +149 -0
  20. package/hooks/config/test_setup_project_paths_constants.py +74 -0
  21. package/hooks/git-hooks/test_config.py +1 -0
  22. package/hooks/git-hooks/test_gate_utils.py +1 -0
  23. package/hooks/git-hooks/test_pre_commit.py +1 -0
  24. package/hooks/git-hooks/test_pre_push.py +1 -0
  25. package/hooks/hooks.json +10 -0
  26. package/hooks/session/test_untracked_repo_detector.py +192 -0
  27. package/hooks/session/untracked_repo_detector.py +103 -0
  28. package/hooks/validators/exempt_paths.py +17 -14
  29. package/hooks/validators/test_exempt_paths.py +65 -0
  30. package/hooks/validators/test_git_checks.py +17 -17
  31. package/package.json +1 -1
  32. package/scripts/config/__init__.py +1 -0
  33. package/scripts/config/groq_bugteam_config.py +118 -0
  34. package/scripts/config/test_groq_bugteam_config.py +72 -0
  35. package/scripts/groq_bugteam.README.md +129 -0
  36. package/scripts/groq_bugteam.py +586 -0
  37. package/scripts/setup_project_paths.py +347 -0
  38. package/scripts/test_groq_bugteam.py +391 -0
  39. package/scripts/test_setup_project_paths.py +532 -0
  40. package/scripts/test_setup_project_paths_config.py +6 -0
  41. package/skills/bugteam/CONSTRAINTS.md +1 -1
  42. package/skills/bugteam/PROMPTS.md +1 -1
  43. package/skills/bugteam/SKILL.md +5 -5
  44. package/skills/bugteam/SKILL_EVALS.md +5 -5
  45. package/skills/bugteam/reference/audit-and-teammates.md +3 -3
  46. package/skills/bugteam/reference/audit-contract.md +159 -0
  47. package/skills/bugteam/reference/team-setup.md +2 -2
  48. package/skills/bugteam/scripts/bugteam_preflight.py +66 -0
  49. package/skills/bugteam/scripts/test_bugteam_preflight.py +189 -0
  50. package/skills/copilot-review/SKILL.md +145 -0
  51. package/skills/findbugs/SKILL.md +14 -22
  52. package/skills/qbug/SKILL.md +56 -12
  53. package/skills/qbug/test_qbug_skill_audit_schema.py +156 -0
  54. package/skills/qbug/test_qbug_skill_post_fix_audit.py +103 -0
@@ -0,0 +1,192 @@
1
+ """Tests for untracked_repo_detector — SessionStart hook for new repo detection."""
2
+
3
+ import json
4
+ import sys
5
+ from io import StringIO
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+
9
+ import pytest
10
+
11
+ _SESSION_DIR = Path(__file__).resolve().parent
12
+ _HOOKS_ROOT = _SESSION_DIR.parent
13
+ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
14
+ if each_sys_path_entry not in sys.path:
15
+ sys.path.insert(0, each_sys_path_entry)
16
+
17
+ import untracked_repo_detector as detector
18
+ from config.project_paths_reader import registry_file_path
19
+ from config.setup_project_paths_constants import GIT_DIRECTORY_SEGMENT_NAME
20
+
21
+
22
+ def _run_main_with_cwd(cwd: str, known_registry: dict) -> tuple[str, str, int]:
23
+ """Return (stdout, stderr, exit_code) from running main() with patched cwd and registry."""
24
+ captured_stdout = StringIO()
25
+ captured_stderr = StringIO()
26
+ exit_code = 0
27
+ try:
28
+ with (
29
+ patch(
30
+ "untracked_repo_detector.current_working_directory", return_value=cwd
31
+ ),
32
+ patch("untracked_repo_detector.load_registry", return_value=known_registry),
33
+ patch("sys.stdout", captured_stdout),
34
+ patch("sys.stderr", captured_stderr),
35
+ ):
36
+ detector.main()
37
+ except SystemExit as e:
38
+ exit_code = e.code or 0
39
+ return captured_stdout.getvalue(), captured_stderr.getvalue(), exit_code
40
+
41
+
42
+ class TestFindGitRoot:
43
+ def test_returns_none_when_no_git_directory_found(self, tmp_path: Path) -> None:
44
+ non_repo_path = tmp_path / "not-a-repo"
45
+ non_repo_path.mkdir()
46
+ original_exists = Path.exists
47
+
48
+ def exists_without_ambient_git(self_path: Path) -> bool:
49
+ if self_path.name == ".git" and tmp_path.resolve() not in self_path.resolve().parents and self_path.resolve().parent != tmp_path.resolve():
50
+ return False
51
+ return original_exists(self_path)
52
+
53
+ with patch.object(Path, "exists", exists_without_ambient_git):
54
+ found_root = detector.find_git_root(str(non_repo_path))
55
+ assert found_root is None
56
+
57
+ def test_returns_root_when_dot_git_exists(self, tmp_path: Path) -> None:
58
+ repo_root = tmp_path / "my-repo"
59
+ repo_root.mkdir()
60
+ (repo_root / ".git").mkdir()
61
+ found_root = detector.find_git_root(str(repo_root))
62
+ assert found_root is not None
63
+ assert Path(found_root).resolve() == repo_root.resolve()
64
+
65
+ def test_returns_root_when_cwd_is_subdirectory(self, tmp_path: Path) -> None:
66
+ repo_root = tmp_path / "my-repo"
67
+ nested_directory = repo_root / "src" / "deep"
68
+ nested_directory.mkdir(parents=True)
69
+ (repo_root / ".git").mkdir()
70
+ found_root = detector.find_git_root(str(nested_directory))
71
+ assert found_root is not None
72
+ assert Path(found_root).resolve() == repo_root.resolve()
73
+
74
+
75
+ class TestRegistryContainsRepo:
76
+ def test_cwd_inside_tracked_repo_produces_no_output(self, tmp_path: Path) -> None:
77
+ repo_root = tmp_path / "tracked-repo"
78
+ repo_root.mkdir()
79
+ (repo_root / ".git").mkdir()
80
+ known_registry = {"tracked-repo": str(repo_root)}
81
+ stdout, _, _ = _run_main_with_cwd(str(repo_root), known_registry)
82
+ assert stdout.strip() == ""
83
+
84
+ def test_cwd_outside_any_git_repo_produces_no_output(self, tmp_path: Path) -> None:
85
+ non_repo = tmp_path / "not-a-repo"
86
+ non_repo.mkdir()
87
+ with patch("untracked_repo_detector.find_git_root", return_value=None):
88
+ stdout, _, _ = _run_main_with_cwd(str(non_repo), {})
89
+ assert stdout.strip() == ""
90
+
91
+
92
+ class TestUntrackedRepoDetection:
93
+ def test_cwd_inside_untracked_repo_produces_additional_context(
94
+ self, tmp_path: Path
95
+ ) -> None:
96
+ repo_root = tmp_path / "new-repo"
97
+ repo_root.mkdir()
98
+ (repo_root / ".git").mkdir()
99
+ stdout, _, _ = _run_main_with_cwd(str(repo_root), {})
100
+ assert stdout.strip() != ""
101
+ emitted = json.loads(stdout)
102
+ assert "additionalContext" in emitted
103
+
104
+ def test_emitted_context_names_the_detected_repo_path(self, tmp_path: Path) -> None:
105
+ repo_root = tmp_path / "new-repo"
106
+ repo_root.mkdir()
107
+ (repo_root / ".git").mkdir()
108
+ stdout, _, _ = _run_main_with_cwd(str(repo_root), {})
109
+ emitted = json.loads(stdout)
110
+ context_text = emitted["additionalContext"]
111
+ assert str(repo_root) in context_text
112
+
113
+ def test_emitted_context_names_the_config_file_path(self, tmp_path: Path) -> None:
114
+ repo_root = tmp_path / "new-repo"
115
+ repo_root.mkdir()
116
+ (repo_root / ".git").mkdir()
117
+ stdout, _, _ = _run_main_with_cwd(str(repo_root), {})
118
+ emitted = json.loads(stdout)
119
+ context_text = emitted["additionalContext"]
120
+ assert "project-paths.json" in context_text
121
+
122
+ def test_emitted_context_instructs_claude_to_use_ask_user_question(
123
+ self, tmp_path: Path
124
+ ) -> None:
125
+ repo_root = tmp_path / "new-repo"
126
+ repo_root.mkdir()
127
+ (repo_root / ".git").mkdir()
128
+ stdout, _, _ = _run_main_with_cwd(str(repo_root), {})
129
+ emitted = json.loads(stdout)
130
+ context_text = emitted["additionalContext"]
131
+ assert "AskUserQuestion" in context_text
132
+
133
+ def test_emitted_context_states_hook_has_written_nothing(
134
+ self, tmp_path: Path
135
+ ) -> None:
136
+ repo_root = tmp_path / "new-repo"
137
+ repo_root.mkdir()
138
+ (repo_root / ".git").mkdir()
139
+ stdout, _, _ = _run_main_with_cwd(str(repo_root), {})
140
+ emitted = json.loads(stdout)
141
+ context_text = emitted["additionalContext"]
142
+ assert (
143
+ "written nothing" in context_text.lower()
144
+ or "has not written" in context_text.lower()
145
+ )
146
+
147
+
148
+ class TestSharedRegistryPath:
149
+ def test_config_file_path_not_a_module_level_attribute(self) -> None:
150
+ """Pin PR #230 round 7: _CONFIG_FILE_PATH inlined into _build_confirm_instruction.
151
+
152
+ Single-consumer module-level constant moved to local per file-global-constants rule.
153
+ """
154
+ assert not hasattr(detector, "_CONFIG_FILE_PATH")
155
+
156
+ def test_confirm_instruction_contains_registry_file_path(
157
+ self, tmp_path: Path
158
+ ) -> None:
159
+ repo_root = str(tmp_path / "some-repo")
160
+ instruction_text = detector._build_confirm_instruction(repo_root)
161
+ assert str(registry_file_path()) in instruction_text
162
+
163
+ def test_confirm_instruction_contains_project_paths_json(
164
+ self, tmp_path: Path
165
+ ) -> None:
166
+ repo_root = str(tmp_path / "some-repo")
167
+ instruction_text = detector._build_confirm_instruction(repo_root)
168
+ assert "project-paths.json" in instruction_text
169
+
170
+
171
+ class TestSharedGitDirectoryConstant:
172
+ def test_find_git_root_uses_shared_git_directory_constant(self, tmp_path: Path) -> None:
173
+ """Pin: find_git_root must use GIT_DIRECTORY_SEGMENT_NAME from shared constants."""
174
+ repo_root = tmp_path / "uses-shared-constant"
175
+ repo_root.mkdir()
176
+ (repo_root / GIT_DIRECTORY_SEGMENT_NAME).mkdir()
177
+ found_root = detector.find_git_root(str(repo_root))
178
+ assert found_root is not None
179
+
180
+
181
+ class TestPathNormalization:
182
+ def test_windows_and_posix_forms_of_same_path_compare_equal(
183
+ self, tmp_path: Path
184
+ ) -> None:
185
+ repo_root = tmp_path / "cross-platform-repo"
186
+ repo_root.mkdir()
187
+ (repo_root / ".git").mkdir()
188
+ windows_path = str(repo_root)
189
+ posix_path = str(repo_root).replace("\\", "/")
190
+ registry_with_posix = {"cross-platform-repo": posix_path}
191
+ stdout, _, _ = _run_main_with_cwd(windows_path, registry_with_posix)
192
+ assert stdout.strip() == ""
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """SessionStart hook: detect git repos not present in ~/.claude/project-paths.json.
3
+
4
+ When Claude Code opens inside a git repo that is not registered, emits an
5
+ additionalContext instruction asking Claude to confirm the mapping with the
6
+ user via AskUserQuestion before persisting anything. The hook itself never
7
+ writes to the config file.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def _insert_hooks_tree_for_imports() -> None:
20
+ hooks_tree = Path(__file__).resolve().parent.parent
21
+ hooks_tree_string = str(hooks_tree)
22
+ if hooks_tree_string not in sys.path:
23
+ sys.path.insert(0, hooks_tree_string)
24
+
25
+
26
+ _insert_hooks_tree_for_imports()
27
+
28
+ from config.dynamic_stderr_handler import DynamicStderrHandler
29
+ from config.project_paths_reader import (
30
+ load_registry,
31
+ registry_contains_path,
32
+ registry_file_path,
33
+ )
34
+ from config.setup_project_paths_constants import GIT_DIRECTORY_SEGMENT_NAME
35
+
36
+
37
+ _logger = logging.getLogger("untracked_repo_detector")
38
+ if not _logger.handlers:
39
+ _stderr_handler = DynamicStderrHandler()
40
+ _stderr_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
41
+ _logger.addHandler(_stderr_handler)
42
+ _logger.setLevel(logging.INFO)
43
+ _logger.propagate = False
44
+
45
+
46
+ def current_working_directory() -> str:
47
+ """Return the process working directory as a string."""
48
+ return os.getcwd()
49
+
50
+
51
+ def find_git_root(start_path: str) -> str | None:
52
+ """Walk upward from start_path looking for a .git directory.
53
+
54
+ The walk is bounded by the user's home directory: once the candidate
55
+ reaches the home directory without finding ``.git``, the search stops.
56
+ This prevents a stray ``.git`` above the user's home (for example a
57
+ parent dotfiles repo) from being falsely reported as the session's repo.
58
+
59
+ Returns the absolute path of the repo root, or None if not found.
60
+ """
61
+ home_directory = Path.home().resolve()
62
+ candidate = Path(start_path).resolve()
63
+ while True:
64
+ if (candidate / GIT_DIRECTORY_SEGMENT_NAME).exists():
65
+ return str(candidate)
66
+ if candidate == home_directory:
67
+ return None
68
+ parent = candidate.parent
69
+ if parent == candidate:
70
+ return None
71
+ candidate = parent
72
+
73
+
74
+ def _build_confirm_instruction(repo_root: str) -> str:
75
+ config_file_path = str(registry_file_path())
76
+ return (
77
+ f"UNTRACKED REPO DETECTED: The current session is running inside a git "
78
+ f"repository at '{repo_root}' that is not present in {config_file_path}. "
79
+ f"Use AskUserQuestion with two options — 'Save mapping' (recommended) and "
80
+ f"'Skip for this session' — to confirm whether to persist this repo path. "
81
+ f"On approval, append a new entry to {config_file_path} mapping the "
82
+ f"repository leaf name to '{repo_root}'. This hook has written nothing."
83
+ )
84
+
85
+
86
+ def main() -> None:
87
+ try:
88
+ session_cwd = current_working_directory()
89
+ git_root = find_git_root(session_cwd)
90
+ if git_root is None:
91
+ sys.exit(0)
92
+ known_registry = load_registry()
93
+ if registry_contains_path(known_registry, git_root):
94
+ sys.exit(0)
95
+ instruction = _build_confirm_instruction(git_root)
96
+ print(json.dumps({"additionalContext": instruction}))
97
+ except Exception as e:
98
+ _logger.error("%s", e)
99
+ sys.exit(0)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -9,20 +9,28 @@ the two produced the "inconsistent verdicts" bug this module prevents.
9
9
  Matching is case-insensitive so paths like ``Config/foo.py`` or
10
10
  ``src/Tests/test_x.py`` are treated the same on case-preserving
11
11
  filesystems (macOS default, Windows NTFS) as on case-sensitive ones.
12
+
13
+ ``is_config_file`` canonical implementation lives in
14
+ ``hooks/blocking/code_rules_path_utils.py`` and is re-exported here so
15
+ both the pre-write gate and the pre-push validator share identical logic.
12
16
  """
13
17
 
14
18
  from __future__ import annotations
15
19
 
20
+ import sys
21
+ from pathlib import Path
22
+
23
+
24
+ def _ensure_blocking_package_on_path() -> None:
25
+ blocking_directory = str(Path(__file__).resolve().parent.parent / "blocking")
26
+ if blocking_directory not in sys.path:
27
+ sys.path.insert(0, blocking_directory)
28
+
29
+
30
+ _ensure_blocking_package_on_path()
31
+
32
+ from code_rules_path_utils import is_config_file # type: ignore[import-not-found] # noqa: E402
16
33
 
17
- CONFIG_PATH_PATTERNS: frozenset[str] = frozenset(
18
- {
19
- "config/",
20
- "config\\",
21
- "/config.",
22
- "\\config.",
23
- "settings.py",
24
- }
25
- )
26
34
 
27
35
  TEST_PATH_PATTERNS: frozenset[str] = frozenset(
28
36
  {
@@ -65,11 +73,6 @@ MIGRATION_PATH_PATTERNS: frozenset[str] = frozenset(
65
73
  )
66
74
 
67
75
 
68
- def is_config_file(file_path: str) -> bool:
69
- path_lower = file_path.lower()
70
- return any(pattern in path_lower for pattern in CONFIG_PATH_PATTERNS)
71
-
72
-
73
76
  def is_test_file(file_path: str) -> bool:
74
77
  path_lower = file_path.lower()
75
78
  return any(pattern in path_lower for pattern in TEST_PATH_PATTERNS)
@@ -0,0 +1,65 @@
1
+ """Tests for exempt_paths path-classification helpers.
2
+
3
+ Covers the is_config_file() contract: only files whose parent directory
4
+ segment is literally 'config' should match. A filename of 'config.py'
5
+ outside a config/ directory must NOT match.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ _BLOCKING_DIR = str(Path(__file__).resolve().parent.parent / "blocking")
14
+ if _BLOCKING_DIR not in sys.path:
15
+ sys.path.insert(0, _BLOCKING_DIR)
16
+
17
+ from validators.exempt_paths import is_config_file # noqa: E402
18
+ from code_rules_path_utils import is_config_file as path_utils_is_config_file # noqa: E402
19
+
20
+
21
+ def test_should_exempt_file_inside_config_directory() -> None:
22
+ assert is_config_file("project/config/constants.py") is True
23
+
24
+
25
+ def test_should_exempt_file_inside_nested_config_directory() -> None:
26
+ assert is_config_file("packages/myapp/config/timing.py") is True
27
+
28
+
29
+ def test_should_not_exempt_file_named_config_dot_py_outside_config_dir() -> None:
30
+ assert is_config_file("scripts/db/config.py") is False
31
+
32
+
33
+ def test_should_not_exempt_file_with_config_in_filename_only() -> None:
34
+ assert is_config_file("src/app_config.py") is False
35
+
36
+
37
+ def test_should_not_exempt_file_with_config_in_parent_partial_match() -> None:
38
+ assert is_config_file("src/reconfigured/constants.py") is False
39
+
40
+
41
+ def test_should_exempt_settings_py_by_filename() -> None:
42
+ assert is_config_file("any/path/settings.py") is True
43
+
44
+
45
+ def test_should_exempt_windows_path_inside_config_directory() -> None:
46
+ assert is_config_file("packages\\myapp\\config\\timing.py") is True
47
+
48
+
49
+ def test_should_not_exempt_filename_ending_with_settings_py_but_not_exactly_settings_py() -> None:
50
+ assert is_config_file("mysettings.py") is False
51
+
52
+
53
+ def test_should_exempt_bare_settings_py_filename() -> None:
54
+ assert is_config_file("settings.py") is True
55
+
56
+
57
+ def test_should_exempt_settings_py_in_nested_path() -> None:
58
+ assert is_config_file("path/to/settings.py") is True
59
+
60
+
61
+ def test_is_config_file_is_identical_function_object_from_path_utils() -> None:
62
+ """After refactor, exempt_paths.is_config_file must be the same object as path_utils."""
63
+ assert is_config_file is path_utils_is_config_file, (
64
+ "exempt_paths.is_config_file must be imported from code_rules_path_utils, not re-defined"
65
+ )
@@ -19,7 +19,7 @@ from .git_checks import (
19
19
  class TestSingleCommitWhenPrExists:
20
20
  """Test that PR branches have exactly 1 commit ahead of base."""
21
21
 
22
- @patch("git_checks.subprocess.run")
22
+ @patch("validators.git_checks.subprocess.run")
23
23
  def test_no_pr_returns_empty(self, mock_run: MagicMock) -> None:
24
24
  """When no PR exists, check should return empty list."""
25
25
  mock_run.side_effect = [
@@ -31,7 +31,7 @@ class TestSingleCommitWhenPrExists:
31
31
 
32
32
  assert violations == []
33
33
 
34
- @patch("git_checks.subprocess.run")
34
+ @patch("validators.git_checks.subprocess.run")
35
35
  def test_single_commit_ahead_passes(self, mock_run: MagicMock) -> None:
36
36
  """Exactly 1 commit ahead should pass."""
37
37
  mock_run.side_effect = [
@@ -44,7 +44,7 @@ class TestSingleCommitWhenPrExists:
44
44
 
45
45
  assert violations == []
46
46
 
47
- @patch("git_checks.subprocess.run")
47
+ @patch("validators.git_checks.subprocess.run")
48
48
  def test_zero_commits_ahead_fails(self, mock_run: MagicMock) -> None:
49
49
  """Zero commits ahead should fail."""
50
50
  mock_run.side_effect = [
@@ -61,7 +61,7 @@ class TestSingleCommitWhenPrExists:
61
61
  assert "exactly 1 commit" in violations[0].message
62
62
  assert "0 commits" in violations[0].message
63
63
 
64
- @patch("git_checks.subprocess.run")
64
+ @patch("validators.git_checks.subprocess.run")
65
65
  def test_multiple_commits_ahead_fails(self, mock_run: MagicMock) -> None:
66
66
  """More than 1 commit ahead should fail."""
67
67
  mock_run.side_effect = [
@@ -76,7 +76,7 @@ class TestSingleCommitWhenPrExists:
76
76
  assert "exactly 1 commit" in violations[0].message
77
77
  assert "3 commits" in violations[0].message
78
78
 
79
- @patch("git_checks.subprocess.run")
79
+ @patch("validators.git_checks.subprocess.run")
80
80
  def test_gh_cli_not_available_returns_empty(self, mock_run: MagicMock) -> None:
81
81
  """When gh CLI not available, should return empty (warning, not failure)."""
82
82
  mock_run.side_effect = [
@@ -88,7 +88,7 @@ class TestSingleCommitWhenPrExists:
88
88
 
89
89
  assert violations == []
90
90
 
91
- @patch("git_checks.subprocess.run")
91
+ @patch("validators.git_checks.subprocess.run")
92
92
  def test_git_not_available_returns_empty(self, mock_run: MagicMock) -> None:
93
93
  """When git not available, should return empty."""
94
94
  mock_run.side_effect = [
@@ -101,7 +101,7 @@ class TestSingleCommitWhenPrExists:
101
101
 
102
102
  assert violations == []
103
103
 
104
- @patch("git_checks.subprocess.run")
104
+ @patch("validators.git_checks.subprocess.run")
105
105
  def test_extracts_base_branch_from_pr_info(self, mock_run: MagicMock) -> None:
106
106
  """Should extract base branch name from gh pr list JSON output, falling back to main when absent."""
107
107
  mock_run.side_effect = [
@@ -140,7 +140,7 @@ class TestSingleCommitWhenPrExists:
140
140
  timeout=30,
141
141
  )
142
142
 
143
- @patch("git_checks.subprocess.run")
143
+ @patch("validators.git_checks.subprocess.run")
144
144
  def test_non_numeric_commit_count_returns_empty(self, mock_run: MagicMock) -> None:
145
145
  """When git rev-list returns non-numeric output, should return empty."""
146
146
  mock_run.side_effect = [
@@ -153,7 +153,7 @@ class TestSingleCommitWhenPrExists:
153
153
 
154
154
  assert violations == []
155
155
 
156
- @patch("git_checks.subprocess.run")
156
+ @patch("validators.git_checks.subprocess.run")
157
157
  def test_gh_timeout_returns_empty(self, mock_run: MagicMock) -> None:
158
158
  """When gh CLI times out, should return empty (warning, not failure)."""
159
159
  mock_run.side_effect = [
@@ -165,7 +165,7 @@ class TestSingleCommitWhenPrExists:
165
165
 
166
166
  assert violations == []
167
167
 
168
- @patch("git_checks.subprocess.run")
168
+ @patch("validators.git_checks.subprocess.run")
169
169
  def test_git_timeout_returns_empty(self, mock_run: MagicMock) -> None:
170
170
  """When git times out, should return empty (warning, not failure)."""
171
171
  mock_run.side_effect = [
@@ -178,7 +178,7 @@ class TestSingleCommitWhenPrExists:
178
178
 
179
179
  assert violations == []
180
180
 
181
- @patch("git_checks.subprocess.run")
181
+ @patch("validators.git_checks.subprocess.run")
182
182
  def test_passes_resolved_branch_name_to_gh(self, mock_run: MagicMock) -> None:
183
183
  """gh pr list must receive the resolved branch name, never the literal 'HEAD'."""
184
184
  mock_run.side_effect = [
@@ -197,7 +197,7 @@ class TestSingleCommitWhenPrExists:
197
197
  timeout=30,
198
198
  )
199
199
 
200
- @patch("git_checks.subprocess.run")
200
+ @patch("validators.git_checks.subprocess.run")
201
201
  def test_unresolved_branch_returns_empty(self, mock_run: MagicMock) -> None:
202
202
  """When current branch cannot be resolved, should return empty."""
203
203
  mock_run.side_effect = [
@@ -213,7 +213,7 @@ class TestSingleCommitWhenPrExists:
213
213
  class TestDraftPrState:
214
214
  """Test that PR is in draft state when pushing review fixes."""
215
215
 
216
- @patch("git_checks.subprocess.run")
216
+ @patch("validators.git_checks.subprocess.run")
217
217
  def test_no_pr_returns_empty(self, mock_run: MagicMock) -> None:
218
218
  """When no PR exists, check should return empty list."""
219
219
  mock_run.return_value = MagicMock(returncode=0, stdout="[]", stderr="")
@@ -222,7 +222,7 @@ class TestDraftPrState:
222
222
 
223
223
  assert violations == []
224
224
 
225
- @patch("git_checks.subprocess.run")
225
+ @patch("validators.git_checks.subprocess.run")
226
226
  def test_draft_pr_passes(self, mock_run: MagicMock) -> None:
227
227
  """Draft PR should pass."""
228
228
  mock_run.return_value = MagicMock(
@@ -235,7 +235,7 @@ class TestDraftPrState:
235
235
 
236
236
  assert violations == []
237
237
 
238
- @patch("git_checks.subprocess.run")
238
+ @patch("validators.git_checks.subprocess.run")
239
239
  def test_non_draft_pr_fails(self, mock_run: MagicMock) -> None:
240
240
  """Non-draft PR should fail."""
241
241
  mock_run.return_value = MagicMock(
@@ -252,7 +252,7 @@ class TestDraftPrState:
252
252
  assert "draft" in violations[0].message.lower()
253
253
  assert "gh pr ready --undo" in violations[0].message
254
254
 
255
- @patch("git_checks.subprocess.run")
255
+ @patch("validators.git_checks.subprocess.run")
256
256
  def test_gh_cli_not_available_returns_empty(self, mock_run: MagicMock) -> None:
257
257
  """When gh CLI not available, should return empty (warning, not failure)."""
258
258
  mock_run.side_effect = FileNotFoundError("gh not found")
@@ -261,7 +261,7 @@ class TestDraftPrState:
261
261
 
262
262
  assert violations == []
263
263
 
264
- @patch("git_checks.subprocess.run")
264
+ @patch("validators.git_checks.subprocess.run")
265
265
  def test_gh_timeout_returns_empty(self, mock_run: MagicMock) -> None:
266
266
  """When gh CLI times out, should return empty (warning, not failure)."""
267
267
  mock_run.side_effect = subprocess.TimeoutExpired(cmd=["gh", "pr", "list"], timeout=30)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1 @@
1
+ """Scripts configuration package — home for scalar constants per CODE_RULES."""