claude-dev-env 2.28.1 → 2.30.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 (157) hide show
  1. package/.agents/agents/AGENTS.md +0 -31
  2. package/.agents/agents/clean-coder.md +1 -1
  3. package/.agents/agents/pr-description-writer.md +21 -6
  4. package/.agents/agents/reference/README.md +6 -0
  5. package/.agents/agents/reference/pr-description-illustrative-voice.md +45 -0
  6. package/.agents/agents/reference/pr-description-verification.md +19 -0
  7. package/.agents/agents/test_agent_frontmatter.py +3 -1
  8. package/.agents/agents/test_pr_description_writer_style.py +47 -0
  9. package/.agents/skills/AGENTS.md +0 -70
  10. package/.agents/skills/_shared/AGENTS.md +0 -44
  11. package/.agents/skills/_shared/advisor/AGENTS.md +0 -8
  12. package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -57
  13. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -8
  14. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -34
  15. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -23
  16. package/.agents/skills/autoconverge/AGENTS.md +0 -35
  17. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -15
  18. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -25
  19. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -15
  20. package/.agents/skills/everything-search/AGENTS.md +0 -16
  21. package/.agents/skills/fresh-branch/AGENTS.md +0 -13
  22. package/.agents/skills/grok-spawn/AGENTS.md +0 -27
  23. package/.agents/skills/orchestrator/.claude/CLAUDE.md +1 -0
  24. package/.agents/skills/orchestrator/AGENTS.md +1 -0
  25. package/.agents/skills/orchestrator/SKILL.md +44 -53
  26. package/.agents/skills/orchestrator/reference/.claude/CLAUDE.md +1 -0
  27. package/.agents/skills/orchestrator/reference/AGENTS.md +1 -0
  28. package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +70 -0
  29. package/.agents/skills/orchestrator/reference/executor-consult-block.md +62 -0
  30. package/.agents/skills/orchestrator/reference/host-detect.md +15 -0
  31. package/.agents/skills/orchestrator/test_orchestrator_skill_contract.py +62 -0
  32. package/.agents/skills/orchestrator-refresh/SKILL.md +16 -34
  33. package/.agents/skills/rebase/AGENTS.md +0 -31
  34. package/.agents/skills/session-log/AGENTS.md +0 -31
  35. package/.agents/skills/session-tidy/AGENTS.md +0 -35
  36. package/.agents/skills/skill-builder/AGENTS.md +0 -48
  37. package/.agents/skills/skill-builder/references/AGENTS.md +0 -24
  38. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -13
  39. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -18
  40. package/.agents/skills/task-build/AGENTS.md +0 -28
  41. package/.agents/skills/update/AGENTS.md +0 -37
  42. package/.agents/skills-archived/AGENTS.md +0 -44
  43. package/.agents/skills-archived/anthropic-plan/AGENTS.md +0 -33
  44. package/.agents/skills-archived/anthropic-plan/scripts/AGENTS.md +0 -10
  45. package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -15
  46. package/.agents/skills-archived/anthropic-plan/templates/AGENTS.md +0 -12
  47. package/.agents/skills-archived/anthropic-plan/workflow/AGENTS.md +0 -13
  48. package/.agents/skills-archived/auditing-claude-config/AGENTS.md +0 -20
  49. package/.agents/skills-archived/bugteam/AGENTS.md +0 -29
  50. package/.agents/skills-archived/bugteam/reference/AGENTS.md +0 -19
  51. package/.agents/skills-archived/bugteam/reference/obstacles/AGENTS.md +0 -23
  52. package/.agents/skills-archived/bugteam/scripts/AGENTS.md +0 -29
  53. package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -17
  54. package/.agents/skills-archived/codex-review/AGENTS.md +0 -45
  55. package/.agents/skills-archived/codex-review/reference/AGENTS.md +0 -14
  56. package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
  57. package/.agents/skills-archived/codex-review/test_skill_scaffold.py +13 -3
  58. package/.agents/skills-archived/copilot-review/AGENTS.md +0 -17
  59. package/.agents/skills-archived/pr-converge/AGENTS.md +0 -31
  60. package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/AGENTS.md +0 -25
  61. package/.agents/skills-archived/pr-converge/reference/AGENTS.md +0 -27
  62. package/.agents/skills-archived/pr-converge/reference/obstacles/AGENTS.md +0 -22
  63. package/.agents/skills-archived/pr-converge/scripts/AGENTS.md +0 -45
  64. package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
  65. package/.agents/skills-archived/pr-converge/workflows/AGENTS.md +0 -15
  66. package/.agents/skills-archived/recall/AGENTS.md +0 -29
  67. package/.agents/skills-archived/remember/AGENTS.md +0 -30
  68. package/AGENTS.md +0 -112
  69. package/_shared/AGENTS.md +0 -16
  70. package/_shared/advisor/AGENTS.md +0 -21
  71. package/_shared/advisor/advisor-protocol.md +4 -1
  72. package/_shared/advisor/reference/consult-format.md +4 -2
  73. package/_shared/pr-loop/AGENTS.md +0 -27
  74. package/_shared/pr-loop/code-rules-gate.md +1 -1
  75. package/_shared/pr-loop/scripts/AGENTS.md +0 -49
  76. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -2
  77. package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +0 -41
  78. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +8 -8
  79. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +2 -2
  80. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +1 -1
  81. package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
  82. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
  83. package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +0 -25
  84. package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +0 -25
  85. package/_shared/pr-loop/scripts/tests/AGENTS.md +0 -43
  86. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +15 -15
  87. package/_shared/process-tree/AGENTS.md +0 -40
  88. package/audit-rubrics/AGENTS.md +0 -42
  89. package/audit-rubrics/category_rubrics/AGENTS.md +0 -36
  90. package/audit-rubrics/prompts/AGENTS.md +0 -36
  91. package/bin/AGENTS.md +0 -118
  92. package/bin/install.test.mjs +1 -1
  93. package/commands/AGENTS.md +0 -14
  94. package/docs/AGENTS.md +0 -31
  95. package/docs/CODE_RULES.md +10 -0
  96. package/docs/references/AGENTS.md +0 -16
  97. package/hooks/AGENTS.md +0 -27
  98. package/hooks/advisory/AGENTS.md +0 -15
  99. package/hooks/blocking/AGENTS.md +0 -112
  100. package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +0 -27
  101. package/hooks/blocking/code_rules_enforcer.py +433 -313
  102. package/hooks/blocking/config/AGENTS.md +0 -9
  103. package/hooks/blocking/inventory_intent_records/AGENTS.md +0 -25
  104. package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +0 -25
  105. package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +0 -23
  106. package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +0 -29
  107. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +1 -1
  108. package/hooks/blocking/test_code_rules_enforcer_class_docstring_methods.py +1 -1
  109. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +117 -22
  110. package/hooks/blocking/test_code_rules_enforcer_docstring_args_signature.py +1 -1
  111. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +1 -1
  112. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +1 -1
  113. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +1 -1
  114. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +1 -1
  115. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +1 -1
  116. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +1 -1
  117. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +2 -2
  118. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +3 -3
  119. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +1 -1
  120. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +1 -1
  121. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +117 -38
  122. package/hooks/blocking/test_code_rules_enforcer_output_snapshot.py +121 -0
  123. package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +17 -15
  124. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +4 -2
  125. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +1 -1
  126. package/hooks/blocking/test_code_rules_enforcer_stage.py +1 -1
  127. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +4 -4
  128. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +2 -2
  129. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +2 -2
  130. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -2
  131. package/hooks/blocking/test_code_rules_validation_phase.py +281 -0
  132. package/hooks/blocking/test_fable_spawn_gate.py +1 -1
  133. package/hooks/blocking/test_pre_tool_use_dispatcher.py +26 -11
  134. package/hooks/git-hooks/AGENTS.md +0 -31
  135. package/hooks/git-hooks/git_hooks_constants/AGENTS.md +0 -20
  136. package/hooks/hooks_constants/AGENTS.md +0 -99
  137. package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
  138. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +21 -0
  139. package/hooks/hooks_constants/validation_phase_constants.py +34 -0
  140. package/hooks/lifecycle/AGENTS.md +0 -17
  141. package/hooks/observability/AGENTS.md +0 -19
  142. package/hooks/session/AGENTS.md +0 -37
  143. package/hooks/validation/AGENTS.md +0 -19
  144. package/hooks/validators/AGENTS.md +0 -51
  145. package/hooks/validators/README.md +1 -1
  146. package/hooks/validators/config/hook_timing_harness_constants.py +16 -0
  147. package/hooks/validators/hook_timing_harness.py +287 -0
  148. package/hooks/validators/test_hook_timing_harness.py +251 -0
  149. package/hooks/workflow/AGENTS.md +0 -15
  150. package/output-styles/AGENTS.md +0 -14
  151. package/package.json +1 -1
  152. package/rules/AGENTS.md +0 -55
  153. package/scripts/AGENTS.md +0 -55
  154. package/scripts/dev_env_scripts_constants/AGENTS.md +0 -20
  155. package/scripts/sync_to_cursor/AGENTS.md +0 -22
  156. package/scripts/tests/AGENTS.md +0 -34
  157. package/system-prompts/AGENTS.md +0 -24
