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.
Files changed (121) hide show
  1. package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
  2. package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
  3. package/.agents/skills/fresh-branch/SKILL.md +1 -0
  4. package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
  5. package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
  6. package/.agents/skills/syncing-submodules/SKILL.md +1 -0
  7. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
  8. package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
  9. package/.agents/skills/test-runner/SKILL.md +1 -0
  10. package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
  11. package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
  12. package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
  13. package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
  14. package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
  15. package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
  16. package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
  23. package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
  24. package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
  25. package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
  26. package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
  27. package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
  28. package/_shared/pr-loop/scripts/preflight.py +7 -0
  29. package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
  30. package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
  31. package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
  32. package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
  33. package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
  34. package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
  35. package/bin/install-constants.mjs +4 -2
  36. package/bin/install.cursor-rules.test.mjs +57 -15
  37. package/bin/install.mjs +10 -15
  38. package/bin/pstack-model-preferences.claude.json +23 -0
  39. package/hooks/advisory/pr_done_reminder.py +7 -0
  40. package/hooks/advisory/refactor_guard.py +8 -0
  41. package/hooks/advisory/test_pr_done_reminder.py +21 -0
  42. package/hooks/advisory/test_refactor_guard.py +40 -0
  43. package/hooks/blocking/block_main_commit.py +4 -0
  44. package/hooks/blocking/code_rules_comments.py +39 -5
  45. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
  46. package/hooks/blocking/precommit_code_rules_gate.py +3 -0
  47. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
  48. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
  49. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
  50. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
  51. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
  52. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
  53. package/hooks/git-hooks/pull_request_handoff.py +3 -0
  54. package/hooks/git-hooks/subprocess_window_access.py +41 -0
  55. package/hooks/git-hooks/test_verification_start.py +4 -0
  56. package/hooks/git-hooks/verification_notice_context.py +3 -0
  57. package/hooks/git-hooks/verification_start.py +7 -8
  58. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
  59. package/hooks/hooks_constants/subprocess_window.py +141 -0
  60. package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
  61. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
  62. package/hooks/hooks_constants/test_subprocess_window.py +128 -0
  63. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
  64. package/hooks/validation/mypy_validator.py +4 -0
  65. package/hooks/validators/README.md +1 -0
  66. package/hooks/validators/git_checks.py +4 -0
  67. package/hooks/validators/health_check.py +3 -0
  68. package/hooks/validators/hook_timing_harness.py +3 -0
  69. package/hooks/validators/mypy_integration.py +4 -0
  70. package/hooks/validators/ruff_integration.py +6 -0
  71. package/hooks/validators/run_all_validators.py +7 -0
  72. package/hooks/validators/subprocess_window_access.py +41 -0
  73. package/hooks/workflow/auto_formatter.py +8 -0
  74. package/hooks/workflow/test_auto_formatter.py +39 -0
  75. package/package.json +1 -1
  76. package/rules/pstack-models.md +4 -3
  77. package/scripts/automatic_advisory/checkout.py +8 -1
  78. package/scripts/automatic_advisory/cli.py +9 -3
  79. package/scripts/automatic_advisory/config/constants.py +0 -2
  80. package/scripts/automatic_advisory/execution.py +7 -1
  81. package/scripts/automatic_advisory/git.py +9 -1
  82. package/scripts/automatic_advisory/process_host.py +3 -0
  83. package/scripts/claude_chain_runner.py +3 -0
  84. package/scripts/gh_artifact_upload.py +3 -0
  85. package/scripts/grok_headless_runner.py +3 -0
  86. package/scripts/grok_patch_artifacts.py +4 -0
  87. package/scripts/grok_worker_preflight.py +3 -0
  88. package/scripts/invoke_code_review.py +3 -0
  89. package/scripts/local_verification/command_runner.py +3 -0
  90. package/scripts/local_verification/git_state.py +3 -0
  91. package/scripts/policy_lint/adapter_detectors.py +48 -2
  92. package/scripts/policy_lint/adapter_pairing.py +150 -2
  93. package/scripts/policy_lint/adapters.py +46 -4
  94. package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
  95. package/scripts/policy_lint/config/constants.py +3 -0
  96. package/scripts/policy_lint/registry.py +1 -1
  97. package/scripts/policy_lint/selection_git.py +11 -0
  98. package/scripts/repository_checks/config/constants.py +5 -0
  99. package/scripts/repository_checks/config/policy_document.py +19 -0
  100. package/scripts/repository_checks/email_exemptions.py +14 -71
  101. package/scripts/repository_checks/path_exemptions.py +35 -0
  102. package/scripts/repository_checks/policy_document.py +120 -0
  103. package/scripts/repository_checks/tracked_secrets.py +27 -4
  104. package/scripts/setup_project_paths.py +3 -0
  105. package/scripts/spawn_grok_batch.py +3 -0
  106. package/scripts/subprocess_window_access.py +41 -0
  107. package/scripts/test_dispatcher_profile_import.py +4 -0
  108. package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
  109. package/scripts/tests/test_email_exemptions.py +37 -0
  110. package/scripts/tests/test_path_exemptions.py +164 -0
  111. package/scripts/tests/test_policy_document.py +104 -0
  112. package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
  113. package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
  114. package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
  115. package/scripts/tests/test_policy_lint_selection.py +34 -0
  116. package/scripts/tests/test_subprocess_window_access.py +118 -0
  117. package/scripts/tests/test_tracked_secrets.py +79 -0
  118. package/scripts/validate_instruction_pairs.py +3 -0
  119. package/scripts/verify_installable_package.py +5 -0
  120. package/scripts/automatic_advisory/window_flags.py +0 -41
  121. 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``) accept any
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), and the TODO-family markers carry
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(...)``) that
60
- build objects without side effects; any other call or a comprehension reads
61
- as import-time behavior.
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=_windows_creation_flags(),
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(