claude-dev-env 8.6.1 → 8.8.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 (119) hide show
  1. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  2. package/.agents/skills/team-advisor/SKILL.md +2 -2
  3. package/_shared/advisor/reference/identity.md +1 -1
  4. package/_shared/advisor/reference/spawn-walk-log.md +1 -1
  5. package/_shared/advisor/reference/warm-up.md +3 -1
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +16 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +23 -5
  8. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +85 -6
  9. package/_shared/pr-loop/scripts/terminology_sweep.py +10 -9
  10. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -0
  11. package/bin/install.test.mjs +25 -0
  12. package/docs/high-trust-agent-delivery.md +132 -0
  13. package/docs/references/advisor-tool.md +3 -2
  14. package/docs/second-claude-account.md +50 -0
  15. package/hooks/advisory/refactor_guard.py +2 -2
  16. package/hooks/advisory/test_refactor_guard.py +21 -0
  17. package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
  18. package/hooks/blocking/code_rules_shared.py +4 -7
  19. package/hooks/blocking/conftest.py +22 -0
  20. package/hooks/blocking/state_description_blocker.py +10 -8
  21. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
  22. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +4 -29
  23. package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
  24. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
  25. package/hooks/blocking/test_code_rules_shared.py +18 -0
  26. package/hooks/blocking/test_conftest.py +35 -0
  27. package/hooks/blocking/test_pii_scanner.py +8 -0
  28. package/hooks/blocking/test_state_description_blocker.py +16 -0
  29. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
  30. package/hooks/git-hooks/pull_request_handoff.py +2 -2
  31. package/hooks/git-hooks/test_config.py +10 -3
  32. package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
  33. package/hooks/git-hooks/test_verification_notice_state.py +50 -0
  34. package/hooks/git-hooks/verification_notice_state.py +3 -1
  35. package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
  36. package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
  37. package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
  38. package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
  39. package/hooks/hooks_constants/state_description_blocker_constants.py +0 -3
  40. package/hooks/hooks_constants/test_state_description_blocker_constants.py +9 -7
  41. package/hooks/validators/exempt_paths.py +3 -18
  42. package/hooks/validators/test_exempt_paths.py +20 -0
  43. package/hooks/validators/test_python_antipattern_checks.py +14 -0
  44. package/package.json +1 -1
  45. package/rules/agent-merges-its-own-green-pull-request.md +54 -0
  46. package/rules/archiving-agent-config.md +10 -0
  47. package/rules/code-standards.md +1 -0
  48. package/rules/correction-lens.md +134 -0
  49. package/rules/flag-non-breaking-findings.md +11 -0
  50. package/rules/long-horizon-autonomy.md +11 -0
  51. package/rules/no-contrast-framing.md +68 -0
  52. package/rules/review-closure-is-a-check.md +44 -0
  53. package/scripts/Invoke-PesterSuites.ps1 +42 -0
  54. package/scripts/agent_merge_check.py +381 -0
  55. package/scripts/check.ps1 +12 -1
  56. package/scripts/ci/install-playtest.mjs +291 -0
  57. package/scripts/ci/install-playtest.test.mjs +84 -0
  58. package/scripts/ci/installer-lifecycle.mjs +11 -30
  59. package/scripts/ci/installer-lifecycle.test.mjs +14 -0
  60. package/scripts/ci/pstack-command-shim.test.mjs +33 -0
  61. package/scripts/ci/scratch-home-install.mjs +73 -0
  62. package/scripts/ci/scratch-home-install.test.mjs +49 -0
  63. package/scripts/claude_account_choice.py +246 -0
  64. package/scripts/claude_account_profile.py +371 -0
  65. package/scripts/claude_chain_usage.py +49 -5
  66. package/scripts/codex_compat_materializer.py +33 -16
  67. package/scripts/contrast_framing.py +101 -0
  68. package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
  69. package/scripts/dev_env_scripts_constants/claude_account_constants.py +166 -0
  70. package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
  71. package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
  72. package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
  73. package/scripts/dev_env_scripts_constants/test_claude_account_constants.py +21 -0
  74. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
  75. package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
  76. package/scripts/durable_post_lint.py +30 -4
  77. package/scripts/followup_cli.py +6 -10
  78. package/scripts/merge_gate_checks.py +236 -0
  79. package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
  80. package/scripts/policy_lint/adapters.py +30 -0
  81. package/scripts/policy_lint/config/constants.py +2 -0
  82. package/scripts/policy_lint/registry.py +5 -0
  83. package/scripts/repository_checks/config/constants.py +12 -0
  84. package/scripts/repository_checks/config/policy_document.py +28 -2
  85. package/scripts/repository_checks/followups.py +54 -0
  86. package/scripts/repository_checks/match_exemptions.py +67 -0
  87. package/scripts/repository_checks/models.py +46 -2
  88. package/scripts/repository_checks/reporting.py +23 -7
  89. package/scripts/repository_checks/tracked_secrets.py +2 -2
  90. package/scripts/repository_policy.py +4 -2
  91. package/scripts/review_closure.py +134 -0
  92. package/scripts/review_closure_github.py +345 -0
  93. package/scripts/review_closure_model.py +360 -0
  94. package/scripts/test_agent_merge_check.py +317 -0
  95. package/scripts/test_claude_account_choice.py +217 -0
  96. package/scripts/test_claude_account_profile.py +220 -0
  97. package/scripts/test_claude_chain_usage.py +40 -0
  98. package/scripts/test_durable_post_lint.py +46 -0
  99. package/scripts/test_followup_cli.py +40 -0
  100. package/scripts/test_merge_gate_checks.py +92 -0
  101. package/scripts/tests/test_code_review_constants.py +78 -0
  102. package/scripts/tests/test_codex_compat_materializer.py +48 -4
  103. package/scripts/tests/test_contrast_framing.py +117 -0
  104. package/scripts/tests/test_followups.py +88 -0
  105. package/scripts/tests/test_match_exemptions.py +291 -0
  106. package/scripts/tests/test_models.py +67 -2
  107. package/scripts/tests/test_package_inventory.py +6 -4
  108. package/scripts/tests/test_pester_suites_run.py +53 -0
  109. package/scripts/tests/test_policy_document.py +39 -1
  110. package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
  111. package/scripts/tests/test_reporting.py +18 -0
  112. package/scripts/tests/test_repository_policy.py +43 -1
  113. package/scripts/tests/test_review_closure.py +126 -0
  114. package/scripts/tests/test_review_closure_github.py +240 -0
  115. package/scripts/tests/test_review_closure_model.py +299 -0
  116. package/scripts/tests/test_tracked_secrets.py +42 -0
  117. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
  118. package/scripts/repository_checks/email_exemptions.py +0 -39
  119. package/scripts/tests/test_email_exemptions.py +0 -158
@@ -13,6 +13,16 @@ from pathlib import Path
13
13
 
14
14
  _ENFORCER_SCRIPT_PATH = Path(__file__).with_name("code_rules_enforcer.py")
15
15
 
16
+ STRIP_CODE_AND_QUOTES_SOURCE = (
17
+ "import re\n"
18
+ "\n"
19
+ "def strip_code_and_quotes(text: str) -> str:\n"
20
+ " without_fences = re.sub(r'```.*?```', '', text, flags=re.DOTALL)\n"
21
+ " without_inline = re.sub(r'`[^`]*`', '', without_fences)\n"
22
+ " without_quotes = re.sub(r'(?m)^>.*$', '', without_inline)\n"
23
+ " return without_quotes.strip()\n"
24
+ )
25
+
16
26
 
17
27
  def repository_root() -> Path:
18
28
  """Return the root of the checkout these tests run inside.
@@ -48,7 +48,7 @@ def get_file_extension(file_path: str) -> str:
48
48
  def is_hook_infrastructure(file_path: str) -> bool:
49
49
  """Check if file is a Claude Code hook (standalone infrastructure, not project code)."""
50
50
  path_lower = "/" + file_path.lower().replace("\\", "/").lstrip("/")
51
- return any(pattern.replace("\\", "/") in path_lower for pattern in ALL_HOOK_INFRASTRUCTURE_PATTERNS)
51
+ return any(pattern in path_lower for pattern in ALL_HOOK_INFRASTRUCTURE_PATTERNS)
52
52
 
53
53
 
54
54
  def is_agent_home_tooling(file_path: str) -> bool:
@@ -66,10 +66,7 @@ def is_agent_home_tooling(file_path: str) -> bool:
66
66
  True when the path sits under a recognized agent home directory.
67
67
  """
68
68
  path_lower = "/" + file_path.lower().replace("\\", "/").lstrip("/")
69
- return any(
70
- each_pattern.replace("\\", "/") in path_lower
71
- for each_pattern in ALL_AGENT_HOME_TOOLING_PATTERNS
72
- )
69
+ return any(each_pattern in path_lower for each_pattern in ALL_AGENT_HOME_TOOLING_PATTERNS)
73
70
 
74
71
 
75
72
  def is_test_file(file_path: str) -> bool:
@@ -106,7 +103,7 @@ def is_workflow_registry_file(file_path: str) -> bool:
106
103
  These are module-level singletons, not misplaced literal constants.
