claude-dev-env 1.42.0 → 1.44.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 (208) 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/pre-compact/SKILL.md +114 -0
  151. package/skills/session-log/SKILL.md +98 -233
  152. package/hooks/config/pr_description_enforcer_constants.py +0 -19
  153. package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
  154. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
  155. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
  156. package/skills/pr-converge/scripts/conftest.py +0 -60
  157. package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
  158. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
  159. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
  160. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +0 -0
  161. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
  162. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
  163. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
  164. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
  165. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
  166. /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
  167. /package/hooks/{config → hooks_constants}/__init__.py +0 -0
  168. /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
  169. /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
  170. /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
  171. /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
  172. /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
  173. /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
  174. /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
  175. /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
  176. /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
  177. /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
  178. /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
  179. /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
  180. /package/hooks/{config → hooks_constants}/messages.py +0 -0
  181. /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
  182. /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
  183. /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
  184. /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
  185. /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
  186. /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
  187. /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
  188. /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
  189. /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
  190. /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
  191. /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
  192. /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
  193. /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
  194. /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
  195. /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
  196. /package/skills/{implement/scripts/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
  197. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
  198. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
  199. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
  200. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/claude_permissions_common_constants.py +0 -0
  201. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
  202. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
  203. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
  204. /package/skills/{pr-converge/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
  205. /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
  206. /package/skills/{pr-converge/scripts/config → implement/scripts/implement_scripts_constants}/__init__.py +0 -0
  207. /package/skills/implement/scripts/{config → implement_scripts_constants}/notes_constants.py +0 -0
  208. /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
@@ -0,0 +1,205 @@
1
+ """Shared exemption rules for the .md blocker and its post-write companion.
2
+
3
+ Both `md_to_html_blocker.py` (PreToolUse) and `md_to_html_companion.py`
4
+ (PostToolUse) must agree on which file paths bypass the .md → .html policy.
5
+ This module is the single source of truth for that decision.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ from pathlib import Path, PureWindowsPath
11
+
12
+
13
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
14
+ if _hooks_directory not in sys.path:
15
+ sys.path.insert(0, _hooks_directory)
16
+
17
+ from hooks_constants.md_to_html_blocker_constants import ( # noqa: E402
18
+ ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES,
19
+ ALL_EXEMPT_ANYWHERE_FILENAMES_LOWER,
20
+ ALL_EXEMPT_HOME_DIRECTORY_PATH_PREFIXES,
21
+ ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS,
22
+ ALL_EXEMPT_ROOT_FILENAMES_LOWER,
23
+ CLAUDE_DEV_ENV_REPO_NAME_SEGMENT,
24
+ CLAUDE_DIRECTORY_PATH_PREFIX,
25
+ CLAUDE_DIRECTORY_SEGMENT_MARKER,
26
+ MINIMUM_SEGMENT_COUNT_TO_MATCH_INDICATOR,
27
+ PACKAGES_TOP_LEVEL_SEGMENT,
28
+ PLUGIN_DIRECTORY_PATH_PREFIX,
29
+ PLUGIN_DIRECTORY_SEGMENT_MARKER,
30
+ PLUGIN_ROOT_MARKER_DIRECTORY_NAME,
31
+ REPO_ROOT_MARKER_NAME,
32
+ RESOLVED_HOME_DIRECTORY_LOWER,
33
+ RESOLVED_TEMP_DIRECTORY_PATH_PREFIX,
34
+ )
35
+
36
+
37
+ def is_exempt_path(file_path: str) -> bool:
38
+ """Return True when the .md file path is exempt from the blocker policy.
39
+
40
+ Exemption sources, in order of evaluation:
41
+ - Any segment under `.claude/` or `.claude-plugin/` (case-insensitive)
42
+ - Basename in `ALL_EXEMPT_ANYWHERE_FILENAMES` (e.g. SKILL.md)
43
+ - Anchored under `packages/claude-dev-env/<one of
44
+ ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES>/...` (docs, rules,
45
+ system-prompts source files in this repo)
46
+ - Path segment in `ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS` (agents/skills/commands)
47
+ - Canonical path under a home-relative exempt directory
48
+ (`ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES`)
49
+ - Canonical path under the OS temp directory
50
+ - Ancestor directory contains `.claude-plugin/` (plugin-root marker walk)
51
+ - Basename in `ALL_EXEMPT_ROOT_FILENAMES` and directory is a repo root
52
+
53
+ Args:
54
+ file_path: Raw file path from the hook payload. May contain tilde,
55
+ backslashes, or be relative.
56
+
57
+ Returns:
58
+ True when the path is exempt, False when the policy applies.
59
+ """
60
+ expanded_path = os.path.expanduser(file_path)
61
+ normalized = os.path.normpath(expanded_path).replace("\\", "/")
62
+ lower_normalized = normalized.lower()
63
+ if (
64
+ CLAUDE_DIRECTORY_SEGMENT_MARKER in lower_normalized
65
+ or lower_normalized.startswith(CLAUDE_DIRECTORY_PATH_PREFIX)
66
+ ):
67
+ return True
68
+ if (
69
+ PLUGIN_DIRECTORY_SEGMENT_MARKER in lower_normalized
70
+ or lower_normalized.startswith(PLUGIN_DIRECTORY_PATH_PREFIX)
71
+ ):
72
+ return True
73
+ basename_lower = os.path.basename(normalized).lower()
74
+ if basename_lower in ALL_EXEMPT_ANYWHERE_FILENAMES_LOWER:
75
+ return True
76
+ if _is_under_claude_dev_env_source_subdirectory(expanded_path, lower_normalized):
77
+ return True
78
+ if _has_plugin_directory_segment(lower_normalized):
79
+ return True
80
+ canonical_normalized_path = os.path.realpath(expanded_path).replace("\\", "/")
81
+ canonical_lower_path = canonical_normalized_path.lower()
82
+ if _is_under_exempt_home_directory(canonical_lower_path):
83
+ return True
84
+ if canonical_lower_path.startswith(RESOLVED_TEMP_DIRECTORY_PATH_PREFIX):
85
+ return True
86
+ if _is_under_plugin_root_marker(canonical_normalized_path):
87
+ return True
88
+ if basename_lower in ALL_EXEMPT_ROOT_FILENAMES_LOWER:
89
+ absolute_directory = _resolve_absolute_directory(normalized)
90
+ if _is_repo_root_directory(absolute_directory):
91
+ return True
92
+ return False
93
+
94
+
95
+ def _resolve_absolute_directory(normalized_path: str) -> str:
96
+ directory = os.path.dirname(normalized_path)
97
+ if not directory or directory == ".":
98
+ return os.getcwd()
99
+ if os.path.isabs(directory):
100
+ return directory
101
+ return os.path.abspath(directory)
102
+
103
+
104
+ def _has_plugin_directory_segment(lower_normalized_path: str) -> bool:
105
+ for each_directory_segment in ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS:
106
+ segment_marker = f"/{each_directory_segment}/"
107
+ if segment_marker in lower_normalized_path:
108
+ return True
109
+ if lower_normalized_path.startswith(f"{each_directory_segment}/"):
110
+ return True
111
+ return False
112
+
113
+
114
+ def _is_absolute_path_cross_platform(file_path: str) -> bool:
115
+ """Detect absolute paths in both POSIX and Windows drive-letter forms.
116
+
117
+ ``os.path.isabs`` is platform-dependent: on Linux/macOS it classifies a
118
+ Windows drive-letter path like ``Y:\\repo\\foo`` as relative. The anchored
119
+ source-subdirectory exemption must scan every starting segment for
120
+ absolute paths regardless of host OS, so a path's absoluteness must be
121
+ decided cross-platform.
122
+
123
+ Args:
124
+ file_path: Tilde-expanded file path.
125
+
126
+ Returns:
127
+ True when the path is absolute under POSIX rules or carries a Windows
128
+ drive-letter root (``[A-Za-z]:[\\\\/]...``).
129
+ """
130
+ if os.path.isabs(file_path):
131
+ return True
132
+ return PureWindowsPath(file_path).is_absolute()
133
+
134
+
135
+ def _is_under_claude_dev_env_source_subdirectory(
136
+ expanded_file_path: str, lower_normalized_path: str
137
+ ) -> bool:
138
+ """Anchored exemption for ``packages/claude-dev-env/<source-dir>/...``.
139
+
140
+ The match requires segment-anchored matching at the start of the path
141
+ (relative) or at the root of an absolute path. A nested path like
142
+ ``notes/packages/claude-dev-env/docs/foo.md`` is NOT exempt — only the
143
+ full three-segment anchor matches.
144
+
145
+ Args:
146
+ expanded_file_path: Tilde-expanded file path; ``os.path.isabs`` on
147
+ this form classifies the path as absolute or relative on the
148
+ current platform.
149
+ lower_normalized_path: Same path lowercased and with separators
150
+ normalized to forward slashes.
151
+
152
+ Returns:
153
+ True when the path is anchored under
154
+ ``packages/claude-dev-env/<one of
155
+ ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES>/``.
156
+ """
157
+ all_segments = [
158
+ each_segment
159
+ for each_segment in lower_normalized_path.split("/")
160
+ if each_segment
161
+ ]
162
+ if not all_segments:
163
+ return False
164
+ if _is_absolute_path_cross_platform(expanded_file_path):
165
+ starting_segment_index_options = list(range(len(all_segments)))
166
+ else:
167
+ starting_segment_index_options = [0]
168
+ for each_starting_index in starting_segment_index_options:
169
+ if (
170
+ len(all_segments) >= each_starting_index + MINIMUM_SEGMENT_COUNT_TO_MATCH_INDICATOR
171
+ and all_segments[each_starting_index] == PACKAGES_TOP_LEVEL_SEGMENT
172
+ and all_segments[each_starting_index + 1] == CLAUDE_DEV_ENV_REPO_NAME_SEGMENT
173
+ and all_segments[each_starting_index + 2] in ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES
174
+ ):
175
+ return True
176
+ return False
177
+
178
+
179
+ def _is_under_plugin_root_marker(normalized_path: str) -> bool:
180
+ directory = os.path.dirname(normalized_path)
181
+ visited_directories: set[str] = set()
182
+ while directory and directory not in visited_directories:
183
+ visited_directories.add(directory)
184
+ marker_path = os.path.join(directory, PLUGIN_ROOT_MARKER_DIRECTORY_NAME)
185
+ if os.path.isdir(marker_path):
186
+ return True
187
+ parent_directory = os.path.dirname(directory)
188
+ if parent_directory == directory:
189
+ break
190
+ directory = parent_directory
191
+ return False
192
+
193
+
194
+ def _is_under_exempt_home_directory(lower_normalized_path: str) -> bool:
195
+ if not RESOLVED_HOME_DIRECTORY_LOWER:
196
+ return False
197
+ for each_exempt_path_prefix in ALL_EXEMPT_HOME_DIRECTORY_PATH_PREFIXES:
198
+ if lower_normalized_path.startswith(each_exempt_path_prefix):
199
+ return True
200
+ return False
201
+
202
+
203
+ def _is_repo_root_directory(directory_path: str) -> bool:
204
+ git_marker_path = os.path.join(directory_path, REPO_ROOT_MARKER_NAME)
205
+ return os.path.exists(git_marker_path)
@@ -6,27 +6,44 @@ that markdown flattens. See https://thariqs.github.io/html-effectiveness/
6
6
  """
7
7
 
8
8
  import json
9
- import os
10
9
  import sys
10
+ from pathlib import Path
11
11
  from typing import TextIO
12
12
 
13
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
14
+ if _hooks_dir not in sys.path:
15
+ sys.path.insert(0, _hooks_dir)
13
16
 
14
- _markdown_extension = ".md"
15
- _html_effectiveness_url = "https://thariqs.github.io/html-effectiveness/"
16
- _exempt_root_filenames = ("readme.md", "changelog.md")
17
+ _blocking_directory = str(Path(__file__).resolve().parent)
18
+ if _blocking_directory not in sys.path:
19
+ sys.path.insert(0, _blocking_directory)
20
+
21
+ from hooks_constants.md_to_html_blocker_constants import ( # noqa: E402
22
+ ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES,
23
+ ALL_EXEMPT_ANYWHERE_FILENAMES,
24
+ ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES,
25
+ ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS,
26
+ CLAUDE_DEV_ENV_REPO_NAME_SEGMENT,
27
+ CLAUDE_DIRECTORY_NAME,
28
+ PACKAGES_TOP_LEVEL_SEGMENT,
29
+ PLUGIN_ROOT_MARKER_DIRECTORY_NAME,
30
+ )
31
+ from md_path_exemptions import is_exempt_path # noqa: E402
17
32
 
18
33
 
19
- def _is_exempt_path(file_path: str) -> bool:
20
- normalized = os.path.normpath(file_path).replace("\\", "/")
21
- lower_normalized = normalized.lower()
22
- if "/.claude/" in lower_normalized or lower_normalized.startswith(".claude/"):
23
- return True
24
- basename = os.path.basename(normalized)
25
- if basename.lower() in _exempt_root_filenames:
26
- directory = os.path.dirname(normalized)
27
- if directory in ("", "."):
28
- return True
29
- return False
34
+ _markdown_extension = ".md"
35
+ _html_effectiveness_url = "https://thariqs.github.io/html-effectiveness/"
36
+ _exempt_anywhere_filenames_summary = ", ".join(ALL_EXEMPT_ANYWHERE_FILENAMES)
37
+ _exempt_plugin_segments_summary = ", ".join(
38
+ f"{each_segment}/" for each_segment in ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS
39
+ )
40
+ _exempt_home_directories_summary = ", ".join(
41
+ f"~/{each_directory}/" for each_directory in ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES
42
+ )
43
+ _claude_dev_env_source_directories_summary = (
44
+ f"{PACKAGES_TOP_LEVEL_SEGMENT}/{CLAUDE_DEV_ENV_REPO_NAME_SEGMENT}/"
45
+ f"{{{','.join(sorted(ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES))}}}/"
46
+ )
30
47
 
31
48
 
32
49
  def _block_reason(file_path: str) -> str:
@@ -46,8 +63,14 @@ def _block_context() -> str:
46
63
  "Reference for HTML effectiveness patterns:\n"
47
64
  f"{_html_effectiveness_url}\n"
48
65
  "Exceptions (.md still allowed):\n"
49
- "- Files inside .claude/ directories\n"
50
- "- README.md and CHANGELOG.md at repo root"
66
+ f"- Files inside {CLAUDE_DIRECTORY_NAME}/ or {PLUGIN_ROOT_MARKER_DIRECTORY_NAME}/ directories\n"
67
+ f"- {_exempt_anywhere_filenames_summary} anywhere\n"
68
+ f"- Files under {_exempt_plugin_segments_summary} directories\n"
69
+ f"- Files under {_claude_dev_env_source_directories_summary} source directories\n"
70
+ f"- Files under any directory whose ancestor contains {PLUGIN_ROOT_MARKER_DIRECTORY_NAME}/\n"
71
+ "- README.md and CHANGELOG.md at any repo root\n"
72
+ f"- Files under {_exempt_home_directories_summary}\n"
73
+ "- Files under the OS temp directory"
51
74
  )
52
75
 
53
76
 
@@ -55,8 +78,13 @@ def _block_system_message() -> str:
55
78
  return (
56
79
  ".md files are blocked in this project — generate a self-contained .html "
57
80
  f"file instead. See {_html_effectiveness_url} for "
58
- "design patterns and examples. Exemptions: .claude/ infrastructure, "
59
- "README.md, CHANGELOG.md at repo root."
81
+ f"design patterns and examples. Exemptions: {CLAUDE_DIRECTORY_NAME}/ and "
82
+ f"{PLUGIN_ROOT_MARKER_DIRECTORY_NAME}/ infrastructure, "
83
+ f"{_exempt_anywhere_filenames_summary} anywhere, {_exempt_plugin_segments_summary} trees, "
84
+ f"{_claude_dev_env_source_directories_summary} source trees, "
85
+ f"files under a {PLUGIN_ROOT_MARKER_DIRECTORY_NAME}/ root, "
86
+ f"README.md/CHANGELOG.md at any repo root, {_exempt_home_directories_summary}, "
87
+ "and the OS temp directory."
60
88
  )
61
89
 
62
90
 
@@ -92,7 +120,7 @@ def main() -> None:
92
120
  if not file_path.lower().endswith(_markdown_extension):
93
121
  sys.exit(0)
94
122
 
95
- if _is_exempt_path(file_path):
123
+ if is_exempt_path(file_path):
96
124
  sys.exit(0)
97
125
 
98
126
  block_payload = {
@@ -25,17 +25,11 @@ import sys
25
25
  from pathlib import Path
26
26
  from typing import TextIO
27
27
 
28
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
29
+ if _hooks_dir not in sys.path:
30
+ sys.path.insert(0, _hooks_dir)
28
31
 
29
- def _insert_hooks_tree_for_imports() -> None:
30
- hooks_tree = Path(__file__).resolve().parent.parent
31
- hooks_tree_string = str(hooks_tree)
32
- if hooks_tree_string not in sys.path:
33
- sys.path.insert(0, hooks_tree_string)
34
-
35
-
36
- _insert_hooks_tree_for_imports()
37
-
38
- from config.pr_converge_bugteam_enforcer_constants import (
32
+ from hooks_constants.pr_converge_bugteam_enforcer_constants import ( # noqa: E402
39
33
  AGENT_TOOL_NAME,
40
34
  ALL_AUDIT_PROMPT_SUBSTRINGS,
41
35
  BUGTEAM_PHASE,
@@ -47,7 +41,7 @@ from config.pr_converge_bugteam_enforcer_constants import (
47
41
  STATE_FIELD_PHASE,
48
42
  STATE_FIELD_TICK_COUNT,
49
43
  )
50
- from config.pr_converge_bugteam_enforcer_state import (
44
+ from hooks_constants.pr_converge_bugteam_enforcer_state import ( # noqa: E402
51
45
  load_state_dictionary,
52
46
  resolve_state_path,
53
47
  )