claude-dev-env 2.28.0 → 2.28.1

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-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  2. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
  3. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  4. package/docs/CODE_RULES.md +3 -3
  5. package/docs/agent-spawn-protocol.md +1 -1
  6. package/docs/references/prose-style-enforcement.md +5 -9
  7. package/hooks/blocking/AGENTS.md +3 -13
  8. package/hooks/blocking/code_rules_docstrings.py +10 -2138
  9. package/hooks/blocking/code_rules_enforcer.py +0 -121
  10. package/hooks/blocking/code_rules_imports_logging.py +1 -236
  11. package/hooks/blocking/code_rules_shared.py +23 -0
  12. package/hooks/blocking/code_rules_test_layout.py +8 -8
  13. package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
  14. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
  15. package/hooks/blocking/precommit_code_rules_gate.py +8 -43
  16. package/hooks/blocking/state_description_blocker.py +1 -7
  17. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  18. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
  19. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
  20. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
  21. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
  22. package/hooks/blocking/test_code_rules_shared.py +12 -0
  23. package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
  24. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
  25. package/hooks/blocking/test_state_description_blocker.py +6 -4
  26. package/hooks/blocking/test_stop_dispatcher.py +5 -29
  27. package/hooks/hooks_constants/AGENTS.md +1 -6
  28. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
  29. package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
  30. package/hooks/hooks_constants/messages.py +0 -2
  31. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
  32. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
  33. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
  34. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
  35. package/hooks/hooks_constants/test_messages.py +5 -3
  36. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
  37. package/package.json +1 -1
  38. package/rules/AGENTS.md +3 -6
  39. package/rules/claims-as-quotes.md +0 -10
  40. package/rules/code-standards.md +6 -6
  41. package/rules/explore-thoroughly.md +0 -1
  42. package/rules/failure-blast-radius.md +0 -8
  43. package/rules/falsify-before-green.md +0 -8
  44. package/rules/file-global-constants.md +2 -2
  45. package/rules/filesystem-search.md +1 -1
  46. package/rules/git-workflow.md +1 -9
  47. package/rules/hedging-claims.md +2 -6
  48. package/rules/long-horizon-autonomy.md +1 -1
  49. package/rules/measurement-denominators.md +0 -9
  50. package/rules/research-mode.md +0 -6
  51. package/rules/verify-before-asking.md +0 -5
  52. package/rules/verify-runtime-state.md +0 -5
  53. package/scripts/codex_compat_materializer.py +0 -12
  54. package/scripts/sync_to_cursor/rules.py +0 -10
  55. package/scripts/tests/test_engine.py +0 -1
  56. package/scripts/tests/test_rules.py +0 -1
  57. package/scripts/tests/test_sync_to_cursor.py +0 -1
  58. package/system-prompts/software-engineer.xml +3 -3
  59. package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
  60. package/hooks/blocking/code_rules_dead_config_field.py +0 -568
  61. package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
  62. package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
  63. package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
  64. package/hooks/blocking/code_rules_mock_completeness.py +0 -295
  65. package/hooks/blocking/code_rules_scope_binding.py +0 -151
  66. package/hooks/blocking/code_rules_unused_imports.py +0 -197
  67. package/hooks/blocking/hedging_language_blocker.py +0 -221
  68. package/hooks/blocking/intent_only_ending_blocker.py +0 -148
  69. package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
  70. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
  71. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
  72. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
  73. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
  74. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
  75. package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
  76. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
  77. package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
  78. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
  79. package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
  80. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
  81. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
  82. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
  83. package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
  84. package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
  86. package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
  87. package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
  88. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
  89. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
  90. package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
  91. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
  92. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
  93. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
  94. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
  95. package/hooks/blocking/test_hedging_language_blocker.py +0 -261
  96. package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
  97. package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
  98. package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
  99. package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
  100. package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
  101. package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
  102. package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
  103. package/rules/conservative-action.md +0 -17
  104. package/rules/context7.md +0 -8
  105. package/rules/few-words.md +0 -3
  106. package/rules/parallel-tools.md +0 -23
@@ -71,14 +71,6 @@ def test_every_check_function_is_called_in_validate_content() -> None:
71
71
  )
72
72
 
73
73
 
74
- def test_dead_module_constant_check_stays_wired() -> None:
75
- validate_content_source = _validate_content_source()
76
- assert "check_dead_module_constants" in validate_content_source, (
77
- "check_dead_module_constants must stay dispatched from validate_content so a "
78
- "dead exported constant (the MEDIUM_TEXT class) is blocked at Write/Edit time."
79
- )
80
-
81
-
82
74
  def test_known_undispatched_set_lists_only_existing_checks() -> None:
