claude-dev-env 8.6.0 → 8.7.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 (106) hide show
  1. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  2. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
  3. package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
  4. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +4 -0
  6. package/_shared/pr-loop/scripts/terminology_sweep.py +155 -18
  7. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +130 -0
  8. package/bin/install.test.mjs +25 -0
  9. package/docs/high-trust-agent-delivery.md +132 -0
  10. package/hooks/advisory/refactor_guard.py +2 -2
  11. package/hooks/advisory/test_refactor_guard.py +21 -0
  12. package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
  13. package/hooks/blocking/code_rules_shared.py +4 -7
  14. package/hooks/blocking/conftest.py +22 -0
  15. package/hooks/blocking/state_description_blocker.py +232 -90
  16. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
  17. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +27 -54
  18. package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
  19. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
  20. package/hooks/blocking/test_code_rules_shared.py +18 -0
  21. package/hooks/blocking/test_conftest.py +35 -0
  22. package/hooks/blocking/test_state_description_blocker.py +59 -0
  23. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
  24. package/hooks/git-hooks/pull_request_handoff.py +2 -2
  25. package/hooks/git-hooks/test_config.py +10 -3
  26. package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
  27. package/hooks/git-hooks/test_verification_notice_state.py +50 -0
  28. package/hooks/git-hooks/verification_notice_state.py +3 -1
  29. package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
  30. package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
  31. package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
  32. package/hooks/hooks_constants/state_description_blocker_constants.py +2 -1
  33. package/hooks/hooks_constants/test_state_description_blocker_constants.py +87 -0
  34. package/hooks/validators/exempt_paths.py +3 -18
  35. package/hooks/validators/test_exempt_paths.py +20 -0
  36. package/hooks/validators/test_python_antipattern_checks.py +14 -0
  37. package/package.json +1 -1
  38. package/rules/agent-merges-its-own-green-pull-request.md +54 -0
  39. package/rules/archiving-agent-config.md +10 -0
  40. package/rules/code-standards.md +1 -0
  41. package/rules/correction-lens.md +134 -0
  42. package/rules/flag-non-breaking-findings.md +11 -0
  43. package/rules/no-contrast-framing.md +68 -0
  44. package/rules/review-closure-is-a-check.md +44 -0
  45. package/scripts/Invoke-PesterSuites.ps1 +42 -0
  46. package/scripts/agent_merge_check.py +381 -0
  47. package/scripts/check.ps1 +12 -1
  48. package/scripts/ci/install-playtest.mjs +291 -0
  49. package/scripts/ci/install-playtest.test.mjs +84 -0
  50. package/scripts/ci/installer-lifecycle.mjs +11 -30
  51. package/scripts/ci/installer-lifecycle.test.mjs +14 -0
  52. package/scripts/ci/pstack-command-shim.test.mjs +33 -0
  53. package/scripts/ci/scratch-home-install.mjs +73 -0
  54. package/scripts/ci/scratch-home-install.test.mjs +49 -0
  55. package/scripts/codex_compat_materializer.py +33 -16
  56. package/scripts/contrast_framing.py +101 -0
  57. package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
  58. package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
  59. package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
  60. package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
  61. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
  62. package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
  63. package/scripts/durable_post_lint.py +30 -4
  64. package/scripts/followup_cli.py +6 -10
  65. package/scripts/merge_gate_checks.py +236 -0
  66. package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
  67. package/scripts/policy_lint/adapter_detectors.py +1 -1
  68. package/scripts/policy_lint/adapter_support.py +3 -14
  69. package/scripts/policy_lint/adapters.py +30 -1
  70. package/scripts/policy_lint/config/constants.py +2 -0
  71. package/scripts/policy_lint/registry.py +5 -0
  72. package/scripts/repository_checks/config/constants.py +12 -0
  73. package/scripts/repository_checks/config/policy_document.py +28 -2
  74. package/scripts/repository_checks/followups.py +54 -0
  75. package/scripts/repository_checks/match_exemptions.py +67 -0
  76. package/scripts/repository_checks/models.py +46 -2
  77. package/scripts/repository_checks/reporting.py +23 -7
  78. package/scripts/repository_checks/tracked_secrets.py +2 -2
  79. package/scripts/repository_policy.py +4 -2
  80. package/scripts/review_closure.py +134 -0
  81. package/scripts/review_closure_github.py +345 -0
  82. package/scripts/review_closure_model.py +360 -0
  83. package/scripts/test_agent_merge_check.py +317 -0
  84. package/scripts/test_durable_post_lint.py +46 -0
  85. package/scripts/test_followup_cli.py +40 -0
  86. package/scripts/test_merge_gate_checks.py +92 -0
  87. package/scripts/tests/test_code_review_constants.py +78 -0
  88. package/scripts/tests/test_codex_compat_materializer.py +48 -4
  89. package/scripts/tests/test_contrast_framing.py +117 -0
  90. package/scripts/tests/test_followups.py +88 -0
  91. package/scripts/tests/test_match_exemptions.py +291 -0
  92. package/scripts/tests/test_models.py +67 -2
  93. package/scripts/tests/test_package_inventory.py +6 -4
  94. package/scripts/tests/test_pester_suites_run.py +53 -0
  95. package/scripts/tests/test_policy_document.py +39 -1
  96. package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
  97. package/scripts/tests/test_policy_lint_rules_engine.py +4 -1
  98. package/scripts/tests/test_reporting.py +18 -0
  99. package/scripts/tests/test_repository_policy.py +43 -1
  100. package/scripts/tests/test_review_closure.py +126 -0
  101. package/scripts/tests/test_review_closure_github.py +240 -0
  102. package/scripts/tests/test_review_closure_model.py +299 -0
  103. package/scripts/tests/test_tracked_secrets.py +42 -0
  104. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
  105. package/scripts/repository_checks/email_exemptions.py +0 -39
  106. package/scripts/tests/test_email_exemptions.py +0 -158
