claude-dev-env 7.5.0 → 7.6.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/everything-search/scripts/everything_search.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
- package/.agents/skills/fresh-branch/SKILL.md +1 -0
- package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
- package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/syncing-submodules/SKILL.md +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
- package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/SKILL.md +1 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
- package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
- package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
- package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
- package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
- package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
- package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
- package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
- package/_shared/pr-loop/scripts/preflight.py +7 -0
- package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
- package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
- package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
- package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
- package/bin/install-constants.mjs +4 -2
- package/bin/install.cursor-rules.test.mjs +57 -15
- package/bin/install.mjs +10 -15
- package/bin/pstack-model-preferences.claude.json +23 -0
- package/hooks/advisory/pr_done_reminder.py +7 -0
- package/hooks/advisory/refactor_guard.py +8 -0
- package/hooks/advisory/test_pr_done_reminder.py +21 -0
- package/hooks/advisory/test_refactor_guard.py +40 -0
- package/hooks/blocking/block_main_commit.py +4 -0
- package/hooks/blocking/code_rules_comments.py +39 -5
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +3 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
- package/hooks/git-hooks/pull_request_handoff.py +3 -0
- package/hooks/git-hooks/subprocess_window_access.py +41 -0
- package/hooks/git-hooks/test_verification_start.py +4 -0
- package/hooks/git-hooks/verification_notice_context.py +3 -0
- package/hooks/git-hooks/verification_start.py +7 -8
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/subprocess_window.py +141 -0
- package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/test_subprocess_window.py +128 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
- package/hooks/validation/mypy_validator.py +4 -0
- package/hooks/validators/README.md +1 -0
- package/hooks/validators/git_checks.py +4 -0
- package/hooks/validators/health_check.py +3 -0
- package/hooks/validators/hook_timing_harness.py +3 -0
- package/hooks/validators/mypy_integration.py +4 -0
- package/hooks/validators/ruff_integration.py +6 -0
- package/hooks/validators/run_all_validators.py +7 -0
- package/hooks/validators/subprocess_window_access.py +41 -0
- package/hooks/workflow/auto_formatter.py +8 -0
- package/hooks/workflow/test_auto_formatter.py +39 -0
- package/package.json +1 -1
- package/rules/pstack-models.md +4 -3
- package/scripts/automatic_advisory/checkout.py +8 -1
- package/scripts/automatic_advisory/cli.py +9 -3
- package/scripts/automatic_advisory/config/constants.py +0 -2
- package/scripts/automatic_advisory/execution.py +7 -1
- package/scripts/automatic_advisory/git.py +9 -1
- package/scripts/automatic_advisory/process_host.py +3 -0
- package/scripts/claude_chain_runner.py +3 -0
- package/scripts/gh_artifact_upload.py +3 -0
- package/scripts/grok_headless_runner.py +3 -0
- package/scripts/grok_patch_artifacts.py +4 -0
- package/scripts/grok_worker_preflight.py +3 -0
- package/scripts/invoke_code_review.py +3 -0
- package/scripts/local_verification/command_runner.py +3 -0
- package/scripts/local_verification/git_state.py +3 -0
- package/scripts/policy_lint/adapter_detectors.py +48 -2
- package/scripts/policy_lint/adapter_pairing.py +150 -2
- package/scripts/policy_lint/adapters.py +46 -4
- package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection_git.py +11 -0
- package/scripts/repository_checks/config/constants.py +5 -0
- package/scripts/repository_checks/config/policy_document.py +19 -0
- package/scripts/repository_checks/email_exemptions.py +14 -71
- package/scripts/repository_checks/path_exemptions.py +35 -0
- package/scripts/repository_checks/policy_document.py +120 -0
- package/scripts/repository_checks/tracked_secrets.py +27 -4
- package/scripts/setup_project_paths.py +3 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/subprocess_window_access.py +41 -0
- package/scripts/test_dispatcher_profile_import.py +4 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
- package/scripts/tests/test_email_exemptions.py +37 -0
- package/scripts/tests/test_path_exemptions.py +164 -0
- package/scripts/tests/test_policy_document.py +104 -0
- package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
- package/scripts/tests/test_policy_lint_selection.py +34 -0
- package/scripts/tests/test_subprocess_window_access.py +118 -0
- package/scripts/tests/test_tracked_secrets.py +79 -0
- package/scripts/validate_instruction_pairs.py +3 -0
- package/scripts/verify_installable_package.py +5 -0
- package/scripts/automatic_advisory/window_flags.py +0 -41
- package/scripts/repository_checks/config/email_exemptions.py +0 -10
|
@@ -30,6 +30,7 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
|
30
30
|
ALL_TOKEN_ANCHORED_EXEMPT_COMMENT_BODIES,
|
|
31
31
|
CHAINED_INLINE_COMMENT_PATTERN,
|
|
32
32
|
MAX_COMMENT_ISSUES,
|
|
33
|
+
STEALTH_KEEP_COMMENT_MARKER,
|
|
33
34
|
)
|
|
34
35
|
_javascript_comment_scanner = importlib.import_module("javascript_comment_scanner")
|
|
35
36
|
extract_javascript_comment_occurrences = (
|
|
@@ -152,10 +153,36 @@ def _python_comment_occurrences(
|
|
|
152
153
|
return all_occurrences, True
|
|
153
154
|
|
|
154
155
|
|
|
156
|
+
def _is_stealth_keep_occurrence(comment_text: str) -> bool:
|
|
157
|
+
"""Return True for a ``# STEALTH: Keep`` occurrence.
|
|
158
|
+
|
|
159
|
+
``STEALTH: Keep`` is meant to be freely added and to survive edits to
|
|
160
|
+
the line it sits on, unlike ``noqa`` or ``TODO``, which AGENTS.md
|
|
161
|
+
requires to be removed rather than added or justified. It is excluded
|
|
162
|
+
here rather than through the shared exempt-marker set that
|
|
163
|
+
``check_comment_changes`` deliberately still blocks on add.
|
|
164
|
+
"""
|
|
165
|
+
return comment_text.startswith("#") and comment_text[1:].lstrip().startswith(
|
|
166
|
+
STEALTH_KEEP_COMMENT_MARKER
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _without_stealth_keep_occurrences(
|
|
171
|
+
all_occurrences: list[tuple[str, int, bool]]
|
|
172
|
+
) -> list[tuple[str, int, bool]]:
|
|
173
|
+
return [
|
|
174
|
+
each_occurrence
|
|
175
|
+
for each_occurrence in all_occurrences
|
|
176
|
+
if not _is_stealth_keep_occurrence(each_occurrence[0])
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
|
|
155
180
|
def check_comment_changes(old_content: str, new_content: str, file_path: str) -> list[str]:
|
|
156
181
|
"""Check for comment additions or removals between old and new content.
|
|
157
182
|
|
|
158
|
-
Inline and standalone comments are blocking findings when added
|
|
183
|
+
Inline and standalone comments are blocking findings when added, except
|
|
184
|
+
``STEALTH: Keep`` occurrences, which are excluded entirely so the marker
|
|
185
|
+
can be added and can persist across edits to its line.
|
|
159
186
|
Existing comments can be removed when the touched code no longer needs them.
|
|
160
187
|
|
|
161
188
|
When the file is Python and either *old_content* or *new_content* cannot
|
|
@@ -178,6 +205,9 @@ def check_comment_changes(old_content: str, new_content: str, file_path: str) ->
|
|
|
178
205
|
old_occurrences = extract_javascript_comment_occurrences(old_content, True)
|
|
179
206
|
new_occurrences = extract_javascript_comment_occurrences(new_content, True)
|
|
180
207
|
|
|
208
|
+
old_occurrences = _without_stealth_keep_occurrences(old_occurrences)
|
|
209
|
+
new_occurrences = _without_stealth_keep_occurrences(new_occurrences)
|
|
210
|
+
|
|
181
211
|
old_occurrence_counts = Counter(
|
|
182
212
|
(each_text, is_inline)
|
|
183
213
|
for each_text, _line_number, is_inline in old_occurrences
|
|
@@ -264,6 +294,8 @@ def _retained_comment_issues(
|
|
|
264
294
|
(old_occurrences, old_tokenize_ok), (new_occurrences, new_tokenize_ok) = (_comment_occurrences(old_content, file_path, True), _comment_occurrences(new_content, file_path, True))
|
|
265
295
|
if not (old_tokenize_ok and new_tokenize_ok):
|
|
266
296
|
return []
|
|
297
|
+
old_occurrences = _without_stealth_keep_occurrences(old_occurrences)
|
|
298
|
+
new_occurrences = _without_stealth_keep_occurrences(new_occurrences)
|
|
267
299
|
all_changed_lines, all_deleted_lines, old_line_by_new_line = _line_diff_data(old_content, new_content)
|
|
268
300
|
old_line_by_key = {
|
|
269
301
|
each_key: [each_line for each_text, each_line, each_is_inline in old_occurrences if (each_text, each_is_inline) == each_key]
|
|
@@ -346,12 +378,14 @@ def _is_exempt_python_comment(comment_token: tokenize.TokenInfo) -> bool:
|
|
|
346
378
|
token-anchored directive body never legitimately carries a ``#``
|
|
347
379
|
(noqa codes, pylint symbols, and pragma directives contain none), so
|
|
348
380
|
any inner ``#`` reliably marks chained prose. Free-form markers
|
|
349
|
-
(``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``)
|
|
350
|
-
trailing prose:
|
|
381
|
+
(``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``, ``STEALTH: Keep``)
|
|
382
|
+
accept any trailing prose:
|
|
351
383
|
``# type:`` participates in the documented justification
|
|
352
384
|
convention enforced by ``check_type_escape_hatches`` (which
|
|
353
|
-
requires a trailing reason),
|
|
354
|
-
annotation text by convention
|
|
385
|
+
requires a trailing reason), the TODO-family markers carry
|
|
386
|
+
annotation text by convention, and ``STEALTH: Keep`` is the
|
|
387
|
+
acknowledged-bypass marker the CDP runtime-usage scanner reads
|
|
388
|
+
from the flagged line together with its reason.
|
|
355
389
|
"""
|
|
356
390
|
comment_string = comment_token.string
|
|
357
391
|
if comment_string.startswith("#!") and comment_token.start == (1, 0):
|
|
@@ -16,6 +16,8 @@ import sys
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from urllib.parse import urlparse
|
|
18
18
|
|
|
19
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
20
|
+
|
|
19
21
|
try:
|
|
20
22
|
_blocking_directory = str(Path(__file__).resolve().parent.parent)
|
|
21
23
|
_hooks_directory = str(Path(__file__).resolve().parent.parent.parent)
|
|
@@ -186,6 +188,7 @@ def _repository_origin_slug(repository_root: Path) -> str | None:
|
|
|
186
188
|
text=True,
|
|
187
189
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
188
190
|
cwd=str(repository_root),
|
|
191
|
+
creationflags=hidden_window_creation_flags(),
|
|
189
192
|
)
|
|
190
193
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
191
194
|
return None
|
|
@@ -18,6 +18,8 @@ from hooks_constants.precommit_code_rules_gate_constants import ( # noqa: E402
|
|
|
18
18
|
GIT_COMMAND_TIMEOUT_SECONDS,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
def resolve_repository_root(working_directory: str | None) -> Path | None:
|
|
23
25
|
"""Resolve the Git repository root for a directory.
|
|
@@ -36,6 +38,7 @@ def resolve_repository_root(working_directory: str | None) -> Path | None:
|
|
|
36
38
|
text=True,
|
|
37
39
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
38
40
|
cwd=working_directory,
|
|
41
|
+
creationflags=hidden_window_creation_flags(),
|
|
39
42
|
)
|
|
40
43
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
41
44
|
return None
|
|
@@ -30,6 +30,10 @@ def _safe_constant_attribute_calls() -> frozenset[tuple[str, str]]:
|
|
|
30
30
|
return frozenset({("re", "compile")})
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _safe_string_literal_methods() -> frozenset[str]:
|
|
34
|
+
return frozenset({"join"})
|
|
35
|
+
|
|
36
|
+
|
|
33
37
|
def _node_is_unsafe_construct(subnode: ast.AST) -> bool:
|
|
34
38
|
return isinstance(
|
|
35
39
|
subnode,
|
|
@@ -37,7 +41,18 @@ def _node_is_unsafe_construct(subnode: ast.AST) -> bool:
|
|
|
37
41
|
)
|
|
38
42
|
|
|
39
43
|
|
|
44
|
+
def _is_safe_string_literal_method_call(function_node: ast.Attribute) -> bool:
|
|
45
|
+
receiver = function_node.value
|
|
46
|
+
return (
|
|
47
|
+
isinstance(receiver, ast.Constant)
|
|
48
|
+
and isinstance(receiver.value, str)
|
|
49
|
+
and function_node.attr in _safe_string_literal_methods()
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
40
53
|
def _attribute_call_is_unsafe(function_node: ast.Attribute) -> bool:
|
|
54
|
+
if _is_safe_string_literal_method_call(function_node):
|
|
55
|
+
return False
|
|
41
56
|
if not isinstance(function_node.value, ast.Name):
|
|
42
57
|
return True
|
|
43
58
|
pair = (function_node.value.id, function_node.attr)
|
|
@@ -56,9 +71,10 @@ def _call_is_unsafe(call_node: ast.Call) -> bool:
|
|
|
56
71
|
def _rhs_has_unsafe_call(rhs_node: ast.AST) -> bool:
|
|
57
72
|
"""Return True when a constant's value contains a non-allowlisted call.
|
|
58
73
|
|
|
59
|
-
Safe calls are value constructors (``Path(...)``, ``re.compile(...)``)
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
Safe calls are value constructors (``Path(...)``, ``re.compile(...)``) and
|
|
75
|
+
a pure string-literal method call (``"|".join(...)``) that build objects
|
|
76
|
+
without side effects; any other call or a comprehension reads as
|
|
77
|
+
import-time behavior.
|
|
62
78
|
|
|
63
79
|
Args:
|
|
64
80
|
rhs_node: The assignment's right-hand-side expression node.
|
|
@@ -29,6 +29,8 @@ from tdd_enforcer_parts.config.tdd_enforcer_constants import (
|
|
|
29
29
|
GIT_PATHSPEC_SEPARATOR,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
33
|
+
|
|
32
34
|
|
|
33
35
|
def _git_ls_files_command(file_name: str) -> list[str]:
|
|
34
36
|
return [GIT_EXECUTABLE_NAME, GIT_LS_FILES_SUBCOMMAND, GIT_PATHSPEC_SEPARATOR, file_name]
|
|
@@ -57,6 +59,7 @@ def _git_tracks_path(path: Path) -> bool:
|
|
|
57
59
|
text=True,
|
|
58
60
|
check=False,
|
|
59
61
|
timeout=GIT_LS_FILES_TIMEOUT_SECONDS,
|
|
62
|
+
creationflags=hidden_window_creation_flags(),
|
|
60
63
|
)
|
|
61
64
|
except (OSError, subprocess.SubprocessError):
|
|
62
65
|
return False
|
|
@@ -80,6 +83,7 @@ def _working_tree_differs_from_head(path: Path) -> bool:
|
|
|
80
83
|
text=True,
|
|
81
84
|
check=False,
|
|
82
85
|
timeout=GIT_DIFF_TIMEOUT_SECONDS,
|
|
86
|
+
creationflags=hidden_window_creation_flags(),
|
|
83
87
|
)
|
|
84
88
|
except (OSError, subprocess.SubprocessError):
|
|
85
89
|
return True
|
|
@@ -12,6 +12,31 @@ def test_constants_only_rejects_function_definition() -> None:
|
|
|
12
12
|
assert content_analysis._is_constants_only_python_content("def f():\n return 1\n") is False
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def test_constants_only_accepts_join_on_a_string_literal_inside_re_compile() -> None:
|
|
16
|
+
content = (
|
|
17
|
+
'"""doc."""\n'
|
|
18
|
+
"import re\n"
|
|
19
|
+
"NAMES = ('one', 'two')\n"
|
|
20
|
+
'PATTERN = re.compile("(" + "|".join(NAMES) + ")")\n'
|
|
21
|
+
)
|
|
22
|
+
assert content_analysis._is_constants_only_python_content(content) is True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_constants_only_rejects_join_on_a_non_literal_receiver() -> None:
|
|
26
|
+
content = (
|
|
27
|
+
'"""doc."""\n'
|
|
28
|
+
'SEPARATOR = "|"\n'
|
|
29
|
+
'NAMES = ("one", "two")\n'
|
|
30
|
+
"PATTERN = SEPARATOR.join(NAMES)\n"
|
|
31
|
+
)
|
|
32
|
+
assert content_analysis._is_constants_only_python_content(content) is False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_constants_only_rejects_a_non_allowlisted_method_on_a_string_literal() -> None:
|
|
36
|
+
content = '"""doc."""\nSHOUT = "hi".upper()\n'
|
|
37
|
+
assert content_analysis._is_constants_only_python_content(content) is False
|
|
38
|
+
|
|
39
|
+
|
|
15
40
|
def test_post_edit_import_only_allows_import_removal() -> None:
|
|
16
41
|
existing = "import os\n\ndef run(): return 1\n"
|
|
17
42
|
tool_input = {"old_string": "import os\n", "new_string": ""}
|
|
@@ -133,6 +133,33 @@ def test_check_comment_changes_allows_comment_removal_without_advisory(
|
|
|
133
133
|
assert captured.err == ""
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def test_check_comment_changes_does_not_flag_added_stealth_keep_marker() -> None:
|
|
137
|
+
old_content = "x = 1\n"
|
|
138
|
+
new_content = "x = 1 # STEALTH: Keep -- reason\n"
|
|
139
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
|
|
140
|
+
assert issues == []
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_check_comment_changes_does_not_flag_stealth_keep_retained_on_touched_line() -> None:
|
|
144
|
+
old_content = "x = 1 # STEALTH: Keep -- reason\n"
|
|
145
|
+
new_content = "x = 2 # STEALTH: Keep -- reason\n"
|
|
146
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
|
|
147
|
+
assert issues == []
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.parametrize(
|
|
151
|
+
"each_marker",
|
|
152
|
+
("# noqa: E501", "# type: ignore[misc]", "# TODO: revisit", "# FIXME: revisit"),
|
|
153
|
+
)
|
|
154
|
+
def test_check_comment_changes_still_flags_other_added_directive_markers(
|
|
155
|
+
each_marker: str,
|
|
156
|
+
) -> None:
|
|
157
|
+
old_content = "x = 1\n"
|
|
158
|
+
new_content = f"x = 1 {each_marker}\n"
|
|
159
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
|
|
160
|
+
assert any("comment added" in each_issue.lower() for each_issue in issues)
|
|
161
|
+
|
|
162
|
+
|
|
136
163
|
def test_python_check_should_exempt_directive_without_space_after_hash() -> None:
|
|
137
164
|
for each_directive in ("#noqa", "#type: ignore", "#pylint: disable", "#pragma: no cover"):
|
|
138
165
|
content = f"{each_directive}\n"
|
|
@@ -156,6 +156,16 @@ def test_should_exempt_fixme_with_trailing_prose() -> None:
|
|
|
156
156
|
assert _is_exempt_python_comment(token) is True
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
def test_should_exempt_stealth_keep_with_trailing_reason() -> None:
|
|
160
|
+
token = _build_comment_token("# STEALTH: Keep bypass reviewed in incident 4021")
|
|
161
|
+
assert _is_exempt_python_comment(token) is True
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_should_flag_stealth_marker_missing_required_space_after_colon() -> None:
|
|
165
|
+
token = _build_comment_token("# STEALTH:Keep bypass reviewed in incident 4021")
|
|
166
|
+
assert _is_exempt_python_comment(token) is False
|
|
167
|
+
|
|
168
|
+
|
|
159
169
|
def test_should_exempt_shebang_at_line_one_column_zero() -> None:
|
|
160
170
|
token = tokenize.TokenInfo(
|
|
161
171
|
type=tokenize.COMMENT,
|
|
@@ -111,6 +111,4 @@ ALL_RUNNER_FIELDS: frozenset[str] = frozenset(
|
|
|
111
111
|
)
|
|
112
112
|
AUTOMATIC_ADVISORY_DIRECTORY_NAME: str = "automatic_advisory"
|
|
113
113
|
AUTOMATIC_ADVISORY_CLI_FILE_NAME: str = "cli.py"
|
|
114
|
-
WINDOWS_PLATFORM: str = "win32"
|
|
115
|
-
CREATE_NO_WINDOW_ATTRIBUTE: str = "CREATE_NO_WINDOW"
|
|
116
114
|
POWERSHELL_CALL_OPERATOR: str = "& "
|
|
@@ -9,6 +9,8 @@ from git_hooks_constants import (
|
|
|
9
9
|
GH_PR_VIEW_TIMEOUT_SECONDS,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
def get_pull_request_url(repo_dir: Path) -> str | None:
|
|
14
16
|
try:
|
|
@@ -19,6 +21,7 @@ def get_pull_request_url(repo_dir: Path) -> str | None:
|
|
|
19
21
|
capture_output=True,
|
|
20
22
|
text=True,
|
|
21
23
|
timeout=GH_PR_VIEW_TIMEOUT_SECONDS,
|
|
24
|
+
creationflags=hidden_window_creation_flags(),
|
|
22
25
|
)
|
|
23
26
|
except (OSError, subprocess.SubprocessError):
|
|
24
27
|
return None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Reach the hidden-window helpers from the scripts this directory deploys.
|
|
2
|
+
|
|
3
|
+
The installer copies this directory into a profile that holds no installed
|
|
4
|
+
Python distribution, and this repository's own tests run the deployed scripts
|
|
5
|
+
under ``python -S -E``, which hides every installed package from the child
|
|
6
|
+
process. On both routes ``hooks_constants`` is reachable only through the
|
|
7
|
+
deployed ``hooks`` directory, so this module puts that directory on
|
|
8
|
+
``sys.path`` and re-exports the helpers a caller asks for::
|
|
9
|
+
|
|
10
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
11
|
+
|
|
12
|
+
The directory goes on the end of ``sys.path``. ``PathFinder`` runs ahead of
|
|
13
|
+
every editable-install finder, so a trailing entry still selects the deployed
|
|
14
|
+
copy, and the head of ``sys.path`` stays exactly as the caller left it. A
|
|
15
|
+
caller that prepended its own roots keeps them first.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
_subprocess_window_hooks_directory = str(
|
|
24
|
+
Path(__file__).resolve().parents[2] / "hooks"
|
|
25
|
+
)
|
|
26
|
+
if _subprocess_window_hooks_directory not in sys.path:
|
|
27
|
+
sys.path.append(_subprocess_window_hooks_directory)
|
|
28
|
+
|
|
29
|
+
from hooks_constants.subprocess_window import (
|
|
30
|
+
HiddenWindowStartupInfo,
|
|
31
|
+
detached_hidden_window_creation_flags,
|
|
32
|
+
hidden_window_creation_flags,
|
|
33
|
+
inherited_stream_startup_info,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"HiddenWindowStartupInfo",
|
|
38
|
+
"detached_hidden_window_creation_flags",
|
|
39
|
+
"hidden_window_creation_flags",
|
|
40
|
+
"inherited_stream_startup_info",
|
|
41
|
+
]
|
|
@@ -15,6 +15,9 @@ from verification_notice import build_verification_notice
|
|
|
15
15
|
from verification_notice_context import VerificationNoticeContext
|
|
16
16
|
from verification_start import start_automatic_advisory
|
|
17
17
|
|
|
18
|
+
WINDOWS_NO_WINDOW_FLAG = 0x08000000
|
|
19
|
+
EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
def build_context(
|
|
20
23
|
repository_root: Path,
|
|
@@ -102,6 +105,7 @@ def _assert_launch_matches_runner(
|
|
|
102
105
|
assert launch_options["stdin"] is subprocess.DEVNULL
|
|
103
106
|
assert launch_options["stdout"] is subprocess.DEVNULL
|
|
104
107
|
assert launch_options["stderr"] is subprocess.DEVNULL
|
|
108
|
+
assert launch_options["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
|
|
105
109
|
assert "--once" not in command
|
|
106
110
|
|
|
107
111
|
|
|
@@ -36,6 +36,8 @@ from git_hooks_constants.verification_notice_constants import (
|
|
|
36
36
|
)
|
|
37
37
|
from local_verification.manifest import ManifestRunFatal, load_manifest
|
|
38
38
|
|
|
39
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
40
|
+
|
|
39
41
|
|
|
40
42
|
@dataclass(frozen=True)
|
|
41
43
|
class VerificationNoticeContext:
|
|
@@ -202,6 +204,7 @@ def _run_git_query(
|
|
|
202
204
|
encoding=GIT_OUTPUT_ENCODING_NAME,
|
|
203
205
|
errors=GIT_OUTPUT_DECODE_ERRORS_POLICY,
|
|
204
206
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
207
|
+
creationflags=hidden_window_creation_flags(),
|
|
205
208
|
)
|
|
206
209
|
except (OSError, subprocess.SubprocessError, UnicodeError):
|
|
207
210
|
return None
|
|
@@ -13,7 +13,6 @@ from git_hooks_constants.verification_notice_constants import (
|
|
|
13
13
|
ALL_RUNNER_FIELDS,
|
|
14
14
|
AUTOMATIC_ADVISORY_CLI_FILE_NAME,
|
|
15
15
|
AUTOMATIC_ADVISORY_DIRECTORY_NAME,
|
|
16
|
-
CREATE_NO_WINDOW_ATTRIBUTE,
|
|
17
16
|
EXECUTOR_DIRECTORY_NAME,
|
|
18
17
|
GIT_DIRECTORY_NAME,
|
|
19
18
|
JSON_ENCODING,
|
|
@@ -22,10 +21,15 @@ from git_hooks_constants.verification_notice_constants import (
|
|
|
22
21
|
RUNNER_PYTHON_FIELD,
|
|
23
22
|
RUNNER_SETTINGS_FIELD,
|
|
24
23
|
TARGET_REPOSITORY_REMOTE,
|
|
25
|
-
WINDOWS_PLATFORM,
|
|
26
24
|
)
|
|
27
25
|
from verification_notice_context import VerificationNoticeContext
|
|
28
26
|
|
|
27
|
+
_subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
|
|
28
|
+
if _subprocess_window_hooks_directory not in sys.path:
|
|
29
|
+
sys.path.append(_subprocess_window_hooks_directory)
|
|
30
|
+
|
|
31
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
32
|
+
|
|
29
33
|
|
|
30
34
|
def start_automatic_advisory(context: VerificationNoticeContext) -> None:
|
|
31
35
|
"""Start the detached advisory poller when native metadata is valid."""
|
|
@@ -66,7 +70,7 @@ def _launch_advisory(
|
|
|
66
70
|
stdin=subprocess.DEVNULL,
|
|
67
71
|
stdout=subprocess.DEVNULL,
|
|
68
72
|
stderr=subprocess.DEVNULL,
|
|
69
|
-
creationflags=
|
|
73
|
+
creationflags=hidden_window_creation_flags(),
|
|
70
74
|
)
|
|
71
75
|
|
|
72
76
|
|
|
@@ -93,8 +97,3 @@ def load_runner_configuration(
|
|
|
93
97
|
return None
|
|
94
98
|
return python_path, settings_path
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
def _windows_creation_flags() -> int:
|
|
98
|
-
if sys.platform != WINDOWS_PLATFORM:
|
|
99
|
-
return 0
|
|
100
|
-
return getattr(subprocess, CREATE_NO_WINDOW_ATTRIBUTE, 0)
|
|
@@ -132,12 +132,14 @@ ALL_TOKEN_ANCHORED_EXEMPT_COMMENT_BODIES: tuple[str, ...] = (
|
|
|
132
132
|
"pragma:",
|
|
133
133
|
)
|
|
134
134
|
ALL_TOKEN_ANCHORED_DIRECTIVE_BOUNDARY_CHARACTERS: frozenset[str] = frozenset({":"})
|
|
135
|
+
STEALTH_KEEP_COMMENT_MARKER: str = "STEALTH: Keep"
|
|
135
136
|
ALL_FREE_FORM_EXEMPT_COMMENT_BODIES: tuple[str, ...] = (
|
|
136
137
|
"type:",
|
|
137
138
|
"TODO",
|
|
138
139
|
"FIXME",
|
|
139
140
|
"HACK",
|
|
140
141
|
"XXX",
|
|
142
|
+
STEALTH_KEEP_COMMENT_MARKER,
|
|
141
143
|
)
|
|
142
144
|
CHAINED_INLINE_COMMENT_PATTERN = re.compile(r"#")
|
|
143
145
|
ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES: tuple[str, ...] = (
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Creation flags that keep a spawned child off the operator's screen on Windows.
|
|
2
|
+
|
|
3
|
+
A hook or script often runs with no console of its own. When such a process
|
|
4
|
+
starts a child on Windows, the operating system gives that child a brand new
|
|
5
|
+
console, and the operator sees a black window flash open and shut while agents
|
|
6
|
+
work. ``CREATE_NO_WINDOW`` tells Windows to skip that console. Every child this
|
|
7
|
+
package spawns passes the flag, so the desktop stays quiet::
|
|
8
|
+
|
|
9
|
+
subprocess.run(
|
|
10
|
+
["git", "status"],
|
|
11
|
+
creationflags=hidden_window_creation_flags(),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
On Windows that call returns ``0x08000000`` and no window appears. On Linux and
|
|
15
|
+
macOS it returns ``0``, which is the value ``subprocess`` already uses when a
|
|
16
|
+
caller passes no ``creationflags`` at all, so behaviour there is unchanged.
|
|
17
|
+
``CREATE_NO_WINDOW`` is absent from a non-Windows Python build, so the lookup
|
|
18
|
+
reads the attribute at call time and falls back to ``0``.
|
|
19
|
+
|
|
20
|
+
``CREATE_NO_WINDOW`` gives the child a console of its own. A child that writes
|
|
21
|
+
to a stream the caller redirected is unaffected, because the redirected handle
|
|
22
|
+
travels with it. A child that inherits the caller's terminal instead loses that
|
|
23
|
+
terminal, and the operator stops seeing its output. A few call sites hand their
|
|
24
|
+
streams straight to the operator, and those pass
|
|
25
|
+
``startupinfo=inherited_stream_startup_info()`` instead. That startup record
|
|
26
|
+
keeps the inherited streams and hides the console window, so the operator still
|
|
27
|
+
reads the output and still sees no window.
|
|
28
|
+
|
|
29
|
+
``subprocess.STARTUPINFO`` is a Windows-only name. Typeshed declares it under a
|
|
30
|
+
``sys.platform == "win32"`` guard, so a type check that runs on Linux, as this
|
|
31
|
+
repository's continuous integration does, cannot see the name at all. The
|
|
32
|
+
startup record is therefore described here as a structural ``Protocol`` naming
|
|
33
|
+
the two fields this module sets, and the record itself is fetched through
|
|
34
|
+
``getattr`` at call time. Both moves match what the module already does for the
|
|
35
|
+
creation flags, and they keep the runtime ``sys.platform`` test in one place, so
|
|
36
|
+
one function serves every platform.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import subprocess
|
|
42
|
+
import sys
|
|
43
|
+
from typing import Protocol
|
|
44
|
+
|
|
45
|
+
from hooks_constants.subprocess_window_constants import (
|
|
46
|
+
CREATE_NEW_PROCESS_GROUP_ATTRIBUTE,
|
|
47
|
+
CREATE_NO_WINDOW_ATTRIBUTE,
|
|
48
|
+
HIDE_WINDOW_COMMAND_ATTRIBUTE,
|
|
49
|
+
NO_CREATION_FLAGS,
|
|
50
|
+
SHOW_WINDOW_FLAG_ATTRIBUTE,
|
|
51
|
+
STARTUP_INFO_ATTRIBUTE,
|
|
52
|
+
WINDOWS_PLATFORM,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"HiddenWindowStartupInfo",
|
|
57
|
+
"detached_hidden_window_creation_flags",
|
|
58
|
+
"hidden_window_creation_flags",
|
|
59
|
+
"inherited_stream_startup_info",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class HiddenWindowStartupInfo(Protocol):
|
|
64
|
+
"""The two ``subprocess.STARTUPINFO`` fields that hide a child's console."""
|
|
65
|
+
|
|
66
|
+
dwFlags: int
|
|
67
|
+
wShowWindow: int
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _windows_flag(flag_attribute_name: str) -> int:
|
|
71
|
+
"""Return one Windows creation flag, or zero on any other platform.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
flag_attribute_name: The name of a ``subprocess`` creation-flag constant.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The flag value on Windows, and zero elsewhere or when the running
|
|
78
|
+
Python build does not define that constant.
|
|
79
|
+
"""
|
|
80
|
+
if sys.platform != WINDOWS_PLATFORM:
|
|
81
|
+
return NO_CREATION_FLAGS
|
|
82
|
+
return int(getattr(subprocess, flag_attribute_name, NO_CREATION_FLAGS))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def hidden_window_creation_flags() -> int:
|
|
86
|
+
"""Return the creation flags that start a child without a console window.
|
|
87
|
+
|
|
88
|
+
Pass the result as the ``creationflags`` argument of ``subprocess.run``,
|
|
89
|
+
``subprocess.Popen``, ``subprocess.check_output``, or ``subprocess.call``::
|
|
90
|
+
|
|
91
|
+
subprocess.run(["git", "rev-parse", "HEAD"],
|
|
92
|
+
creationflags=hidden_window_creation_flags())
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The hidden-console flag on Windows, and zero on every other platform.
|
|
96
|
+
"""
|
|
97
|
+
return _windows_flag(CREATE_NO_WINDOW_ATTRIBUTE)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def detached_hidden_window_creation_flags() -> int:
|
|
101
|
+
"""Return the creation flags for a long-lived child that outlives its parent.
|
|
102
|
+
|
|
103
|
+
A detached poller needs its own process group so a console interrupt aimed
|
|
104
|
+
at the parent never reaches it, and it needs the hidden console for the same
|
|
105
|
+
reason every other child does::
|
|
106
|
+
|
|
107
|
+
subprocess.Popen(argv,
|
|
108
|
+
creationflags=detached_hidden_window_creation_flags())
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A new process group combined with the hidden-console flag on Windows,
|
|
112
|
+
and zero on every other platform.
|
|
113
|
+
"""
|
|
114
|
+
return _windows_flag(CREATE_NEW_PROCESS_GROUP_ATTRIBUTE) | hidden_window_creation_flags()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def inherited_stream_startup_info() -> HiddenWindowStartupInfo | None:
|
|
118
|
+
"""Return a startup record that hides the console of a stream-inheriting child.
|
|
119
|
+
|
|
120
|
+
Use this at a call site that lets the child write straight to the operator's
|
|
121
|
+
terminal, where ``hidden_window_creation_flags`` would take that terminal
|
|
122
|
+
away::
|
|
123
|
+
|
|
124
|
+
subprocess.run(["python", "-m", "pytest"],
|
|
125
|
+
startupinfo=inherited_stream_startup_info())
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A Windows startup record asking for a hidden window, and ``None`` on
|
|
129
|
+
every other platform, which is the value ``subprocess`` already uses
|
|
130
|
+
when a caller passes no ``startupinfo`` at all.
|
|
131
|
+
"""
|
|
132
|
+
if sys.platform != WINDOWS_PLATFORM:
|
|
133
|
+
return None
|
|
134
|
+
startup_information = getattr(subprocess, STARTUP_INFO_ATTRIBUTE)()
|
|
135
|
+
startup_information.dwFlags |= getattr(
|
|
136
|
+
subprocess, SHOW_WINDOW_FLAG_ATTRIBUTE, NO_CREATION_FLAGS
|
|
137
|
+
)
|
|
138
|
+
startup_information.wShowWindow = getattr(
|
|
139
|
+
subprocess, HIDE_WINDOW_COMMAND_ATTRIBUTE, NO_CREATION_FLAGS
|
|
140
|
+
)
|
|
141
|
+
return startup_information
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Configuration constants for the shared hidden-console creation flags."""
|
|
2
|
+
|
|
3
|
+
WINDOWS_PLATFORM: str = "win32"
|
|
4
|
+
CREATE_NO_WINDOW_ATTRIBUTE: str = "CREATE_NO_WINDOW"
|
|
5
|
+
CREATE_NEW_PROCESS_GROUP_ATTRIBUTE: str = "CREATE_NEW_PROCESS_GROUP"
|
|
6
|
+
STARTUP_INFO_ATTRIBUTE: str = "STARTUPINFO"
|
|
7
|
+
SHOW_WINDOW_FLAG_ATTRIBUTE: str = "STARTF_USESHOWWINDOW"
|
|
8
|
+
HIDE_WINDOW_COMMAND_ATTRIBUTE: str = "SW_HIDE"
|
|
9
|
+
NO_CREATION_FLAGS: int = 0
|
|
@@ -17,6 +17,7 @@ if str(_HOOKS_ROOT) not in sys.path:
|
|
|
17
17
|
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
18
18
|
|
|
19
19
|
from hooks_constants.code_rules_enforcer_constants import (
|
|
20
|
+
ALL_FREE_FORM_EXEMPT_COMMENT_BODIES,
|
|
20
21
|
ALL_VALIDATION_PHASES,
|
|
21
22
|
CONFIG_DIRECTORY_SEGMENT,
|
|
22
23
|
CONSTANTS_MODULE_SUFFIX,
|
|
@@ -26,6 +27,7 @@ from hooks_constants.code_rules_enforcer_constants import (
|
|
|
26
27
|
JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN,
|
|
27
28
|
RETURN_CALL_OPENING_PARENTHESIS_PATTERN,
|
|
28
29
|
SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN,
|
|
30
|
+
STEALTH_KEEP_COMMENT_MARKER,
|
|
29
31
|
UNKNOWN_VALIDATION_PHASE_MESSAGE_TEMPLATE,
|
|
30
32
|
apply_edits,
|
|
31
33
|
edits_for_tool,
|
|
@@ -106,6 +108,10 @@ def test_constants_module_suffix_and_config_directory_segment_are_defined() -> N
|
|
|
106
108
|
assert CONFIG_DIRECTORY_SEGMENT == "config"
|
|
107
109
|
|
|
108
110
|
|
|
111
|
+
def test_free_form_exempt_comment_bodies_include_stealth_keep_marker() -> None:
|
|
112
|
+
assert STEALTH_KEEP_COMMENT_MARKER in ALL_FREE_FORM_EXEMPT_COMMENT_BODIES
|
|
113
|
+
|
|
114
|
+
|
|
109
115
|
def test_validation_phase_constants_are_reexported_and_render() -> None:
|
|
110
116
|
assert ALL_VALIDATION_PHASES == {EDIT_LANE_PHASE, FULL_GATE_PHASE}
|
|
111
117
|
rendered_message = UNKNOWN_VALIDATION_PHASE_MESSAGE_TEMPLATE.format(
|