claude-dev-env 7.4.1 → 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 (178) 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 -16
  38. package/bin/install.policy-lint.test.mjs +1 -2
  39. package/bin/install.test.mjs +0 -1
  40. package/bin/pstack-model-preferences.claude.json +23 -0
  41. package/hooks/advisory/msys_path_conversion_advisor.py +200 -0
  42. package/hooks/advisory/pr_done_reminder.py +14 -13
  43. package/hooks/advisory/refactor_guard.py +8 -0
  44. package/hooks/advisory/test_msys_path_conversion_advisor.py +223 -0
  45. package/hooks/advisory/test_pr_done_reminder.py +22 -0
  46. package/hooks/advisory/test_refactor_guard.py +40 -0
  47. package/hooks/blocking/bash_post_call_dispatcher.py +6 -11
  48. package/hooks/blocking/block_main_commit.py +4 -0
  49. package/hooks/blocking/code_rules_comments.py +39 -5
  50. package/hooks/blocking/msys_rev_path_rewriter.py +192 -0
  51. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
  52. package/hooks/blocking/precommit_code_rules_gate.py +3 -0
  53. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
  54. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
  55. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
  56. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +16 -3
  57. package/hooks/blocking/test_bash_post_call_dispatcher.py +2 -0
  58. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
  59. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
  60. package/hooks/blocking/test_msys_rev_path_rewriter.py +141 -0
  61. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
  62. package/hooks/git-hooks/pull_request_handoff.py +3 -0
  63. package/hooks/git-hooks/subprocess_window_access.py +41 -0
  64. package/hooks/git-hooks/test_verification_start.py +4 -0
  65. package/hooks/git-hooks/verification_notice_context.py +3 -0
  66. package/hooks/git-hooks/verification_start.py +7 -8
  67. package/hooks/hooks.json +10 -0
  68. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +9 -0
  69. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +6 -1
  70. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
  71. package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +48 -0
  72. package/hooks/hooks_constants/msys_rev_path_rewriter_constants.py +37 -0
  73. package/hooks/hooks_constants/post_tool_use_context.py +34 -0
  74. package/hooks/hooks_constants/pr_done_reminder_constants.py +0 -2
  75. package/hooks/hooks_constants/pre_tool_use_allow_output.py +48 -0
  76. package/hooks/hooks_constants/subprocess_window.py +141 -0
  77. package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
  78. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +9 -2
  79. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +9 -2
  80. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
  81. package/hooks/hooks_constants/test_failure_recorder_constants.py +1 -2
  82. package/hooks/hooks_constants/test_post_tool_use_context.py +22 -0
  83. package/hooks/hooks_constants/test_pre_tool_use_allow_output.py +27 -0
  84. package/hooks/hooks_constants/test_subprocess_window.py +128 -0
  85. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
  86. package/hooks/observability/test_failure_recorder.py +3 -1
  87. package/hooks/validation/mypy_validator.py +4 -0
  88. package/hooks/validators/README.md +1 -0
  89. package/hooks/validators/git_checks.py +4 -0
  90. package/hooks/validators/health_check.py +3 -0
  91. package/hooks/validators/hook_timing_harness.py +3 -0
  92. package/hooks/validators/mypy_integration.py +4 -0
  93. package/hooks/validators/ruff_integration.py +6 -0
  94. package/hooks/validators/run_all_validators.py +7 -0
  95. package/hooks/validators/subprocess_window_access.py +41 -0
  96. package/hooks/workflow/auto_formatter.py +8 -0
  97. package/hooks/workflow/test_auto_formatter.py +39 -0
  98. package/package.json +1 -1
  99. package/rules/ask-user-question-required.md +3 -3
  100. package/rules/code-standards.md +3 -2
  101. package/rules/confirm-implementation-forks.md +2 -0
  102. package/rules/destructive-commands.md +19 -19
  103. package/rules/doc-inventory-integrity.md +4 -4
  104. package/rules/docstring-prose-matches-implementation.md +2 -2
  105. package/rules/failure-blast-radius.md +4 -2
  106. package/rules/filesystem-search.md +1 -1
  107. package/rules/git-workflow.md +34 -0
  108. package/rules/long-horizon-autonomy.md +2 -0
  109. package/rules/nas-ssh-invocation.md +3 -3
  110. package/rules/no-cross-skill-duplicate-helpers.md +6 -6
  111. package/rules/orphan-css-class.md +5 -5
  112. package/rules/paired-test-coverage.md +6 -6
  113. package/rules/plain-illustrative-docstrings.md +3 -3
  114. package/rules/pstack-models.md +4 -3
  115. package/rules/re-stage-before-commit.md +7 -7
  116. package/rules/retired-hook-prose.md +16 -0
  117. package/rules/shell-invocation.md +17 -1
  118. package/rules/testing.md +3 -5
  119. package/rules/verify-runtime-state.md +1 -1
  120. package/rules/windows-filesystem-safe.md +2 -2
  121. package/rules/workflow-substitution-slots.md +1 -1
  122. package/scripts/automatic_advisory/checkout.py +8 -1
  123. package/scripts/automatic_advisory/cli.py +9 -3
  124. package/scripts/automatic_advisory/config/constants.py +0 -2
  125. package/scripts/automatic_advisory/execution.py +7 -1
  126. package/scripts/automatic_advisory/git.py +9 -1
  127. package/scripts/automatic_advisory/process_host.py +3 -0
  128. package/scripts/claude_chain_runner.py +3 -0
  129. package/scripts/durable_post_lint_config/config/constants.py +0 -3
  130. package/scripts/durable_post_lint_config/config/test_constants.py +5 -0
  131. package/scripts/gh_artifact_upload.py +3 -0
  132. package/scripts/grok_headless_runner.py +3 -0
  133. package/scripts/grok_patch_artifacts.py +4 -0
  134. package/scripts/grok_worker_preflight.py +3 -0
  135. package/scripts/invoke_code_review.py +3 -0
  136. package/scripts/local_verification/command_runner.py +3 -0
  137. package/scripts/local_verification/git_state.py +3 -0
  138. package/scripts/policy_lint/adapter_configuration.py +24 -0
  139. package/scripts/policy_lint/adapter_detectors.py +48 -2
  140. package/scripts/policy_lint/adapter_pairing.py +150 -2
  141. package/scripts/policy_lint/adapter_retired_hook_prose.py +201 -0
  142. package/scripts/policy_lint/adapters.py +82 -5
  143. package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
  144. package/scripts/policy_lint/config/constants.py +77 -0
  145. package/scripts/policy_lint/registry.py +6 -1
  146. package/scripts/policy_lint/selection.py +37 -18
  147. package/scripts/policy_lint/selection_git.py +17 -2
  148. package/scripts/repository_checks/config/constants.py +5 -0
  149. package/scripts/repository_checks/config/policy_document.py +19 -0
  150. package/scripts/repository_checks/email_exemptions.py +14 -71
  151. package/scripts/repository_checks/path_exemptions.py +35 -0
  152. package/scripts/repository_checks/policy_document.py +120 -0
  153. package/scripts/repository_checks/tracked_secrets.py +27 -4
  154. package/scripts/setup_project_paths.py +3 -0
  155. package/scripts/spawn_grok_batch.py +3 -0
  156. package/scripts/subprocess_window_access.py +41 -0
  157. package/scripts/test_dispatcher_profile_import.py +4 -0
  158. package/scripts/test_durable_post_lint.py +42 -9
  159. package/scripts/tests/test_adapter_configuration.py +8 -0
  160. package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
  161. package/scripts/tests/test_email_exemptions.py +37 -0
  162. package/scripts/tests/test_path_exemptions.py +164 -0
  163. package/scripts/tests/test_policy_document.py +104 -0
  164. package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
  165. package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
  166. package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
  167. package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +197 -0
  168. package/scripts/tests/test_policy_lint_selection.py +141 -0
  169. package/scripts/tests/test_subprocess_window_access.py +118 -0
  170. package/scripts/tests/test_tracked_secrets.py +79 -0
  171. package/scripts/validate_instruction_pairs.py +3 -0
  172. package/scripts/verify_installable_package.py +5 -0
  173. package/hooks/hooks_constants/convergence_branch_constants.py +0 -8
  174. package/hooks/hooks_constants/destructive_command_environment_constants.py +0 -18
  175. package/hooks/hooks_constants/test_text_stripping.py +0 -39
  176. package/hooks/hooks_constants/text_stripping.py +0 -36
  177. package/scripts/automatic_advisory/window_flags.py +0 -41
  178. package/scripts/repository_checks/config/email_exemptions.py +0 -10
@@ -8,7 +8,13 @@ from types import ModuleType
8
8
 
9
9
  from shared_tree_paths import resolve_shared_scripts_directory
10
10
 
11
- from . import adapter_configuration, adapter_detectors, adapter_pairing, adapter_support
11
+ from . import (
12
+ adapter_configuration,
13
+ adapter_detectors,
14
+ adapter_pairing,
15
+ adapter_retired_hook_prose,
16
+ adapter_support,
17
+ )
12
18
  from .config import constants
13
19
  from .model import Diagnostic, Document, DocumentSet
14
20
 
@@ -27,9 +33,13 @@ def _hooks_module(module_name: str) -> ModuleType:
27
33
 
28
34
 
29
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
+ )
30
40
  scripts_directory = str(resolve_shared_scripts_directory(
31
41
  __file__, os.environ, constants.PR_LOOP_DIRECTORY_NAME,
32
- f"{module_name}{constants.PYTHON_SUFFIX}", constants.SHARED_ROOT_PARENT_INDEX
42
+ marker_relative_path, constants.SHARED_ROOT_PARENT_INDEX
33
43
  ))
34
44
  if scripts_directory not in sys.path:
35
45
  sys.path.insert(0, scripts_directory)
@@ -66,7 +76,7 @@ def code_rule_diagnostics(
66
76
  Diagnostics from the code-rule engine.
67
77
  """
68
78
  return adapter_detectors.code_rule_diagnostics(
69
- document, repository_root, _hooks_module
79
+ document, repository_root, _hooks_module, _pr_loop_script_module
70
80
  )
71
81
 
72
82
 
@@ -270,6 +280,25 @@ def accepts_python(document: Document) -> bool:
270
280
  return document.path.suffix.lower() == constants.PYTHON_SUFFIX
271
281
 
272
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
+
273
302
  def accepts_code(document: Document) -> bool:
274
303
  """Return whether the document contains supported source code.
275
304
 
@@ -286,6 +315,21 @@ def accepts_markdown(document: Document) -> bool:
286
315
  return document.path.suffix.lower() in constants.ALL_MARKDOWN_SUFFIXES
287
316
 
288
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
+
289
333
  def accepts_source_or_markdown(document: Document) -> bool:
290
334
  """Return whether a state-description rule can inspect the document.
291
335
 
@@ -293,8 +337,10 @@ def accepts_source_or_markdown(document: Document) -> bool:
293
337
  document: Candidate document.
294
338
 
295
339
  Returns:
296
- True for supported source or Markdown.
340
+ True for authored source or Markdown, and False for a generated document.
297
341
  """
342
+ if _is_generated_document(document):
343
+ return False
298
344
  return accepts_code(document) or accepts_markdown(document)
299
345
 
300
346
 
@@ -305,8 +351,10 @@ def accepts_stored_prompt(document: Document) -> bool:
305
351
  document: Candidate document.
306
352
 
307
353
  Returns:
308
- True for Markdown in an instruction directory.
354
+ True for authored Markdown in an instruction directory.
309
355
  """
356
+ if _is_generated_document(document):
357
+ return False
310
358
  normalized_path = f"/{document.path.as_posix().lower()}"
311
359
  return accepts_markdown(document) and any(
312
360
  each_segment in normalized_path
@@ -377,3 +425,32 @@ def accepts_plans(document: Document) -> bool:
377
425
  )
378
426
  )
379
427
  )
428
+
429
+
430
+ def accepts_rules_markdown(document: Document) -> bool:
431
+ """Return whether the document is Markdown in a rules directory.
432
+
433
+ Args:
434
+ document: Candidate document.
435
+
436
+ Returns:
437
+ True for a Markdown file directly under a ``rules`` directory.
438
+ """
439
+ return adapter_retired_hook_prose.accepts_rules_markdown(document)
440
+
441
+
442
+ def retired_hook_prose_diagnostics(
443
+ document: Document, repository_root: Path
444
+ ) -> tuple[Diagnostic, ...]:
445
+ """Report rules prose that credits an unregistered hook with a live action.
446
+
447
+ Args:
448
+ document: Current rules Markdown text and path.
449
+ repository_root: Request repository root for package resolution.
450
+
451
+ Returns:
452
+ Retired-hook prose diagnostics.
453
+ """
454
+ return adapter_retired_hook_prose.retired_hook_prose_diagnostics(
455
+ document, repository_root
456
+ )
@@ -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/",
@@ -57,6 +58,9 @@ ALL_ACTION_BOUNDARY_SEGMENTS = frozenset(
57
58
  }
58
59
  )
59
60
  ALL_ACTION_BOUNDARY_PREFIXES = ("deny_", "block_", "ask_")
61
+ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
62
+ "blocking/bash_pre_tool_use_dispatcher.py",
63
+ )
60
64
  ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
61
65
  ALL_TEST_FILE_PREFIXES = ("test_",)
62
66
  ALL_TEST_FILE_SUFFIXES = ("_test.py",)
@@ -80,3 +84,76 @@ TERMINOLOGY_MESSAGE_GROUP = "message"
80
84
 
81
85
  PR_LOOP_DIRECTORY_NAME = "pr-loop"
82
86
  SHARED_ROOT_PARENT_INDEX = 2
87
+ POSIX_PATH_SEPARATOR = "/"
88
+ VIOLATION_SCOPING_MODULE_NAME = "code_rules_gate_parts.violation_scoping"
89
+
90
+ RETIRED_HOOK_PROSE_RULE_ID = "retired-hook-prose"
91
+ RULES_DIRECTORY_NAME = "rules"
92
+ HOOKS_DIRECTORY_NAME = "hooks"
93
+ HOOKS_CONFIGURATION_FILE_NAME = "hooks.json"
94
+ HOOK_CONSTANTS_DIRECTORY_NAME = "hooks_constants"
95
+ DISPATCHER_CONSTANTS_GLOB = "*dispatcher_constants.py"
96
+ ALL_INSTALLER_PATH_SEGMENTS = ("bin", "install.mjs")
97
+ PYTHON_GLOB = "*.py"
98
+ REPLACEMENT_CHARACTER_ERRORS = "replace"
99
+ ALL_HOOK_MODULE_NAME_SUFFIXES = (
100
+ "_advisor",
101
+ "_blocker",
102
+ "_dispatcher",
103
+ "_enforcer",
104
+ "_gate",
105
+ "_guard",
106
+ "_recorder",
107
+ "_rewriter",
108
+ "_tracker",
109
+ "_validator",
110
+ )
111
+ ALL_LIVE_CLAIM_VERBS = frozenset(
112
+ {
113
+ "allows",
114
+ "applies",
115
+ "blocks",
116
+ "catches",
117
+ "checks",
118
+ "denies",
119
+ "enforces",
120
+ "fires",
121
+ "flags",
122
+ "gates",
123
+ "hosts",
124
+ "matches",
125
+ "prevents",
126
+ "reads",
127
+ "records",
128
+ "refuses",
129
+ "rejects",
130
+ "reports",
131
+ "requires",
132
+ "runs",
133
+ "scans",
134
+ "stops",
135
+ "watches",
136
+ }
137
+ )
138
+ ALL_REPLACEMENT_DELIVERY_PHRASES = (
139
+ "staged policy lint",
140
+ "policy lint",
141
+ "cde_lint",
142
+ "repository_checks/",
143
+ "repository_policy.py",
144
+ "repository check",
145
+ )
146
+ INLINE_CODE_PATTERN = r"`([^`\n]+)`"
147
+ HOOK_MODULE_NAME_PATTERN = r"[a-z_][a-z0-9_]*"
148
+ PROSE_WORD_PATTERN = r"[a-z]+"
149
+ RETIRED_ROSTER_PATTERN = (
150
+ r"(?s)RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS\s*=\s*new Set\(\[(.*?)\]\)"
151
+ )
152
+ QUOTED_HOOK_PATH_PATTERN = r"['\"]([a-z_]+/[a-z_0-9]+\.py)['\"]"
153
+ HOOKS_JSON_PATH_PATTERN = r"hooks/[a-z_]+/([a-z_0-9]+)\.py"
154
+ SENTENCE_BOUNDARY_PATTERN = r"[.!?](?:\s|$)"
155
+ RETIRED_HOOK_PROSE_MESSAGE = (
156
+ "{hook_name} is named by no hooks.json entry and no dispatcher roster, so it "
157
+ "reaches no tool call. Say what the reader faces now, or drop the "
158
+ "present-tense action."
159
+ )
@@ -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(
@@ -104,6 +104,11 @@ def _text_document_rules() -> tuple[model.Rule, ...]:
104
104
  adapters.accepts_workflow,
105
105
  adapters.workflow_substitution_diagnostics,
106
106
  ),
107
+ build_document_rule(
108
+ "retired-hook-prose",
109
+ adapters.accepts_rules_markdown,
110
+ adapters.retired_hook_prose_diagnostics,
111
+ ),
107
112
  )
108
113
 
109
114
 
@@ -18,6 +18,7 @@ from .model import (
18
18
  TextDocument,
19
19
  )
20
20
  from .selection_git import (
21
+ GitBlobItemBlocked,
21
22
  GitSelectionError,
22
23
  git_bytes_for,
23
24
  head_revision,
@@ -30,6 +31,10 @@ class SelectionRunFatal(ValueError):
30
31
  """Raised when a source selection cannot be resolved safely."""
31
32
 
32
33
 
34
+ class SelectionItemBlocked(ValueError):
35
+ """Raised when one file holds bytes that are not a policy document."""
36
+
37
+
33
38
  SelectionError = SelectionRunFatal
34
39
 
35
40
 
@@ -54,9 +59,7 @@ def select_documents(request: LintRequest) -> DocumentSet:
54
59
 
55
60
  def _select_source(repository_root: Path, source: object) -> DocumentSet:
56
61
  if isinstance(source, ExplicitFiles):
57
- all_documents = tuple(
58
- _worktree_document(repository_root, each_path) for each_path in source.paths
59
- )
62
+ all_documents = _explicit_documents(repository_root, source.paths)
60
63
  return DocumentSet(all_documents, SelectionKind.FILES, repository_root)
61
64
  if isinstance(source, StagedChanges):
62
65
  return _select_changes(repository_root, True, None)
@@ -89,13 +92,28 @@ def _validated_repository_root(repository_root: Path) -> Path:
89
92
  return resolved_root
90
93
 
91
94
 
95
+ def _explicit_documents(
96
+ repository_root: Path, all_paths: tuple[Path, ...]
97
+ ) -> tuple[Document, ...]:
98
+ all_documents: list[Document] = []
99
+ for each_path in all_paths:
100
+ try:
101
+ all_documents.append(_worktree_document(repository_root, each_path))
102
+ except SelectionItemBlocked:
103
+ continue
104
+ return tuple(all_documents)
105
+
106
+
92
107
  def _select_repository_tree(repository_root: Path) -> DocumentSet:
93
- all_documents = tuple(
94
- _tracked_worktree_document(repository_root, each_path)
95
- for each_path in _tracked_paths(repository_root)
96
- if (repository_root / each_path).is_file()
97
- )
98
- return DocumentSet(all_documents, SelectionKind.REPOSITORY, repository_root)
108
+ all_documents: list[Document] = []
109
+ for each_path in _tracked_paths(repository_root):
110
+ if not (repository_root / each_path).is_file():
111
+ continue
112
+ try:
113
+ all_documents.append(_tracked_worktree_document(repository_root, each_path))
114
+ except SelectionItemBlocked:
115
+ continue
116
+ return DocumentSet(tuple(all_documents), SelectionKind.REPOSITORY, repository_root)
99
117
 
100
118
 
101
119
  def _tracked_worktree_document(repository_root: Path, raw_path: str) -> Document:
@@ -189,13 +207,16 @@ def _append_change(
189
207
  old_path, new_path = all_paths if status[:1] in {"R", "C"} else (all_paths[0], all_paths[0])
190
208
  normalized_old_path = _normalize_path(repository_root, old_path)
191
209
  normalized_new_path = _normalize_path(repository_root, new_path)
192
- document = _change_document(
193
- repository_root, old_path, new_path, normalized_old_path,
194
- normalized_new_path, is_staged, base_revision, status,
195
- )
196
- if document is None:
210
+ if status == "D":
197
211
  all_deleted_paths.append(normalized_old_path)
198
212
  return
213
+ try:
214
+ document = _change_document(
215
+ repository_root, old_path, new_path, normalized_old_path,
216
+ normalized_new_path, is_staged, base_revision, status,
217
+ )
218
+ except (SelectionItemBlocked, GitBlobItemBlocked):
219
+ return
199
220
  all_documents.append(document)
200
221
  if document.prior_path is not None:
201
222
  all_renamed_paths.append((normalized_old_path, normalized_new_path))
@@ -210,9 +231,7 @@ def _change_document(
210
231
  is_staged: bool,
211
232
  base_revision: str | None,
212
233
  status: str,
213
- ) -> Document | None:
214
- if status == "D":
215
- return None
234
+ ) -> Document:
216
235
  is_copy = status[:1] == "C"
217
236
  is_rename = status[:1] == "R"
218
237
  prior_text = _change_prior_text(
@@ -274,7 +293,7 @@ def _read_worktree_text(file_path: Path) -> str:
274
293
  try:
275
294
  return file_path.read_bytes().decode(constants.UTF8_ENCODING)
276
295
  except UnicodeDecodeError as error:
277
- raise SelectionRunFatal(f"File is not UTF-8: {file_path}") from error
296
+ raise SelectionItemBlocked(f"File is not UTF-8: {file_path}") from error
278
297
  except OSError as error:
279
298
  raise SelectionRunFatal(f"File cannot be read: {file_path}") from error
280
299
 
@@ -16,11 +16,23 @@ 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."""
22
30
 
23
31
 
32
+ class GitBlobItemBlocked(GitSelectionError):
33
+ """Raised when one Git blob holds bytes that are not UTF-8 text."""
34
+
35
+
24
36
  def git_bytes_for(repository_root: Path, all_arguments: tuple[str, ...]) -> bytes:
25
37
  """Run a fixed Git argument vector and return raw stdout bytes.
26
38
 
@@ -40,6 +52,7 @@ def git_bytes_for(repository_root: Path, all_arguments: tuple[str, ...]) -> byte
40
52
  capture_output=True,
41
53
  check=False,
42
54
  env=_git_subprocess_environment(),
55
+ creationflags=hidden_window_creation_flags(),
43
56
  )
44
57
  if completed.returncode != 0:
45
58
  error_text = completed.stderr.decode(UTF8_ENCODING, errors="replace").strip()
@@ -65,6 +78,7 @@ def head_revision(repository_root: Path) -> str | None:
65
78
  capture_output=True,
66
79
  check=False,
67
80
  env=_git_subprocess_environment(),
81
+ creationflags=hidden_window_creation_flags(),
68
82
  )
69
83
  if completed.returncode != 0:
70
84
  error_text = completed.stderr.decode(UTF8_ENCODING, errors="replace").strip()
@@ -89,7 +103,7 @@ def read_blob(repository_root: Path, revision: str, relative_path: str) -> str |
89
103
  Decoded blob text, or None when the blob is absent.
90
104
 
91
105
  Raises:
92
- GitSelectionError: If the blob is not UTF-8.
106
+ GitBlobItemBlocked: If the blob is not UTF-8.
93
107
  """
94
108
  blob_bytes = _read_blob_bytes(repository_root, revision, relative_path)
95
109
  if blob_bytes is None:
@@ -97,7 +111,7 @@ def read_blob(repository_root: Path, revision: str, relative_path: str) -> str |
97
111
  try:
98
112
  return blob_bytes.decode(UTF8_ENCODING)
99
113
  except UnicodeDecodeError as error:
100
- raise GitSelectionError(f"Git blob is not UTF-8: {relative_path}") from error
114
+ raise GitBlobItemBlocked(f"Git blob is not UTF-8: {relative_path}") from error
101
115
 
102
116
 
103
117
  def _git_subprocess_environment() -> dict[str, str]:
@@ -121,6 +135,7 @@ def _read_blob_bytes(repository_root: Path, revision: str, relative_path: str) -
121
135
  capture_output=True,
122
136
  check=False,
123
137
  env=_git_subprocess_environment(),
138
+ creationflags=hidden_window_creation_flags(),
124
139
  )
125
140
  if completed.returncode == 0:
126
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"
@@ -2,21 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
5
  from pathlib import Path
7
6
 
8
- from repository_checks.config.email_exemptions import (
9
- ALL_ENTRY_FIELDS,
10
- ALL_FORBIDDEN_PATH_SEGMENTS,
11
- CONFIG_RELATIVE_PATH,
12
- DIGEST_PATTERN,
13
- FIRST_PRINTABLE_CODEPOINT,
14
- FORBIDDEN_PATH_CHARACTERS,
7
+ from repository_checks.config.policy_document import (
8
+ EMAIL_CATEGORY,
9
+ EMAIL_EXEMPTION_SUBJECT,
10
+ EMAIL_EXEMPTIONS_FIELD,
11
+ )
12
+ from repository_checks.policy_document import (
13
+ PolicyConfigurationRunFatal,
14
+ entry_path_and_digest,
15
+ read_policy_entries,
15
16
  )
16
-
17
-
18
- class PolicyConfigurationRunFatal(ValueError):
19
- """Reject the entire policy run when exception configuration is invalid."""
20
17
 
21
18
 
22
19
  def load_email_exemptions(repository_root: Path) -> frozenset[tuple[str, str, str]]:
@@ -31,66 +28,12 @@ def load_email_exemptions(repository_root: Path) -> frozenset[tuple[str, str, st
31
28
  OSError: Configuration cannot be read.
32
29
  """
33
30
  exemptions: set[tuple[str, str, str]] = set()
34
- for each_entry in _read_entries(repository_root):
35
- identity = _entry_identity(each_entry)
31
+ for each_entry in read_policy_entries(repository_root, EMAIL_EXEMPTIONS_FIELD):
32
+ relative_path, digest = entry_path_and_digest(
33
+ each_entry, EMAIL_EXEMPTION_SUBJECT
34
+ )
35
+ identity = (relative_path, EMAIL_CATEGORY, digest)
36
36
  if identity in exemptions:
37
37
  raise PolicyConfigurationRunFatal("Duplicate repository email exemption")
38
38
  exemptions.add(identity)
39
39
  return frozenset(exemptions)
40
-
41
-
42
- def _read_entries(repository_root: Path) -> list[object]:
43
- config_path = repository_root / CONFIG_RELATIVE_PATH
44
- if not config_path.exists() and not config_path.is_symlink():
45
- return []
46
- if not config_path.resolve().is_relative_to(repository_root.resolve()):
47
- raise ValueError(
48
- "Repository policy configuration must remain inside the repository"
49
- )
50
- document = json.loads(
51
- config_path.read_text(encoding="utf-8"), object_pairs_hook=_unique_fields
52
- )
53
- if (
54
- not isinstance(document, dict)
55
- or set(document) != {"version", "email_exemptions"}
56
- or type(document["version"]) is not int
57
- or document["version"] != 1
58
- or not isinstance(document["email_exemptions"], list)
59
- ):
60
- raise ValueError("Invalid repository policy configuration schema")
61
- return document["email_exemptions"]
62
-
63
-
64
- def _unique_fields(all_pairs: list[tuple[str, object]]) -> dict[str, object]:
65
- fields: dict[str, object] = {}
66
- for each_key, each_content in all_pairs:
67
- if each_key in fields:
68
- raise PolicyConfigurationRunFatal("Duplicate repository policy field")
69
- fields[each_key] = each_content
70
- return fields
71
-
72
-
73
- def _entry_identity(entry: object) -> tuple[str, str, str]:
74
- if not isinstance(entry, dict) or set(entry) != ALL_ENTRY_FIELDS:
75
- raise ValueError("Email exemptions require path, sha256, and reason")
76
- relative_path, digest, reason = entry["path"], entry["sha256"], entry["reason"]
77
- if not isinstance(relative_path, str) or not relative_path:
78
- raise ValueError("Email exemption path must name one repository file")
79
- if (
80
- any(character in FORBIDDEN_PATH_CHARACTERS for character in relative_path)
81
- or any(
82
- segment in ALL_FORBIDDEN_PATH_SEGMENTS
83
- for segment in relative_path.split("/")
84
- )
85
- or any(
86
- ord(character) < FIRST_PRINTABLE_CODEPOINT for character in relative_path
87
- )
88
- ):
89
- raise ValueError("Email exemption path must be a literal relative file path")
90
- if not isinstance(digest, str) or DIGEST_PATTERN.fullmatch(digest) is None:
91
- raise ValueError("Email exemption sha256 must contain 64 lowercase hex digits")
92
- if not isinstance(reason, str) or not reason.strip():
93
- raise ValueError(
94
- "Email exemption reason must explain its public contact purpose"
95
- )
96
- return relative_path, "email", digest
@@ -0,0 +1,35 @@
1
+ """Load frozen-file path exceptions owned by a repository."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from repository_checks.config.policy_document import (
8
+ PATH_EXEMPTION_SUBJECT,
9
+ PATH_EXEMPTIONS_FIELD,
10
+ )
11
+ from repository_checks.policy_document import (
12
+ PolicyConfigurationRunFatal,
13
+ entry_path_and_digest,
14
+ read_policy_entries,
15
+ )
16
+
17
+
18
+ def load_path_exemptions(repository_root: Path) -> frozenset[tuple[str, str]]:
19
+ """Read validated frozen-file path exceptions.
20
+
21
+ Args:
22
+ repository_root: Repository containing the optional policy configuration.
23
+ Returns:
24
+ Exact path and recorded file-content digest identities.
25
+ Raises:
26
+ ValueError: Configuration contains invalid JSON or schema fields.
27
+ OSError: Configuration cannot be read.
28
+ """
29
+ exemptions: set[tuple[str, str]] = set()
30
+ for each_entry in read_policy_entries(repository_root, PATH_EXEMPTIONS_FIELD):
31
+ identity = entry_path_and_digest(each_entry, PATH_EXEMPTION_SUBJECT)
32
+ if identity in exemptions:
33
+ raise PolicyConfigurationRunFatal("Duplicate repository path exemption")
34
+ exemptions.add(identity)
35
+ return frozenset(exemptions)