claude-dev-env 1.25.1 → 1.26.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 (105) hide show
  1. package/CLAUDE.md +6 -0
  2. package/agents/clean-coder.md +1 -1
  3. package/docs/CODE_RULES.md +3 -1
  4. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +54 -0
  5. package/hooks/blocking/{code-rules-enforcer.py → code_rules_enforcer.py} +150 -5
  6. package/hooks/blocking/{destructive-command-blocker.py → destructive_command_blocker.py} +12 -4
  7. package/hooks/blocking/{tdd-enforcer.py → tdd_enforcer.py} +12 -0
  8. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +2 -2
  9. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +2 -2
  10. package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +1 -1
  11. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +2 -2
  12. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +181 -0
  13. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +4 -4
  14. package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +1 -1
  15. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  16. package/hooks/blocking/test_code_rules_enforcer_magic_string_masking.py +104 -0
  17. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +2 -2
  18. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +2 -2
  19. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +1 -1
  20. package/hooks/blocking/test_destructive_command_blocker.py +63 -4
  21. package/hooks/blocking/test_gh_body_arg_blocker.py +1 -1
  22. package/hooks/blocking/test_pr_description_enforcer.py +8 -8
  23. package/hooks/blocking/test_tdd_enforcer.py +53 -1
  24. package/hooks/github-action/pre-push-review.yml +27 -0
  25. package/hooks/hooks.json +28 -28
  26. package/hooks/lifecycle/{config-change-guard.py → config_change_guard.py} +27 -12
  27. package/hooks/lifecycle/test_config_change_guard.py +3 -3
  28. package/hooks/notification/{attention-needed-notify.py → attention_needed_notify.py} +7 -0
  29. package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py} +8 -0
  30. package/hooks/notification/notification_utils.py +60 -2
  31. package/hooks/notification/subagent_complete_notify.py +381 -0
  32. package/hooks/notification/test_attention_needed_notify.py +47 -0
  33. package/hooks/notification/test_claude_notification_handler.py +54 -0
  34. package/hooks/notification/test_notification_utils.py +91 -0
  35. package/hooks/notification/test_subagent_complete_notify.py +72 -0
  36. package/hooks/validators/README.md +5 -1
  37. package/hooks/validators/abbreviation_checks.py +1 -1
  38. package/hooks/validators/code_quality_checks.py +1 -1
  39. package/hooks/validators/config.py +5 -0
  40. package/hooks/validators/conftest.py +10 -0
  41. package/hooks/validators/exempt_paths.py +1 -1
  42. package/hooks/validators/git_checks.py +80 -0
  43. package/hooks/validators/magic_value_checks.py +2 -2
  44. package/hooks/validators/pr_reference_checks.py +1 -1
  45. package/hooks/validators/python_antipattern_checks.py +1 -1
  46. package/hooks/validators/run_all_validators.py +53 -105
  47. package/hooks/validators/security_checks.py +1 -1
  48. package/hooks/validators/test_abbreviation_checks.py +2 -2
  49. package/hooks/validators/test_code_quality_checks.py +2 -2
  50. package/hooks/validators/test_file_structure_checks.py +1 -1
  51. package/hooks/validators/test_git_checks.py +79 -13
  52. package/hooks/validators/test_health_check.py +1 -1
  53. package/hooks/validators/test_magic_value_checks.py +2 -2
  54. package/hooks/validators/test_mypy_integration.py +1 -1
  55. package/hooks/validators/test_output_formatter.py +3 -1
  56. package/hooks/validators/test_pr_reference_checks.py +2 -2
  57. package/hooks/validators/test_python_antipattern_checks.py +2 -2
  58. package/hooks/validators/test_python_style_checks.py +2 -4
  59. package/hooks/validators/test_react_checks.py +1 -1
  60. package/hooks/validators/test_ruff_integration.py +1 -1
  61. package/hooks/validators/test_run_all_validators.py +75 -43
  62. package/hooks/validators/test_run_all_validators_integration.py +14 -37
  63. package/hooks/validators/test_security_checks.py +2 -2
  64. package/hooks/validators/test_test_safety_checks.py +1 -1
  65. package/hooks/validators/test_todo_checks.py +2 -2
  66. package/hooks/validators/test_type_safety_checks.py +2 -2
  67. package/hooks/validators/test_useless_test_checks.py +2 -2
  68. package/hooks/validators/test_validator_base.py +1 -1
  69. package/hooks/validators/test_verify_paths.py +2 -4
  70. package/hooks/validators/todo_checks.py +1 -1
  71. package/hooks/validators/type_safety_checks.py +1 -1
  72. package/hooks/validators/useless_test_checks.py +1 -1
  73. package/package.json +1 -1
  74. package/rules/file-global-constants.md +71 -0
  75. package/rules/gh-body-file.md +1 -1
  76. package/rules/prompt-workflow-context-controls.md +48 -0
  77. package/scripts/sync_to_cursor/rules.py +2 -2
  78. package/scripts/tests/test_sync_to_cursor.py +2 -2
  79. package/skills/bugteam/CONSTRAINTS.md +37 -0
  80. package/skills/bugteam/EXAMPLES.md +64 -0
  81. package/skills/bugteam/PROMPTS.md +175 -0
  82. package/skills/bugteam/SKILL.md +204 -295
  83. package/skills/bugteam/SKILL_EVALS.md +346 -0
  84. package/skills/bugteam/scripts/README.md +37 -0
  85. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +334 -0
  86. package/skills/bugteam/scripts/bugteam_preflight.py +135 -0
  87. package/skills/rule-audit/SKILL.md +4 -4
  88. /package/hooks/advisory/{migration-safety-advisor.py → migration_safety_advisor.py} +0 -0
  89. /package/hooks/advisory/{refactor-guard.py → refactor_guard.py} +0 -0
  90. /package/hooks/blocking/{block-main-commit.py → block_main_commit.py} +0 -0
  91. /package/hooks/blocking/{content-search-to-zoekt-redirector.py → content_search_to_zoekt_redirector.py} +0 -0
  92. /package/hooks/blocking/{gh-body-arg-blocker.py → gh_body_arg_blocker.py} +0 -0
  93. /package/hooks/blocking/{hedging-language-blocker.py → hedging_language_blocker.py} +0 -0
  94. /package/hooks/blocking/{pr-description-enforcer.py → pr_description_enforcer.py} +0 -0
  95. /package/hooks/blocking/{sensitive-file-protector.py → sensitive_file_protector.py} +0 -0
  96. /package/hooks/blocking/{test-preflight-check.py → test_preflight_check.py} +0 -0
  97. /package/hooks/blocking/{write-existing-file-blocker.py → write_existing_file_blocker.py} +0 -0
  98. /package/hooks/git-hooks/{post-commit.py → post_commit.py} +0 -0
  99. /package/hooks/lifecycle/{session-end-cleanup.py → session_end_cleanup.py} +0 -0
  100. /package/hooks/{rewrite-plugin-paths.py → rewrite_plugin_paths.py} +0 -0
  101. /package/hooks/session/{plugin-data-dir-cleanup.py → plugin_data_dir_cleanup.py} +0 -0
  102. /package/hooks/validation/{hook-format-validator.py → hook_format_validator.py} +0 -0
  103. /package/hooks/workflow/{auto-formatter.py → auto_formatter.py} +0 -0
  104. /package/hooks/workflow/{investigation-tracker-reset.py → investigation_tracker_reset.py} +0 -0
  105. /package/scripts/{sync-to-cursor.py → sync_to_cursor.py} +0 -0
@@ -0,0 +1,54 @@
1
+ """Unit tests for claude-notification-handler Discord wiring."""
2
+
3
+ import importlib.util
4
+ import pathlib
5
+ import types
6
+ from unittest.mock import patch
7
+
8
+ HOOK_DIRECTORY = pathlib.Path(__file__).parent
9
+ MODULE_PATH = HOOK_DIRECTORY / "claude_notification_handler.py"
10
+
11
+ FIXTURE_ATTENTION_SECRET_ID = "fixture-attention-id-0001"
12
+ FIXTURE_PROJECT_NAME = "fixture-project"
13
+ FIXTURE_MESSAGE = "attention required"
14
+ FIXTURE_PRIORITY = "default"
15
+ NON_WINDOWS_NON_WSL_PLATFORM = "Darwin"
16
+
17
+
18
+ def load_handler_with_environment(
19
+ environment_overrides: dict[str, str],
20
+ ) -> types.ModuleType:
21
+ module_specification = importlib.util.spec_from_file_location(
22
+ "claude_notification_handler_under_test",
23
+ MODULE_PATH,
24
+ )
25
+ assert module_specification is not None
26
+ assert module_specification.loader is not None
27
+ module_under_test = importlib.util.module_from_spec(module_specification)
28
+ with patch.dict("os.environ", environment_overrides, clear=False):
29
+ module_specification.loader.exec_module(module_under_test)
30
+ return module_under_test
31
+
32
+
33
+ def test_send_desktop_and_push_notification_forwards_attention_secret_id_to_notify_discord() -> (
34
+ None
35
+ ):
36
+ module_under_test = load_handler_with_environment(
37
+ {"BWS_DISCORD_ATTENTION_SECRET_ID": FIXTURE_ATTENTION_SECRET_ID}
38
+ )
39
+ with (
40
+ patch.object(module_under_test, "notify_ntfy"),
41
+ patch.object(module_under_test, "notify_discord") as discord_spy,
42
+ patch.object(module_under_test, "platform") as platform_stub,
43
+ ):
44
+ platform_stub.system.return_value = NON_WINDOWS_NON_WSL_PLATFORM
45
+ module_under_test.send_desktop_and_push_notification(
46
+ project_name=FIXTURE_PROJECT_NAME,
47
+ notification_message=FIXTURE_MESSAGE,
48
+ ntfy_priority=FIXTURE_PRIORITY,
49
+ )
50
+ assert discord_spy.call_count == 1
51
+ call_kwargs = discord_spy.call_args.kwargs
52
+ assert call_kwargs["webhook_secret_id"] == FIXTURE_ATTENTION_SECRET_ID
53
+ assert call_kwargs["title"] == FIXTURE_PROJECT_NAME
54
+ assert call_kwargs["message"] == FIXTURE_MESSAGE
@@ -0,0 +1,91 @@
1
+ """Unit tests for notification_utils ntfy guard behavior."""
2
+
3
+ import importlib.util
4
+ import json
5
+ import pathlib
6
+ import subprocess
7
+ import types
8
+ from unittest.mock import patch
9
+
10
+ HOOK_DIRECTORY = pathlib.Path(__file__).parent
11
+ MODULE_PATH = HOOK_DIRECTORY / "notification_utils.py"
12
+
13
+ FIXTURE_DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/111/aaa-fixture"
14
+ FIXTURE_SECRET_ID = "fixture-secret-id-0000"
15
+
16
+
17
+ def load_notification_utils_with_environment(
18
+ environment_overrides: dict[str, str],
19
+ ) -> types.ModuleType:
20
+ module_specification = importlib.util.spec_from_file_location(
21
+ "notification_utils_under_test",
22
+ MODULE_PATH,
23
+ )
24
+ assert module_specification is not None
25
+ assert module_specification.loader is not None
26
+ module_under_test = importlib.util.module_from_spec(module_specification)
27
+ with patch.dict("os.environ", environment_overrides, clear=False):
28
+ module_specification.loader.exec_module(module_under_test)
29
+ return module_under_test
30
+
31
+
32
+ def test_should_skip_curl_when_topic_environment_variable_is_unset() -> None:
33
+ environment_with_topic_removed = {"NTFY_TOPIC": ""}
34
+ module_under_test = load_notification_utils_with_environment(
35
+ environment_with_topic_removed
36
+ )
37
+ with patch("subprocess.Popen") as popen_spy:
38
+ module_under_test.notify_ntfy(title="Test", message="payload")
39
+ assert popen_spy.call_count == 0
40
+
41
+
42
+ def test_should_invoke_curl_when_topic_environment_variable_is_set() -> None:
43
+ environment_with_topic_set = {"NTFY_TOPIC": "private-topic-for-test"}
44
+ module_under_test = load_notification_utils_with_environment(
45
+ environment_with_topic_set
46
+ )
47
+ with patch("subprocess.Popen") as popen_spy:
48
+ module_under_test.notify_ntfy(title="Test", message="payload")
49
+ assert popen_spy.call_count == 1
50
+ curl_arguments = popen_spy.call_args.args[0]
51
+ assert "https://ntfy.sh/private-topic-for-test" in curl_arguments
52
+
53
+
54
+ def test_fetch_bws_secret_returns_none_when_secret_id_is_empty() -> None:
55
+ module_under_test = load_notification_utils_with_environment({})
56
+ with patch("subprocess.run") as run_spy:
57
+ fetched_value = module_under_test.fetch_bws_secret("")
58
+ assert fetched_value is None
59
+ assert run_spy.call_count == 0
60
+
61
+
62
+ def test_notify_discord_skips_curl_when_secret_id_is_empty() -> None:
63
+ module_under_test = load_notification_utils_with_environment({})
64
+ with patch("subprocess.Popen") as popen_spy:
65
+ module_under_test.notify_discord(
66
+ title="Test",
67
+ message="payload",
68
+ webhook_secret_id="",
69
+ )
70
+ assert popen_spy.call_count == 0
71
+
72
+
73
+ def test_notify_discord_invokes_curl_when_bws_returns_url() -> None:
74
+ module_under_test = load_notification_utils_with_environment({})
75
+ bws_completed = subprocess.CompletedProcess(
76
+ args=[],
77
+ returncode=0,
78
+ stdout=json.dumps({"value": FIXTURE_DISCORD_WEBHOOK_URL}),
79
+ stderr="",
80
+ )
81
+ with patch("subprocess.run", return_value=bws_completed), patch(
82
+ "subprocess.Popen"
83
+ ) as popen_spy:
84
+ module_under_test.notify_discord(
85
+ title="Test",
86
+ message="payload",
87
+ webhook_secret_id=FIXTURE_SECRET_ID,
88
+ )
89
+ assert popen_spy.call_count == 1
90
+ popen_arguments = popen_spy.call_args.args[0]
91
+ assert FIXTURE_DISCORD_WEBHOOK_URL in popen_arguments
@@ -0,0 +1,72 @@
1
+ """Unit tests for subagent-complete-notify Discord wiring."""
2
+
3
+ import importlib.util
4
+ import pathlib
5
+ import types
6
+ from unittest.mock import patch
7
+
8
+ HOOK_DIRECTORY = pathlib.Path(__file__).parent
9
+ MODULE_PATH = HOOK_DIRECTORY / "subagent_complete_notify.py"
10
+
11
+ FIXTURE_ACTIVITY_SECRET_ID = "fixture-activity-id-0003"
12
+ FIXTURE_TASK_DESCRIPTION = "subagent finished research task"
13
+ FIXTURE_PROJECT_NAME = "fixture-project"
14
+ NON_WINDOWS_NON_WSL_PLATFORM = "Darwin"
15
+
16
+
17
+ def load_hook_with_environment(
18
+ environment_overrides: dict[str, str],
19
+ ) -> types.ModuleType:
20
+ module_specification = importlib.util.spec_from_file_location(
21
+ "subagent_complete_notify_under_test",
22
+ MODULE_PATH,
23
+ )
24
+ assert module_specification is not None
25
+ assert module_specification.loader is not None
26
+ module_under_test = importlib.util.module_from_spec(module_specification)
27
+ with patch.dict("os.environ", environment_overrides, clear=False):
28
+ module_specification.loader.exec_module(module_under_test)
29
+ return module_under_test
30
+
31
+
32
+ def test_main_forwards_activity_secret_id_to_notify_discord() -> None:
33
+ module_under_test = load_hook_with_environment(
34
+ {"BWS_DISCORD_ACTIVITY_SECRET_ID": FIXTURE_ACTIVITY_SECRET_ID}
35
+ )
36
+ with (
37
+ patch.object(
38
+ module_under_test,
39
+ "get_task_info_from_stdin",
40
+ return_value=FIXTURE_TASK_DESCRIPTION,
41
+ ),
42
+ patch.object(
43
+ module_under_test, "get_project_name", return_value=FIXTURE_PROJECT_NAME
44
+ ),
45
+ patch.object(module_under_test, "notify_ntfy"),
46
+ patch.object(module_under_test, "notify_discord") as discord_spy,
47
+ patch.object(module_under_test, "is_wsl", return_value=False),
48
+ patch.object(module_under_test, "platform") as platform_stub,
49
+ ):
50
+ platform_stub.system.return_value = NON_WINDOWS_NON_WSL_PLATFORM
51
+ module_under_test.main()
52
+ assert discord_spy.call_count == 1
53
+ call_kwargs = discord_spy.call_args.kwargs
54
+ assert call_kwargs["webhook_secret_id"] == FIXTURE_ACTIVITY_SECRET_ID
55
+ assert call_kwargs["title"] == FIXTURE_PROJECT_NAME
56
+ assert call_kwargs["message"] == FIXTURE_TASK_DESCRIPTION
57
+
58
+
59
+ def test_main_skips_notify_discord_when_task_description_is_empty() -> None:
60
+ module_under_test = load_hook_with_environment(
61
+ {"BWS_DISCORD_ACTIVITY_SECRET_ID": FIXTURE_ACTIVITY_SECRET_ID}
62
+ )
63
+ with (
64
+ patch.object(module_under_test, "get_task_info_from_stdin", return_value=""),
65
+ patch.object(
66
+ module_under_test, "get_project_name", return_value=FIXTURE_PROJECT_NAME
67
+ ),
68
+ patch.object(module_under_test, "notify_ntfy"),
69
+ patch.object(module_under_test, "notify_discord") as discord_spy,
70
+ ):
71
+ module_under_test.main()
72
+ assert discord_spy.call_count == 0
@@ -119,7 +119,11 @@ repos:
119
119
  hooks:
120
120
  - id: python-style-checks
121
121
  name: Python Style Checks
122
- entry: python hooks/validators/python_style_checks.py
122
+ entry: python packages/claude-dev-env/hooks/validators/python_style_checks.py
123
+ args: []
124
+ pass_filenames: true
125
+ # Invokes the script directly via its ``__main__`` block so the
126
+ # ``validators`` package qualifier does not need PYTHONPATH setup.
123
127
  language: system
124
128
  types: [python]
125
129
  ```
@@ -13,7 +13,7 @@ import sys
13
13
  from pathlib import Path
14
14
  from typing import List, Set
15
15
 
16
- from validator_base import Violation
16
+ from .validator_base import Violation
17
17
 
18
18
 
19
19
  ALLOWED_SINGLE_LETTERS: Set[str] = frozenset({"i", "j", "k", "_"})
@@ -11,7 +11,7 @@ import sys
11
11
  from pathlib import Path
12
12
  from typing import List
13
13
 
14
- from validator_base import Violation
14
+ from .validator_base import Violation
15
15
 
16
16
 
17
17
  MAX_FUNCTION_LINES = 30
@@ -0,0 +1,5 @@
1
+ """Shared constants for the validators package."""
2
+
3
+ # pragma: no-tdd-gate
4
+
5
+ DEFAULT_BASE_BRANCH_WHEN_UNKNOWN = "main"
@@ -0,0 +1,10 @@
1
+ """Pytest fixture module ensuring validators directory is importable regardless of invocation cwd."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ VALIDATORS_DIRECTORY = Path(__file__).resolve().parent
8
+
9
+ if str(VALIDATORS_DIRECTORY) not in sys.path:
10
+ sys.path.insert(0, str(VALIDATORS_DIRECTORY))
@@ -2,7 +2,7 @@
2
2
 
3
3
  Single source of truth for CONFIG / TEST / HOOK-INFRASTRUCTURE /
4
4
  WORKFLOW-REGISTRY / MIGRATION path pattern sets. Both Pre-Write
5
- (``code-rules-enforcer.py``) and pre-push (``magic_value_checks.py``)
5
+ (``code_rules_enforcer.py``) and pre-push (``magic_value_checks.py``)
6
6
  scanners must short-circuit on the same file categories; drift between
7
7
  the two produced the "inconsistent verdicts" bug this module prevents.
8
8
 
@@ -6,6 +6,8 @@ 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
10
+
9
11
 
10
12
  SUBPROCESS_TIMEOUT_SECONDS = 30
11
13
 
@@ -33,6 +35,83 @@ def get_current_branch() -> str:
33
35
  return ""
34
36
 
35
37
 
38
+ def check_single_commit_when_pr_exists() -> List[Violation]:
39
+ """
40
+ Check that a PR branch has exactly 1 commit ahead of its base.
41
+
42
+ Returns empty list if:
43
+ - No PR exists for current branch
44
+ - gh CLI or git is not available
45
+ - gh/git times out
46
+ - Branch is exactly 1 commit ahead of base
47
+ - git rev-list output is non-numeric
48
+
49
+ Returns violation if:
50
+ - Branch is 0 commits ahead of base
51
+ - Branch is more than 1 commit ahead of base
52
+ """
53
+ branch = get_current_branch()
54
+ if not branch:
55
+ return []
56
+
57
+ try:
58
+ pr_info = subprocess.run(
59
+ ["gh", "pr", "list", "--head", branch, "--json", "baseRefName,number"],
60
+ capture_output=True,
61
+ text=True,
62
+ check=True,
63
+ timeout=SUBPROCESS_TIMEOUT_SECONDS,
64
+ )
65
+ except FileNotFoundError:
66
+ return []
67
+ except subprocess.CalledProcessError:
68
+ return []
69
+ except subprocess.TimeoutExpired:
70
+ return []
71
+
72
+ try:
73
+ pr_data = json.loads(pr_info.stdout)
74
+ except json.JSONDecodeError:
75
+ return []
76
+
77
+ if not pr_data:
78
+ return []
79
+
80
+ base_branch = pr_data[0].get("baseRefName", DEFAULT_BASE_BRANCH_WHEN_UNKNOWN)
81
+
82
+ try:
83
+ rev_list = subprocess.run(
84
+ ["git", "rev-list", "--count", f"{base_branch}..HEAD"],
85
+ capture_output=True,
86
+ text=True,
87
+ check=True,
88
+ timeout=SUBPROCESS_TIMEOUT_SECONDS,
89
+ )
90
+ except FileNotFoundError:
91
+ return []
92
+ except subprocess.CalledProcessError:
93
+ return []
94
+ except subprocess.TimeoutExpired:
95
+ return []
96
+
97
+ commit_count_text = rev_list.stdout.strip()
98
+ try:
99
+ commit_count = int(commit_count_text)
100
+ except ValueError:
101
+ return []
102
+
103
+ if commit_count == 1:
104
+ return []
105
+
106
+ return [
107
+ Violation(
108
+ file="",
109
+ line=0,
110
+ message=f"Branch must have exactly 1 commit ahead of {base_branch}, found {commit_count} commits",
111
+ )
112
+ ]
113
+
114
+
36
115
  def check_draft_pr_state() -> List[Violation]:
37
116
  """
38
117
  Check that PR is in draft state.
@@ -93,6 +172,7 @@ def main() -> None:
93
172
  """Run all git checks and exit with appropriate code."""
94
173
  violations: List[Violation] = []
95
174
 
175
+ violations.extend(check_single_commit_when_pr_exists())
96
176
  violations.extend(check_draft_pr_state())
97
177
 
98
178
  if violations:
@@ -12,11 +12,11 @@ import sys
12
12
  from pathlib import Path
13
13
  from typing import Dict, FrozenSet, List, Set, Tuple, Type
14
14
 
15
- from exempt_paths import (
15
+ from .exempt_paths import (
16
16
  is_config_file,
17
17
  is_test_file,
18
18
  )
19
- from validator_base import Violation
19
+ from .validator_base import Violation
20
20
 
21
21
 
22
22
  ALLOWED_NUMBERS: FrozenSet[int] = frozenset({-1, 0, 1})
@@ -9,7 +9,7 @@ import sys
9
9
  from pathlib import Path
10
10
  from typing import List
11
11
 
12
- from validator_base import Violation
12
+ from .validator_base import Violation
13
13
 
14
14
 
15
15
  PR_PATTERN = re.compile(r"#.*\bPR\s*#?\d+", re.IGNORECASE)
@@ -11,7 +11,7 @@ import sys
11
11
  from pathlib import Path
12
12
  from typing import List
13
13
 
14
- from validator_base import Violation
14
+ from .validator_base import Violation
15
15
 
16
16
 
17
17
  def check_mutable_default_args(tree: ast.AST, filename: str) -> List[Violation]: