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