107
104
  """
108
105
  path_lower = file_path.lower().replace("\\", "/")
109
- return any(pattern.replace("\\", "/") in path_lower for pattern in ALL_WORKFLOW_REGISTRY_PATTERNS)
106
+ return any(pattern in path_lower for pattern in ALL_WORKFLOW_REGISTRY_PATTERNS)
110
107
 
111
108
 
112
109
  def is_spec_file(file_path: str) -> bool:
@@ -523,7 +520,7 @@ def _is_dedicated_constants_module(file_path: str) -> bool:
523
520
  def is_migration_file(file_path: str) -> bool:
524
521
  """Check if file is a Django migration (must be self-contained)."""
525
522
  path_lower = file_path.lower().replace("\\", "/")
526
- return any(pattern.replace("\\", "/") in path_lower for pattern in ALL_MIGRATION_PATH_PATTERNS)
523
+ return any(pattern in path_lower for pattern in ALL_MIGRATION_PATH_PATTERNS)
527
524
 
528
525
 
529
526
  def docstring_line_numbers(content: str) -> set[int]:
@@ -1,8 +1,11 @@
1
1
  """Add hooks/blocking and hooks/ to sys.path for every test collected under this directory."""
2
2
 
3
3
  import json
4
+ import shutil
4
5
  import subprocess
5
6
  import sys
7
+ import tempfile
8
+ from collections.abc import Iterator
6
9
  from pathlib import Path
7
10
  from typing import Callable
8
11
 
@@ -14,6 +17,25 @@ for each_directory in (_BLOCKING_DIRECTORY, _HOOKS_DIRECTORY):
14
17
  if each_directory not in sys.path:
15
18
  sys.path.insert(0, each_directory)
16
19
 
20
+ _HOOK_INFRASTRUCTURE_TAIL = Path("packages") / "claude-dev-env" / "hooks" / "blocking"
21
+
22
+
23
+ @pytest.fixture
24
+ def hook_blocking_dir() -> Iterator[Path]:
25
+ """Build a temporary tree whose tail mirrors a production hook directory.
26
+
27
+ ``is_hook_infrastructure`` matches on the path tail, so a target inside
28
+ the yielded directory routes the way a
29
+ ``packages/claude-dev-env/hooks/blocking`` target does.
30
+ """
31
+ base_directory = Path(tempfile.mkdtemp())
32
+ blocking_directory = base_directory / _HOOK_INFRASTRUCTURE_TAIL
33
+ blocking_directory.mkdir(parents=True)
34
+ try:
35
+ yield blocking_directory
36
+ finally:
37
+ shutil.rmtree(base_directory, ignore_errors=False)
38
+
17
39
 
18
40
  @pytest.fixture
19
41
  def three_operation_patch() -> str:
@@ -22,6 +22,10 @@ try:
22
22
  sys.path.insert(0, _hooks_dir)
23
23
 
24
24
  from blocking.code_rules_shared import is_ephemeral_path
25
+ from hooks_constants.code_rules_enforcer_constants import (
26
+ JAVASCRIPT_BLOCK_COMMENT_CLOSER,
27
+ JAVASCRIPT_BLOCK_COMMENT_OPENER,
28
+ )
25
29
  from hooks_constants.hook_block_logger import log_hook_block
26
30
  from hooks_constants.multi_edit_reconstruction import joined_new_strings
27
31
  from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
@@ -34,8 +38,6 @@ try:
34
38
  ALL_HASH_AND_SLASH_EXTENSIONS,
35
39
  ALL_HASH_ONLY_EXTENSIONS,
36
40
  ALL_MARKDOWN_EXTENSIONS,
37
- BLOCK_COMMENT_CLOSE_MARKER,
38
- BLOCK_COMMENT_OPEN_MARKER,
39
41
  DOUBLE_QUOTE_BODY_GROUP,
40
42
  DOUBLE_QUOTED_SPAN_PATTERN,
41
43
  INLINE_CODE_PATTERN,
@@ -90,13 +92,13 @@ def _consume_block_comment_open(
90
92
  on this same line (``None`` when the whole line was consumed), and
91
93
  whether the comment is still open past this line.
92
94
  """
93
- slash_star_index = stripped.find(BLOCK_COMMENT_OPEN_MARKER)
95
+ slash_star_index = stripped.find(JAVASCRIPT_BLOCK_COMMENT_OPENER)
94
96
  close_star_index = stripped.find(
95
- BLOCK_COMMENT_CLOSE_MARKER, slash_star_index + len(BLOCK_COMMENT_OPEN_MARKER)
97
+ JAVASCRIPT_BLOCK_COMMENT_CLOSER, slash_star_index + len(JAVASCRIPT_BLOCK_COMMENT_OPENER)
96
98
  )
97
99
  if close_star_index < 0:
98
100
  return [(each_line_number, stripped[slash_star_index:])], None, True
99
- close_end = close_star_index + len(BLOCK_COMMENT_CLOSE_MARKER)
101
+ close_end = close_star_index + len(JAVASCRIPT_BLOCK_COMMENT_CLOSER)
100
102
  entry = (each_line_number, stripped[slash_star_index:close_end])
101
103
  after_close = stripped[close_end:].lstrip()
102
104
  return [entry], (after_close or None), False
