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
@@ -27,6 +27,8 @@ from dev_env_scripts_constants.grok_run_ledger_constants import (
27
27
  UTF8_ENCODING,
28
28
  )
29
29
 
30
+ from subprocess_window_access import hidden_window_creation_flags
31
+
30
32
 
31
33
  def compute_sha256_hex(content: bytes) -> str:
32
34
  """Return the hex SHA-256 digest of raw bytes.
@@ -58,11 +60,13 @@ def extract_worktree_diff(
58
60
  ["git", "-C", str(worktree_path), "diff", base_sha],
59
61
  text=True,
60
62
  encoding=UTF8_ENCODING,
63
+ creationflags=hidden_window_creation_flags(),
61
64
  )
62
65
  changed_paths_listing = subprocess.check_output(
63
66
  ["git", "-C", str(worktree_path), "diff", "--name-only", base_sha],
64
67
  text=True,
65
68
  encoding=UTF8_ENCODING,
69
+ creationflags=hidden_window_creation_flags(),
66
70
  )
67
71
  changed_paths = tuple(
68
72
  each_line.strip()
@@ -90,6 +90,8 @@ from dev_env_scripts_constants.grok_worker_constants import (
90
90
  UTF8_ENCODING,
91
91
  )
92
92
 
93
+ from subprocess_window_access import hidden_window_creation_flags
94
+
93
95
  preflight_which = shutil.which
94
96
  preflight_subprocess_runner = subprocess.run
95
97
  preflight_time = time.time
@@ -259,6 +261,7 @@ def _run_grok_command(
259
261
  errors=UTF8_DECODE_ERRORS,
260
262
  timeout=timeout_seconds,
261
263
  check=False,
264
+ creationflags=hidden_window_creation_flags(),
262
265
  )
263
266
  except OSError:
264
267
  return None
@@ -104,6 +104,8 @@ from dev_env_scripts_constants.timing import ( # noqa: E402
104
104
  DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
105
105
  )
106
106
 
107
+ from subprocess_window_access import hidden_window_creation_flags
108
+
107
109
 
108
110
  @dataclass(frozen=True)
109
111
  class CodeReviewOutcome:
@@ -265,6 +267,7 @@ def is_working_tree_dirty(working_directory: Path) -> bool:
265
267
  capture_output=True,
266
268
  text=True,
267
269
  check=False,
270
+ creationflags=hidden_window_creation_flags(),
268
271
  )
269
272
  if completion.returncode != 0:
270
273
  return True
@@ -11,6 +11,8 @@ from .config import (
11
11
  )
12
12
  from .model import CommandCapture
13
13
 
14
+ from subprocess_window_access import hidden_window_creation_flags
15
+
14
16
 
15
17
  def run_command(
16
18
  all_arguments: tuple[str, ...], check_directory: Path, timeout_seconds: float
@@ -73,6 +75,7 @@ def _run_subprocess(
73
75
  encoding=UTF8_ENCODING,
74
76
  errors="replace",
75
77
  timeout=timeout_seconds,
78
+ creationflags=hidden_window_creation_flags(),
76
79
  )
77
80
 
78
81
 
@@ -23,6 +23,8 @@ from .config import (
23
23
  )
24
24
  from .config.timing import GIT_METADATA_TIMEOUT_SECONDS
25
25
 
26
+ from subprocess_window_access import hidden_window_creation_flags
27
+
26
28
 
27
29
  @dataclass(frozen=True)
28
30
  class CandidateSnapshot:
@@ -82,6 +84,7 @@ def _run_git_bytes(
82
84
  capture_output=True,
83
85
  check=False,
84
86
  timeout=GIT_METADATA_TIMEOUT_SECONDS,
87
+ creationflags=hidden_window_creation_flags(),
85
88
  )
86
89
  except (OSError, subprocess.SubprocessError):
87
90
  return None
@@ -4,6 +4,7 @@ from collections.abc import Iterable
4
4
  from contextlib import redirect_stderr, redirect_stdout
5
5
  from io import StringIO
6
6
  from pathlib import Path, PurePosixPath
7
+ from types import ModuleType
7
8
 
8
9
  from . import adapter_support
9
10
  from .config import constants
@@ -118,10 +119,49 @@ def validator_diagnostics(
118
119
  return adapter_support._diagnostics_for_messages(document, "validators", all_messages)
119
120
 
120
121
 
122
+ def _is_untouched_function(
123
+ message: str, all_changed_lines: frozenset[int], scoping_module: ModuleType
124
+ ) -> bool:
125
+ span_range = scoping_module.function_length_span_range(message)
126
+ return span_range is not None and all_changed_lines.isdisjoint(span_range)
127
+
128
+
129
+ def _change_scoped_messages(
130
+ all_messages: Iterable[str],
131
+ document: Document,
132
+ load_pr_loop_module: adapter_support.HookModuleLoader,
133
+ ) -> tuple[str, ...]:
134
+ """Drop function-length messages for functions the change never touched.
135
+
136
+ ::
137
+
138
+ an import added above an existing long ``main`` -> dropped
139
+ a statement added inside that ``main`` -> kept
140
+
141
+ Args:
142
+ all_messages: Every message the code-rule engine returned.
143
+ document: Current document and the lines this change touched.
144
+ load_pr_loop_module: Loader for the shared pull-request-loop scripts.
145
+
146
+ Returns:
147
+ The messages that remain in scope for this change.
148
+ """
149
+ all_changed_lines = document.changed_lines
150
+ if all_changed_lines is None:
151
+ return tuple(all_messages)
152
+ scoping_module = load_pr_loop_module(constants.VIOLATION_SCOPING_MODULE_NAME)
153
+ return tuple(
154
+ each_message
155
+ for each_message in all_messages
156
+ if not _is_untouched_function(each_message, all_changed_lines, scoping_module)
157
+ )
158
+
159
+
121
160
  def code_rule_diagnostics(
122
161
  document: Document,
123
162
  repository_root: Path,
124
163
  load_module: adapter_support.HookModuleLoader,
164
+ load_pr_loop_module: adapter_support.HookModuleLoader,
125
165
  ) -> tuple[Diagnostic, ...]:
126
166
  """Run the code-rule engine on one document.
127
167
 
@@ -129,9 +169,10 @@ def code_rule_diagnostics(
129
169
  document: Current text and optional prior text.
130
170
  repository_root: Request repository root for sibling-file resolution.
131
171
  load_module: Hook module loader.
172
+ load_pr_loop_module: Loader for the shared pull-request-loop scripts.
132
173
 
133
174
  Returns:
134
- Diagnostics from the code-rule engine.
175
+ Diagnostics from the code-rule engine, scoped to the change.
135
176
  """
136
177
  absolute_path = adapter_support._document_path(repository_root, document)
137
178
  legacy_module = load_module("blocking.code_rules_enforcer")
@@ -146,7 +187,12 @@ def code_rule_diagnostics(
146
187
  sibling_directory=absolute_path.parent,
147
188
  include_comment_policy=True,
148
189
  )
149
- return adapter_support._diagnostics_for_messages(document, "code-rules", all_messages)
190
+ all_scoped_messages = _change_scoped_messages(
191
+ all_messages, document, load_pr_loop_module
192
+ )
193
+ return adapter_support._diagnostics_for_messages(
194
+ document, "code-rules", all_scoped_messages
195
+ )
150
196
 
151
197
 
152
198
  def state_description_diagnostics(
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from collections import Counter
4
5
  from pathlib import PurePosixPath
5
6
 
6
7
  from . import adapter_support
@@ -52,12 +53,19 @@ def _remove_docstrings(parsed_tree: ast.AST) -> None:
52
53
  _remove_docstrings(each_child)
53
54
 
54
55
 
55
- def _ast_without_docstrings(source_text: str) -> str | None:
56
+ def _parsed_without_docstrings(source_text: str) -> ast.Module | None:
56
57
  try:
57
58
  parsed_tree = ast.parse(source_text)
58
- except SyntaxError:
59
+ except (SyntaxError, ValueError):
59
60
  return None
60
61
  _remove_docstrings(parsed_tree)
62
+ return parsed_tree
63
+
64
+
65
+ def _ast_without_docstrings(source_text: str) -> str | None:
66
+ parsed_tree = _parsed_without_docstrings(source_text)
67
+ if parsed_tree is None:
68
+ return None
61
69
  return ast.dump(parsed_tree, include_attributes=False)
62
70
 
63
71
 
@@ -71,6 +79,144 @@ def _is_docstring_only_python_document(document: Document) -> bool:
71
79
  return current_ast is not None and current_ast == prior_ast
72
80
 
73
81
 
82
+ def _is_import_statement(node: object) -> bool:
83
+ return isinstance(node, (ast.Import, ast.ImportFrom))
84
+
85
+
86
+ def _dumped_import_tally(all_nodes: list[object]) -> Counter[str]:
87
+ return Counter(
88
+ ast.dump(each_node, include_attributes=False)
89
+ for each_node in all_nodes
90
+ if isinstance(each_node, ast.AST) and _is_import_statement(each_node)
91
+ )
92
+
93
+
94
+ def _matches_positional_list(
95
+ all_current_nodes: list[object], all_prior_nodes: list[object]
96
+ ) -> bool:
97
+ if len(all_current_nodes) != len(all_prior_nodes):
98
+ return False
99
+ return all(
100
+ _matches_field(each_current_node, each_prior_node)
101
+ for each_current_node, each_prior_node in zip(
102
+ all_current_nodes, all_prior_nodes, strict=True
103
+ )
104
+ )
105
+
106
+
107
+ def _matches_statement_list(
108
+ all_current_nodes: list[object], all_prior_nodes: list[object]
109
+ ) -> bool:
110
+ if _dumped_import_tally(all_prior_nodes) - _dumped_import_tally(all_current_nodes):
111
+ return False
112
+ return _matches_positional_list(
113
+ [
114
+ each_node
115
+ for each_node in all_current_nodes
116
+ if not _is_import_statement(each_node)
117
+ ],
118
+ [
119
+ each_node
120
+ for each_node in all_prior_nodes
121
+ if not _is_import_statement(each_node)
122
+ ],
123
+ )
124
+
125
+
126
+ def _is_statement_list(all_nodes: list[object]) -> bool:
127
+ return all(isinstance(each_node, ast.stmt) for each_node in all_nodes)
128
+
129
+
130
+ def _matches_node_list(
131
+ all_current_nodes: list[object], all_prior_nodes: list[object]
132
+ ) -> bool:
133
+ if _is_statement_list(all_current_nodes) and _is_statement_list(all_prior_nodes):
134
+ return _matches_statement_list(all_current_nodes, all_prior_nodes)
135
+ return _matches_positional_list(all_current_nodes, all_prior_nodes)
136
+
137
+
138
+ def _matches_field(current_field: object, prior_field: object) -> bool:
139
+ if isinstance(current_field, ast.AST) and isinstance(prior_field, ast.AST):
140
+ return _matches_node(current_field, prior_field)
141
+ if isinstance(current_field, list) and isinstance(prior_field, list):
142
+ return _matches_node_list(current_field, prior_field)
143
+ if type(current_field) is not type(prior_field):
144
+ return False
145
+ return bool(current_field == prior_field)
146
+
147
+
148
+ def _matches_keyword(current_keyword: ast.keyword, prior_keyword: ast.keyword) -> bool:
149
+ return current_keyword.arg == prior_keyword.arg and _matches_node(
150
+ current_keyword.value, prior_keyword.value
151
+ )
152
+
153
+
154
+ def _keywords_after_match(
155
+ all_candidate_keywords: list[ast.keyword], prior_keyword: ast.keyword
156
+ ) -> list[ast.keyword] | None:
157
+ for each_index, each_candidate_keyword in enumerate(all_candidate_keywords):
158
+ if _matches_keyword(each_candidate_keyword, prior_keyword):
159
+ return all_candidate_keywords[each_index + 1 :]
160
+ return None
161
+
162
+
163
+ def _prior_keywords_survive(
164
+ all_current_keywords: list[ast.keyword], all_prior_keywords: list[ast.keyword]
165
+ ) -> bool:
166
+ all_remaining_keywords: list[ast.keyword] | None = list(all_current_keywords)
167
+ for each_prior_keyword in all_prior_keywords:
168
+ if all_remaining_keywords is None:
169
+ return False
170
+ all_remaining_keywords = _keywords_after_match(
171
+ all_remaining_keywords, each_prior_keyword
172
+ )
173
+ return all_remaining_keywords is not None
174
+
175
+
176
+ def _mapping_unpacking_count(all_keywords: list[ast.keyword]) -> int:
177
+ return sum(1 for each_keyword in all_keywords if each_keyword.arg is None)
178
+
179
+
180
+ def _matches_call(current_call: ast.Call, prior_call: ast.Call) -> bool:
181
+ if not _matches_node(current_call.func, prior_call.func):
182
+ return False
183
+ if not _matches_positional_list(list(current_call.args), list(prior_call.args)):
184
+ return False
185
+ if _mapping_unpacking_count(current_call.keywords) != _mapping_unpacking_count(
186
+ prior_call.keywords
187
+ ):
188
+ return False
189
+ return _prior_keywords_survive(
190
+ list(current_call.keywords), list(prior_call.keywords)
191
+ )
192
+
193
+
194
+ def _matches_node(current_node: ast.AST, prior_node: ast.AST) -> bool:
195
+ if type(current_node) is not type(prior_node):
196
+ return False
197
+ if isinstance(current_node, ast.Call) and isinstance(prior_node, ast.Call):
198
+ return _matches_call(current_node, prior_node)
199
+ return all(
200
+ _matches_field(
201
+ getattr(current_node, each_field_name, None),
202
+ getattr(prior_node, each_field_name, None),
203
+ )
204
+ for each_field_name in current_node._fields
205
+ )
206
+
207
+
208
+ def _is_import_and_keyword_addition_python_document(document: Document) -> bool:
209
+ if document.path.suffix.lower() != constants.PYTHON_SUFFIX:
210
+ return False
211
+ if document.prior_text is None:
212
+ return False
213
+ current_tree = _parsed_without_docstrings(document.text)
214
+ prior_tree = _parsed_without_docstrings(document.prior_text)
215
+ if current_tree is None or prior_tree is None:
216
+ return False
217
+ return _matches_node(current_tree, prior_tree)
218
+
219
+
74
220
  def _candidate_test_names(path: PurePosixPath) -> frozenset[str]:
75
221
  stem = path.stem
76
222
  suffix = path.suffix.lower()
@@ -157,6 +303,8 @@ def _is_unpaired_production(
157
303
  return False
158
304
  if _is_docstring_only_python_document(production_document):
159
305
  return False
306
+ if _is_import_and_keyword_addition_python_document(production_document):
307
+ return False
160
308
  return not _has_changed_test(production_path, all_changed_test_paths)
161
309
 
162
310
 
@@ -33,9 +33,13 @@ def _hooks_module(module_name: str) -> ModuleType:
33
33
 
34
34
 
35
35
  def _pr_loop_script_module(module_name: str) -> ModuleType:
36
+ marker_relative_path = (
37
+ module_name.replace(".", constants.POSIX_PATH_SEPARATOR)
38
+ + constants.PYTHON_SUFFIX
39
+ )
36
40
  scripts_directory = str(resolve_shared_scripts_directory(
37
41
  __file__, os.environ, constants.PR_LOOP_DIRECTORY_NAME,
38
- f"{module_name}{constants.PYTHON_SUFFIX}", constants.SHARED_ROOT_PARENT_INDEX
42
+ marker_relative_path, constants.SHARED_ROOT_PARENT_INDEX
39
43
  ))
40
44
  if scripts_directory not in sys.path:
41
45
  sys.path.insert(0, scripts_directory)
@@ -72,7 +76,7 @@ def code_rule_diagnostics(
72
76
  Diagnostics from the code-rule engine.
73
77
  """
74
78
  return adapter_detectors.code_rule_diagnostics(
75
- document, repository_root, _hooks_module
79
+ document, repository_root, _hooks_module, _pr_loop_script_module
76
80
  )
77
81
 
78
82
 
@@ -276,6 +280,25 @@ def accepts_python(document: Document) -> bool:
276
280
  return document.path.suffix.lower() == constants.PYTHON_SUFFIX
277
281
 
278
282
 
283
+ def accepts_production_python(document: Document) -> bool:
284
+ """Return whether the document is Python outside the test tree.
285
+
286
+ The hook lane exempts a test module so a test can stage an undercounting
287
+ fixture freely. This gate reuses the hook lane's own ``is_test_file``
288
+ predicate, so the two lanes cannot drift on what counts as a test file.
289
+
290
+ Args:
291
+ document: Candidate document.
292
+
293
+ Returns:
294
+ True for a Python document whose path is not a test path.
295
+ """
296
+ if not accepts_python(document):
297
+ return False
298
+ shared_module = _hooks_module("blocking.code_rules_shared")
299
+ return not shared_module.is_test_file(f"/{document.path.as_posix()}")
300
+
301
+
279
302
  def accepts_code(document: Document) -> bool:
280
303
  """Return whether the document contains supported source code.
281
304
 
@@ -292,6 +315,21 @@ def accepts_markdown(document: Document) -> bool:
292
315
  return document.path.suffix.lower() in constants.ALL_MARKDOWN_SUFFIXES
293
316
 
294
317
 
318
+ def _is_generated_document(document: Document) -> bool:
319
+ """Return whether a release tool writes the document from commit history.
320
+
321
+ A changelog is rebuilt from merged commit subjects, so its wording belongs
322
+ to the commits rather than to an author. Prose rules skip it.
323
+
324
+ Args:
325
+ document: Candidate document.
326
+
327
+ Returns:
328
+ True for a generated document name.
329
+ """
330
+ return document.path.name.lower() in constants.ALL_GENERATED_DOCUMENT_NAMES
331
+
332
+
295
333
  def accepts_source_or_markdown(document: Document) -> bool:
296
334
  """Return whether a state-description rule can inspect the document.
297
335
 
@@ -299,8 +337,10 @@ def accepts_source_or_markdown(document: Document) -> bool:
299
337
  document: Candidate document.
300
338
 
301
339
  Returns:
302
- True for supported source or Markdown.
340
+ True for authored source or Markdown, and False for a generated document.
303
341
  """
342
+ if _is_generated_document(document):
343
+ return False
304
344
  return accepts_code(document) or accepts_markdown(document)
305
345
 
306
346
 
@@ -311,8 +351,10 @@ def accepts_stored_prompt(document: Document) -> bool:
311
351
  document: Candidate document.
312
352
 
313
353
  Returns:
314
- True for Markdown in an instruction directory.
354
+ True for authored Markdown in an instruction directory.
315
355
  """
356
+ if _is_generated_document(document):
357
+ return False
316
358
  normalized_path = f"/{document.path.as_posix().lower()}"
317
359
  return accepts_markdown(document) and any(
318
360
  each_segment in normalized_path
@@ -5,6 +5,31 @@ from pathlib import PurePosixPath
5
5
  APPROVED_TEST_PATHS_BY_PRODUCTION_PATH: dict[
6
6
  PurePosixPath, frozenset[PurePosixPath]
7
7
  ] = {
8
+ PurePosixPath("packages/claude-dev-env/bin/install-constants.mjs"): frozenset(
9
+ {
10
+ PurePosixPath("packages/claude-dev-env/bin/install.cursor-rules.test.mjs"),
11
+ }
12
+ ),
13
+ PurePosixPath("packages/claude-dev-env/bin/install.mjs"): frozenset(
14
+ {
15
+ PurePosixPath("packages/claude-dev-env/bin/install.cursor-rules.test.mjs"),
16
+ }
17
+ ),
18
+ PurePosixPath(
19
+ "packages/claude-dev-env/hooks/blocking/code_rules_comments.py"
20
+ ): frozenset(
21
+ {
22
+ PurePosixPath(
23
+ "packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py"
24
+ ),
25
+ PurePosixPath(
26
+ "packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py"
27
+ ),
28
+ PurePosixPath(
29
+ "packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_javascript_comments.py"
30
+ ),
31
+ }
32
+ ),
8
33
  PurePosixPath(
9
34
  "packages/claude-dev-env/hooks/git-hooks/verification_notice_context.py"
10
35
  ): frozenset(
@@ -152,15 +177,6 @@ APPROVED_TEST_PATHS_BY_PRODUCTION_PATH: dict[
152
177
  ),
153
178
  }
154
179
  ),
155
- PurePosixPath(
156
- "packages/claude-dev-env/scripts/automatic_advisory/window_flags.py"
157
- ): frozenset(
158
- {
159
- PurePosixPath(
160
- "packages/claude-dev-env/scripts/tests/test_automatic_advisory_window_flags.py"
161
- ),
162
- }
163
- ),
164
180
  PurePosixPath(
165
181
  "packages/claude-dev-env/scripts/automatic_advisory/windows_job.py"
166
182
  ): frozenset(
@@ -34,6 +34,7 @@ RENAME_PATH_COUNT = 2
34
34
  PYTHON_SUFFIX = ".py"
35
35
  ALL_CODE_SUFFIXES = frozenset({".py", ".js", ".jsx", ".ts", ".tsx", ".mjs"})
36
36
  ALL_MARKDOWN_SUFFIXES = frozenset({".md", ".mdx"})
37
+ ALL_GENERATED_DOCUMENT_NAMES = frozenset({"changelog.md"})
37
38
  ALL_STORED_PROMPT_SEGMENTS = (
38
39
  "/.agents/",
39
40
  "/commands/",
@@ -83,6 +84,8 @@ TERMINOLOGY_MESSAGE_GROUP = "message"
83
84
 
84
85
  PR_LOOP_DIRECTORY_NAME = "pr-loop"
85
86
  SHARED_ROOT_PARENT_INDEX = 2
87
+ POSIX_PATH_SEPARATOR = "/"
88
+ VIOLATION_SCOPING_MODULE_NAME = "code_rules_gate_parts.violation_scoping"
86
89
 
87
90
  RETIRED_HOOK_PROSE_RULE_ID = "retired-hook-prose"
88
91
  RULES_DIRECTORY_NAME = "rules"
@@ -73,7 +73,7 @@ def _python_document_rules() -> tuple[model.Rule, ...]:
73
73
  ),
74
74
  build_document_rule(
75
75
  "subprocess-budget",
76
- adapters.accepts_python,
76
+ adapters.accepts_production_python,
77
77
  adapters.subprocess_budget_diagnostics,
78
78
  ),
79
79
  build_document_rule(
@@ -16,6 +16,14 @@ from .config.constants import (
16
16
  UTF8_ENCODING,
17
17
  )
18
18
 
19
+ import sys
20
+
21
+ _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
22
+ if _subprocess_window_hooks_directory not in sys.path:
23
+ sys.path.append(_subprocess_window_hooks_directory)
24
+
25
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
26
+
19
27
 
20
28
  class GitSelectionError(ValueError):
21
29
  """Raised when Git cannot provide the requested source bytes."""
@@ -44,6 +52,7 @@ def git_bytes_for(repository_root: Path, all_arguments: tuple[str, ...]) -> byte
44
52
  capture_output=True,
45
53
  check=False,
46
54
  env=_git_subprocess_environment(),
55
+ creationflags=hidden_window_creation_flags(),
47
56
  )
48
57
  if completed.returncode != 0:
49
58
  error_text = completed.stderr.decode(UTF8_ENCODING, errors="replace").strip()
@@ -69,6 +78,7 @@ def head_revision(repository_root: Path) -> str | None:
69
78
  capture_output=True,
70
79
  check=False,
71
80
  env=_git_subprocess_environment(),
81
+ creationflags=hidden_window_creation_flags(),
72
82
  )
73
83
  if completed.returncode != 0:
74
84
  error_text = completed.stderr.decode(UTF8_ENCODING, errors="replace").strip()
@@ -125,6 +135,7 @@ def _read_blob_bytes(repository_root: Path, revision: str, relative_path: str) -
125
135
  capture_output=True,
126
136
  check=False,
127
137
  env=_git_subprocess_environment(),
138
+ creationflags=hidden_window_creation_flags(),
128
139
  )
129
140
  if completed.returncode == 0:
130
141
  return completed.stdout
@@ -131,5 +131,10 @@ ALL_TRACKED_SECRET_EXACT_EXEMPTIONS: frozenset[tuple[str, str, str]] = frozenset
131
131
  "home-path",
132
132
  "5d956228802fbd4a65af8fa7fb4183f5a9f8793dbe218877aefe887b905f726d",
133
133
  ),
134
+ (
135
+ "shared_utils/theme_assets/contracts/evidence/theme_editor_asset_resolution_master.oneui9.v1.json",
136
+ "home-path",
137
+ "ec69fe88e2f9d3f1ed3504532c7484120591cbec7ae6fda017b0f9ca57a3fc25",
138
+ ),
134
139
  )
135
140
  )
@@ -0,0 +1,19 @@
1
+ """Repository policy document and exception-entry schema."""
2
+
3
+ import re
4
+
5
+ CONFIG_RELATIVE_PATH = "config/repository-policy.json"
6
+ DIGEST_PATTERN = re.compile(r"[0-9a-f]{64}")
7
+ FORBIDDEN_PATH_CHARACTERS = frozenset("\\:*?[]")
8
+ ALL_ENTRY_FIELDS = frozenset({"path", "sha256", "reason"})
9
+ ALL_FORBIDDEN_PATH_SEGMENTS = frozenset({"", ".", ".."})
10
+ FIRST_PRINTABLE_CODEPOINT = 32
11
+ VERSION_FIELD = "version"
12
+ SUPPORTED_DOCUMENT_VERSION = 1
13
+ EMAIL_EXEMPTIONS_FIELD = "email_exemptions"
14
+ PATH_EXEMPTIONS_FIELD = "path_exemptions"
15
+ ALL_EXEMPTION_FIELDS = frozenset({EMAIL_EXEMPTIONS_FIELD, PATH_EXEMPTIONS_FIELD})
16
+ ALL_DOCUMENT_FIELDS = ALL_EXEMPTION_FIELDS | {VERSION_FIELD}
17
+ EMAIL_EXEMPTION_SUBJECT = "Email exemption"
18
+ PATH_EXEMPTION_SUBJECT = "Path exemption"
19
+ EMAIL_CATEGORY = "email"