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.
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/docs/CODE_RULES.md +3 -3
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/references/prose-style-enforcement.md +5 -9
- package/hooks/blocking/AGENTS.md +3 -13
- package/hooks/blocking/code_rules_docstrings.py +10 -2138
- package/hooks/blocking/code_rules_enforcer.py +0 -121
- package/hooks/blocking/code_rules_imports_logging.py +1 -236
- package/hooks/blocking/code_rules_shared.py +23 -0
- package/hooks/blocking/code_rules_test_layout.py +8 -8
- package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
- package/hooks/blocking/precommit_code_rules_gate.py +8 -43
- package/hooks/blocking/state_description_blocker.py +1 -7
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
- package/hooks/blocking/test_code_rules_shared.py +12 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
- package/hooks/blocking/test_state_description_blocker.py +6 -4
- package/hooks/blocking/test_stop_dispatcher.py +5 -29
- package/hooks/hooks_constants/AGENTS.md +1 -6
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/messages.py +0 -2
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/test_messages.py +5 -3
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
- package/package.json +1 -1
- package/rules/AGENTS.md +3 -6
- package/rules/claims-as-quotes.md +0 -10
- package/rules/code-standards.md +6 -6
- package/rules/explore-thoroughly.md +0 -1
- package/rules/failure-blast-radius.md +0 -8
- package/rules/falsify-before-green.md +0 -8
- package/rules/file-global-constants.md +2 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +1 -9
- package/rules/hedging-claims.md +2 -6
- package/rules/long-horizon-autonomy.md +1 -1
- package/rules/measurement-denominators.md +0 -9
- package/rules/research-mode.md +0 -6
- package/rules/verify-before-asking.md +0 -5
- package/rules/verify-runtime-state.md +0 -5
- package/scripts/codex_compat_materializer.py +0 -12
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/tests/test_engine.py +0 -1
- package/scripts/tests/test_rules.py +0 -1
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/system-prompts/software-engineer.xml +3 -3
- package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
- package/hooks/blocking/code_rules_dead_config_field.py +0 -568
- package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
- package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
- package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
- package/hooks/blocking/code_rules_mock_completeness.py +0 -295
- package/hooks/blocking/code_rules_scope_binding.py +0 -151
- package/hooks/blocking/code_rules_unused_imports.py +0 -197
- package/hooks/blocking/hedging_language_blocker.py +0 -221
- package/hooks/blocking/intent_only_ending_blocker.py +0 -148
- package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
- package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
- package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
- package/hooks/blocking/test_hedging_language_blocker.py +0 -261
- package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
- package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
- package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
- package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
- package/rules/conservative-action.md +0 -17
- package/rules/context7.md +0 -8
- package/rules/few-words.md +0 -3
- 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
|
|
1
|
+
"""Tests for the module-docstring check roster.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
|
|
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}"
|
|
@@ -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
|
|
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
|
|
1
|
+
"""Behavior tests for the precommit_code_rules_gate library module.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
assert
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
)
|
|
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
|
-
"
|
|
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/
|
|
111
|
+
"blocking/first_stub_hook.py",
|
|
112
112
|
"blocking/question_to_user_enforcer.py",
|
|
113
|
-
"blocking/
|
|
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"] == "
|
|
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/
|
|
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` |
|
|
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
|
|
1
|
+
"""Constants for the precommit_code_rules_gate library module.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
to
|
|
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",
|