83
75
  all_check_names = set(_all_check_function_names())
84
76
  stale_names = KNOWN_UNDISPATCHED_CHECKS - all_check_names
@@ -1,11 +1,9 @@
1
- """Tests for the module-docstring check roster and docstring tuple-enumeration checks.
1
+ """Tests for the module-docstring check roster.
2
2
 
3
- Both checks catch docstring-prose-vs-implementation drift in a check-registry
4
- module — a hook module that exposes several public ``check_*`` functions and a
5
- module-level tuple of literal attribute names. The drift the
3
+ Catches docstring-prose-vs-implementation drift in a check-registry module — a
4
+ hook module that exposes several public ``check_*`` functions. The drift the
6
5
  ``code_rules_test_assertions.py`` module hit at PR #713 HEAD: a one-line module
7
- docstring that names four of its five public checks, and a function docstring
8
- that enumerates three plumbing attributes while the tuple it reads holds four.
6
+ docstring that names four of its five public checks.
9
7
  """
10
8
 
11
9
  from __future__ import annotations
@@ -32,10 +30,6 @@ def check_module_docstring_names_public_checks(content: str, file_path: str) ->
32
30
  return code_rules_enforcer.check_module_docstring_names_public_checks(content, file_path)
33
31
 
34
32
 
35
- def check_docstring_tuple_enumeration_match(content: str, file_path: str) -> list[str]:
36
- return code_rules_enforcer.check_docstring_tuple_enumeration_match(content, file_path)
37
-
38
-
39
33
  def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
40
34
  return code_rules_enforcer.validate_content(content, file_path, old_content)
41
35
 
@@ -202,101 +196,6 @@ def test_should_handle_module_roster_syntax_error_gracefully() -> None:
202
196
  assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
203
197
 
204
198
 
205
- def _module_with_drifted_tuple_enumeration() -> str:
206
- return (
207
- '"""Call-args plumbing detection."""\n'
208
- "\n"
209
- '_CALL_ARGS_PLUMBING_ATTRIBUTES = ("call_args", "call_args_list", "called", "call_count")\n'
210
- "\n"
211
- "def check_plumbing(content: str, file_path: str) -> list[str]:\n"
212
- ' """Advise when a test asserts only on call-args plumbing.\n'
213
- "\n"
214
- " A body asserting on call-args plumbing (``call_args``, ``call_args_list``,\n"
215
- " ``.kwargs``) reaches into mock internals rather than observed behavior.\n"
216
- ' """\n'
217
- " for each_name in _CALL_ARGS_PLUMBING_ATTRIBUTES:\n"
218
- " if each_name in content:\n"
219
- " return [each_name]\n"
220
- " return []\n"
221
- )
222
-
223
-
224
- def test_should_flag_docstring_tuple_enumeration_drift() -> None:
225
- issues = check_docstring_tuple_enumeration_match(
226
- _module_with_drifted_tuple_enumeration(), HOOK_INFRASTRUCTURE_PATH
227
- )
228
- assert any("check_plumbing" in each for each in issues), (
229
- f"Expected the drifted enumeration to flag, got: {issues!r}"
230
- )
231
- assert any("kwargs" in each for each in issues), (
232
- f"Expected the docstring-only token named in the message, got: {issues!r}"
233
- )
234
- assert any("called" in each for each in issues), (
235
- f"Expected a tuple-only member named in the message, got: {issues!r}"
236
- )
237
- assert len(issues) == 1
238
-
239
-
240
- def test_should_not_flag_docstring_tuple_enumeration_match() -> None:
241
- source = (
242
- '"""Call-args plumbing detection."""\n'
243
- "\n"
244
- '_CALL_ARGS_PLUMBING_ATTRIBUTES = ("call_args", "call_args_list", "called", "call_count")\n'
245
- "\n"
246
- "def check_plumbing(content: str, file_path: str) -> list[str]:\n"
247
- ' """Advise when a test asserts only on call-args plumbing.\n'
248
- "\n"
249
- " A body asserting on call-args plumbing (``call_args``, ``call_args_list``,\n"
250
- " ``called``, ``call_count``) reaches into mock internals.\n"
251
- ' """\n'
252
- " for each_name in _CALL_ARGS_PLUMBING_ATTRIBUTES:\n"
253
- " if each_name in content:\n"
254
- " return [each_name]\n"
255
- " return []\n"
256
- )
257
- issues = check_docstring_tuple_enumeration_match(source, HOOK_INFRASTRUCTURE_PATH)
258
- assert issues == [], f"Matching enumeration must not flag, got: {issues!r}"
259
-
260
-
261
- def test_should_not_flag_docstring_without_tuple_overlap() -> None:
262
- source = (
263
- '"""Call-args plumbing detection."""\n'
264
- "\n"
265
- '_CALL_ARGS_PLUMBING_ATTRIBUTES = ("call_args", "call_args_list", "called", "call_count")\n'
266
- "\n"
267
- "def check_plumbing(content: str, file_path: str) -> list[str]:\n"
268
- ' """Advise when a test asserts only on mock internals.\n'
269
- "\n"
270
- " A body reading ``foo``, ``bar``, ``baz`` is unrelated to the tuple.\n"
271
- ' """\n'
272
- " for each_name in _CALL_ARGS_PLUMBING_ATTRIBUTES:\n"
273
- " if each_name in content:\n"
274
- " return [each_name]\n"
275
- " return []\n"
276
- )
277
- issues = check_docstring_tuple_enumeration_match(source, HOOK_INFRASTRUCTURE_PATH)
278
- assert issues == [], f"A docstring not naming the tuple must not flag, got: {issues!r}"
279
-
280
-
281
- def test_should_not_flag_when_function_does_not_reference_the_tuple() -> None:
282
- source = (
283
- '"""Call-args plumbing detection."""\n'
284
- "\n"
285
- '_CALL_ARGS_PLUMBING_ATTRIBUTES = ("call_args", "call_args_list", "called", "call_count")\n'
286
- "\n"
287
- "def check_unrelated(content: str, file_path: str) -> list[str]:\n"
288
- ' """Advise on ``call_args`` and ``call_args_list`` usage in tests."""\n'
289
- " return [content[:0]]\n"
290
- )
291
- issues = check_docstring_tuple_enumeration_match(source, HOOK_INFRASTRUCTURE_PATH)
292
- assert issues == [], f"A function not reading the tuple must not flag, got: {issues!r}"
293
-
294
-
295
- def test_should_handle_tuple_enumeration_syntax_error_gracefully() -> None:
296
- issues = check_docstring_tuple_enumeration_match("def broken(\n", HOOK_INFRASTRUCTURE_PATH)
297
- assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
298
-
299
-
300
199
  def test_validate_content_surfaces_module_roster_drift() -> None:
301
200
  issues = validate_content(
302
201
  _registry_module_omitting_a_check(), HOOK_INFRASTRUCTURE_PATH, old_content=""
@@ -309,11 +208,11 @@ def test_validate_content_surfaces_module_roster_drift() -> None:
309
208
  )
310
209
 
311
210
 
312
- def test_validate_content_surfaces_tuple_enumeration_drift() -> None:
313
- issues = validate_content(
314
- _module_with_drifted_tuple_enumeration(), HOOK_INFRASTRUCTURE_PATH, old_content=""
315
- )
316
- matching_issues = [each for each in issues if "check_plumbing" in each and "enumerat" in each]
317
- assert matching_issues, (
318
- f"Expected validate_content to surface the tuple-enumeration drift, got: {issues!r}"
211
+ def test_imports_logging_module_docstring_names_every_public_check() -> None:
212
+ """The shipped imports-logging module's own roster stays complete after trims."""
213
+ module_path = Path(__file__).resolve().parent / "code_rules_imports_logging.py"
214
+ module_source = module_path.read_text(encoding="utf-8")
215
+ issues = check_module_docstring_names_public_checks(
216
+ module_source, HOOK_INFRASTRUCTURE_PATH
319
217
  )
218
+ assert issues == [], f"Roster drift in code_rules_imports_logging.py: {issues!r}"
@@ -39,7 +39,6 @@ class NarrowEditFixture:
39
39
  ALL_SCOPE_AWARE_RULE_NAMES = frozenset(
40
40
  {
41
41
  "check_blast_radius_declared",
42
- "check_import_block_sorted",
43
42
  "check_magic_values",
44
43
  "check_duplicate_function_body_across_files",
45
44
  "check_same_file_inline_duplicate_body",
@@ -28,8 +28,6 @@ code_rules_enforcer = SimpleNamespace(
28
28
 
29
29
  DUPLICATED_FORMAT_PRODUCTION_FILE_PATH = "packages/app/services/api_client.py"
30
30
 
31
- INCOMPLETE_MOCK_TEST_FILE_PATH = "packages/app/tests/test_orders.py"
32
-
33
31
 
34
32
  def _oversized_function_source(name: str) -> str:
35
33
  body_line_count = code_rules_enforcer.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
@@ -39,24 +37,9 @@ def _oversized_function_source(name: str) -> str:
39
37
  return f"def {name}() -> None:\n" + "\n".join(body_lines) + "\n"
40
38
 
41
39
 
42
- def test_should_emit_advisories_for_incomplete_mocks_and_format_patterns_via_validate_content(
40
+ def test_should_emit_advisory_for_duplicated_format_patterns_via_validate_content(
43
41
  capsys: object,
44
42
  ) -> None:
45
- incomplete_mock_source = (
46
- "mock_order = {'id': 1}\n"
47
- "\n"
48
- "def test_order_total() -> None:\n"
49
- " total = mock_order['total']\n"
50
- " assert total > 0\n"
51
- )
52
- code_rules_enforcer.validate_content(
53
- incomplete_mock_source, INCOMPLETE_MOCK_TEST_FILE_PATH
54
- )
55
- captured = getattr(capsys, "readouterr")()
56
- assert "mock_order" in captured.err and "total" in captured.err, (
57
- f"Expected incomplete-mock advisory from validate_content, got: {captured.err!r}"
58
- )
59
-
60
43
  repeated_pattern_source = (
61
44
  "def get_user(user_id: str) -> str:\n"
62
45
  " return f'/api/{user_id}'\n"
@@ -279,3 +279,15 @@ def test_is_ephemeral_path_reads_session_id_from_environment(
279
279
  throwaway_script = scratchpad_directory / "probe.py"
280
280
 
281
281
  assert _SHARED_MODULE.is_ephemeral_path(str(throwaway_script)) is True
282
+
283
+
284
+ def test_is_dedicated_constants_module_true_for_constants_suffix() -> None:
285
+ assert _SHARED_MODULE._is_dedicated_constants_module("hooks_constants/foo_constants.py") is True
286
+
287
+
288
+ def test_is_dedicated_constants_module_true_for_config_directory() -> None:
289
+ assert _SHARED_MODULE._is_dedicated_constants_module("app/config/timing.py") is True
290
+
291
+
292
+ def test_is_dedicated_constants_module_false_for_ordinary_module() -> None:
293
+ assert _SHARED_MODULE._is_dedicated_constants_module("app/services/orders.py") is False
@@ -1,111 +1,23 @@
1
- """Behavior tests for the pass-through precommit_code_rules_gate hook.
1
+ """Behavior tests for the precommit_code_rules_gate library module.
2
2
 
3
- Each test builds a real git repository in a temporary directory, stages
4
- real files, and runs the hook script as a subprocess with a PreToolUse
5
- JSON payload on stdin through the exact production invocation path.
3
+ The module carries no hook entry point of its own; it exports
4
+ ``resolve_repository_root`` for its importers (``pii_prevention_blocker.py``,
5
+ ``pii_payload_scan.py``, ``session_edit_stage_gate.py``).
6
6
  """
7
7
 
8
- import json
9
8
  import subprocess
10
9
  import sys
11
10
  from pathlib import Path
12
11
 
13
- HOOK_PATH = Path(__file__).resolve().parent / "precommit_code_rules_gate.py"
14
-
15
- CLEAN_MODULE_SOURCE = '''"""Increment helper used by the precommit gate tests."""
16
-
17
-
18
- def add_one(number: int) -> int:
19
- """Return *number* plus one.
20
-
21
- Args:
22
- number: The integer to increment.
23
-
24
- Returns:
25
- The incremented integer.
26
- """
27
- return number + 1
28
- '''
29
-
30
- VIOLATING_MODULE_SOURCE = '''"""Module carrying a banned identifier for the precommit gate tests."""
31
-
32
-
33
- def compute_total() -> int:
34
- """Return a fixed total.
35
-
36
- Returns:
37
- The fixed total.
38
- """
39
- result = 1
40
- return result
41
- '''
42
-
43
- WIDGET_MODULE_BASE_SOURCE = '''"""Arithmetic helpers used by the precommit gate worktree tests."""
44
-
45
-
46
- def add_one(number: int) -> int:
47
- """Return *number* plus one.
48
-
49
- Args:
50
- number: The integer to increment.
51
-
52
- Returns:
53
- The incremented integer.
54
- """
55
- return number + 1
56
- '''
57
-
58
- WIDGET_MODULE_EXPANDED_SOURCE = '''"""Arithmetic helpers used by the precommit gate worktree tests."""
59
-
60
-
61
- def add_one(number: int) -> int:
62
- """Return *number* plus one.
63
-
64
- Args:
65
- number: The integer to increment.
66
-
67
- Returns:
68
- The incremented integer.
69
- """
70
- return number + 1
71
-
72
-
73
- def subtract_one(number: int) -> int:
74
- """Return *number* minus one.
75
-
76
- Args:
77
- number: The integer to decrement.
78
-
79
- Returns:
80
- The decremented integer.
81
- """
82
- return number - 1
83
- '''
84
-
85
- WIDGET_TEST_BASE_SOURCE = '''"""Behavior tests for the widget arithmetic helpers."""
86
-
87
- from widget import add_one
88
-
89
-
90
- def test_add_one_increments() -> None:
91
- """add_one returns its argument plus one."""
92
- assert add_one(1) == 2
93
- '''
94
-
95
- WIDGET_TEST_EXPANDED_SOURCE = '''"""Behavior tests for the widget arithmetic helpers."""
96
-
97
- from widget import add_one, subtract_one
98
-
99
-
100
- def test_add_one_increments() -> None:
101
- """add_one returns its argument plus one."""
102
- assert add_one(1) == 2
103
-
104
-
105
- def test_subtract_one_decrements() -> None:
106
- """subtract_one returns its argument minus one."""
107
- assert subtract_one(2) == 1
108
- '''
12
+ try:
13
+ import precommit_code_rules_gate as gate_module
14
+ from precommit_code_rules_gate import resolve_repository_root
15
+ except ModuleNotFoundError:
16
+ _BLOCKING_DIR = str(Path(__file__).resolve().parent)
17
+ if _BLOCKING_DIR not in sys.path:
18
+ sys.path.insert(0, _BLOCKING_DIR)
19
+ import precommit_code_rules_gate as gate_module
20
+ from precommit_code_rules_gate import resolve_repository_root
109
21
 
110
22
 
111
23
  def run_git(repository_root: Path, *git_arguments: str) -> None:
@@ -123,88 +35,29 @@ def initialize_repository(repository_root: Path) -> None:
123
35
  run_git(repository_root, "commit", "--allow-empty", "-m", "initial")
124
36
 
125
37
 
126
- def stage_file(repository_root: Path, relative_name: str, source_text: str) -> None:
127
- (repository_root / relative_name).write_text(source_text, encoding="utf-8")
128
- run_git(repository_root, "add", relative_name)
129
-
130
-
131
- def run_hook(bash_command: str, working_directory: Path) -> subprocess.CompletedProcess[str]:
132
- payload = json.dumps({"tool_input": {"command": bash_command}})
133
- return subprocess.run(
134
- [sys.executable, str(HOOK_PATH)],
135
- input=payload,
136
- capture_output=True,
137
- text=True,
138
- cwd=str(working_directory),
139
- timeout=120,
140
- )
141
-
142
-
143
- def test_non_commit_command_passes_through(tmp_path: Path) -> None:
144
- initialize_repository(tmp_path)
145
- completed_hook = run_hook("git status", tmp_path)
146
- assert completed_hook.returncode == 0
147
- assert completed_hook.stdout.strip() == ""
148
-
149
-
150
- def test_commit_with_clean_staged_python_file_is_allowed(tmp_path: Path) -> None:
151
- initialize_repository(tmp_path)
152
- stage_file(tmp_path, "incrementer.py", CLEAN_MODULE_SOURCE)
153
- completed_hook = run_hook("git commit -m add", tmp_path)
154
- assert completed_hook.returncode == 0
155
- assert completed_hook.stdout.strip() == ""
156
- assert "Git commit proceeds to configured Git hooks." in completed_hook.stderr
157
-
158
-
159
- def test_commit_with_staged_python_file_passes_through(tmp_path: Path) -> None:
38
+ def test_resolve_repository_root_returns_root_for_directory_inside_repository(
39
+ tmp_path: Path,
40
+ ) -> None:
160
41
  initialize_repository(tmp_path)
161
- stage_file(tmp_path, "totals.py", VIOLATING_MODULE_SOURCE)
162
- completed_hook = run_hook("git commit -m add", tmp_path)
163
- assert completed_hook.returncode == 0
164
- assert completed_hook.stdout.strip() == ""
165
- assert "Git commit proceeds to configured Git hooks." in completed_hook.stderr
42
+ nested_directory = tmp_path / "nested"
43
+ nested_directory.mkdir()
44
+ resolved_root = resolve_repository_root(str(nested_directory))
45
+ assert resolved_root == tmp_path.resolve()
166
46
 
167
47
 
168
- def test_git_dash_c_commit_form_passes_through(tmp_path: Path) -> None:
169
- repository_root = tmp_path / "repo"
170
- repository_root.mkdir()
171
- initialize_repository(repository_root)
172
- stage_file(repository_root, "totals.py", VIOLATING_MODULE_SOURCE)
173
- elsewhere = tmp_path / "elsewhere"
174
- elsewhere.mkdir()
175
- quoted_root = str(repository_root)
176
- completed_hook = run_hook(f'git -C "{quoted_root}" commit -m add', elsewhere)
177
- assert completed_hook.returncode == 0
178
- assert completed_hook.stdout.strip() == ""
179
- assert "Git commit proceeds to configured Git hooks." in completed_hook.stderr
48
+ def test_resolve_repository_root_returns_none_outside_a_repository(
49
+ tmp_path: Path,
50
+ ) -> None:
51
+ non_repository_directory = tmp_path / "not_a_repo"
52
+ non_repository_directory.mkdir()
53
+ assert resolve_repository_root(str(non_repository_directory)) is None
180
54
 
181
55
 
182
- def test_commit_with_no_staged_python_files_is_allowed(tmp_path: Path) -> None:
183
- initialize_repository(tmp_path)
184
- stage_file(tmp_path, "notes.md", "# Notes\n")
185
- completed_hook = run_hook("git commit -m docs", tmp_path)
186
- assert completed_hook.returncode == 0
187
- assert completed_hook.stdout.strip() == ""
56
+ def test_module_carries_no_main_entry_point() -> None:
57
+ """The module has no dispatcher-facing hook entry point."""
58
+ assert not hasattr(gate_module, "main")
188
59
 
189
60
 
190
- def test_worktree_commit_passes_through(
191
- tmp_path: Path,
192
- ) -> None:
193
- main_checkout = tmp_path / "main_checkout"
194
- main_checkout.mkdir()
195
- initialize_repository(main_checkout)
196
- (main_checkout / "widget.py").write_text(WIDGET_MODULE_BASE_SOURCE, encoding="utf-8")
197
- (main_checkout / "test_widget.py").write_text(WIDGET_TEST_BASE_SOURCE, encoding="utf-8")
198
- run_git(main_checkout, "add", "widget.py", "test_widget.py")
199
- run_git(main_checkout, "commit", "-m", "base")
200
- worktree_root = tmp_path / "sibling_worktree"
201
- run_git(main_checkout, "worktree", "add", str(worktree_root), "-b", "feature")
202
- stage_file(worktree_root, "widget.py", WIDGET_MODULE_EXPANDED_SOURCE)
203
- stage_file(worktree_root, "test_widget.py", WIDGET_TEST_EXPANDED_SOURCE)
204
- quoted_worktree = str(worktree_root)
205
- completed_hook = run_hook(
206
- f'git -C "{quoted_worktree}" commit -m expand', main_checkout
207
- )
208
- assert completed_hook.returncode == 0
209
- assert completed_hook.stdout.strip() == ""
210
- assert "Git commit proceeds to configured Git hooks." in completed_hook.stderr
61
+ def test_module_carries_no_commit_invocation_detector() -> None:
62
+ """The commit-detection helper lives in block_main_commit, not duplicated here."""
63
+ assert not hasattr(gate_module, "is_git_commit_invocation")
@@ -183,7 +183,6 @@ def test_agent_dispatcher_passes_commit_through_without_running_gate(
183
183
 
184
184
  assert completed_dispatcher.returncode == 0, completed_dispatcher.stderr
185
185
  assert completed_dispatcher.stdout.strip() == ""
186
- assert "Git commit proceeds to configured Git hooks." in completed_dispatcher.stderr
187
186
  assert not gate_marker_path.exists()
188
187
 
189
188
 
@@ -74,7 +74,7 @@ class _RunHook:
74
74
  _run_hook = _RunHook()
75
75
 
76
76
 
77
- def test_historical_phrase_scan_is_default_off(
77
+ def test_historical_phrase_scan_runs_with_the_env_flag_unset(
78
78
  monkeypatch: pytest.MonkeyPatch,
79
79
  ) -> None:
80
80
  monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
@@ -87,8 +87,8 @@ def test_historical_phrase_scan_is_default_off(
87
87
  is_prose_style_enabled=False,
88
88
  )
89
89
  assert result.returncode == 0
90
- assert result.stdout == ""
91
- assert _evaluate_without_opt_in(
90
+ assert result.stdout.strip() != ""
91
+ deny_reason = _evaluate_without_opt_in(
92
92
  {
93
93
  "tool_name": "Write",
94
94
  "tool_input": {
@@ -96,7 +96,9 @@ def test_historical_phrase_scan_is_default_off(
96
96
  "content": VIOLATION_INSTEAD_OF_COMMENT,
97
97
  },
98
98
  }
99
- ) is None
99
+ )
100
+ assert deny_reason is not None
101
+ assert "instead of" in deny_reason
100
102
 
101
103
 
102
104
  def test_block_clean_python_comment_passes():
@@ -90,7 +90,7 @@ def test_dispatch_re_emits_first_block_with_system_message_and_suppress(
90
90
  HostedHookRun(captured_stdout="", did_crash=False),
91
91
  HostedHookRun(
92
92
  captured_stdout=_block_json(
93
- "hedging blocked",
93
+ "keep fields",
94
94
  system_message="rewrite the ending",
95
95
  suppress_output=True,
96
96
  ),
@@ -108,15 +108,15 @@ def test_dispatch_re_emits_first_block_with_system_message_and_suppress(
108
108
  monkeypatch.setattr(
109
109
  "stop_dispatcher.ALL_STOP_HOSTED_HOOK_PATHS",
110
110
  (
111
- "blocking/hedging_language_blocker.py",
111
+ "blocking/first_stub_hook.py",
112
112
  "blocking/question_to_user_enforcer.py",
113
- "blocking/intent_only_ending_blocker.py",
113
+ "blocking/second_stub_hook.py",
114
114
  ),
115
115
  )
116
116
  dispatch('{"session_id": "test"}')
117
117
  emitted_payload = json.loads(capsys.readouterr().out.strip())
118
118
  assert emitted_payload["decision"] == "block"
119
- assert emitted_payload["reason"] == "hedging blocked"
119
+ assert emitted_payload["reason"] == "keep fields"
120
120
  assert emitted_payload["systemMessage"] == "rewrite the ending"
121
121
  assert emitted_payload["suppressOutput"] is True
122
122
 
@@ -132,7 +132,7 @@ def test_dispatch_writes_nothing_when_no_hook_blocks(
132
132
  )
133
133
  monkeypatch.setattr(
134
134
  "stop_dispatcher.ALL_STOP_HOSTED_HOOK_PATHS",
135
- ("blocking/hedging_language_blocker.py",),
135
+ ("blocking/first_stub_hook.py",),
136
136
  )
137
137
  dispatch('{"session_id": "test"}')
138
138
  assert capsys.readouterr().out == ""
@@ -151,30 +151,6 @@ def test_dispatcher_exits_zero_on_empty_payload() -> None:
151
151
  assert completed.stdout.strip() == ""
152
152
 
153
153
 
154
- def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
155
- """A hedging last_assistant_message blocks through the dispatcher."""
156
- payload_text = json.dumps(
157
- {
158
- "stop_hook_active": False,
159
- "last_assistant_message": "This is probably correct.",
160
- }
161
- )
162
- environment_by_key = {**os.environ, "CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}
163
- completed = subprocess.run(
164
- [sys.executable, _DISPATCHER_SCRIPT],
165
- check=False,
166
- input=payload_text,
167
- capture_output=True,
168
- text=True,
169
- encoding="utf-8",
170
- env=environment_by_key,
171
- )
172
- assert completed.returncode == 0
173
- parsed = json.loads(completed.stdout)
174
- assert parsed["decision"] == "block"
175
- assert "probably" in parsed["reason"].lower() or "hedging" in parsed["reason"].lower()
176
-
177
-
178
154
  def test_dispatcher_imports_standalone_with_only_blocking_on_the_path() -> None:
179
155
  """The dispatcher's bootstrap resolves hooks_constants without hooks/ on PYTHONPATH."""
180
156
  subprocess_environment = {**os.environ, "PYTHONPATH": str(_BLOCKING_DIR)}
@@ -22,10 +22,6 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
22
22
  | `command_dispatch_constants.py` | Command-word regex, command-key access pattern, tokenization pattern, and anchors for the unanchored command-dispatch meta-gate |
23
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
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
- | `dead_argparse_argument_constants.py` | Patterns for detecting unused argparse arguments |
26
- | `dead_config_field_constants.py` | Patterns for detecting unused `*Config` / `*Selectors` dataclass fields |
27
- | `dead_dataclass_field_constants.py` | Patterns for detecting unused dataclass fields |
28
- | `dead_module_constant_constants.py` | Patterns for detecting unexported `UPPER_SNAKE` constants in `*_constants.py` modules |
29
25
  | `destructive_command_environment_constants.py` | Environment keys and shared truthy values used by destructive-command hooks |
30
26
  | `destructive_command_segment_constants.py` | The list of destructive shell command patterns the blocker matches |
31
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 |
@@ -36,7 +32,6 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
36
32
  | `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
37
33
  | `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
38
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 |
39
- | `hedging_uncertainty_constants.py` | Sentence-boundary pattern, explicit uncertainty label patterns, hedge-term list separator, and positive corrective block text for OP-07C sentence-scoped hedging |
40
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` |
41
36
  | `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
42
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 |
@@ -58,7 +53,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
58
53
  | `pytest_invocation.py` | Pure parsed-segment classifier for pytest runs through interpreters, shims, wrappers, and shell string-exec forms; no pipeline deny decision |
59
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 |
60
55
  | `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
61
- | `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
56
+ | `precommit_code_rules_gate_constants.py` | Git-repository-root resolution command and timeout for the `precommit_code_rules_gate` library module |
62
57
  | project_paths_reader.py | Loads ~/.claude/project-paths.json and finds a home-bounded git root for registry checks |
63
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 |
64
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 |
@@ -72,7 +72,6 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
72
72
  ),
73
73
  BashHostedHookEntry("blocking/conventional_pr_title_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
74
74
  BashHostedHookEntry("blocking/block_main_commit.py", ALL_BASH_ONLY_TOOL_NAMES),
75
- BashHostedHookEntry("blocking/precommit_code_rules_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
76
75
  BashHostedHookEntry("blocking/session_edit_stage_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
77
76
  BashHostedHookEntry("blocking/test_preflight_check.py", ALL_BASH_ONLY_TOOL_NAMES),
78
77
  BashHostedHookEntry("blocking/windows_rmtree_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
@@ -32,6 +32,9 @@ ALL_HOOK_INFRASTRUCTURE_PATTERNS = {"/.claude/hooks/", "\\.claude\\hooks\\", "\\
32
32
  ALL_WORKFLOW_REGISTRY_PATTERNS = {"/workflow/", "\\workflow\\", "_tab.py", "/states.py", "\\states.py", "/modules.py", "\\modules.py"}
33
33
  ALL_MIGRATION_PATH_PATTERNS = {"/migrations/", "\\migrations\\"}
34
34
 
35
+ CONSTANTS_MODULE_SUFFIX: str = "_constants.py"
36
+ CONFIG_DIRECTORY_SEGMENT: str = "config"
37
+
35
38
  ADVISORY_LINE_THRESHOLD_SOFT = 400
36
39
  ADVISORY_LINE_THRESHOLD_HARD = 1000
37
40
 
@@ -1,11 +1,9 @@
1
1
  """User-facing notice messages for blocking hooks."""
2
2
 
3
- USER_FACING_NOTICE = "Agent was found guessing - sourcing opinions..."
4
3
  USER_FACING_TDD_NOTICE = "TDD gate held - writing the failing test first..."
5
4
  USER_FACING_ASKUSERQUESTION_NOTICE = (
6
5
  "Agent asked the user in prose - rerouting through AskUserQuestion..."
7
6
  )
8
- USER_FACING_INTENT_ENDING_NOTICE = "Agent ended on a promise - doing the work now..."
9
7
  USER_FACING_CONTEXT_REASSURANCE_NOTICE = (
10
8
  "Agent moved to wrap up early - continuing the work..."
11
9
  )
@@ -1,24 +1,10 @@
1
- """Constants for the precommit_code_rules_gate PreToolUse hook.
1
+ """Constants for the precommit_code_rules_gate library module.
2
2
 
3
- Command parsing, git timeouts, and the staged-gate invocation surface used
4
- to run the shared code_rules_gate engine before a git commit.
3
+ The git-repository-root resolution command and its timeout, used by
4
+ ``resolve_repository_root`` to find the repository a directory belongs to.
5
5
  """
6
6
 
7
- from pathlib import Path
8
-
9
- GIT_DASH_C_COMMIT_PATTERN: str = r"git\s+-C\s+[\"']?[^\"';&|]+?[\"']?\s+commit\b"
10
7
  GIT_COMMAND_TIMEOUT_SECONDS: int = 5
11
- GATE_TIMEOUT_SECONDS: int = 240
12
- GATE_RELATIVE_PATH: Path = Path("_shared") / "pr-loop" / "scripts" / "code_rules_gate.py"
13
- ALL_STAGED_PYTHON_FILES_COMMAND: tuple[str, ...] = (
14
- "git",
15
- "diff",
16
- "--cached",
17
- "--name-only",
18
- "--diff-filter=ACMR",
19
- "--",
20
- "*.py",
21
- )
22
8
  ALL_GIT_REPOSITORY_ROOT_COMMAND: tuple[str, ...] = (
23
9
  "git",
24
10
  "rev-parse",