@@ -0,0 +1,281 @@
1
+ """Tests for the validate_content edit-lane and full-gate phase split."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import importlib
7
+ import importlib.util
8
+ import inspect
9
+ import re
10
+ import shutil
11
+ import sys
12
+ import tempfile
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+ from unittest.mock import DEFAULT, MagicMock, patch
16
+
17
+ import pytest
18
+
19
+
20
+ def _load_module_from_path(module_name: str, module_path: Path) -> ModuleType:
21
+ """Load and execute the module found at module_path, under module_name."""
22
+ specification = importlib.util.spec_from_file_location(module_name, module_path)
23
+ assert specification is not None
24
+ assert specification.loader is not None
25
+ module = importlib.util.module_from_spec(specification)
26
+ specification.loader.exec_module(module)
27
+ return module
28
+
29
+
30
+ _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
31
+ _HOOKS_DIRECTORY = _BLOCKING_DIRECTORY.parent
32
+ _PACKAGE_ROOT = _HOOKS_DIRECTORY.parent
33
+ _PR_LOOP_SCRIPTS_DIRECTORY = str(_PACKAGE_ROOT / "_shared" / "pr-loop" / "scripts")
34
+
35
+ code_rules_enforcer = _load_module_from_path(
36
+ "code_rules_enforcer", _BLOCKING_DIRECTORY / "code_rules_enforcer.py"
37
+ )
38
+ validation_phase_constants = _load_module_from_path(
39
+ "validation_phase_constants",
40
+ _HOOKS_DIRECTORY / "hooks_constants" / "validation_phase_constants.py",
41
+ )
42
+
43
+ if _PR_LOOP_SCRIPTS_DIRECTORY not in sys.path:
44
+ sys.path.insert(0, _PR_LOOP_SCRIPTS_DIRECTORY)
45
+ enforcer_loading = importlib.import_module("code_rules_gate_parts.enforcer_loading")
46
+ gate_running = importlib.import_module("code_rules_gate_parts.gate_running")
47
+ repo_test_helpers = importlib.import_module("code_rules_gate_parts.tests._repo_test_helpers")
48
+
49
+ PRODUCTION_FILE_PATH = "packages/app/services.py"
50
+ SAMPLE_PYTHON_CONTENT = "def add_one(value: int) -> int:\n return value + 1\n"
51
+ UNKNOWN_PHASE_NAME = "not_a_real_phase"
52
+ HOOK_INFRASTRUCTURE_FILE_PATH = "/repo/packages/claude-dev-env/hooks/blocking/example.py"
53
+ _HOOK_MODULE_DUPLICATE_BODY_SOURCE = (
54
+ "def compute_shipping_total(order_total: float, tax_rate: float) -> float:\n"
55
+ " taxed_total = order_total * (1 + tax_rate)\n"
56
+ " rounded_total = round(taxed_total, 2)\n"
57
+ " return rounded_total\n"
58
+ )
59
+
60
+
61
+ def test_all_validation_phases_holds_exactly_the_two_phases() -> None:
62
+ assert validation_phase_constants.ALL_VALIDATION_PHASES == {
63
+ validation_phase_constants.EDIT_LANE_PHASE,
64
+ validation_phase_constants.FULL_GATE_PHASE,
65
+ }
66
+
67
+
68
+ def test_unknown_phase_raises_value_error_naming_the_rejected_value() -> None:
69
+ with pytest.raises(ValueError, match=UNKNOWN_PHASE_NAME):
70
+ code_rules_enforcer.validate_content_for_phase(
71
+ SAMPLE_PYTHON_CONTENT, PRODUCTION_FILE_PATH, phase=UNKNOWN_PHASE_NAME
72
+ )
73
+
74
+
75
+ def test_validate_content_for_phase_requires_phase_keyword() -> None:
76
+ with pytest.raises(TypeError):
77
+ code_rules_enforcer.validate_content_for_phase(SAMPLE_PYTHON_CONTENT, PRODUCTION_FILE_PATH)
78
+
79
+
80
+ def _full_gate_only_default_kwargs() -> dict[str, object]:
81
+ return {
82
+ each_name: DEFAULT
83
+ for each_name in validation_phase_constants.ALL_FULL_GATE_ONLY_CHECK_NAMES
84
+ }
85
+
86
+
87
+ def _patched_full_gate_only_checks() -> contextlib.AbstractContextManager[dict[str, MagicMock]]:
88
+ """Return the patch.multiple context manager for the six full-gate-only checks.
89
+
90
+ ::
91
+
92
+ mypy's patch.multiple stub only accepts an ``Any | str`` target, so a
93
+ dynamically loaded ``ModuleType`` target needs a local type: ignore.
94
+ """
95
+ return patch.multiple( # type: ignore[call-overload] # mypy stub rejects ModuleType targets
96
+ code_rules_enforcer, **_full_gate_only_default_kwargs()
97
+ )
98
+
99
+
100
+ def _configure_full_gate_only_mock_returns(all_mocks: dict[str, MagicMock]) -> None:
101
+ for each_name, each_mock in all_mocks.items():
102
+ each_mock.return_value = None if each_name == "advise_cross_skill_duplicate_helper" else []
103
+
104
+
105
+ def test_edit_lane_calls_none_of_the_six_full_gate_only_checks() -> None:
106
+ with _patched_full_gate_only_checks() as all_mocks:
107
+ _configure_full_gate_only_mock_returns(all_mocks)
108
+ code_rules_enforcer.validate_content_for_edit_lane(
109
+ SAMPLE_PYTHON_CONTENT, PRODUCTION_FILE_PATH
110
+ )
111
+ for each_name, each_mock in all_mocks.items():
112
+ assert each_mock.call_count == 0, f"edit lane must not call {each_name}"
113
+
114
+
115
+ def test_full_gate_calls_all_six_full_gate_only_checks() -> None:
116
+ with _patched_full_gate_only_checks() as all_mocks:
117
+ _configure_full_gate_only_mock_returns(all_mocks)
118
+ code_rules_enforcer.validate_content_for_full_gate(
119
+ SAMPLE_PYTHON_CONTENT, PRODUCTION_FILE_PATH
120
+ )
121
+ for each_name, each_mock in all_mocks.items():
122
+ assert each_mock.call_count >= 1, f"full gate must call {each_name}"
123
+
124
+
125
+ def test_both_phases_report_identical_findings_with_the_six_spied_out() -> None:
126
+ with _patched_full_gate_only_checks() as all_mocks:
127
+ _configure_full_gate_only_mock_returns(all_mocks)
128
+ edit_lane_issues = code_rules_enforcer.validate_content_for_edit_lane(
129
+ SAMPLE_PYTHON_CONTENT, PRODUCTION_FILE_PATH
130
+ )
131
+ full_gate_issues = code_rules_enforcer.validate_content_for_full_gate(
132
+ SAMPLE_PYTHON_CONTENT, PRODUCTION_FILE_PATH
133
+ )
134
+ assert edit_lane_issues == full_gate_issues
135
+
136
+
137
+ def test_no_caller_should_reach_a_phase_less_validator() -> None:
138
+ assert not hasattr(code_rules_enforcer, "validate_content"), (
139
+ "validate_content must not survive as a callable on the enforcer module"
140
+ )
141
+ enforcer_source = inspect.getsource(code_rules_enforcer)
142
+ assert re.search(r"\bvalidate_content\(", enforcer_source) is None, (
143
+ "no call site in code_rules_enforcer.py may reach a phase-less validator"
144
+ )
145
+
146
+
147
+ def test_precheck_selects_the_full_gate() -> None:
148
+ precheck_source = inspect.getsource(code_rules_enforcer._run_precheck)
149
+ assert "validate_content_for_full_gate(" in precheck_source, (
150
+ "the --check precheck must run the full-gate verdict"
151
+ )
152
+
153
+
154
+ def test_codex_patch_issues_selects_the_edit_lane() -> None:
155
+ """Pin the apply_patch mutation path to the edit-lane verdict."""
156
+ codex_patch_issues_source = inspect.getsource(code_rules_enforcer._codex_patch_issues)
157
+ assert "validate_content_for_edit_lane(" in codex_patch_issues_source, (
158
+ "the apply_patch branch must run the edit-lane verdict"
159
+ )
160
+
161
+
162
+ def test_forecast_full_file_violations_selects_the_edit_lane() -> None:
163
+ """Pin the full-file forecast pass to the edit-lane verdict."""
164
+ forecast_source = inspect.getsource(code_rules_enforcer._forecast_full_file_violations)
165
+ assert "validate_content_for_edit_lane(" in forecast_source, (
166
+ "the full-file forecast pass must run the edit-lane verdict"
167
+ )
168
+
169
+
170
+ def test_report_blocking_violations_selects_the_edit_lane() -> None:
171
+ """Pin the PreToolUse Write and Edit main path to the edit-lane verdict."""
172
+ report_blocking_violations_source = inspect.getsource(
173
+ code_rules_enforcer._report_blocking_violations
174
+ )
175
+ assert "validate_content_for_edit_lane(" in report_blocking_violations_source, (
176
+ "the PreToolUse Write and Edit main path must run the edit-lane verdict"
177
+ )
178
+
179
+
180
+ def _write_scoped_service_file_with_a_committed_baseline(repository_root: Path) -> Path:
181
+ repo_test_helpers.init_repository(repository_root)
182
+ return repo_test_helpers.write_commit_and_stage_change(
183
+ repository_root,
184
+ "service.py",
185
+ "def read_port() -> int:\n return 0\n",
186
+ "def read_port() -> int:\n port_number = 9999\n return port_number\n",
187
+ )
188
+
189
+
190
+ def test_hook_infrastructure_edit_lane_skips_the_cross_file_duplicate_check() -> None:
191
+ with patch.object(
192
+ code_rules_enforcer, "check_duplicate_function_body_across_files"
193
+ ) as mock_check_duplicate_across_files:
194
+ mock_check_duplicate_across_files.return_value = []
195
+ code_rules_enforcer._hook_infrastructure_blocking_issues(
196
+ SAMPLE_PYTHON_CONTENT,
197
+ HOOK_INFRASTRUCTURE_FILE_PATH,
198
+ phase=validation_phase_constants.EDIT_LANE_PHASE,
199
+ )
200
+ assert mock_check_duplicate_across_files.call_count == 0, (
201
+ "the hook-infrastructure edit lane must not call the cross-file duplicate-body check"
202
+ )
203
+
204
+
205
+ def test_hook_infrastructure_full_gate_calls_the_cross_file_duplicate_check() -> None:
206
+ with patch.object(
207
+ code_rules_enforcer, "check_duplicate_function_body_across_files"
208
+ ) as mock_check_duplicate_across_files:
209
+ mock_check_duplicate_across_files.return_value = []
210
+ code_rules_enforcer._hook_infrastructure_blocking_issues(
211
+ SAMPLE_PYTHON_CONTENT,
212
+ HOOK_INFRASTRUCTURE_FILE_PATH,
213
+ phase=validation_phase_constants.FULL_GATE_PHASE,
214
+ )
215
+ assert mock_check_duplicate_across_files.call_count == 1, (
216
+ "the hook-infrastructure full gate must still call the cross-file duplicate-body check"
217
+ )
218
+
219
+
220
+ def _write_duplicate_helper_pair_under_a_non_test_named_tempdir() -> tuple[Path, str]:
221
+ """Write a hook-infra sibling pair under a ``tempfile.mkdtemp()`` root.
222
+
223
+ ``tempfile.mkdtemp()`` names its directory ``tmp<random>``, carrying no
224
+ ``test_`` segment, unlike pytest's own ``tmp_path`` fixture — whose
225
+ directory embeds the test's name and so reads as a test path to
226
+ ``is_test_file``'s full-path substring match, silently exempting every
227
+ check the caller means to exercise.
228
+ """
229
+ base_directory = Path(tempfile.mkdtemp())
230
+ hook_blocking_directory = base_directory / "packages" / "claude-dev-env" / "hooks" / "blocking"
231
+ hook_blocking_directory.mkdir(parents=True)
232
+ (hook_blocking_directory / "shipping_helper.py").write_text(
233
+ _HOOK_MODULE_DUPLICATE_BODY_SOURCE, encoding="utf-8"
234
+ )
235
+ return base_directory, str(hook_blocking_directory / "shipping_target.py")
236
+
237
+
238
+ def test_full_gate_still_catches_a_helper_duplicated_across_two_hook_modules() -> None:
239
+ base_directory, target_path = _write_duplicate_helper_pair_under_a_non_test_named_tempdir()
240
+ try:
241
+ full_gate_issues = code_rules_enforcer._hook_infrastructure_blocking_issues(
242
+ _HOOK_MODULE_DUPLICATE_BODY_SOURCE,
243
+ target_path,
244
+ phase=validation_phase_constants.FULL_GATE_PHASE,
245
+ )
246
+ edit_lane_issues = code_rules_enforcer._hook_infrastructure_blocking_issues(
247
+ _HOOK_MODULE_DUPLICATE_BODY_SOURCE,
248
+ target_path,
249
+ phase=validation_phase_constants.EDIT_LANE_PHASE,
250
+ )
251
+ finally:
252
+ shutil.rmtree(base_directory, ignore_errors=False)
253
+
254
+ assert any("duplicates" in each_issue for each_issue in full_gate_issues), (
255
+ "the full gate must still catch a helper duplicated across two hook "
256
+ f"modules, got: {full_gate_issues!r}"
257
+ )
258
+ assert not any("duplicates" in each_issue for each_issue in edit_lane_issues), (
259
+ "the edit lane must not run the cross-file duplicate-body check against "
260
+ f"hook infrastructure, got: {edit_lane_issues!r}"
261
+ )
262
+
263
+
264
+ def test_gate_running_selects_the_full_gate(tmp_path: Path) -> None:
265
+ repository_root = tmp_path / "repo"
266
+ repository_root.mkdir()
267
+ file_path = _write_scoped_service_file_with_a_committed_baseline(repository_root)
268
+
269
+ loaded_validate_content = enforcer_loading.load_validate_content_for_full_gate()
270
+ assert loaded_validate_content.__name__ == "validate_content_for_full_gate"
271
+
272
+ scoped_violations = gate_running._scoped_violations_for_file(
273
+ validate_content=loaded_validate_content,
274
+ resolved_path=file_path,
275
+ repository_root=repository_root,
276
+ all_added_lines_for_file={2, 3},
277
+ should_read_staged_content=True,
278
+ )
279
+ assert scoped_violations is not None
280
+ blocking_violations, _advisory_violations = scoped_violations
281
+ assert any("9999" in each_issue for each_issue in blocking_violations)
@@ -72,7 +72,7 @@ _ADVISOR_LIFECYCLE_TEXT = (_ADVISOR_REFERENCE_DIR / "lifecycle.md").read_text(
72
72
  _ADVISOR_WARM_UP_TEXT = (_ADVISOR_REFERENCE_DIR / "warm-up.md").read_text(
73
73
  encoding="utf-8"
74
74
  )
75
- _ALL_CONSUMING_SKILL_NAMES = ("team-advisor", "orchestrator", "orchestrator-refresh")
75
+ _ALL_CONSUMING_SKILL_NAMES = ("team-advisor",)
76
76
  _ALL_CONSUMING_SKILL_PATHS = tuple(
77
77
  _PACKAGE_ROOT / ".agents" / "skills" / each_skill_name / "SKILL.md"
78
78
  for each_skill_name in _ALL_CONSUMING_SKILL_NAMES
@@ -734,16 +734,31 @@ def test_dispatcher_multi_edit_applies_only_group_b() -> None:
734
734
  )
735
735
 
736
736
 
737
- def test_proceed_after_run_all_validators_removal_allows() -> None:
738
- """The PreToolUse dispatcher allows a Python edit that the removed gate would have processed.
739
-
740
- The inline run_all_validators runner was a PostToolUse gate removed in Stage 4;
741
- it was never a PreToolUse hook and never hosted by the PreToolUse dispatcher.
742
- A Python Write payload that run_all_validators would have flagged (mypy errors, for
743
- instance) still produces ALLOW from the PreToolUse dispatcher because the PreToolUse
744
- dispatcher covers only its 23 hosted blocking hooks — none of which includes the
745
- validators runner.
737
+ def _assert_roster_names_no_run_all_validators_entry() -> None:
738
+ """Assert no hosted-roster ``script_relative_path`` names run_all_validators."""
739
+ all_roster_script_paths = {
740
+ each_entry.script_relative_path for each_entry in ALL_HOSTED_HOOK_ENTRIES
741
+ }
742
+ assert not any(
743
+ "run_all_validators" in each_script_path
744
+ for each_script_path in all_roster_script_paths
745
+ ), (
746
+ "ALL_HOSTED_HOOK_ENTRIES must name no run_all_validators entry, got: "
747
+ f"{sorted(all_roster_script_paths)}"
748
+ )
749
+
750
+
751
+ def test_run_all_validators_is_not_hosted_by_pre_tool_use_dispatcher() -> None:
752
+ """The main PreToolUse dispatcher's hosted roster names no run_all_validators entry.
753
+
754
+ ``hooks.json`` registers the ``run_all_validators`` runner as its own
755
+ PreToolUse ``Write|Edit`` command, separate from the main dispatcher. A
756
+ Python Write payload that ``run_all_validators`` would flag (a type error,
757
+ for instance) still produces ALLOW from the main dispatcher, because its
758
+ roster carries no entry for that runner.
746
759
  """
760
+ _assert_roster_names_no_run_all_validators_entry()
761
+
747
762
  python_content_with_type_error = (
748
763
  "def add_one(value: int) -> int:\n"
749
764
  " return value + 1\n\n\n"
@@ -754,8 +769,8 @@ def test_proceed_after_run_all_validators_removal_allows() -> None:
754
769
  is_deny, _reason = _parse_hook_decision(dispatcher_result)
755
770
  assert not is_deny, (
756
771
  "PreToolUse dispatcher must allow a Python Write with a type error — "
757
- "mypy validation is PostToolUse-only; the removed run_all_validators gate "
758
- "was never a PreToolUse hook"
772
+ "run_all_validators runs as its own separate PreToolUse command, not "
773
+ "through this dispatcher's roster"
759
774
  )
760
775
  assert dispatcher_result.returncode == 0, (
761
776
  f"Dispatcher must exit 0, got {dispatcher_result.returncode}"
@@ -1,32 +1 @@
1
- # hooks/git-hooks
2
1
 
3
- Native git hooks that run outside the Claude Code lifecycle — invoked directly by git at commit and push time. The installer copies these scripts into the user's shared git-hooks directory (`core.hooksPath`).
4
-
5
- ## Key files
6
-
7
- | File | Git hook | What it does |
8
- |---|---|---|
9
- | `pre_commit.py` | `pre-commit` | Runs immediate CODE_RULES and terminology validation over staged changes; exits 1 when any staged file has a blocking violation. CI runs package tests for package Python changes. |
10
- | `pre_push.py` | `pre-push` | Blocks a push that would land a non-`main` local branch onto remote `main` (or `master`), then runs the CODE_RULES gate. An existing branch's gate base is the merge base with the remote default branch; the gate process still diffs that base against checkout HEAD, so the surface matches the pushed tip only when HEAD is that tip. |
11
- | `pre_push_base_reference.py` | — | Resolves a usable gate base for `pre_push.py`: reads the pushed remote name from git's arguments, then turns a symbolic default-branch head into a reference that git can resolve |
12
- | `post_commit.py` | `post-commit` | Runs bookkeeping, then best-effort reminds the agent about its PR |
13
- | `pull_request_handoff.py` | — | Prints the full PR title and description handoff prompt |
14
- | `gate_utils.py` | — | Shared helpers: resolves the gate script path, checks that the path is a safe regular file |
15
- | `test_config.py` | — | Test configuration helpers |
16
- | `test_gate_utils.py` | — | Tests for `gate_utils.py` |
17
- | `test_pre_commit.py` | — | Tests for `pre_commit.py` |
18
- | `test_pre_push.py` | — | Tests for `pre_push.py` |
19
- | `test_pre_push_base_reference.py` | — | Tests for the gate base resolution in `pre_push.py` |
20
- | `conftest.py` | — | Puts this directory on `sys.path` and clears cached `config` modules, so each test module imports the hooks at the top of the file |
21
-
22
- ## Subdirectory
23
-
24
- | Directory | Role |
25
- |---|---|
26
- | `git_hooks_constants/` | Shared constants imported by the git-hook scripts |
27
-
28
- ## Conventions
29
-
30
- - The installer strips the `_` and `.py` suffix when copying into the live git-hooks path (e.g. `pre_commit.py` becomes `pre-commit`).
31
- - Constants (exit codes, argument names, error messages) live in `git_hooks_constants/` and are imported at the top of each script.
32
- - Run tests with `python -m pytest git-hooks/test_<name>.py`.
@@ -1,21 +1 @@
1
- # hooks/git-hooks/git_hooks_constants
2
1
 
3
- Shared constants imported by the git-hook scripts in `git-hooks/`. Centralizes exit codes, argument names, and error messages so every tunable lives in one place.
4
-
5
- ## Files
6
-
7
- | File | Contents |
8
- |---|---|
9
- | `__init__.py` | Exports all constants; marks this as a package so `from git_hooks_constants import ...` resolves |
10
-
11
- ## Key constants (defined in `__init__.py`)
12
-
13
- - `GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE` — exit code when the gate script cannot be found or launched
14
- - `GATE_SCRIPT_NOT_FOUND_MESSAGE` — error message when the gate script path does not exist
15
- - `INVOKE_GATE_FAILURE_MESSAGE` — error message when the gate subprocess fails to start
16
- - `STAGED_SCOPE_ARGUMENT` — CLI argument passed to the gate script to scope it to staged changes
17
-
18
- ## Conventions
19
-
20
- - Import with `from git_hooks_constants import <CONSTANT>` from within the `git-hooks/` directory.
21
- - Add new constants here rather than inline in the hook scripts.
@@ -1,100 +1 @@
1
- # hooks/hooks_constants
2
1
 
3
- Shared constant modules imported by hooks throughout the `hooks/` tree. Each file holds the tunables for one hook or one cross-cutting concern, keeping magic values out of the hook scripts themselves.
4
-
5
- ## Files
6
-
7
- | File | What it holds |
8
- |---|---|
9
- | `__init__.py` | Package marker (`# pragma: no-tdd-gate`) |
10
- | `any_type_config.py` | Config for the `Any`-type escape-hatch check |
11
- | `ask_user_question_shape.py` | Pure AskUserQuestion lean-block shape analyzer: grades question and option-description prose against shared chat-detail markers and sentence/word caps with no hook I/O |
12
- | `banned_identifiers_constants.py` | The set of banned short identifiers and banned function-name prefixes |
13
- | `bash_pre_tool_use_dispatcher_constants.py` | Permission outcomes, tool-name sets, and the ordered hosted-hook roster for the Bash PreToolUse dispatcher (also covers PowerShell-shared gates) |
14
- | `pre_tool_use_dispatcher_constants.py` | Shared Write, Edit, and MultiEdit tool-name set and the ordered hosted-hook roster for the Write/Edit/MultiEdit PreToolUse dispatcher |
15
- | `blocking_check_limits.py` | Max issue counts and preview lengths for blocking hooks |
16
- | `bot_mention_comment_blocker_constants.py` | Patterns for detecting bot @-mentions in PR comments |
17
- | `claude_md_orphan_file_blocker_constants.py` | Table patterns, file extensions, scan budget, and block-message text for the CLAUDE.md orphan-file blocker |
18
- | `code_rules_enforcer_constants.py` | File-extension sets, test-path patterns, advisory line thresholds, boolean-name prefixes |
19
- | `enter_worktree_prefetch_constants.py` | Tool name, origin-remote ref names, and git-command timeouts for the EnterWorktree origin-prefetch hook |
20
- | `env_var_table_code_drift_constants.py` | Table patterns, env-var-name and code-file recognizers, scan budget, and block-message text for the env-var-table code-drift blocker |
21
- | `code_rules_path_utils_constants.py` | Path-matching helpers used by the code-rules check modules |
22
- | `command_dispatch_constants.py` | Command-word regex, command-key access pattern, tokenization pattern, and anchors for the unanchored command-dispatch meta-gate |
23
- | `conventional_pr_title_gate_constants.py` | Bash tool name, gh executable basenames and pr create/edit subcommand tokens, title/repo flag names, semantic-title-CI workflow markers, the Conventional Commits type list and title pattern, the semantic-pull-request action `types:` input pattern, and block-message text for the conventional-PR-title gate |
24
- | `cursor_cli_python_misfire_blocker_constants.py` | Detection patterns and deny message for Cursor launches that mistreat a Python code-rules-gate script |
25
- | `destructive_command_environment_constants.py` | Environment keys and shared truthy values used by destructive-command hooks |
26
- | `destructive_command_segment_constants.py` | The list of destructive shell command patterns the blocker matches |
27
- | `docstring_rule_gate_count_blocker_constants.py` | Target rule basename, spelled-out-number lookup, count-clause and `check_*` validator patterns, and block-message text for the docstring-rule gate-count staleness blocker |
28
- | `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
29
- | `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
30
- | `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
31
- | `fable_spawn_gate_constants.py` | The spawn tool-name set, the fable model alias and the model-id segment split pattern, the `FABLE-SPAWN-AUTHORIZED` authorization token, the advisor-protocol document path, payload field names, the deny message, and the deny `additionalContext` recovery text for the fable-tier spawn gate |
32
- | `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
33
- | `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
34
- | `harness_scratchpad_constants.py` | Fixed path components (`claude` / `claude-` user-directory name and prefix, `scratchpad` leaf name), the PreToolUse session-id payload key, and the `CLAUDE_CODE_SESSION_ID` environment variable name that the code-rules and TDD gates use to recognize an existing harness session scratchpad directory |
35
- | `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
36
- | `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
37
- | `hosted_hook_runner.py` | `run_hook_capturing_output()` — shared runner that runs one dispatcher-hosted hook in-process via runpy and returns its captured stdout and crash flag |
38
- | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
39
- | `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
40
- | `local_identity.py` | Loader for local machine values: NAS host, ssh user, and ssh port the ssh enforcer guards (`CLAUDE_NAS_*` / `~/.claude/local-identity.json` with placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`, keyed by owner/repo slug) read from the git-ignored local file whose path `CLAUDE_LOCAL_IDENTITY_PATH` may point elsewhere; also composes the ssh enforcer's two deny messages that quote the NAS values |
41
- | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
42
- | `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
43
- | `mypy_integration_constants.py` | Path markers (``.git``, ``.py``, ``pyproject.toml``) for mypy project-root resolution, plus detached-file `--follow-imports=skip` flags, subprocess timeout, and timeout skip message |
44
- | `mypy_validator_cache_constants.py` | Cache paths and tunables for the mypy_validator per-session caches |
45
- | `nas_ssh_binary_enforcer_constants.py` | Bash tool name, ssh-family basenames, OpenSSH binary path suffixes, and the batch-mode pattern for the NAS ssh binary enforcer; segment helpers come from `shell_command_segments.py` |
46
- | `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
47
- | `orphan_css_class_constants.py` | Scan radius and selector patterns for the orphan-CSS-class check |
48
- | `package_inventory_stale_blocker_constants.py` | Inventory document names, production code extensions, backtick token pattern, smallest inventory size, exempt names, scan budget, and block-message text for the package-inventory stale-entry blocker |
49
- | `paired_test_coverage_constants.py` | Test-directory name, stem-test filename affixes, test-file globs, exempt public-function names, scan budget, coverage threshold, and guidance text for the public-function paired-test coverage check |
50
- | `path_rewriter_constants.py` | Path rewriting patterns for the Everything-search path rewriter |
51
- | `pii_prevention_constants.py` | Patterns, allowlists, path exemptions, and deny-message text for `pii_prevention_blocker` |
52
- | `piped_pytest_blocker_constants.py` | Bash tool name, pytest program basenames and the python-interpreter basename pattern, the pipe and segment-reset operator token sets, and the deny message for the piped-pytest blocker; classification that consumes those program and wrapper constants lives in `pytest_invocation.py` |
53
- | `pytest_invocation.py` | Pure parsed-segment classifier for pytest runs through interpreters, shims, wrappers, and shell string-exec forms; no pipeline deny decision |
54
- | `prose_matcher_precision_constants.py` | Sample floor, keep/narrow precision floors, matcher ids, label decisions, advisory log path, emit caps, and context fingerprint length for OP-07B prose-matcher advisory telemetry |
55
- | `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
56
- | `precommit_code_rules_gate_constants.py` | Git-repository-root resolution command and timeout for the `precommit_code_rules_gate` library module |
57
- | project_paths_reader.py | Loads ~/.claude/project-paths.json and finds a home-bounded git root for registry checks |
58
- | `pyproject_config_discovery_constants.py` | Table names (``tool`` key, ``mypy``, ``ruff``) for resolving a validator's config from a pyproject.toml `[tool.<name>]` table |
59
- | `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
60
- | `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
61
- | `ruff_integration_constants.py` | ``NO_COLOR`` / ``FORCE_COLOR`` environment variable names for plain ruff diagnostics |
62
- | `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the `Invoke-Item` block message for the open-locally attach blocker |
63
- | `sensitive_file_protector_constants.py` | Sensitive filename patterns, committed-template suffixes that earn an exemption, and the deny decision and message template for `sensitive_file_protector` |
64
- | `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
65
- | `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
66
- | `session_start_injector.py` | Pure SessionStart context injector: normalize `source` (`startup`/`resume`/`clear`/`compact`), honor enable/timeout, return structured injection results for starter hooks |
67
- | `session_start_injector_constants.py` | Known SessionStart source values, injection status codes, enable env var, default timeout for the shared injector |
68
- | `test_session_start_injector.py` | Behavioral tests for every known source, disabled, timeout, and unknown-source paths |
69
- | `orchestrator_auto_starter_constants.py` | Opt-in env var, enable values, timeout, and SessionStart directive text for the orchestrator auto-starter |
70
- | `issue_tracker_session_starter_constants.py` | Opt-in env var, enable values, timeout, and SessionStart directive text for the repository-gated issue-tracker starter |
71
- | `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
72
- | `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
73
- | `shell_command_pipeline.py` | Quote-aware shell pipeline parsing: operator-paired segments after line-continuation join, comment strip, heredoc body drop, and parenthesis-group join |
74
- | `shell_command_segments.py` | Shell tokenization helpers shared by the segment-walking blockers: segment splitting on control operators, a segment's effective leading program, and a token's basename |
75
- | `shell_substitution_blocker_constants.py` | Tool name, payload keys, the four substitution-detection patterns, the deny decision shape, and the corrective message for the shell-substitution blocker |
76
- | `stale_comment_reference_blocker_constants.py` | Identifier pattern, comment stopwords, and denial text for the stale-comment-reference blocker |
77
- | `state_description_blocker_constants.py` | The set of historical/comparative phrases the state-description blocker rejects, plus the docstring-extraction and mention-span patterns for its Python docstring scan |
78
- | `stop_dispatcher_constants.py` | Ordered hosted-hook roster and Stop block payload field names for the Stop-chain dispatcher |
79
- | `style_reminder_prompt_constants.py` | The style reminder text ("small words. few words. always. forever.") for the style_reminder_prompt hook |
80
- | `stuttering_check_config.py` | Config for the stuttering (repeated-phrase) check |
81
- | `stuttering_import_binding_constants.py` | Import-binding patterns for the stuttering check |
82
- | `subprocess_budget_completeness_constants.py` | Required argument names for the subprocess-budget completeness check |
83
- | `sys_path_insert_constants.py` | Patterns for detecting unguarded `sys.path.insert` calls |
84
- | `task_list_loop_starter_constants.py` | The one-line task-list instruction and the full session-start directive text for the task-list loop starter hook |
85
- | `test_layout_constants.py` | The named constants for the test-layout write-time checks |
86
- | `text_stripping.py` | `strip_code_and_quotes()` — shared helper that removes fenced code blocks, inline code, and blockquotes from prose, imported by the Stop-hook prose blockers |
87
- | `unscoped_search_blocker_constants.py` | Tool names, `find` and listing program basenames, Get-ChildItem option sets, unscoped-root patterns, the flag/value token stride, and the deny message for the unscoped-search blocker |
88
- | `unused_module_import_constants.py` | Patterns for detecting unused module-level imports |
89
- | `volatile_path_in_post_blocker_constants.py` | Volatile path markers, affected `gh` post subcommands, MCP body param names, and the corrective message for the volatile-path post blocker |
90
- | `windows_rmtree_blocker_constants.py` | The unsafe `shutil.rmtree` pattern and the safe replacement pattern |
91
- | `workflow_substitution_slot_blocker_constants.py` | Per-iteration token patterns for the workflow-slot blocker |
92
- | `working_style_prompt_constants.py` | The fixed multi-paragraph working-style prompt text for the SessionStart working_style_prompt hook |
93
-
94
- ## Conventions
95
-
96
- - Most files in this package are pure constants modules — no side effects, no I/O.
97
- - Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
98
- - Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
99
- - `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, `hosted_hook_runner.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they hold logic shared across hook surfaces.
100
- - `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` (its path overridable via `CLAUDE_LOCAL_IDENTITY_PATH`) to resolve private NAS values (with committed placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`).
@@ -6,6 +6,14 @@ Extracted from code_rules_enforcer.py to satisfy the constants-location rule.
6
6
  import re
7
7
  import tokenize
8
8
 
9
+ from hooks_constants.validation_phase_constants import ( # noqa: F401 re-exported for code_rules_enforcer.py
10
+ ALL_HOOK_INFRASTRUCTURE_EDIT_LANE_CHECK_NAMES,
11
+ ALL_VALIDATION_PHASES,
12
+ EDIT_LANE_PHASE,
13
+ FULL_GATE_PHASE,
14
+ UNKNOWN_VALIDATION_PHASE_MESSAGE_TEMPLATE,
15
+ )
16
+
9
17
  ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS: tuple[type[BaseException], ...] = (
10
18
  tokenize.TokenError,
11
19
  IndentationError,
@@ -17,12 +17,17 @@ if str(_HOOKS_ROOT) not in sys.path:
17
17
  sys.path.insert(0, str(_HOOKS_ROOT))
18
18
 
19
19
  from hooks_constants.code_rules_enforcer_constants import (
20
+ ALL_HOOK_INFRASTRUCTURE_EDIT_LANE_CHECK_NAMES,
21
+ ALL_VALIDATION_PHASES,
20
22
  CONFIG_DIRECTORY_SEGMENT,
21
23
  CONSTANTS_MODULE_SUFFIX,
24
+ EDIT_LANE_PHASE,
25
+ FULL_GATE_PHASE,
22
26
  FUNCTION_WITH_JSDOC_PATTERN,
23
27
  JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN,
24
28
  RETURN_CALL_OPENING_PARENTHESIS_PATTERN,
25
29
  SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN,
30
+ UNKNOWN_VALIDATION_PHASE_MESSAGE_TEMPLATE,
26
31
  )
27
32
 
28
33
 
@@ -98,3 +103,19 @@ def test_schema_property_key_pattern_matches_only_the_options_key() -> None:
98
103
  def test_constants_module_suffix_and_config_directory_segment_are_defined() -> None:
99
104
  assert CONSTANTS_MODULE_SUFFIX == "_constants.py"
100
105
  assert CONFIG_DIRECTORY_SEGMENT == "config"
106
+
107
+
108
+ def test_validation_phase_constants_are_reexported_and_render() -> None:
109
+ assert ALL_VALIDATION_PHASES == {EDIT_LANE_PHASE, FULL_GATE_PHASE}
110
+ rendered_message = UNKNOWN_VALIDATION_PHASE_MESSAGE_TEMPLATE.format(
111
+ phase="bogus_phase", all_phases=sorted(ALL_VALIDATION_PHASES)
112
+ )
113
+ assert "bogus_phase" in rendered_message
114
+
115
+
116
+ def test_hook_infrastructure_edit_lane_check_names_are_reexported() -> None:
117
+ assert ALL_HOOK_INFRASTRUCTURE_EDIT_LANE_CHECK_NAMES == {
118
+ "check_same_file_inline_duplicate_body",
119
+ "check_zero_payload_function_alias",
120
+ "check_unanchored_command_dispatch",
121
+ }
@@ -0,0 +1,34 @@
1
+ """Constants for the validate_content edit-lane and full-gate phase split.
2
+
3
+ The edit lane and the full gate share every check except six that read
4
+ sibling modules from disk. These names identify each phase, name the checks
5
+ the full gate alone runs, name the hook-infrastructure checks the edit lane
6
+ still runs, and template the error for an unrecognized phase.
7
+ """
8
+
9
+ EDIT_LANE_PHASE: str = "edit_lane"
10
+ FULL_GATE_PHASE: str = "full_gate"
11
+ ALL_VALIDATION_PHASES: frozenset[str] = frozenset({EDIT_LANE_PHASE, FULL_GATE_PHASE})
12
+
13
+ ALL_FULL_GATE_ONLY_CHECK_NAMES: frozenset[str] = frozenset(
14
+ {
15
+ "check_duplicate_function_body_across_files",
16
+ "advise_cross_skill_duplicate_helper",
17
+ "check_public_function_missing_paired_test",
18
+ "check_test_file_omits_module_public_function",
19
+ "check_orphan_css_classes",
20
+ "check_config_duplicate_path_anchor",
21
+ }
22
+ )
23
+
24
+ ALL_HOOK_INFRASTRUCTURE_EDIT_LANE_CHECK_NAMES: frozenset[str] = frozenset(
25
+ {
26
+ "check_same_file_inline_duplicate_body",
27
+ "check_zero_payload_function_alias",
28
+ "check_unanchored_command_dispatch",
29
+ }
30
+ )
31
+
32
+ UNKNOWN_VALIDATION_PHASE_MESSAGE_TEMPLATE: str = (
33
+ "unrecognized validation phase {phase!r}; expected one of {all_phases}"
34
+ )
@@ -1,18 +1 @@
1
- # hooks/lifecycle
2
1
 
3
- Hooks that run at session or config-change boundaries rather than on individual tool calls.
4
-
5
- ## Key files
6
-
7
- | File | Event | What it does |
8
- |---|---|---|
9
- | `config_change_guard.py` | PostToolUse (Write/Edit on `settings.json`) | Counts hooks in the edited `settings.json` and logs any change to `~/.claude/cache/config-change-audit.log`; alerts when the hook count drops below the last known value |
10
- | `enter_worktree_origin_prefetch.py` | PreToolUse (EnterWorktree) | Fetches origin's default branch before a worktree creation call, keeping the `refs/remotes/origin/<default-branch>` ref that `fresh` mode reads current; never blocks on fetch failure |
11
- | `session_end_cleanup.py` | SessionEnd | Purges stale cache entries from `~/.claude/cache/` (entries older than the configured threshold) and old backup files |
12
- | `test_config_change_guard.py` | — | Tests for `config_change_guard.py` |
13
- | `test_enter_worktree_origin_prefetch.py` | — | Tests for `enter_worktree_origin_prefetch.py` |
14
-
15
- ## Conventions
16
-
17
- - Constants for these hooks (stale-age threshold, cache directory, known-hook-count file, EnterWorktree prefetch tuning) live in `hooks_constants/session_env_cleanup_constants.py` and `hooks_constants/enter_worktree_prefetch_constants.py`.
18
- - Tests run with `python -m pytest lifecycle/test_<name>.py`.
@@ -1,20 +1 @@
1
- # hooks/observability
2
1
 
3
- PostToolUse hooks that record agent behavior for later review. These hooks do not block tool calls; they log or annotate them.
4
-
5
- ## Key files
6
-
7
- | File | Event | What it does |
8
- |---|---|---|
9
- | `instructions_loaded_logger.py` | PostToolUse (file load events) | Appends a JSONL record to `~/.claude/logs/instructions_loaded.jsonl` each time Claude Code loads a context file (CLAUDE.md, rules, skills), capturing the file path, load reason, memory type, and session ID |
10
- | `session_file_edit_tracker.py` | PostToolUse (Write/Edit/MultiEdit) | Records the resolved absolute path of each file edited this session into a per-session JSON file under the system temp directory, for the session edit stage gate to read at commit time |
11
- | `prose_matcher_advisory.py` | Advisory emit from prose matchers | Privacy-safe JSONL candidates (`matcher_id`, surface, context fingerprint, label) plus keep/narrow/drop/advisory classification against the sample floor |
12
- | `test_instructions_loaded_logger.py` | — | Tests for `instructions_loaded_logger.py` |
13
- | `test_session_file_edit_tracker.py` | — | Tests for `session_file_edit_tracker.py` |
14
- | `test_prose_matcher_advisory.py` | — | Tests for advisory emit and precision classification |
15
-
16
- ## Conventions
17
-
18
- - The log file is append-only; the hook creates the parent directory if needed.
19
- - Errors during logging are caught and written as error records rather than propagated — the hook never blocks the tool call.
20
- - Tests run with `python -m pytest observability/test_instructions_loaded_logger.py`.