@@ -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
+ )
@@ -26,6 +26,7 @@ if _HOOKS_ROOT not in sys.path:
26
26
  from pre_tool_use_dispatcher import NativeHook, run_native_hook # noqa: E402
27
27
  from state_description_blocker import ( # noqa: E402
28
28
  build_deny_payload,
29
+ find_violations_with_lines,
29
30
  )
30
31
  from state_description_blocker import evaluate as _evaluate_without_opt_in # noqa: E402
31
32
 
@@ -33,6 +34,11 @@ from code_rules_enforcer_test_support import (
33
34
  repository_file_path,
34
35
  repository_root,
35
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
36
42
 
37
43
  HOOK_SCRIPT_PATH = os.path.join(
38
44
  os.path.dirname(__file__), "state_description_blocker.py"
@@ -952,3 +958,56 @@ def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
952
958
  logged_record = json.loads(all_records[0])
953
959
  assert logged_record["hook"] == "state_description_blocker.py"
954
960
  assert logged_record["event"] == "PreToolUse"
961
+
962
+
963
+ def test_find_violations_with_lines_names_the_docstring_line_not_the_fixture_line() -> None:
964
+ module_source = (
965
+ 'fixture_text = "the field was previously required"\n'
966
+ "\n"
967
+ "\n"
968
+ "def read_field():\n"
969
+ ' """Read the field.\n'
970
+ "\n"
971
+ " The field was previously required.\n"
972
+ ' """\n'
973
+ " return 1\n"
974
+ )
975
+
976
+ all_violations = find_violations_with_lines(module_source, "src/module.py")
977
+
978
+ assert all_violations == [("previously", 7), ("was previously", 7)]
979
+
980
+
981
+ def test_find_violations_with_lines_reports_first_matching_comment_line() -> None:
982
+ module_source = "x = 1\n# no longer needed\ny = 2\n# no longer used\n"
983
+
984
+ all_violations = find_violations_with_lines(module_source, "src/module.py")
985
+
986
+ assert all_violations == [("no longer", 2)]
987
+
988
+
989
+ def test_find_violations_with_lines_reports_markdown_line_past_a_code_fence() -> None:
990
+ markdown_source = (
991
+ "# Title\n"
992
+ "\n"
993
+ "```\n"
994
+ "used to fail\n"
995
+ "```\n"
996
+ "\n"
997
+ "The API was previously public.\n"
998
+ )
999
+
1000
+ all_violations = find_violations_with_lines(markdown_source, "docs/api.md")
1001
+
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)'
@@ -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
@@ -1,9 +1,9 @@
1
1
  """Constants for the session-edit stage gate hook trio.
2
2
 
3
3
  Shared by the PostToolUse tracker
4
- (``hooks/observability/session_file_edit_tracker.py``), the PreToolUse gate
5
- (``hooks/blocking/session_edit_stage_gate.py``), and the SessionStart cleanup
6
- (``hooks/session/session_edit_tracker_cleanup.py``): the per-session tracker
4
+ (``hooks/observability/session_file_edit_tracker.py``) and the SessionStart
5
+ cleanup (``hooks/session/session_edit_tracker_cleanup.py``): the per-session
6
+ tracker
7
7
  filename shape and JSON payload key, the session-id sanitize pattern, the
8
8
  SessionStart source key and the fresh-startup source value the cleanup keys its
9
9
  deletion on, the edit-tool name set, the per-session lock filename suffix and
@@ -62,7 +62,6 @@ ALL_TRACKED_UNSTAGED_FILES_COMMAND: tuple[str, ...] = (
62
62
 
63
63
  PARTIAL_COMMIT_BYPASS_MARKER: str = "# partial-commit"
64
64
  COMMIT_SUBCOMMAND_TOKEN: str = "commit"
65
- GIT_EXECUTABLE_TOKEN: str = "git"
66
65
  ENV_ASSIGNMENT_PREFIX_PATTERN: re.Pattern[str] = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
67
66
  ALL_STAGING_SUBCOMMAND_TOKENS: frozenset[str] = frozenset({"add", "stage"})
68
67
  ALL_STAGE_ALL_ADD_FLAG_TOKENS: frozenset[str] = frozenset({"-A", "--all", "-u", "--update"})
@@ -40,12 +40,13 @@ ALL_COMMENT_TRANSITION_PATTERNS: list[Pattern[str]] = [
40
40
  compile(r"\bas of\b", IGNORECASE),
41
41
  ]
42
42
 
43
- CODE_FENCE_PATTERN: Pattern[str] = compile(r"```[\s\S]*?```")
44
43
  INLINE_CODE_PATTERN: Pattern[str] = compile(r"``[^`]+``|`[^`]+`")
45
44
  DOUBLE_QUOTED_SPAN_PATTERN: Pattern[str] = compile(r'"[^"\n]*"')
46
45
  TRIPLE_QUOTED_BLOCK_PATTERN: Pattern[str] = compile(
47
46
  r'"""([\s\S]*?)"""|\'\'\'([\s\S]*?)\'\'\''
48
47
  )
48
+ DOUBLE_QUOTE_BODY_GROUP: int = 1
49
+ SINGLE_QUOTE_BODY_GROUP: int = 2
49
50
 
50
51
  PYTHON_EXTENSION: str = ".py"
51
52
 
@@ -0,0 +1,87 @@
1
+ """Behavior tests for state_description_blocker configuration constants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _HOOKS_ROOT = Path(__file__).resolve().parent.parent
9
+ if str(_HOOKS_ROOT) not in sys.path:
10
+ sys.path.insert(0, str(_HOOKS_ROOT))
11
+
12
+ from blocking.state_description_blocker import _extract_comment_lines
13
+ from hooks_constants.code_rules_enforcer_constants import (
14
+ JAVASCRIPT_BLOCK_COMMENT_CLOSER,
15
+ JAVASCRIPT_BLOCK_COMMENT_OPENER,
16
+ )
17
+ from hooks_constants.state_description_blocker_constants import (
18
+ ALL_BLOCK_COMMENT_EXTENSIONS,
19
+ ALL_COMMENT_TRANSITION_PATTERNS,
20
+ ALL_HASH_ONLY_EXTENSIONS,
21
+ ALL_MARKDOWN_EXTENSIONS,
22
+ DOUBLE_QUOTE_BODY_GROUP,
23
+ SINGLE_QUOTE_BODY_GROUP,
24
+ TRIPLE_QUOTED_BLOCK_PATTERN,
25
+ )
26
+
27
+
28
+ def test_transition_patterns_match_their_own_phrase() -> None:
29
+ all_phrases = [
30
+ "instead of",
31
+ "previously",
32
+ "was previously",
33
+ "used to",
34
+ "no longer",
35
+ "switched to",
36
+ ]
37
+ for each_phrase in all_phrases:
38
+ sentence = f"The field is {each_phrase} required."
39
+ assert any(
40
+ each_pattern.search(sentence) for each_pattern in ALL_COMMENT_TRANSITION_PATTERNS
41
+ ), each_phrase
42
+
43
+
44
+ def test_transition_patterns_do_not_match_unrelated_prose() -> None:
45
+ sentence = "The path selects the scan strategy for the file."
46
+ assert not any(
47
+ each_pattern.search(sentence) for each_pattern in ALL_COMMENT_TRANSITION_PATTERNS
48
+ )
49
+
50
+
51
+ def test_block_comment_markers_close_a_single_line_block_comment() -> None:
52
+ text = f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} note {JAVASCRIPT_BLOCK_COMMENT_CLOSER}\ncode();"
53
+ all_comment_lines = _extract_comment_lines(text, ".js")
54
+ assert all_comment_lines == [
55
+ (1, f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} note {JAVASCRIPT_BLOCK_COMMENT_CLOSER}")
56
+ ]
57
+
58
+
59
+ def test_block_comment_markers_carry_an_unclosed_comment_to_the_next_line() -> None:
60
+ text = f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} still open\nclosed here {JAVASCRIPT_BLOCK_COMMENT_CLOSER}"
61
+ all_comment_lines = _extract_comment_lines(text, ".js")
62
+ assert all_comment_lines == [
63
+ (1, f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} still open"),
64
+ (2, f"closed here {JAVASCRIPT_BLOCK_COMMENT_CLOSER}"),
65
+ ]
66
+
67
+
68
+ def test_double_and_single_quote_body_groups_index_the_matching_quote_style() -> None:
69
+ double_quoted_match = TRIPLE_QUOTED_BLOCK_PATTERN.search('"""double body"""')
70
+ assert double_quoted_match is not None
71
+ assert double_quoted_match.group(DOUBLE_QUOTE_BODY_GROUP) == "double body"
72
+ assert double_quoted_match.group(SINGLE_QUOTE_BODY_GROUP) is None
73
+
74
+ single_quoted_match = TRIPLE_QUOTED_BLOCK_PATTERN.search("'''single body'''")
75
+ assert single_quoted_match is not None
76
+ assert single_quoted_match.group(SINGLE_QUOTE_BODY_GROUP) == "single body"
77
+ assert single_quoted_match.group(DOUBLE_QUOTE_BODY_GROUP) is None
78
+
79
+
80
+ def test_extension_sets_are_disjoint_from_markdown_extensions() -> None:
81
+ assert not (ALL_HASH_ONLY_EXTENSIONS & ALL_MARKDOWN_EXTENSIONS)
82
+ assert not (ALL_BLOCK_COMMENT_EXTENSIONS & ALL_MARKDOWN_EXTENSIONS)
83
+
84
+
85
+ def test_python_is_hash_only_and_never_block_comment() -> None:
86
+ assert ".py" in ALL_HASH_ONLY_EXTENSIONS
87
+ assert ".py" not in ALL_BLOCK_COMMENT_EXTENSIONS
@@ -48,27 +48,21 @@ TEST_PATH_PATTERNS: frozenset[str] = frozenset(
48
48
  HOOK_INFRASTRUCTURE_PATTERNS: frozenset[str] = frozenset(
49
49
  {
50
50
  "/.claude/hooks/",
51
- "\\.claude\\hooks\\",
52
- "\\.claude/hooks/",
53
51
  }
54
52
  )
55
53
 
56
54
  WORKFLOW_REGISTRY_PATTERNS: frozenset[str] = frozenset(
57
55
  {
58
56
  "/workflow/",
59
- "\\workflow\\",
60
57
  "_tab.py",
61
58
  "/states.py",
62
- "\\states.py",
63
59
  "/modules.py",
64
- "\\modules.py",
65
60
  }
66
61
  )
67
62
 
68
63
  MIGRATION_PATH_PATTERNS: frozenset[str] = frozenset(
69
64
  {
70
65
  "/migrations/",
71
- "\\migrations\\",
72
66
  }
73
67
  )
74
68
 
@@ -80,23 +74,14 @@ def is_test_file(file_path: str) -> bool:
80
74
 
81
75
  def is_hook_infrastructure(file_path: str) -> bool:
82
76
  path_normalized = file_path.lower().replace("\\", "/")
83
- return any(
84
- pattern.replace("\\", "/") in path_normalized
85
- for pattern in HOOK_INFRASTRUCTURE_PATTERNS
86
- )
77
+ return any(pattern in path_normalized for pattern in HOOK_INFRASTRUCTURE_PATTERNS)
87
78
 
88
79
 
89
80
  def is_workflow_registry_file(file_path: str) -> bool:
90
81
  path_normalized = file_path.lower().replace("\\", "/")
91
- return any(
92
- pattern.replace("\\", "/") in path_normalized
93
- for pattern in WORKFLOW_REGISTRY_PATTERNS
94
- )
82
+ return any(pattern in path_normalized for pattern in WORKFLOW_REGISTRY_PATTERNS)
95
83
 
96
84
 
97
85
  def is_migration_file(file_path: str) -> bool:
98
86
  path_normalized = file_path.lower().replace("\\", "/")
99
- return any(
100
- pattern.replace("\\", "/") in path_normalized
101
- for pattern in MIGRATION_PATH_PATTERNS
102
- )
87
+ return any(pattern in path_normalized for pattern in MIGRATION_PATH_PATTERNS)
@@ -14,6 +14,9 @@ _BLOCKING_DIR = str(Path(__file__).resolve().parent.parent / "blocking")
14
14
  if _BLOCKING_DIR not in sys.path:
15
15
  sys.path.insert(0, _BLOCKING_DIR)
16
16
 
17
+ import pytest # noqa: E402
18
+
19
+ from validators import exempt_paths # noqa: E402
17
20
  from validators.exempt_paths import is_config_file # noqa: E402
18
21
  from code_rules_path_utils import is_config_file as path_utils_is_config_file # noqa: E402
19
22
 
@@ -63,3 +66,20 @@ def test_is_config_file_is_identical_function_object_from_path_utils() -> None:
63
66
  assert is_config_file is path_utils_is_config_file, (
64
67
  "exempt_paths.is_config_file must be imported from code_rules_path_utils, not re-defined"
65
68
  )
69
+
70
+
71
+ @pytest.mark.parametrize(
72
+ "normalized_pattern_set_name",
73
+ [
74
+ "HOOK_INFRASTRUCTURE_PATTERNS",
75
+ "WORKFLOW_REGISTRY_PATTERNS",
76
+ "MIGRATION_PATH_PATTERNS",
77
+ ],
78
+ )
79
+ def test_normalized_pattern_set_carries_no_backslash_entry(
80
+ normalized_pattern_set_name: str,
81
+ ) -> None:
82
+ """Each set's consumer normalizes the path with .replace("\\\\", "/") but
83
+ not the pattern, so a backslash entry can never match."""
84
+ pattern_set = getattr(exempt_paths, normalized_pattern_set_name)
85
+ assert all("\\" not in each_pattern for each_pattern in pattern_set)
@@ -150,6 +150,18 @@ class TestPrintInCliEntryPoint:
150
150
  tree = ast.parse(CLI_ENTRY_POINT_WITH_PRINTS)
151
151
  assert check_print_in_production(tree, "packages/foo/scripts/run_job.py") == []
152
152
 
153
+ def test_diagnostics_path_allows_print(self) -> None:
154
+ tree = ast.parse(CLI_ENTRY_POINT_WITH_PRINTS)
155
+ assert check_print_in_production(tree, "theme_exports/tools/diagnostics/probe.py") == []
156
+
157
+ def test_windows_diagnostics_path_allows_print(self) -> None:
158
+ tree = ast.parse(CLI_ENTRY_POINT_WITH_PRINTS)
159
+ assert check_print_in_production(tree, "theme_exports\\tools\\diagnostics\\probe.py") == []
160
+
161
+ def test_near_miss_diagnostics_name_still_flagged(self) -> None:
162
+ tree = ast.parse(NEAR_MISS_WITH_PRINT)
163
+ assert len(check_print_in_production(tree, "run_diagnostics.py")) == 1
164
+
153
165
  def test_near_miss_name_still_flagged(self) -> None:
154
166
  tree = ast.parse(NEAR_MISS_WITH_PRINT)
155
167
  assert len(check_print_in_production(tree, "publicli.py")) == 1
@@ -180,6 +192,8 @@ class TestCrossSurfaceConsistency:
180
192
  "stp_preview/renderer.py",
181
193
  "publicli.py",
182
194
  "packages/foo/scripts/run_job.py",
195
+ "theme_exports/tools/diagnostics/probe.py",
196
+ "run_diagnostics.py",
183
197
  ):
184
198
  production_allows = check_print_in_production(tree, path) == []
185
199
  library_allows = check_library_print(source, path) == []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.6.0",
3
+ "version": "8.7.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {