claude-dev-env 1.42.0 → 1.43.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 (207) hide show
  1. package/_shared/pr-loop/scripts/_claude_permissions_common.py +1 -5
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +293 -8
  3. package/_shared/pr-loop/scripts/fix_hookspath.py +96 -5
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +3 -16
  5. package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
  6. package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_permissions_constants.py +1 -1
  7. package/_shared/pr-loop/scripts/preflight.py +13 -31
  8. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -16
  9. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +3 -16
  10. package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
  11. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +4 -4
  12. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
  13. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +4 -2
  14. package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +4 -2
  15. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +4 -2
  16. package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +6 -2
  17. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +2 -2
  18. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1 -2
  19. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +4 -2
  20. package/_shared/pr-loop/scripts/tests/test_preflight.py +17 -52
  21. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +6 -2
  22. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +2 -2
  23. package/agents/pr-description-writer.md +50 -140
  24. package/docs/PR_DESCRIPTION_GUIDE.md +101 -102
  25. package/hooks/_gh_pr_author_swap_utils.py +1 -1
  26. package/hooks/blocking/bot_mention_comment_blocker.py +4 -10
  27. package/hooks/blocking/code_rules_enforcer.py +217 -99
  28. package/hooks/blocking/code_rules_path_utils.py +8 -1
  29. package/hooks/blocking/destructive_command_blocker.py +1 -1
  30. package/hooks/blocking/es_exe_path_rewriter.py +7 -13
  31. package/hooks/blocking/gh_body_arg_blocker.py +6 -1
  32. package/hooks/blocking/gh_pr_author_enforcer.py +5 -5
  33. package/hooks/blocking/gh_pr_author_restore.py +5 -5
  34. package/hooks/blocking/hedging_language_blocker.py +4 -10
  35. package/hooks/blocking/md_path_exemptions.py +205 -0
  36. package/hooks/blocking/md_to_html_blocker.py +48 -20
  37. package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -11
  38. package/hooks/blocking/pr_description_enforcer.py +626 -41
  39. package/hooks/blocking/question_to_user_enforcer.py +4 -10
  40. package/hooks/blocking/state_description_blocker.py +6 -12
  41. package/hooks/blocking/tdd_enforcer.py +1 -1
  42. package/hooks/blocking/test_bot_mention_comment_blocker.py +1 -1
  43. package/hooks/blocking/test_code_rules_enforcer.py +3 -3
  44. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +1 -1
  45. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -2
  46. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +184 -0
  47. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +82 -0
  48. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +29 -29
  49. package/hooks/blocking/test_gh_body_arg_blocker.py +7 -8
  50. package/hooks/blocking/test_gh_pr_author_enforcer.py +1 -1
  51. package/hooks/blocking/test_gh_pr_author_restore.py +1 -1
  52. package/hooks/blocking/test_hedging_language_blocker.py +2 -2
  53. package/hooks/blocking/test_md_to_html_blocker.py +463 -8
  54. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +1 -1
  55. package/hooks/blocking/test_pr_description_enforcer.py +1210 -13
  56. package/hooks/blocking/test_question_to_user_enforcer.py +1 -1
  57. package/hooks/blocking/windows_rmtree_blocker.py +5 -11
  58. package/hooks/diagnostic/hook_log_extractor.py +1 -1
  59. package/hooks/diagnostic/hook_log_init.py +1 -1
  60. package/hooks/diagnostic/hook_log_stop_wrapper.py +1 -1
  61. package/hooks/diagnostic/test_hook_log_extractor.py +1 -1
  62. package/hooks/diagnostic/test_hook_log_init.py +2 -2
  63. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +1 -1
  64. package/hooks/git-hooks/gate_utils.py +1 -1
  65. package/hooks/git-hooks/pre_commit.py +1 -1
  66. package/hooks/git-hooks/pre_push.py +1 -1
  67. package/hooks/git-hooks/test_config.py +5 -5
  68. package/hooks/git-hooks/test_pre_push.py +6 -6
  69. package/hooks/{config → hooks_constants}/code_rules_enforcer_constants.py +37 -0
  70. package/hooks/hooks_constants/code_rules_path_utils_constants.py +28 -0
  71. package/hooks/hooks_constants/md_to_html_blocker_constants.py +82 -0
  72. package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_state.py +1 -1
  73. package/hooks/hooks_constants/pr_description_enforcer_constants.py +154 -0
  74. package/hooks/{config → hooks_constants}/pre_tool_use_stdin.py +1 -1
  75. package/hooks/{config → hooks_constants}/project_paths_reader.py +2 -2
  76. package/hooks/{config → hooks_constants}/test_banned_identifiers_constants.py +1 -1
  77. package/hooks/{config → hooks_constants}/test_dynamic_stderr_handler.py +1 -1
  78. package/hooks/{config → hooks_constants}/test_hardcoded_user_path_constants.py +1 -1
  79. package/hooks/{config → hooks_constants}/test_hook_log_extractor_constants.py +2 -2
  80. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +110 -0
  81. package/hooks/{config → hooks_constants}/test_messages.py +2 -6
  82. package/hooks/{config → hooks_constants}/test_path_rewriter_constants.py +1 -1
  83. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +292 -0
  84. package/hooks/{config → hooks_constants}/test_pre_tool_use_stdin.py +2 -2
  85. package/hooks/{config → hooks_constants}/test_project_paths_reader.py +3 -3
  86. package/hooks/{config → hooks_constants}/test_session_env_cleanup_constants.py +1 -1
  87. package/hooks/{config → hooks_constants}/test_setup_project_paths_constants.py +2 -2
  88. package/hooks/{config → hooks_constants}/test_unused_module_import_constants.py +1 -1
  89. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +5 -11
  90. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +1 -1
  91. package/hooks/session/gh_pr_author_session_cleanup.py +5 -6
  92. package/hooks/session/session_env_cleanup.py +4 -10
  93. package/hooks/session/test_gh_pr_author_session_cleanup.py +1 -1
  94. package/hooks/session/test_untracked_repo_detector.py +2 -2
  95. package/hooks/session/untracked_repo_detector.py +6 -12
  96. package/hooks/test__gh_pr_author_swap_utils.py +1 -1
  97. package/hooks/validators/run_all_validators.py +16 -5
  98. package/hooks/validators/test_output_formatter.py +46 -0
  99. package/hooks/workflow/doc_gist_auto_publish.py +1 -1
  100. package/hooks/workflow/md_to_html_companion.py +8 -15
  101. package/hooks/workflow/test_md_to_html_companion.py +184 -23
  102. package/package.json +1 -1
  103. package/rules/ask-user-question-required.md +1 -1
  104. package/rules/vault-context.md +1 -1
  105. package/scripts/{config → dev_env_scripts_constants}/timing.py +1 -1
  106. package/scripts/setup_project_paths.py +49 -11
  107. package/scripts/sweep_empty_dirs.py +10 -1
  108. package/scripts/test_setup_project_paths.py +2 -2
  109. package/scripts/test_sweep_empty_dirs.py +2 -6
  110. package/skills/_shared/pr-loop/scripts/_path_resolver.py +1 -1
  111. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  112. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  113. package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -1
  114. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -1
  115. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  116. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  117. package/skills/bugteam/PROMPTS.md +1 -1
  118. package/skills/bugteam/SKILL.md +1 -1
  119. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  120. package/skills/bugteam/scripts/{_claude_permissions_common.py → _bugteam_permissions_common.py} +1 -13
  121. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +1 -13
  122. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +1 -16
  123. package/skills/bugteam/scripts/bugteam_preflight.py +1 -13
  124. package/skills/bugteam/scripts/grant_project_claude_permissions.py +2 -8
  125. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +1 -1
  126. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  127. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +2 -8
  128. package/skills/bugteam/scripts/{test__claude_permissions_common.py → test__bugteam_permissions_common.py} +4 -4
  129. package/skills/bugteam/scripts/test_agent_config_carveout.py +2 -2
  130. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -26
  131. package/skills/bugteam/scripts/{test_claude_permissions_common.py → test_bugteam_permissions_common.py} +3 -66
  132. package/skills/bugteam/scripts/test_bugteam_preflight.py +2 -27
  133. package/skills/bugteam/scripts/windows_safe_rmtree.py +1 -1
  134. package/skills/doc-gist/SKILL.md +1 -1
  135. package/skills/doc-gist/scripts/gist_upload.py +1 -1
  136. package/skills/implement/SKILL.md +2 -2
  137. package/skills/implement/scripts/append_note.py +1 -1
  138. package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
  139. package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +1 -1
  140. package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
  141. package/skills/pr-converge/scripts/check_convergence.py +11 -4
  142. package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
  143. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
  144. package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
  145. package/skills/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
  146. package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py +1 -1
  147. package/skills/pr-converge/scripts/reflow_skill_md.py +90 -16
  148. package/skills/pr-converge/scripts/test_check_convergence.py +18 -0
  149. package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
  150. package/skills/session-log/SKILL.md +98 -233
  151. package/hooks/config/pr_description_enforcer_constants.py +0 -19
  152. package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
  153. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
  154. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
  155. package/skills/pr-converge/scripts/conftest.py +0 -60
  156. package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
  157. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
  158. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
  159. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +0 -0
  160. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
  161. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
  162. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
  163. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
  164. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
  165. /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
  166. /package/hooks/{config → hooks_constants}/__init__.py +0 -0
  167. /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
  168. /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
  169. /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
  170. /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
  171. /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
  172. /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
  173. /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
  174. /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
  175. /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
  176. /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
  177. /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
  178. /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
  179. /package/hooks/{config → hooks_constants}/messages.py +0 -0
  180. /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
  181. /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
  182. /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
  183. /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
  184. /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
  185. /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
  186. /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
  187. /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
  188. /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
  189. /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
  190. /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
  191. /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
  192. /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
  193. /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
  194. /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
  195. /package/skills/{implement/scripts/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
  196. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
  197. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
  198. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
  199. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/claude_permissions_common_constants.py +0 -0
  200. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
  201. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
  202. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
  203. /package/skills/{pr-converge/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
  204. /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
  205. /package/skills/{pr-converge/scripts/config → implement/scripts/implement_scripts_constants}/__init__.py +0 -0
  206. /package/skills/implement/scripts/{config → implement_scripts_constants}/notes_constants.py +0 -0
  207. /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
@@ -17,11 +17,7 @@ from collections.abc import Callable
17
17
  from pathlib import Path
18
18
  from typing import NoReturn
19
19
 
20
- sys.modules.pop("config", None)
21
- if str(Path(__file__).resolve().parent) not in sys.path:
22
- sys.path.insert(0, str(Path(__file__).resolve().parent))
23
-
24
- from config.claude_permissions_constants import (
20
+ from pr_loop_shared_constants.claude_permissions_constants import (
25
21
  ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS,
26
22
  CLAUDE_SETTINGS_DIRECTORY_NAME,
27
23
  GIT_DIRECTORY_NAME,
@@ -7,11 +7,11 @@ import sys
7
7
  from collections.abc import Callable, Iterator
8
8
  from pathlib import Path
9
9
 
10
- sys.modules.pop("config", None)
11
- if str(Path(__file__).resolve().parent) not in sys.path:
12
- sys.path.insert(0, str(Path(__file__).resolve().parent))
10
+ parent_directory = str(Path(__file__).resolve().parent)
11
+ if parent_directory not in sys.path:
12
+ sys.path.insert(0, parent_directory)
13
13
 
14
- from config.code_rules_gate_constants import (
14
+ from pr_loop_shared_constants.code_rules_gate_constants import ( # noqa: E402
15
15
  ALL_CODE_FILE_EXTENSIONS,
16
16
  ALL_GIT_DIFF_CACHED_NAME_ONLY_NULL_TERMINATED_COMMAND,
17
17
  ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX,
@@ -41,6 +41,18 @@ def violation_line_pattern() -> re.Pattern[str]:
41
41
 
42
42
 
43
43
  def resolve_claude_dev_env_root(starting_path: Path) -> Path:
44
+ """Walk up from *starting_path* to the claude-dev-env package root.
45
+
46
+ Args:
47
+ starting_path: A path inside the worktree; the function climbs to
48
+ find the ancestor containing ``hooks/blocking/code_rules_enforcer.py``.
49
+
50
+ Returns:
51
+ The resolved package root that contains the enforcer file.
52
+
53
+ Raises:
54
+ SystemExit: When no ancestor contains the enforcer.
55
+ """
44
56
  starting = Path(starting_path).resolve()
45
57
  enforcer_relative = Path("hooks") / "blocking" / "code_rules_enforcer.py"
46
58
  for each_candidate in [starting, *starting.parents]:
@@ -66,6 +78,15 @@ def _resolve_package_root_absolute(starting_path: Path) -> Path:
66
78
 
67
79
 
68
80
  def load_validate_content() -> ValidateContentCallable:
81
+ """Load ``code_rules_enforcer.validate_content`` for in-process use.
82
+
83
+ Returns:
84
+ The ``validate_content`` callable from the enforcer module.
85
+
86
+ Raises:
87
+ SystemExit: When the package root cannot be located or the
88
+ enforcer module cannot be loaded from disk.
89
+ """
69
90
  package_root = resolve_claude_dev_env_root(Path(__file__).resolve())
70
91
  enforcer_path = package_root / "hooks" / "blocking" / "code_rules_enforcer.py"
71
92
  if not enforcer_path.is_file():
@@ -85,11 +106,11 @@ def load_validate_content() -> ValidateContentCallable:
85
106
  while hooks_root_path in sys.path:
86
107
  sys.path.remove(hooks_root_path)
87
108
  sys.path.insert(0, hooks_root_path)
88
- saved_config_modules = {
109
+ saved_hooks_constants_modules = {
89
110
  each_module_name: sys.modules.pop(each_module_name)
90
111
  for each_module_name in [
91
112
  each_key for each_key in list(sys.modules)
92
- if each_key == "config" or each_key.startswith("config.")
113
+ if each_key == "hooks_constants" or each_key.startswith("hooks_constants.")
93
114
  ]
94
115
  }
95
116
  try:
@@ -99,14 +120,26 @@ def load_validate_content() -> ValidateContentCallable:
99
120
  sys.path.remove(hooks_root_path)
100
121
  for each_module_name in [
101
122
  each_key for each_key in list(sys.modules)
102
- if each_key == "config" or each_key.startswith("config.")
123
+ if each_key == "hooks_constants" or each_key.startswith("hooks_constants.")
103
124
  ]:
104
125
  sys.modules.pop(each_module_name, None)
105
- sys.modules.update(saved_config_modules)
126
+ sys.modules.update(saved_hooks_constants_modules)
106
127
  return module.validate_content
107
128
 
108
129
 
109
130
  def resolve_merge_base(repository_root: Path, base_reference: str) -> str:
131
+ """Return the merge-base SHA between HEAD and *base_reference*.
132
+
133
+ Args:
134
+ repository_root: Repository root used as the ``git -C`` target.
135
+ base_reference: The git reference to merge-base against.
136
+
137
+ Returns:
138
+ The stripped merge-base SHA.
139
+
140
+ Raises:
141
+ SystemExit: When ``git merge-base`` returns non-zero.
142
+ """
110
143
  merge_result = subprocess.run(
111
144
  ["git", "merge-base", "HEAD", base_reference],
112
145
  cwd=str(repository_root),
@@ -131,6 +164,19 @@ def filter_paths_under_prefixes(
131
164
  repository_root: Path,
132
165
  all_prefixes: list[str],
133
166
  ) -> list[Path]:
167
+ """Filter *all_file_paths* to entries falling under the supplied prefixes.
168
+
169
+ Args:
170
+ all_file_paths: Resolved file paths to filter.
171
+ repository_root: Repository root used to compute relative paths.
172
+ all_prefixes: Repository-relative POSIX prefixes; each path must
173
+ equal one prefix or be nested beneath it to pass through.
174
+
175
+ Returns:
176
+ The subset of *all_file_paths* whose relative POSIX path matches one
177
+ of the prefixes. When *all_prefixes* is empty, returns the input
178
+ list unchanged.
179
+ """
134
180
  if not all_prefixes:
135
181
  return all_file_paths
136
182
  normalized_prefixes = [
@@ -157,6 +203,19 @@ def filter_paths_under_prefixes(
157
203
 
158
204
 
159
205
  def paths_from_git_staged(repository_root: Path) -> list[Path]:
206
+ """Return absolute paths for every file in the staged index.
207
+
208
+ Args:
209
+ repository_root: Repository root used as the ``git -C`` target.
210
+
211
+ Returns:
212
+ List of absolute paths for staged files. Names whose bytes cannot
213
+ be decoded as Unicode are logged and skipped.
214
+
215
+ Raises:
216
+ SystemExit: When ``git diff --cached --name-only -z`` returns
217
+ non-zero.
218
+ """
160
219
  name_result = subprocess.run(
161
220
  list(ALL_GIT_DIFF_CACHED_NAME_ONLY_NULL_TERMINATED_COMMAND),
162
221
  cwd=str(repository_root),
@@ -191,6 +250,19 @@ def staged_file_line_count(
191
250
  repository_root: Path,
192
251
  relative_path_posix: str,
193
252
  ) -> int:
253
+ """Return the staged-blob line count for *relative_path_posix*.
254
+
255
+ Args:
256
+ repository_root: Repository root used as the ``git -C`` target.
257
+ relative_path_posix: Repository-relative POSIX path of the staged
258
+ file.
259
+
260
+ Returns:
261
+ The staged content line count, or zero when the blob is empty.
262
+
263
+ Raises:
264
+ SystemExit: When ``git show :<path>`` returns non-zero.
265
+ """
194
266
  show_result = subprocess.run(
195
267
  ["git", "show", f":{relative_path_posix}"],
196
268
  cwd=str(repository_root),
@@ -217,6 +289,20 @@ def is_staged_file_newly_added(
217
289
  repository_root: Path,
218
290
  relative_path_posix: str,
219
291
  ) -> bool:
292
+ """Check whether *relative_path_posix* is newly added in the staged diff.
293
+
294
+ Args:
295
+ repository_root: Repository root used as the ``git -C`` target.
296
+ relative_path_posix: Repository-relative POSIX path to inspect.
297
+
298
+ Returns:
299
+ True when the first non-empty name-status line begins with the git
300
+ added-prefix; False otherwise.
301
+
302
+ Raises:
303
+ SystemExit: When ``git diff --cached --name-status`` returns
304
+ non-zero.
305
+ """
220
306
  status_result = subprocess.run(
221
307
  ["git", "diff", "--cached", "--name-status", "--", relative_path_posix],
222
308
  cwd=str(repository_root),
@@ -244,6 +330,19 @@ def added_lines_for_staged_file(
244
330
  repository_root: Path,
245
331
  relative_path_posix: str,
246
332
  ) -> set[int]:
333
+ """Return added line numbers within the staged diff for one file.
334
+
335
+ Args:
336
+ repository_root: Repository root used as the ``git -C`` target.
337
+ relative_path_posix: Repository-relative POSIX path to inspect.
338
+
339
+ Returns:
340
+ Set of line numbers (1-indexed) added in the staged diff. When the
341
+ file is newly added, returns every line in the staged blob.
342
+
343
+ Raises:
344
+ SystemExit: When the staged diff command returns non-zero.
345
+ """
247
346
  diff_result = subprocess.run(
248
347
  ["git", "diff", "--cached", "--unified=0", "--", relative_path_posix],
249
348
  cwd=str(repository_root),
@@ -273,6 +372,16 @@ def added_lines_by_file_staged(
273
372
  repository_root: Path,
274
373
  all_file_paths: list[Path],
275
374
  ) -> dict[Path, set[int]]:
375
+ """Build a per-file map of staged-added line numbers.
376
+
377
+ Args:
378
+ repository_root: Repository root for diff invocations.
379
+ all_file_paths: File paths whose added lines should be collected.
380
+
381
+ Returns:
382
+ Mapping from resolved file path to the set of staged-added line
383
+ numbers.
384
+ """
276
385
  resolved_root = repository_root.resolve()
277
386
  added_by_path: dict[Path, set[int]] = {}
278
387
  for each_path in all_file_paths:
@@ -291,6 +400,20 @@ def added_lines_by_file_staged(
291
400
 
292
401
 
293
402
  def paths_from_git_diff(repository_root: Path, base_reference: str) -> list[Path]:
403
+ """Return absolute paths for every file changed since *base_reference*.
404
+
405
+ Args:
406
+ repository_root: Repository root used as the ``git -C`` target.
407
+ base_reference: The git reference to merge-base against.
408
+
409
+ Returns:
410
+ List of absolute paths changed since the merge-base of HEAD and
411
+ *base_reference*.
412
+
413
+ Raises:
414
+ SystemExit: When the ``git diff --name-only`` command returns
415
+ non-zero.
416
+ """
294
417
  merge_base = resolve_merge_base(repository_root, base_reference)
295
418
  diff_command = list(ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX) + [
296
419
  f"{merge_base}..HEAD"
@@ -336,6 +459,13 @@ def is_test_path(file_path: str) -> bool:
336
459
  Mirrors the test-file detection rule documented in CODE_RULES.md:
337
460
  filename matches test_*.py OR *_test.py OR *.test.* OR *.spec.* OR
338
461
  conftest.py, OR path contains the segment /tests/.
462
+
463
+ Args:
464
+ file_path: Path string to classify; backslashes are normalized to
465
+ forward slashes before pattern matching.
466
+
467
+ Returns:
468
+ True when the path matches any test-file pattern; False otherwise.
339
469
  """
340
470
  normalized_posix = file_path.replace("\\", "/")
341
471
  filename_only = normalized_posix.rsplit("/", maxsplit=1)[-1]
@@ -395,6 +525,15 @@ def check_wrapper_plumb_through(content: str, file_path: str) -> list[str]:
395
525
  separate call sites; only the enclosing Call is inspected. This avoids
396
526
  false positives where a callee nested as an argument is confused with a
397
527
  top-level delegate invocation (for example `delegate(helper(x))`).
528
+
529
+ Args:
530
+ content: File content as a single string for AST parsing.
531
+ file_path: Repository-relative POSIX path of the file (used to
532
+ skip non-Python code extensions early).
533
+
534
+ Returns:
535
+ List of violation strings, one per dropped optional kwarg. Returns
536
+ an empty list when the file is not Python or has a syntax error.
398
537
  """
399
538
  non_python_code_extensions = ALL_CODE_FILE_EXTENSIONS - {PYTHON_FILE_EXTENSION}
400
539
  lowercase_file_path = file_path.lower()
@@ -464,6 +603,15 @@ def check_wrapper_plumb_through(content: str, file_path: str) -> list[str]:
464
603
 
465
604
 
466
605
  def parse_added_line_numbers(unified_diff_text: str) -> set[int]:
606
+ """Extract added line numbers from unified-diff text.
607
+
608
+ Args:
609
+ unified_diff_text: Output from ``git diff --unified=0``.
610
+
611
+ Returns:
612
+ Set of newly-added line numbers (1-indexed) extracted from the
613
+ hunk headers.
614
+ """
467
615
  header_regex = hunk_header_pattern()
468
616
  added_line_numbers: set[int] = set()
469
617
  for each_line in unified_diff_text.splitlines():
@@ -485,6 +633,17 @@ def is_file_new_at_base(
485
633
  merge_base: str,
486
634
  relative_path_posix: str,
487
635
  ) -> bool:
636
+ """Check whether *relative_path_posix* did not exist at *merge_base*.
637
+
638
+ Args:
639
+ repository_root: Repository root used as the ``git -C`` target.
640
+ merge_base: The merge-base SHA against which to check existence.
641
+ relative_path_posix: Repository-relative POSIX path to inspect.
642
+
643
+ Returns:
644
+ True when ``git cat-file -e`` fails to find the blob at the merge
645
+ base (i.e. the file was added on the HEAD side); False otherwise.
646
+ """
488
647
  cat_result = subprocess.run(
489
648
  ["git", "cat-file", "-e", f"{merge_base}:{relative_path_posix}"],
490
649
  cwd=str(repository_root),
@@ -502,6 +661,19 @@ def added_lines_for_file(
502
661
  merge_base: str,
503
662
  relative_path_posix: str,
504
663
  ) -> set[int]:
664
+ """Return added line numbers for *relative_path_posix* since *merge_base*.
665
+
666
+ Args:
667
+ repository_root: Repository root used as the ``git -C`` target.
668
+ merge_base: The merge-base SHA against which to diff.
669
+ relative_path_posix: Repository-relative POSIX path to inspect.
670
+
671
+ Returns:
672
+ Set of line numbers (1-indexed) added on the HEAD side of the diff.
673
+
674
+ Raises:
675
+ SystemExit: When the diff command returns non-zero.
676
+ """
505
677
  diff_result = subprocess.run(
506
678
  [
507
679
  "git",
@@ -531,6 +703,15 @@ def added_lines_for_file(
531
703
 
532
704
 
533
705
  def whole_file_line_set(file_path: Path) -> set[int]:
706
+ """Return the set of line numbers covering an entire file.
707
+
708
+ Args:
709
+ file_path: Path to the file whose line span should be summarized.
710
+
711
+ Returns:
712
+ Set of line numbers (1-indexed) covering every line in *file_path*,
713
+ or an empty set when the file is unreadable or empty.
714
+ """
534
715
  try:
535
716
  total_lines = len(file_path.read_text(encoding="utf-8").splitlines())
536
717
  except (OSError, UnicodeDecodeError) as read_error:
@@ -558,6 +739,17 @@ def renamed_file_source_map_since(
558
739
  splitting; rename records emit three null-terminated tokens in
559
740
  sequence (status, source, destination), other status records emit
560
741
  two (status, path).
742
+
743
+ Args:
744
+ repository_root: Repository root used as the ``git -C`` target.
745
+ merge_base: The merge-base SHA against which to diff.
746
+
747
+ Returns:
748
+ Mapping from rename-destination POSIX path to rename-source POSIX
749
+ path. Empty when no rename records are present.
750
+
751
+ Raises:
752
+ SystemExit: When ``git diff --name-status`` returns non-zero.
561
753
  """
562
754
  name_status_result = subprocess.run(
563
755
  ["git", "diff", "--name-status", "-M", "-z", f"{merge_base}..HEAD"],
@@ -613,6 +805,16 @@ def added_lines_for_renamed_file(
613
805
  in the source file before the rename. Falls back to whole-file coverage
614
806
  when the source blob is absent at the merge base (i.e. the source was
615
807
  itself a new or renamed file that landed earlier in the branch).
808
+
809
+ Args:
810
+ repository_root: Repository root used as the ``git -C`` target.
811
+ merge_base: The merge-base SHA against which to compare blobs.
812
+ source_posix: Rename-source POSIX path at the merge base.
813
+ destination_posix: Rename-destination POSIX path at HEAD.
814
+
815
+ Returns:
816
+ Set of line numbers (1-indexed) added on the HEAD side of the
817
+ comparison; empty on diff failure.
616
818
  """
617
819
  diff_result = subprocess.run(
618
820
  [
@@ -647,6 +849,18 @@ def added_lines_by_file(
647
849
  base_reference: str,
648
850
  all_file_paths: list[Path],
649
851
  ) -> dict[Path, set[int]]:
852
+ """Build a per-file map of added line numbers across the branch.
853
+
854
+ Args:
855
+ repository_root: Repository root for diff invocations.
856
+ base_reference: The git reference to merge-base against.
857
+ all_file_paths: File paths whose added lines should be collected.
858
+
859
+ Returns:
860
+ Mapping from resolved file path to the set of line numbers added
861
+ on the HEAD side, with renames resolved to compare against the
862
+ original source path.
863
+ """
650
864
  merge_base = resolve_merge_base(repository_root, base_reference)
651
865
  resolved_root = repository_root.resolve()
652
866
  rename_source_map = renamed_file_source_map_since(resolved_root, merge_base)
@@ -680,6 +894,15 @@ def added_lines_by_file(
680
894
 
681
895
 
682
896
  def extract_violation_line_number(violation_text: str) -> int | None:
897
+ """Return the line number captured by the gate's violation-line regex.
898
+
899
+ Args:
900
+ violation_text: A single violation string of the form ``Line N: ...``.
901
+
902
+ Returns:
903
+ The integer line number captured in the prefix, or None when the
904
+ text does not match the violation-line pattern.
905
+ """
683
906
  match_result = violation_line_pattern().match(violation_text)
684
907
  if match_result is None:
685
908
  return None
@@ -690,6 +913,19 @@ def split_violations_by_scope(
690
913
  all_issues: list[str],
691
914
  all_added_line_numbers: set[int] | None,
692
915
  ) -> tuple[list[str], list[str]]:
916
+ """Partition issues into blocking vs advisory based on touched lines.
917
+
918
+ Args:
919
+ all_issues: Violation strings emitted by the enforcer.
920
+ all_added_line_numbers: Lines added in the current diff, or None
921
+ to treat every violation as blocking.
922
+
923
+ Returns:
924
+ Tuple ``(blocking, advisory)``. When *all_added_line_numbers* is
925
+ None, every issue is blocking; otherwise issues whose ``Line N:``
926
+ prefix matches an added line are blocking and the rest are
927
+ advisory.
928
+ """
693
929
  if all_added_line_numbers is None:
694
930
  return list(all_issues), []
695
931
  blocking: list[str] = []
@@ -711,6 +947,14 @@ def print_violation_section(
711
947
  violations_by_file: dict[Path, list[str]],
712
948
  repository_root: Path,
713
949
  ) -> None:
950
+ """Print a labeled block of violations grouped by relative path.
951
+
952
+ Args:
953
+ header_message: Section header to write to stderr.
954
+ violations_by_file: Mapping from absolute file path to the list of
955
+ violation strings to render under that path.
956
+ repository_root: Repository root used to compute relative paths.
957
+ """
714
958
  print(header_message, file=sys.stderr)
715
959
  resolved_root = repository_root.resolve()
716
960
  for each_path in sorted(violations_by_file.keys()):
@@ -723,6 +967,16 @@ def print_violation_section(
723
967
  def read_prior_committed_content(
724
968
  repository_root: Path, relative_path_posix: str
725
969
  ) -> str:
970
+ """Return the HEAD-committed content for *relative_path_posix*.
971
+
972
+ Args:
973
+ repository_root: Repository root used as the ``git -C`` target.
974
+ relative_path_posix: Repository-relative POSIX path to read.
975
+
976
+ Returns:
977
+ The committed file content at HEAD, or an empty string when the
978
+ path is not tracked or ``git show`` returns non-zero.
979
+ """
726
980
  show_result = subprocess.run(
727
981
  ["git", "show", f"HEAD:{relative_path_posix}"],
728
982
  cwd=str(repository_root),
@@ -743,6 +997,19 @@ def run_gate(
743
997
  repository_root: Path,
744
998
  all_added_lines_by_path: dict[Path, set[int]] | None = None,
745
999
  ) -> int:
1000
+ """Run the gate over *all_file_paths* and emit a partitioned report.
1001
+
1002
+ Args:
1003
+ validate_content: The enforcer ``validate_content`` callable.
1004
+ all_file_paths: File paths to inspect.
1005
+ repository_root: Repository root used to resolve relative paths.
1006
+ all_added_lines_by_path: Optional per-file added-line maps used to
1007
+ partition issues into blocking vs advisory.
1008
+
1009
+ Returns:
1010
+ ``1`` when at least one blocking violation is reported, ``0``
1011
+ otherwise.
1012
+ """
746
1013
  blocking_by_file: dict[Path, list[str]] = {}
747
1014
  advisory_by_file: dict[Path, list[str]] = {}
748
1015
  for each_path in sorted(set(all_file_paths)):
@@ -814,6 +1081,15 @@ def run_gate(
814
1081
 
815
1082
 
816
1083
  def parse_arguments(all_arguments: list[str]) -> argparse.Namespace:
1084
+ """Parse the command-line arguments for the code-rules gate.
1085
+
1086
+ Args:
1087
+ all_arguments: Command-line argument list forwarded to argparse.
1088
+
1089
+ Returns:
1090
+ The parsed argparse namespace with ``repo_root``, ``base``,
1091
+ ``staged``, ``only_under``, and ``paths`` attributes.
1092
+ """
817
1093
  parser = argparse.ArgumentParser(
818
1094
  description=(
819
1095
  "Run CODE_RULES validators (validate_content) on files in the working tree. "
@@ -862,6 +1138,15 @@ def parse_arguments(all_arguments: list[str]) -> argparse.Namespace:
862
1138
 
863
1139
 
864
1140
  def main(all_arguments: list[str]) -> int:
1141
+ """Run the gate using the parsed CLI arguments.
1142
+
1143
+ Args:
1144
+ all_arguments: Command-line argument list forwarded to argparse.
1145
+
1146
+ Returns:
1147
+ The exit code from ``run_gate`` (``0`` clean, ``1`` blocking
1148
+ violations).
1149
+ """
865
1150
  arguments = parse_arguments(all_arguments)
866
1151
  repository_root = (
867
1152
  arguments.repo_root.resolve()
@@ -3,11 +3,11 @@ import subprocess
3
3
  import sys
4
4
  from pathlib import Path
5
5
 
6
- sys.modules.pop("config", None)
7
- if str(Path(__file__).resolve().parent) not in sys.path:
8
- sys.path.insert(0, str(Path(__file__).resolve().parent))
6
+ parent_directory = str(Path(__file__).resolve().parent)
7
+ if parent_directory not in sys.path:
8
+ sys.path.insert(0, parent_directory)
9
9
 
10
- from config.fix_hookspath_constants import (
10
+ from pr_loop_shared_constants.fix_hookspath_constants import ( # noqa: E402
11
11
  ALL_CANONICAL_HOOKS_DIRECTORY_COMPONENTS,
12
12
  ALL_GIT_GLOBAL_GET_CORE_HOOKS_PATH_COMMAND,
13
13
  ALL_HOME_ENV_VAR_NAMES,
@@ -15,12 +15,22 @@ from config.fix_hookspath_constants import (
15
15
  PREFLIGHT_NO_PYTEST_FLAG,
16
16
  PREFLIGHT_REPO_ROOT_FLAG,
17
17
  )
18
- from config.preflight_constants import GIT_DIRECTORY_NAME
18
+ from pr_loop_shared_constants.preflight_constants import GIT_DIRECTORY_NAME # noqa: E402
19
19
 
20
20
 
21
21
  def resolve_canonical_hooks_directory(
22
22
  all_environment_overrides: dict[str, str] | None,
23
23
  ) -> Path:
24
+ """Return the canonical claude-dev-env git hooks directory path.
25
+
26
+ Args:
27
+ all_environment_overrides: Optional environment variable mapping used
28
+ to discover the user's home directory (HOME / USERPROFILE).
29
+
30
+ Returns:
31
+ The absolute path to the canonical hooks directory beneath the
32
+ resolved home location.
33
+ """
24
34
  if all_environment_overrides is not None:
25
35
  for each_env_var_name in ALL_HOME_ENV_VAR_NAMES:
26
36
  home_value = all_environment_overrides.get(each_env_var_name)
@@ -33,6 +43,17 @@ def list_local_core_hooks_path_values(
33
43
  repository_root: Path,
34
44
  all_environment_overrides: dict[str, str] | None,
35
45
  ) -> list[str]:
46
+ """Return all repo-local ``core.hooksPath`` values configured on the repo.
47
+
48
+ Args:
49
+ repository_root: Repository root used as the ``git -C`` target.
50
+ all_environment_overrides: Optional environment variable mapping
51
+ forwarded to ``subprocess.run``.
52
+
53
+ Returns:
54
+ Non-empty stripped values from ``git config --local --get-all``, or
55
+ an empty list when no values are configured.
56
+ """
36
57
  git_command = [
37
58
  "git",
38
59
  "-C",
@@ -71,6 +92,16 @@ def list_local_core_hooks_path_values(
71
92
  def read_global_core_hooks_path(
72
93
  all_environment_overrides: dict[str, str] | None,
73
94
  ) -> str:
95
+ """Return the global-scope ``core.hooksPath`` value from git config.
96
+
97
+ Args:
98
+ all_environment_overrides: Optional environment variable mapping
99
+ forwarded to ``subprocess.run``.
100
+
101
+ Returns:
102
+ The stripped global value, or an empty string when unset or when git
103
+ returns non-zero.
104
+ """
74
105
  git_command = list(ALL_GIT_GLOBAL_GET_CORE_HOOKS_PATH_COMMAND)
75
106
  completed_process = subprocess.run(
76
107
  git_command,
@@ -97,6 +128,16 @@ def unset_local_core_hooks_path(
97
128
  repository_root: Path,
98
129
  all_environment_overrides: dict[str, str] | None,
99
130
  ) -> int:
131
+ """Remove every repo-local ``core.hooksPath`` entry from the repo config.
132
+
133
+ Args:
134
+ repository_root: Repository root used as the ``git -C`` target.
135
+ all_environment_overrides: Optional environment variable mapping
136
+ forwarded to ``subprocess.run``.
137
+
138
+ Returns:
139
+ The ``git config --unset-all`` exit code (zero on success).
140
+ """
100
141
  git_command = [
101
142
  "git",
102
143
  "-C",
@@ -120,6 +161,16 @@ def set_global_core_hooks_path(
120
161
  target_value: str,
121
162
  all_environment_overrides: dict[str, str] | None,
122
163
  ) -> int:
164
+ """Write the global-scope ``core.hooksPath`` value into git config.
165
+
166
+ Args:
167
+ target_value: Path value to install at global scope.
168
+ all_environment_overrides: Optional environment variable mapping
169
+ forwarded to ``subprocess.run``.
170
+
171
+ Returns:
172
+ The ``git config --global`` exit code (zero on success).
173
+ """
123
174
  git_command = ["git", "config", "--global", "core.hooksPath", target_value]
124
175
  completed_process = subprocess.run(
125
176
  git_command,
@@ -142,6 +193,15 @@ def is_canonical_hooks_path(raw_value: str) -> bool:
142
193
 
143
194
 
144
195
  def find_repository_root(start: Path) -> Path:
196
+ """Walk up from *start* to the nearest directory containing a git marker.
197
+
198
+ Args:
199
+ start: The directory to start the upward search from.
200
+
201
+ Returns:
202
+ The resolved ancestor that contains a ``.git`` directory or file, or
203
+ the resolved *start* path when no git marker is found.
204
+ """
145
205
  resolved_start = start.resolve()
146
206
  candidate_paths = [resolved_start, *resolved_start.parents]
147
207
  for each_candidate in candidate_paths:
@@ -155,6 +215,17 @@ def rerun_preflight(
155
215
  repository_root: Path,
156
216
  all_environment_overrides: dict[str, str] | None,
157
217
  ) -> int:
218
+ """Re-invoke ``preflight.py`` after the hooks path has been repaired.
219
+
220
+ Args:
221
+ repository_root: Repository root passed through to preflight as
222
+ ``--repo-root``.
223
+ all_environment_overrides: Optional environment variable mapping
224
+ forwarded to ``subprocess.run``.
225
+
226
+ Returns:
227
+ The preflight subprocess exit code.
228
+ """
158
229
  preflight_script_path = Path(__file__).resolve().parent / "preflight.py"
159
230
  rerun_command = [
160
231
  sys.executable,
@@ -172,6 +243,15 @@ def rerun_preflight(
172
243
 
173
244
 
174
245
  def parse_arguments(all_arguments: list[str] | None) -> argparse.Namespace:
246
+ """Parse the command-line arguments for the fix_hookspath script.
247
+
248
+ Args:
249
+ all_arguments: Command-line argument list, or None to read from
250
+ ``sys.argv``.
251
+
252
+ Returns:
253
+ The parsed argparse namespace with a ``repo_root`` attribute.
254
+ """
175
255
  parser = argparse.ArgumentParser(
176
256
  description=(
177
257
  "Auto-fix core.hooksPath when bugteam preflight detects a stale override. "
@@ -192,6 +272,17 @@ def main(
192
272
  all_arguments: list[str],
193
273
  all_environment_overrides: dict[str, str] | None,
194
274
  ) -> int:
275
+ """Run the fix_hookspath repair routine and re-invoke preflight.
276
+
277
+ Args:
278
+ all_arguments: Command-line argument list forwarded to argparse.
279
+ all_environment_overrides: Optional environment variable mapping
280
+ forwarded to every git invocation and to the preflight rerun.
281
+
282
+ Returns:
283
+ Zero on success. Non-zero on the first failing git command or on a
284
+ non-zero preflight rerun exit code.
285
+ """
195
286
  arguments = parse_arguments(all_arguments)
196
287
  start_directory = Path.cwd()
197
288
  repository_root = (