@@ -110,10 +112,10 @@ def _consume_block_comment_continuation(
110
112
  Returns the extracted entries, whether the comment is still open past
111
113
  this line, and whether the caller should move to the next source line.
112
114
  """
113
- close_index = stripped.find(BLOCK_COMMENT_CLOSE_MARKER)
115
+ close_index = stripped.find(JAVASCRIPT_BLOCK_COMMENT_CLOSER)
114
116
  if close_index < 0:
115
117
  return [(each_line_number, stripped)], True, True
116
- close_end = close_index + len(BLOCK_COMMENT_CLOSE_MARKER)
118
+ close_end = close_index + len(JAVASCRIPT_BLOCK_COMMENT_CLOSER)
117
119
  return [(each_line_number, stripped[:close_end])], False, False
118
120
 
119
121
 
@@ -140,7 +142,7 @@ def _extract_comment_lines(text: str, extension: str = "") -> list[tuple[int, st
140
142
  ):
141
143
  all_comment_lines.append((each_line_number, stripped))
142
144
  continue
143
- if BLOCK_COMMENT_OPEN_MARKER in stripped and not is_in_block_comment:
145
+ if JAVASCRIPT_BLOCK_COMMENT_OPENER in stripped and not is_in_block_comment:
144
146
  entries, leftover, is_in_block_comment = _consume_block_comment_open(
145
147
  each_line_number, stripped
146
148
  )
@@ -24,6 +24,8 @@ from collections.abc import Iterator
24
24
 
25
25
  import pytest
26
26
 
27
+ from code_rules_enforcer_test_support import STRIP_CODE_AND_QUOTES_SOURCE as SHARED_HELPER_SOURCE
28
+
27
29
  _HOOK_DIRECTORY = pathlib.Path(__file__).parent
28
30
  if str(_HOOK_DIRECTORY) not in sys.path:
29
31
  sys.path.insert(0, str(_HOOK_DIRECTORY))
@@ -39,17 +41,6 @@ _hook_spec.loader.exec_module(_hook_module)
39
41
  check_duplicate_function_body_across_files = _hook_module.check_duplicate_function_body_across_files
40
42
 
41
43
 
42
- SHARED_HELPER_SOURCE = (
43
- "import re\n"
44
- "\n"
45
- "def strip_code_and_quotes(text: str) -> str:\n"
46
- " without_fences = re.sub(r'```.*?```', '', text, flags=re.DOTALL)\n"
47
- " without_inline = re.sub(r'`[^`]*`', '', without_fences)\n"
48
- " without_quotes = re.sub(r'(?m)^>.*$', '', without_inline)\n"
49
- " return without_quotes.strip()\n"
50
- )
51
-
52
-
53
44
  @pytest.fixture
54
45
  def module_dir() -> Iterator[pathlib.Path]:
55
46
  base_directory = pathlib.Path(tempfile.mkdtemp())
@@ -9,18 +9,15 @@ points (the ``main()`` stdin path and the pre-check CLI) with a hook-infrastruct
9
9
  target, so each lane's behavior is proven on the same path a live Write or a
10
10
  pre-check run would take, rather than by calling the check function directly.
11
11
 
12
- Each test builds a temporary tree whose tail mirrors a production hook directory
13
- (``packages/claude-dev-env/hooks/blocking``) so ``is_hook_infrastructure`` matches the
14
- target path the same way it would for the production directory.
12
+ Each test takes the ``hook_blocking_dir`` fixture from ``conftest.py``, which builds
13
+ a temporary tree matching the production hook directory so ``is_hook_infrastructure``
14
+ routes a target inside it the same way it would for the production directory.
15
15
  """
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
19
  import pathlib
20
- import shutil
21
20
  import sys
22
- import tempfile
23
- from collections.abc import Iterator
24
21
 
25
22
  import pytest
26
23
 
@@ -33,35 +30,13 @@ if str(_HOOKS_PARENT) not in sys.path:
33
30
 
34
31
  from code_rules_enforcer import main # noqa: E402
35
32
  from code_rules_enforcer_test_support import ( # noqa: E402
33
+ STRIP_CODE_AND_QUOTES_SOURCE as SHARED_HELPER_SOURCE,
36
34
  run_precheck,
37
35
  run_write_entrypoint,
38
36
  )
39
37
 
40
38
  pytestmark = pytest.mark.usefixtures("ephemeral_exempt_off")
41
39
 
42
- SHARED_HELPER_SOURCE = (
43
- "import re\n"
44
- "\n"
45
- "def strip_code_and_quotes(text: str) -> str:\n"
46
- " without_fences = re.sub(r'```.*?```', '', text, flags=re.DOTALL)\n"
47
- " without_inline = re.sub(r'`[^`]*`', '', without_fences)\n"
48
- " without_quotes = re.sub(r'(?m)^>.*$', '', without_inline)\n"
49
- " return without_quotes.strip()\n"
50
- )
51
-
52
- _HOOK_INFRASTRUCTURE_TAIL = pathlib.Path("packages") / "claude-dev-env" / "hooks" / "blocking"
53
-
54
-
55
- @pytest.fixture
56
- def hook_blocking_dir() -> Iterator[pathlib.Path]:
57
- base_directory = pathlib.Path(tempfile.mkdtemp())
58
- blocking_directory = base_directory / _HOOK_INFRASTRUCTURE_TAIL
59
- blocking_directory.mkdir(parents=True)
60
- try:
61
- yield blocking_directory
62
- finally:
63
- shutil.rmtree(base_directory, ignore_errors=False)
64
-
65
40
 
66
41
  def _run_main_with_write_payload(
67
42
  file_path: str,
@@ -0,0 +1,21 @@
1
+ """Tests for the shared constants this directory's test-support module provides."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import cast
7
+
8
+ from code_rules_enforcer_test_support import STRIP_CODE_AND_QUOTES_SOURCE
9
+
10
+
11
+ def should_define_a_strip_code_and_quotes_function_that_removes_fenced_code() -> None:
12
+ module_namespace: dict[str, object] = {}
13
+ exec(STRIP_CODE_AND_QUOTES_SOURCE, module_namespace)
14
+ strip_code_and_quotes = cast("Callable[[str], str]", module_namespace["strip_code_and_quotes"])
15
+
16
+ stripped = strip_code_and_quotes("keep this\n```\ncode block\n```\n> a quote\nkeep this too")
17
+
18
+ assert "code block" not in stripped
19
+ assert "a quote" not in stripped
20
+ assert "keep this" in stripped
21
+ assert "keep this too" in stripped
@@ -7,19 +7,16 @@ through ``validate_content_for_edit_lane``, which hook files never reach at PreT
7
7
  tests drive the real ``main()`` stdin entry point and the pre-check CLI with a
8
8
  hook-infrastructure target.
9
9
 
10
- Each test builds a temporary tree whose tail mirrors a production hook directory
11
- (``packages/claude-dev-env/hooks/blocking``) so ``is_hook_infrastructure`` matches
12
- the target path the same way it would for the real directory.
10
+ Each test takes the ``hook_blocking_dir`` fixture from ``conftest.py``, which builds
11
+ a temporary tree matching the production hook directory so ``is_hook_infrastructure``
12
+ routes a target inside it the same way it would for the production directory.
13
13
  """
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
17
  import json
18
18
  import pathlib
19
- import shutil
20
19
  import sys
21
- import tempfile
22
- from collections.abc import Iterator
23
20
 
24
21
  import pytest
25
22
 
@@ -46,19 +43,6 @@ PASS_THROUGH_ALIAS_SOURCE = (
46
43
  " return find_bare_path_segments(content)\n"
47
44
  )
48
45
 
49
- _HOOK_INFRASTRUCTURE_TAIL = pathlib.Path("packages") / "claude-dev-env" / "hooks" / "blocking"
50
-
51
-
52
- @pytest.fixture
53
- def hook_blocking_dir() -> Iterator[pathlib.Path]:
54
- base_directory = pathlib.Path(tempfile.mkdtemp())
55
- blocking_directory = base_directory / _HOOK_INFRASTRUCTURE_TAIL
56
- blocking_directory.mkdir(parents=True)
57
- try:
58
- yield blocking_directory
59
- finally:
60
- shutil.rmtree(base_directory, ignore_errors=False)
61
-
62
46
 
63
47
  def test_write_of_pass_through_alias_into_hook_directory_denies(
64
48
  hook_blocking_dir: pathlib.Path,
@@ -291,3 +291,21 @@ def test_is_dedicated_constants_module_true_for_config_directory() -> None:
291
291
 
292
292
  def test_is_dedicated_constants_module_false_for_ordinary_module() -> None:
293
293
  assert _SHARED_MODULE._is_dedicated_constants_module("app/services/orders.py") is False
294
+
295
+
296
+ @pytest.mark.parametrize(
297
+ "normalized_marker_table_name",
298
+ [
299
+ "ALL_AGENT_HOME_TOOLING_PATTERNS",
300
+ "ALL_HOOK_INFRASTRUCTURE_PATTERNS",
301
+ "ALL_WORKFLOW_REGISTRY_PATTERNS",
302
+ "ALL_MIGRATION_PATH_PATTERNS",
303
+ ],
304
+ )
305
+ def test_normalized_path_marker_table_carries_no_backslash_entry(
306
+ normalized_marker_table_name: str,
307
+ ) -> None:
308
+ """Each table's consumer normalizes the path with .replace("\\\\", "/") but
309
+ not the marker, so a backslash entry can never match."""
310
+ marker_table = getattr(_SHARED_MODULE, normalized_marker_table_name)
311
+ assert all("\\" not in each_marker for each_marker in marker_table)
@@ -0,0 +1,35 @@
1
+ """Tests for the shared fixtures this directory's ``conftest.py`` provides."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pathlib
6
+
7
+ from code_rules_shared import is_hook_infrastructure
8
+
9
+
10
+ def should_yield_a_directory_a_target_inside_it_routes_as_hook_infrastructure(
11
+ hook_blocking_dir: pathlib.Path,
12
+ ) -> None:
13
+ target_file = hook_blocking_dir / "new_blocker.py"
14
+ assert is_hook_infrastructure(str(target_file)) is True
15
+
16
+
17
+ def should_yield_a_directory_that_exists_on_disk(
18
+ hook_blocking_dir: pathlib.Path,
19
+ ) -> None:
20
+ assert hook_blocking_dir.is_dir()
21
+
22
+
23
+ def should_keep_the_hook_blocking_dir_explanation_only_on_the_fixture() -> None:
24
+ directory = pathlib.Path(__file__).parent
25
+ exempt_paths = {directory / "conftest.py", pathlib.Path(__file__)}
26
+ explanation_fragment = "tail mirrors a production hook directory"
27
+ offending_files = [
28
+ path.name
29
+ for path in sorted(directory.glob("*.py"))
30
+ if path not in exempt_paths and explanation_fragment in path.read_text(encoding="utf-8")
31
+ ]
32
+ assert offending_files == [], (
33
+ "the hook_blocking_dir fixture's explanation belongs on conftest.py alone, "
34
+ f"got it repeated in: {offending_files}"
35
+ )
@@ -24,6 +24,7 @@ SYNTHETIC_AWS_KEY = "AKIA" + ("B" * 16)
24
24
  SYNTHETIC_PEM_HEADER = "-----BEGIN RSA PRIVATE KEY-----"
25
25
  SYNTHETIC_REAL_EMAIL = "person.fixture@company-example.io"
26
26
  SYNTHETIC_SAFE_EMAIL = "user@example.com"
27
+ SYNTHETIC_GITHUB_NOREPLY_EMAIL = "24366590+octocat@users.noreply.github.com"
27
28
  SYNTHETIC_HOME_PATH = r"C:\Users\fixture_real_user\notes.txt"
28
29
  SYNTHETIC_PLACEHOLDER_HOME = r"C:\Users\<you>\notes.txt"
29
30
  SYNTHETIC_PRIVATE_IP = "192.168.42.17"
@@ -42,6 +43,13 @@ def test_flags_real_email_and_allows_example_domain() -> None:
42
43
  assert all_safe_hits == []
43
44
 
44
45
 
46
+ def test_allows_github_noreply_email_and_still_flags_a_company_lookalike() -> None:
47
+ all_noreply_hits = scan_text_for_pii(f"commit author {SYNTHETIC_GITHUB_NOREPLY_EMAIL}")
48
+ all_lookalike_hits = scan_text_for_pii("contact person@users.noreply.github.com.evil.io")
49
+ assert all_noreply_hits == []
50
+ assert any(each.category == "email" for each in all_lookalike_hits)
51
+
52
+
45
53
  def test_flags_home_path_and_allows_placeholder_user() -> None:
46
54
  all_home_hits = scan_text_for_pii(f"path is {SYNTHETIC_HOME_PATH}")
47
55
  all_placeholder_hits = scan_text_for_pii(f"path is {SYNTHETIC_PLACEHOLDER_HOME}")
@@ -34,6 +34,11 @@ from code_rules_enforcer_test_support import (
34
34
  repository_file_path,
35
35
  repository_root,
36
36
  )
37
+ from hooks_constants.code_rules_enforcer_constants import (
38
+ JAVASCRIPT_BLOCK_COMMENT_CLOSER,
39
+ JAVASCRIPT_BLOCK_COMMENT_OPENER,
40
+ )
41
+ import state_description_blocker as _state_description_blocker_module
37
42
 
38
43
  HOOK_SCRIPT_PATH = os.path.join(
39
44
  os.path.dirname(__file__), "state_description_blocker.py"
@@ -995,3 +1000,14 @@ def test_find_violations_with_lines_reports_markdown_line_past_a_code_fence() ->
995
1000
  all_violations = find_violations_with_lines(markdown_source, "docs/api.md")
996
1001
 
997
1002
  assert all_violations == [("previously", 7), ("was previously", 7)]
1003
+
1004
+
1005
+ def test_block_comment_markers_are_the_javascript_constants() -> None:
1006
+ assert (
1007
+ _state_description_blocker_module.JAVASCRIPT_BLOCK_COMMENT_OPENER
1008
+ is JAVASCRIPT_BLOCK_COMMENT_OPENER
1009
+ )
1010
+ assert (
1011
+ _state_description_blocker_module.JAVASCRIPT_BLOCK_COMMENT_CLOSER
1012
+ is JAVASCRIPT_BLOCK_COMMENT_CLOSER
1013
+ )
@@ -47,20 +47,7 @@ LOCAL_SHA_FIELD_INDEX: int = 1
47
47
  NO_PARSEABLE_STDIN_LINES_MESSAGE: str = (
48
48
  "claude-dev-env pre-push: no parseable stdin lines; aborting"
49
49
  )
50
- NO_PARSEABLE_STDIN_LINES_SENTINEL: str = "__no_parseable_stdin_lines__"
51
- UNRESOLVABLE_MERGE_BASE_SENTINEL: str = "__unresolvable_merge_base__"
52
- UNRESOLVABLE_MERGE_BASE_MESSAGE: str = (
53
- "claude-dev-env pre-push: CODE_RULES validation is pending because the pushed "
54
- "object and default branch have no shared merge base. Restore shared history "
55
- "or provide a reachable default branch, then run the push again."
56
- )
57
50
  LOCAL_BRANCH_REFERENCE_PREFIX: str = "refs/heads/"
58
- ORIGIN_HEAD_SYMBOLIC_REFERENCE: str = "refs/remotes/origin/HEAD"
59
- ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX: str = "refs/remotes/origin/"
60
- ALL_DEFAULT_BRANCH_FALLBACK_REFERENCES: tuple[str, ...] = (
61
- "refs/remotes/origin/main",
62
- "refs/remotes/origin/master",
63
- )
64
51
  REMOTE_BRANCH_REFERENCE_TEMPLATE: str = "refs/remotes/{remote}/{branch}"
65
52
  ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX: tuple[str, ...] = (
66
53
  "git",
@@ -99,7 +86,7 @@ GIT_SYMBOLIC_REFERENCE_SUBCOMMAND: str = "symbolic-ref"
99
86
  GIT_COMMAND_TIMEOUT_SECONDS: int = 30
100
87
  GH_EXECUTABLE_NAME: str = "gh"
101
88
  GH_PR_VIEW_TIMEOUT_SECONDS: int = 5
102
- GH_PR_VIEW_ARGUMENTS: tuple[str, ...] = ("pr", "view", "--json", "url", "--jq", ".url")
89
+ ALL_GH_PR_VIEW_ARGUMENTS: tuple[str, ...] = ("pr", "view", "--json", "url", "--jq", ".url")
103
90
  GIT_FOR_EACH_REF_SUBCOMMAND: str = "for-each-ref"
104
91
  GIT_REFERENCE_SHORT_NAME_FORMAT_ARGUMENT: str = "--format=%(refname:short)"
105
92
  COMMIT_OBJECT_NAME_SUFFIX: str = "^{commit}"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  from git_hooks_constants import (
7
7
  GH_EXECUTABLE_NAME,
8
- GH_PR_VIEW_ARGUMENTS,
8
+ ALL_GH_PR_VIEW_ARGUMENTS,
9
9
  GH_PR_VIEW_TIMEOUT_SECONDS,
10
10
  )
11
11
 
@@ -15,7 +15,7 @@ from subprocess_window_access import hidden_window_creation_flags
15
15
  def get_pull_request_url(repo_dir: Path) -> str | None:
16
16
  try:
17
17
  result = subprocess.run(
18
- [GH_EXECUTABLE_NAME, *GH_PR_VIEW_ARGUMENTS],
18
+ [GH_EXECUTABLE_NAME, *ALL_GH_PR_VIEW_ARGUMENTS],
19
19
  cwd=repo_dir,
20
20
  check=False,
21
21
  capture_output=True,
@@ -11,6 +11,13 @@ def test_no_parseable_stdin_lines_message_exists_and_describes_problem() -> None
11
11
  assert "no parseable stdin lines" in git_hooks_constants.NO_PARSEABLE_STDIN_LINES_MESSAGE
12
12
 
13
13
 
14
- def test_no_parseable_stdin_lines_sentinel_is_distinct_sentinel_value() -> None:
15
- assert git_hooks_constants.NO_PARSEABLE_STDIN_LINES_SENTINEL is not None
16
- assert git_hooks_constants.NO_PARSEABLE_STDIN_LINES_SENTINEL != git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE
14
+ def test_pre_push_cut_leftovers_are_removed() -> None:
15
+ for removed_name in (
16
+ "ALL_DEFAULT_BRANCH_FALLBACK_REFERENCES",
17
+ "ORIGIN_HEAD_SYMBOLIC_REFERENCE",
18
+ "ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX",
19
+ "UNRESOLVABLE_MERGE_BASE_SENTINEL",
20
+ "UNRESOLVABLE_MERGE_BASE_MESSAGE",
21
+ "NO_PARSEABLE_STDIN_LINES_SENTINEL",
22
+ ):
23
+ assert not hasattr(git_hooks_constants, removed_name)
@@ -0,0 +1,49 @@
1
+ """Tests for the pull-request URL query the commit handoff runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ import pull_request_handoff
11
+ from git_hooks_constants import (
12
+ ALL_GH_PR_VIEW_ARGUMENTS,
13
+ GH_EXECUTABLE_NAME,
14
+ GH_PR_VIEW_TIMEOUT_SECONDS,
15
+ )
16
+
17
+
18
+ def test_should_query_the_url_through_the_named_executable_and_arguments(
19
+ monkeypatch: pytest.MonkeyPatch,
20
+ ) -> None:
21
+ all_calls: list[tuple[list[str], object]] = []
22
+
23
+ def record_call(
24
+ command: list[str], **keyword_arguments: object
25
+ ) -> subprocess.CompletedProcess[str]:
26
+ all_calls.append((command, keyword_arguments["timeout"]))
27
+ return subprocess.CompletedProcess(command, 0, "https://example/pull/1\n", "")
28
+
29
+ monkeypatch.setattr(pull_request_handoff.subprocess, "run", record_call)
30
+
31
+ url = pull_request_handoff.get_pull_request_url(Path("/repo"))
32
+
33
+ assert url == "https://example/pull/1"
34
+ assert all_calls == [
35
+ ([GH_EXECUTABLE_NAME, *ALL_GH_PR_VIEW_ARGUMENTS], GH_PR_VIEW_TIMEOUT_SECONDS)
36
+ ]
37
+
38
+
39
+ def test_should_report_no_url_when_the_query_exits_non_zero(
40
+ monkeypatch: pytest.MonkeyPatch,
41
+ ) -> None:
42
+ def failing_call(
43
+ command: list[str], **keyword_arguments: object
44
+ ) -> subprocess.CompletedProcess[str]:
45
+ return subprocess.CompletedProcess(command, 1, "", "no pull request")
46
+
47
+ monkeypatch.setattr(pull_request_handoff.subprocess, "run", failing_call)
48
+
49
+ assert pull_request_handoff.get_pull_request_url(Path("/repo")) is None
@@ -0,0 +1,50 @@
1
+ """Tests for the base-reference construction in ``_base_matches_current``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ import verification_notice
10
+ import verification_notice_state
11
+ from git_hooks_constants import COMMIT_OBJECT_NAME_SUFFIX
12
+ from git_hooks_constants.verification_notice_constants import (
13
+ ALL_GIT_BASE_QUERY_PREFIX,
14
+ BASE_REFERENCE,
15
+ )
16
+
17
+
18
+ def test_base_matches_current_queries_base_reference_plus_commit_suffix() -> None:
19
+ captured_queries: list[tuple[str, ...]] = []
20
+
21
+ def _fake_git_query(
22
+ repository_root: Path, arguments: tuple[str, ...], capture_stderr: bool
23
+ ) -> str | None:
24
+ captured_queries.append(arguments)
25
+ return "deadbeef"
26
+
27
+ verification_notice_state._base_matches_current(Path("/repo"), "deadbeef", _fake_git_query)
28
+
29
+ assert captured_queries == [
30
+ (*ALL_GIT_BASE_QUERY_PREFIX, BASE_REFERENCE + COMMIT_OBJECT_NAME_SUFFIX)
31
+ ]
32
+
33
+
34
+ def test_base_matches_current_derives_its_query_from_the_base_reference_constant(
35
+ monkeypatch: pytest.MonkeyPatch,
36
+ ) -> None:
37
+ monkeypatch.setattr(verification_notice_state, "BASE_REFERENCE", "custom/base")
38
+ captured_queries: list[tuple[str, ...]] = []
39
+
40
+ def _fake_git_query(
41
+ repository_root: Path, arguments: tuple[str, ...], capture_stderr: bool
42
+ ) -> str | None:
43
+ captured_queries.append(arguments)
44
+ return None
45
+
46
+ verification_notice_state._base_matches_current(Path("/repo"), "deadbeef", _fake_git_query)
47
+
48
+ assert captured_queries == [
49
+ (*ALL_GIT_BASE_QUERY_PREFIX, "custom/base" + COMMIT_OBJECT_NAME_SUFFIX)
50
+ ]
@@ -6,8 +6,10 @@ from collections.abc import Callable, Mapping
6
6
  from pathlib import Path
7
7
  from typing import Protocol
8
8
 
9
+ from git_hooks_constants import COMMIT_OBJECT_NAME_SUFFIX
9
10
  from git_hooks_constants.verification_notice_constants import (
10
11
  ALL_GIT_BASE_QUERY_PREFIX,
12
+ BASE_REFERENCE,
11
13
  GIT_COMMAND_SUCCESS_EXIT_CODE,
12
14
  HEX_DIGITS,
13
15
  NOTICE_FAILED_LINE,
@@ -180,7 +182,7 @@ def _base_matches_current(
180
182
  ) -> bool:
181
183
  current_base_revision = run_git_query(
182
184
  repository_root,
183
- (*ALL_GIT_BASE_QUERY_PREFIX, "origin/main^{commit}"),
185
+ (*ALL_GIT_BASE_QUERY_PREFIX, BASE_REFERENCE + COMMIT_OBJECT_NAME_SUFFIX),
184
186
  False,
185
187
  )
186
188
  return current_base_revision == base_revision
@@ -38,10 +38,13 @@ CLAUDE_JOB_DIR_SCRATCH_SUBDIRECTORY: str = "tmp"
38
38
  EPHEMERAL_EXEMPT_DISABLE_ENVIRONMENT_VARIABLE_NAME: str = "CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT"
39
39
  ALL_EPHEMERAL_EXEMPT_DISABLE_TRUTHY_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
40
40
  LEADING_DRIVE_LETTER_PATTERN: re.Pattern[str] = re.compile(r"^[a-z]:")
41
- ALL_AGENT_HOME_TOOLING_PATTERNS = {"/.grok/", "\\.grok\\", "\\.grok/"}
42
- ALL_HOOK_INFRASTRUCTURE_PATTERNS = {"/.claude/hooks/", "\\.claude\\hooks\\", "\\.claude/hooks/", "/packages/claude-dev-env/hooks/", "\\packages\\claude-dev-env\\hooks\\"} | ALL_AGENT_HOME_TOOLING_PATTERNS
43
- ALL_WORKFLOW_REGISTRY_PATTERNS = {"/workflow/", "\\workflow\\", "_tab.py", "/states.py", "\\states.py", "/modules.py", "\\modules.py"}
44
- ALL_MIGRATION_PATH_PATTERNS = {"/migrations/", "\\migrations\\"}
41
+ ALL_AGENT_HOME_TOOLING_PATTERNS = {"/.grok/"}
42
+ ALL_HOOK_INFRASTRUCTURE_PATTERNS = {
43
+ "/.claude/hooks/",
44
+ "/packages/claude-dev-env/hooks/",
45
+ } | ALL_AGENT_HOME_TOOLING_PATTERNS
46
+ ALL_WORKFLOW_REGISTRY_PATTERNS = {"/workflow/", "_tab.py", "/states.py", "/modules.py"}
47
+ ALL_MIGRATION_PATH_PATTERNS = {"/migrations/"}
45
48
 
46
49
  CONSTANTS_MODULE_SUFFIX: str = "_constants.py"
47
50
  CONFIG_DIRECTORY_SEGMENT: str = "config"
@@ -192,7 +195,12 @@ ALL_COLLECTION_TYPE_NAMES: frozenset[str] = frozenset({
192
195
  })
193
196
  ALL_SUBSCRIPT_ONLY_COLLECTION_TYPE_NAMES: frozenset[str] = frozenset({"dict"})
194
197
  COLLECTION_BY_NAME_PATTERN: re.Pattern[str] = re.compile(r"^[a-z][a-z0-9]*_by_[a-z][a-z0-9_]*$")
195
- ALL_CLI_FILE_PATH_MARKERS: tuple[str, ...] = ("/scripts/", "\\scripts\\", "_cli.py", "/cli.py", "\\cli.py")
198
+ ALL_CLI_FILE_PATH_MARKERS: tuple[str, ...] = (
199
+ "/scripts/",
200
+ "/diagnostics/",
201
+ "_cli.py",
202
+ "/cli.py",
203
+ )
196
204
 
197
205
  LOGGING_FSTRING_PATTERN = re.compile(
198
206
  r'\b(?:log_(?:debug|info|warning|error|critical|exception)'
@@ -136,6 +136,7 @@ ALL_SAFE_EMAIL_DOMAINS: frozenset[str] = frozenset(
136
136
  "test",
137
137
  "local",
138
138
  "test.local",
139
+ "users.noreply.github.com",
139
140
  }
140
141
  )
141
142
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ GIT_EXECUTABLE_TOKEN: str = "git"
6
+ GIT_COMMAND_TIMEOUT_SECONDS: int = 5
5
7
  MAXIMUM_REFACTOR_LINE_DELTA: int = 3
6
8
  REFACTOR_LINE_DELTA_DIVISOR: int = 2
7
9
  CHANGED_SURFACE_MATCH_RATIO: float = 0.5