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
@@ -40,43 +40,43 @@ if _HOOKS_DIR not in sys.path:
40
40
  sys.path.insert(0, _HOOKS_DIR)
41
41
 
42
42
  from code_rules_path_utils import is_config_file # noqa: E402
43
- from config.banned_identifiers_constants import ( # noqa: E402
43
+ from hooks_constants.banned_identifiers_constants import ( # noqa: E402
44
44
  ALL_BANNED_IDENTIFIERS,
45
45
  BANNED_IDENTIFIER_MESSAGE_SUFFIX,
46
46
  BANNED_IDENTIFIER_SKIP_ADVISORY,
47
47
  MAX_BANNED_IDENTIFIER_ISSUES,
48
48
  )
49
- from config.hardcoded_user_path_constants import ( # noqa: E402
49
+ from hooks_constants.hardcoded_user_path_constants import ( # noqa: E402
50
50
  HARDCODED_USER_PATH_GUIDANCE,
51
51
  HARDCODED_USER_PATH_PATTERN,
52
52
  MAX_HARDCODED_USER_PATH_ISSUES,
53
53
  )
54
- from config.inline_tuple_string_magic_constants import ( # noqa: E402
54
+ from hooks_constants.inline_tuple_string_magic_constants import ( # noqa: E402
55
55
  ALL_SNAKE_CASE_KEYWORD_EXEMPTIONS,
56
56
  EXPECTED_TUPLE_PAIR_LENGTH,
57
57
  INLINE_TUPLE_STRING_MAGIC_MESSAGE_SUFFIX,
58
58
  MAX_INLINE_TUPLE_STRING_MAGIC_ISSUES,
59
59
  SNAKE_CASE_LITERAL_PATTERN,
60
60
  )
61
- from config.stuttering_check_config import ( # noqa: E402
61
+ from hooks_constants.stuttering_check_config import ( # noqa: E402
62
62
  MAX_STUTTERING_PREFIX_ISSUES,
63
63
  STUTTERING_ALL_PREFIX_PATTERN,
64
64
  )
65
- from config.sys_path_insert_constants import MAX_SYS_PATH_INSERT_ISSUES, SYS_PATH_INSERT_GUIDANCE # noqa: E402
66
- from config.unused_module_import_constants import ( # noqa: E402
65
+ from hooks_constants.sys_path_insert_constants import MAX_SYS_PATH_INSERT_ISSUES, SYS_PATH_INSERT_GUIDANCE # noqa: E402
66
+ from hooks_constants.unused_module_import_constants import ( # noqa: E402
67
67
  ALL_TYPING_MODULE_NAMES,
68
68
  MAX_UNUSED_IMPORT_ISSUES,
69
69
  TYPE_CHECKING_IDENTIFIER,
70
70
  UNUSED_IMPORT_GUIDANCE,
71
71
  line_suppresses_unused_import_via_noqa,
72
72
  )
73
- from config.stuttering_import_binding_constants import ( # noqa: E402
73
+ from hooks_constants.stuttering_import_binding_constants import ( # noqa: E402
74
74
  AST_LINENO_ATTRIBUTE,
75
75
  MODULE_PATH_SEPARATOR,
76
76
  WILDCARD_IMPORT_SENTINEL,
77
77
  )
78
- from config.any_type_config import ALL_ANY_ALLOWED_PATTERNS # noqa: E402
79
- from config.blocking_check_limits import ( # noqa: E402
78
+ from hooks_constants.any_type_config import ALL_ANY_ALLOWED_PATTERNS # noqa: E402
79
+ from hooks_constants.blocking_check_limits import ( # noqa: E402
80
80
  ALL_BANNED_PREFIX_NAMES,
81
81
  ALL_BARE_EXCEPT_BANNED_HANDLER_NAMES,
82
82
  ALL_BOUNDARY_TYPE_EXEMPT_FILENAMES,
@@ -95,7 +95,7 @@ from config.blocking_check_limits import ( # noqa: E402
95
95
  MAX_THIN_WRAPPER_ISSUES,
96
96
  )
97
97
 
98
- from config.code_rules_enforcer_constants import ( # noqa: E402
98
+ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
99
99
  ADVISORY_LINE_THRESHOLD_HARD,
100
100
  ADVISORY_LINE_THRESHOLD_SOFT,
101
101
  ALL_CODE_EXTENSIONS,
@@ -109,9 +109,14 @@ from config.code_rules_enforcer_constants import ( # noqa: E402
109
109
  ALL_SUBSCRIPT_ONLY_COLLECTION_TYPE_NAMES,
110
110
  DOTTED_SEGMENT_PATTERN,
111
111
  EACH_PREFIX,
112
+ ALL_EXEMPT_PYTHON_COMMENT_BODIES,
113
+ ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES,
114
+ ALL_JAVASCRIPT_EXEMPT_INLINE_COMMENT_PREFIXES,
115
+ ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS,
112
116
  FILE_GLOBAL_UPPER_SNAKE_PATTERN,
113
117
  ALL_HOOK_INFRASTRUCTURE_PATTERNS,
114
118
  ALL_IMPORT_STATEMENT_PREFIXES,
119
+ MAX_COMMENT_ISSUES,
115
120
  INLINE_COLLECTION_MIN_LENGTH,
116
121
  ALL_JAVASCRIPT_EXTENSIONS,
117
122
  LOGGING_FSTRING_PATTERN,
@@ -121,6 +126,9 @@ from config.code_rules_enforcer_constants import ( # noqa: E402
121
126
  ALL_PYTHON_EXTENSIONS,
122
127
  ALL_SELF_AND_CLS_PARAMETER_NAMES,
123
128
  ALL_TEST_PATH_PATTERNS,
129
+ TRIPLE_DOUBLE_QUOTE_DELIMITER,
130
+ TRIPLE_QUOTE_PARITY_DIVISOR,
131
+ TRIPLE_SINGLE_QUOTE_DELIMITER,
124
132
  TYPE_CHECKING_BLOCK_PATTERN,
125
133
  ALL_UNION_TYPING_NAMES,
126
134
  UPPER_SNAKE_CONSTANT_PATTERN,
@@ -168,54 +176,29 @@ def is_spec_file(file_path: str) -> bool:
168
176
 
169
177
 
170
178
  def check_comments_python(content: str) -> list[str]:
171
- """Check for comments in Python code."""
179
+ """Check for comments in Python code.
180
+
181
+ Uses ``tokenize.generate_tokens`` to find true ``COMMENT`` tokens.
182
+ Hash characters that appear inside string literals (hex color codes,
183
+ URL fragments, and the hash inside an f-string interpolation pattern)
184
+ are correctly skipped because the tokenizer recognizes them as parts
185
+ of string tokens rather than comment tokens.
186
+
187
+ When the tokenizer cannot parse the file (partial content during
188
+ Edit, invalid syntax), the check returns no findings rather than
189
+ falling back to a line-walker scan — false negatives on
190
+ syntactically-invalid drafts are preferable to false positives that
191
+ mis-classify string-interior hash characters as comments.
192
+ """
172
193
  issues = []
173
- lines = content.split("\n")
174
-
175
- for line_number, line in enumerate(lines, 1):
176
- stripped = line.strip()
177
-
178
- if not stripped:
179
- continue
180
-
181
- if stripped.startswith("#!"):
182
- continue
183
-
184
- if stripped.startswith("# type:"):
194
+ for each_comment_token in _comment_tokens(content):
195
+ if _is_exempt_python_comment(each_comment_token):
185
196
  continue
186
-
187
- if stripped.startswith("# noqa"):
188
- continue
189
-
190
- if stripped.startswith("# pylint:"):
191
- continue
192
-
193
- if stripped.startswith("# pragma:"):
194
- continue
195
-
196
- if stripped.startswith(("# TODO", "# FIXME", "# HACK", "# XXX")):
197
- continue
198
-
199
- comment_index = line.find("#")
200
- if comment_index != -1:
201
- before_comment = line[:comment_index]
202
- if not before_comment.strip().startswith(("'", '"')):
203
- is_in_string = False
204
- quote_char = None
205
- for i, each_char in enumerate(before_comment):
206
- if each_char in ("'", '"') and (i == 0 or before_comment[i - 1] != "\\"):
207
- if not is_in_string:
208
- is_in_string = True
209
- quote_char = each_char
210
- elif each_char == quote_char:
211
- is_in_string = False
212
-
213
- if not is_in_string:
214
- comment_text = line[comment_index + 1 :].strip()
215
- if comment_text and not comment_text.startswith(("type:", "noqa", "pylint:", "pragma:", "TODO", "FIXME", "HACK", "XXX")):
216
- issues.append(f"Line {line_number}: Comment found - refactor to self-documenting code")
217
-
218
- if len(issues) >= 3:
197
+ line_number = each_comment_token.start[0]
198
+ issues.append(
199
+ f"Line {line_number}: Comment found - refactor to self-documenting code"
200
+ )
201
+ if len(issues) >= MAX_COMMENT_ISSUES:
219
202
  break
220
203
 
221
204
  return issues
@@ -245,10 +228,10 @@ def check_comments_javascript(content: str) -> list[str]:
245
228
  continue
246
229
 
247
230
  if stripped.startswith("//"):
248
- if not stripped.startswith(("// @ts-", "// eslint-", "// prettier-", "/// ", "// TODO", "// FIXME", "// HACK", "// XXX")):
231
+ if not stripped.startswith(ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES):
249
232
  issues.append(f"Line {each_line_number}: Comment found - refactor to self-documenting code")
250
233
 
251
- if len(issues) >= 3:
234
+ if len(issues) >= MAX_COMMENT_ISSUES:
252
235
  break
253
236
 
254
237
  return issues
@@ -268,36 +251,13 @@ def extract_comment_texts(content: str, file_path: str) -> tuple[set[str], set[s
268
251
  if not content:
269
252
  return inline_comments, standalone_comments
270
253
 
271
- lines = content.split("\n")
272
-
273
254
  if extension in ALL_PYTHON_EXTENSIONS:
274
- for line in lines:
275
- stripped = line.strip()
276
- if not stripped:
277
- continue
278
- if stripped.startswith("#"):
279
- if stripped.startswith(("#!", "# type:", "# noqa", "# pylint:", "# pragma:", "# TODO", "# FIXME", "# HACK", "# XXX")):
280
- continue
281
- standalone_comments.add(stripped)
282
- elif "#" in line:
283
- comment_index = line.find("#")
284
- before_comment = line[:comment_index]
285
- if not before_comment.strip().startswith(("'", '"')):
286
- is_in_string = False
287
- quote_char = None
288
- for i, char in enumerate(before_comment):
289
- if char in ("'", '"') and (i == 0 or before_comment[i - 1] != "\\"):
290
- if not is_in_string:
291
- is_in_string = True
292
- quote_char = char
293
- elif char == quote_char:
294
- is_in_string = False
295
- if not is_in_string:
296
- comment_text = line[comment_index + 1 :].strip()
297
- if comment_text and not comment_text.startswith(("type:", "noqa", "pylint:", "pragma:", "TODO", "FIXME", "HACK", "XXX")):
298
- inline_comments.add(line[comment_index:].strip())
255
+ inline_comments, standalone_comments, _ = _extract_python_comment_sets(content)
256
+ return inline_comments, standalone_comments
299
257
 
300
- elif extension in ALL_JAVASCRIPT_EXTENSIONS:
258
+ lines = content.split("\n")
259
+
260
+ if extension in ALL_JAVASCRIPT_EXTENSIONS:
301
261
  is_in_multiline = False
302
262
  for line in lines:
303
263
  stripped = line.strip()
@@ -313,14 +273,14 @@ def extract_comment_texts(content: str, file_path: str) -> tuple[set[str], set[s
313
273
  standalone_comments.add(stripped)
314
274
  continue
315
275
  if stripped.startswith("//"):
316
- if not stripped.startswith(("// @ts-", "// eslint-", "// prettier-", "/// ", "// TODO", "// FIXME", "// HACK", "// XXX")):
276
+ if not stripped.startswith(ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES):
317
277
  standalone_comments.add(stripped)
318
278
  elif "//" in line:
319
279
  before_slash = line[:line.index("//")]
320
280
  if before_slash.strip():
321
281
  comment_start = stripped.index("//")
322
282
  comment_text = stripped[comment_start + 2 :].strip()
323
- if not comment_text.startswith(("TODO", "FIXME", "HACK", "XXX")):
283
+ if not comment_text.startswith(ALL_JAVASCRIPT_EXEMPT_INLINE_COMMENT_PREFIXES):
324
284
  inline_comments.add(stripped[comment_start:])
325
285
 
326
286
  return inline_comments, standalone_comments
@@ -332,11 +292,26 @@ def check_comment_changes(old_content: str, new_content: str, file_path: str) ->
332
292
  Inline comments (after code on same line): BLOCK when added.
333
293
  Standalone comment lines: NUDGE (print advisory) when added.
334
294
  Existing comments being removed: BLOCK (comment preservation principle).
295
+
296
+ When the file is Python and either *old_content* or *new_content* cannot
297
+ be tokenized (common for mid-edit Edit fragments), the comparison is
298
+ indeterminate: the per-side tokenize failure would empty one set and
299
+ misrepresent every comment on the other side as either added or
300
+ removed. The check returns no issues in that case — false negatives on
301
+ syntactically-invalid drafts are preferable to false positives that
302
+ flag legitimate comments as deleted.
335
303
  """
336
304
  issues: list[str] = []
337
305
 
338
- old_inline, old_standalone = extract_comment_texts(old_content, file_path)
339
- new_inline, new_standalone = extract_comment_texts(new_content, file_path)
306
+ extension = get_file_extension(file_path)
307
+ if extension in ALL_PYTHON_EXTENSIONS:
308
+ old_inline, old_standalone, old_tokenize_ok = _extract_python_comment_sets(old_content)
309
+ new_inline, new_standalone, new_tokenize_ok = _extract_python_comment_sets(new_content)
310
+ if not (old_tokenize_ok and new_tokenize_ok):
311
+ return issues
312
+ else:
313
+ old_inline, old_standalone = extract_comment_texts(old_content, file_path)
314
+ new_inline, new_standalone = extract_comment_texts(new_content, file_path)
340
315
 
341
316
  added_inline = new_inline - old_inline
342
317
  if added_inline:
@@ -382,14 +357,29 @@ def check_imports_at_top(content: str) -> list[str]:
382
357
  simpler single-level tracking is preferred over maintaining a stack of indent
383
358
  levels. The pinned behavior is covered by
384
359
  ``test_should_track_only_innermost_type_checking_block``.
360
+
361
+ Triple-quoted-string interior lines are skipped. Once a line opens a
362
+ multi-line triple-double-quote or triple-single-quote string (odd count
363
+ of the delimiter), every subsequent line is treated as docstring content
364
+ and exempt from the import-prefix scan until the matching delimiter
365
+ closes the string. Without this tracking, docstring sentences that
366
+ happen to start with ``from `` or ``import `` after stripping (a common
367
+ pattern in narrative docstrings) would fire a false positive.
385
368
  """
386
369
  issues: list[str] = []
387
370
  lines = content.split("\n")
388
371
  is_inside_function = False
389
372
  function_indent = 0
390
373
  type_checking_block_indent = NOT_INSIDE_TYPE_CHECKING_BLOCK
374
+ active_triple_quote_delimiter: str | None = None
391
375
 
392
376
  for line_number, each_line in enumerate(lines, 1):
377
+ if active_triple_quote_delimiter is not None:
378
+ active_triple_quote_delimiter = _update_triple_quote_state_for_line(
379
+ each_line, active_triple_quote_delimiter
380
+ )
381
+ continue
382
+
393
383
  stripped = each_line.strip()
394
384
 
395
385
  if not stripped:
@@ -404,12 +394,18 @@ def check_imports_at_top(content: str) -> list[str]:
404
394
  type_checking_match = TYPE_CHECKING_BLOCK_PATTERN.match(each_line)
405
395
  if type_checking_match:
406
396
  type_checking_block_indent = len(type_checking_match.group("indent"))
397
+ active_triple_quote_delimiter = _update_triple_quote_state_for_line(
398
+ each_line, active_triple_quote_delimiter
399
+ )
407
400
  continue
408
401
 
409
402
  function_match = re.match(r"^(\s*)(async\s+)?def\s+\w+", each_line)
410
403
  if function_match:
411
404
  is_inside_function = True
412
405
  function_indent = len(function_match.group(1)) if function_match.group(1) else 0
406
+ active_triple_quote_delimiter = _update_triple_quote_state_for_line(
407
+ each_line, active_triple_quote_delimiter
408
+ )
413
409
  continue
414
410
 
415
411
  if is_inside_function:
@@ -421,9 +417,51 @@ def check_imports_at_top(content: str) -> list[str]:
421
417
  if stripped.startswith(ALL_IMPORT_STATEMENT_PREFIXES):
422
418
  issues.append(f"Line {line_number}: Import inside function - move to top of file")
423
419
 
420
+ active_triple_quote_delimiter = _update_triple_quote_state_for_line(
421
+ each_line, active_triple_quote_delimiter
422
+ )
423
+
424
424
  return issues
425
425
 
426
426
 
427
+ def _update_triple_quote_state_for_line(
428
+ line_text: str, current_delimiter: str | None
429
+ ) -> str | None:
430
+ """Return the triple-quote delimiter that remains active after the line.
431
+
432
+ Naively counts triple-double-quote and triple-single-quote occurrences.
433
+ An odd count of either delimiter toggles the active state: ``None``
434
+ becomes that delimiter, the same delimiter becomes ``None``. Even counts
435
+ mean the line opens and closes the same delimiter in place (single-line
436
+ docstring or balanced pair) and the active state is unchanged.
437
+
438
+ Known limitation: the counter does not distinguish triple quotes that
439
+ appear inside other string contexts (for example, a raw f-string
440
+ containing the literal substring of triple quotes). Such constructs are
441
+ rare in docstring-bearing code; the false-negative risk is acceptable
442
+ to keep the line-walker simple and dependency-free.
443
+
444
+ Args:
445
+ line_text: The raw source line whose triple-quote balance is being
446
+ integrated into the running state.
447
+ current_delimiter: The active delimiter at the start of this line,
448
+ or ``None`` when no multi-line string is open.
449
+
450
+ Returns:
451
+ The delimiter that remains active after this line, or ``None`` when
452
+ no string is open.
453
+ """
454
+ if current_delimiter is not None:
455
+ if line_text.count(current_delimiter) % TRIPLE_QUOTE_PARITY_DIVISOR == 1:
456
+ return None
457
+ return current_delimiter
458
+ if line_text.count(TRIPLE_DOUBLE_QUOTE_DELIMITER) % TRIPLE_QUOTE_PARITY_DIVISOR == 1:
459
+ return TRIPLE_DOUBLE_QUOTE_DELIMITER
460
+ if line_text.count(TRIPLE_SINGLE_QUOTE_DELIMITER) % TRIPLE_QUOTE_PARITY_DIVISOR == 1:
461
+ return TRIPLE_SINGLE_QUOTE_DELIMITER
462
+ return None
463
+
464
+
427
465
  def check_logging_fstrings(content: str) -> list[str]:
428
466
  """Check for f-strings in logging calls."""
429
467
  issues = []
@@ -739,16 +777,96 @@ def _find_any_annotation_lines(source: str) -> list[int]:
739
777
  return offending_line_numbers
740
778
 
741
779
 
742
- def _comment_tokens(source: str) -> list[tokenize.TokenInfo]:
743
- """Return COMMENT tokens from source, or an empty list when tokenization fails."""
780
+ def _python_tokens(source: str) -> Iterator[tokenize.TokenInfo]:
781
+ """Yield Python tokens from *source* one at a time.
782
+
783
+ Centralizes the ``tokenize.generate_tokens`` entry-point so a future
784
+ change to the API lands in exactly one place. Iteration may raise
785
+ any of ``ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS`` when the source is
786
+ not valid Python (mid-edit Edit fragments, unterminated strings,
787
+ mismatched indentation) — callers handle the exception according to
788
+ their own contract (silently stop, return an indeterminate flag, etc.).
789
+ """
790
+ yield from tokenize.generate_tokens(io.StringIO(source).readline)
791
+
792
+
793
+ def _comment_tokens(source: str) -> Iterator[tokenize.TokenInfo]:
794
+ """Yield COMMENT tokens from *source* one at a time.
795
+
796
+ Streams from ``_python_tokens`` so consumers that early-exit (e.g.
797
+ ``check_comments_python`` caps at ``MAX_COMMENT_ISSUES``) avoid
798
+ materializing the entire token list. Silently stops on tokenize
799
+ failure so callers receive only valid comment tokens — no
800
+ indeterminate signal is exposed at this layer because the consumers
801
+ that need it (``_extract_python_comment_sets``) bypass this helper.
802
+ """
744
803
  try:
745
- return [
746
- each_token
747
- for each_token in tokenize.generate_tokens(io.StringIO(source).readline)
748
- if each_token.type == tokenize.COMMENT
749
- ]
750
- except (tokenize.TokenError, IndentationError, SyntaxError):
751
- return []
804
+ for each_token in _python_tokens(source):
805
+ if each_token.type == tokenize.COMMENT:
806
+ yield each_token
807
+ except ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS:
808
+ return
809
+
810
+
811
+ def _is_exempt_python_comment(comment_token: tokenize.TokenInfo) -> bool:
812
+ """Return True for shebangs and tooling-directive comments.
813
+
814
+ The shebang exemption applies only when the comment token starts
815
+ at line 1, column 0 — matching the OS-level convention that a
816
+ shebang line is meaningful only as the first line of an executable
817
+ file. An inline shebang-lookalike later in the file (an
818
+ after-code occurrence on any line, or a standalone occurrence on
819
+ line 2 or later) is NOT a real shebang and remains subject to the
820
+ no-comments rule.
821
+
822
+ Matches any prefix listed in ``ALL_EXEMPT_PYTHON_COMMENT_BODIES``
823
+ regardless of whether the directive sits flush against the leading
824
+ hash character or carries one or more whitespace characters (space
825
+ or tab) between the hash and the directive body. The pre-tokenize
826
+ implementation accepted both forms via its line-slice-then-strip
827
+ step; this helper preserves that behavior on top of the
828
+ tokenize-based scan.
829
+ """
830
+ comment_string = comment_token.string
831
+ if comment_string.startswith("#!") and comment_token.start == (1, 0):
832
+ return True
833
+ directive_body = comment_string[1:].lstrip()
834
+ if not directive_body:
835
+ return True
836
+ return directive_body.startswith(ALL_EXEMPT_PYTHON_COMMENT_BODIES)
837
+
838
+
839
+ def _extract_python_comment_sets(content: str) -> tuple[set[str], set[str], bool]:
840
+ """Return (inline_comments, standalone_comments, tokenize_succeeded).
841
+
842
+ Streams *content* once via ``_python_tokens``. A tokenize failure
843
+ (mid-edit fragment, syntax error) returns empty sets and ``False``
844
+ so callers can treat the situation as indeterminate rather than as
845
+ "no comments present". Inline vs standalone is decided by inspecting
846
+ the column offset of each ``COMMENT`` token against its source
847
+ line: an all-whitespace prefix means standalone.
848
+ """
849
+ inline_comments: set[str] = set()
850
+ standalone_comments: set[str] = set()
851
+ lines = content.split("\n")
852
+ try:
853
+ for each_token in _python_tokens(content):
854
+ if each_token.type != tokenize.COMMENT:
855
+ continue
856
+ if _is_exempt_python_comment(each_token):
857
+ continue
858
+ line_number = each_token.start[0]
859
+ column_offset = each_token.start[1]
860
+ source_line = lines[line_number - 1] if line_number - 1 < len(lines) else ""
861
+ text_before_comment = source_line[:column_offset]
862
+ normalized_comment_text = each_token.string.strip()
863
+ if not text_before_comment.strip():
864
+ standalone_comments.add(normalized_comment_text)
865
+ else:
866
+ inline_comments.add(normalized_comment_text)
867
+ except ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS:
868
+ return set(), set(), False
869
+ return inline_comments, standalone_comments, True
752
870
 
753
871
 
754
872
  def _find_unjustified_type_ignore_lines(source: str) -> list[int]:
@@ -11,8 +11,15 @@ the canonical implementation lives here.
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import sys
14
15
  from pathlib import Path
15
16
 
17
+ hooks_root_directory = str(Path(__file__).resolve().parent.parent)
18
+ if hooks_root_directory not in sys.path:
19
+ sys.path.insert(0, hooks_root_directory)
20
+
21
+ from hooks_constants.code_rules_path_utils_constants import ALL_CONFIG_DIRECTORY_NAMES # noqa: E402
22
+
16
23
 
17
24
  def is_config_file(file_path: str) -> bool:
18
25
  """Return True when the path points to a config file.
@@ -28,4 +35,4 @@ def is_config_file(file_path: str) -> bool:
28
35
  if normalized.endswith("/settings.py") or normalized == "settings.py":
29
36
  return True
30
37
  path_parts = Path(normalized).parts
31
- return "config" in path_parts[:-1]
38
+ return any(directory_segment in ALL_CONFIG_DIRECTORY_NAMES for directory_segment in path_parts[:-1])
@@ -13,7 +13,7 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
13
13
  if _hooks_dir not in sys.path:
14
14
  sys.path.insert(0, _hooks_dir)
15
15
 
16
- from config.convergence_branch_constants import ( # noqa: E402
16
+ from hooks_constants.convergence_branch_constants import ( # noqa: E402
17
17
  ALL_CONVERGENCE_BRANCH_PREFIXES,
18
18
  CONVERGENCE_BRANCH_SUFFIX_PATTERN,
19
19
  CONVERGENCE_FORCE_PUSH_DETECTION_PATTERN,
@@ -15,25 +15,19 @@ import re
15
15
  import sys
16
16
  from pathlib import Path, PurePosixPath, PureWindowsPath
17
17
 
18
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
19
+ if _hooks_dir not in sys.path:
20
+ sys.path.insert(0, _hooks_dir)
18
21
 
19
- def _insert_hooks_tree_for_imports() -> None:
20
- hooks_tree = Path(__file__).resolve().parent.parent
21
- hooks_tree_string = str(hooks_tree)
22
- if hooks_tree_string not in sys.path:
23
- sys.path.insert(0, hooks_tree_string)
24
-
25
-
26
- _insert_hooks_tree_for_imports()
27
-
28
- from config.dynamic_stderr_handler import DynamicStderrHandler
29
- from config.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
30
- from config.path_rewriter_constants import (
22
+ from hooks_constants.dynamic_stderr_handler import DynamicStderrHandler # noqa: E402
23
+ from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin # noqa: E402
24
+ from hooks_constants.path_rewriter_constants import ( # noqa: E402
31
25
  BASH_TOOL_NAME,
32
26
  HOOK_EVENT_NAME,
33
27
  PERMISSION_ALLOW,
34
28
  PLACEHOLDER_TOKEN_PATTERN,
35
29
  )
36
- from config.project_paths_reader import load_registry
30
+ from hooks_constants.project_paths_reader import load_registry # noqa: E402
37
31
 
38
32
  _ES_EXE_TRIGGER_PATTERN = re.compile(
39
33
  r"(?i)(?<![\w.])(?:Everything[/\\])?es\.exe(?![\w.])",
@@ -22,8 +22,13 @@ pattern this hook must catch); otherwise approves unparseable input.
22
22
  import json
23
23
  import re
24
24
  import sys
25
+ from pathlib import Path
25
26
 
26
- from _gh_body_arg_utils import (
27
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
28
+ if _hooks_dir not in sys.path:
29
+ sys.path.insert(0, _hooks_dir)
30
+
31
+ from blocking._gh_body_arg_utils import ( # noqa: E402
27
32
  _is_bash_continuation,
28
33
  all_body_flags,
29
34
  all_body_flag_prefixes,
@@ -33,11 +33,11 @@ import subprocess
33
33
  import sys
34
34
  from pathlib import Path
35
35
 
36
- _hooks_tree_path = str(Path(__file__).absolute().parent.parent)
37
- if _hooks_tree_path not in sys.path:
38
- sys.path.insert(0, _hooks_tree_path)
36
+ hooks_parent_directory = str(Path(__file__).resolve().parent.parent)
37
+ if hooks_parent_directory not in sys.path:
38
+ sys.path.insert(0, hooks_parent_directory)
39
39
 
40
- from _gh_pr_author_swap_utils import ( # noqa: E402 # sys.path shim above must run first
40
+ from _gh_pr_author_swap_utils import ( # noqa: E402
41
41
  _all_gh_pr_create_segments,
42
42
  _command_invokes_gh_pr_create_in_stripped,
43
43
  _delete_state_file,
@@ -47,7 +47,7 @@ from _gh_pr_author_swap_utils import ( # noqa: E402 # sys.path shim above must
47
47
  _switch_gh_account,
48
48
  _write_line,
49
49
  )
50
- from config.gh_pr_author_swap_constants import ( # noqa: E402 # sys.path shim above must run first
50
+ from hooks_constants.gh_pr_author_swap_constants import (
51
51
  ALL_GH_API_USER_COMMAND,
52
52
  BASH_TOOL_NAME,
53
53
  GH_API_USER_TIMEOUT_SECONDS,
@@ -31,11 +31,11 @@ import json
31
31
  import sys
32
32
  from pathlib import Path
33
33
 
34
- _hooks_tree_path = str(Path(__file__).absolute().parent.parent)
35
- if _hooks_tree_path not in sys.path:
36
- sys.path.insert(0, _hooks_tree_path)
34
+ hooks_parent_directory = str(Path(__file__).resolve().parent.parent)
35
+ if hooks_parent_directory not in sys.path:
36
+ sys.path.insert(0, hooks_parent_directory)
37
37
 
38
- from _gh_pr_author_swap_utils import ( # noqa: E402 # sys.path shim above must run first
38
+ from _gh_pr_author_swap_utils import ( # noqa: E402
39
39
  _command_invokes_gh_pr_create_in_stripped,
40
40
  _delete_state_file,
41
41
  _preprocess_command_for_matching,
@@ -45,7 +45,7 @@ from _gh_pr_author_swap_utils import ( # noqa: E402 # sys.path shim above must
45
45
  _switch_gh_account,
46
46
  _write_line,
47
47
  )
48
- from config.gh_pr_author_swap_constants import BASH_TOOL_NAME # noqa: E402 # sys.path shim above must run first
48
+ from hooks_constants.gh_pr_author_swap_constants import BASH_TOOL_NAME # noqa: E402
49
49
 
50
50
 
51
51
  def main() -> None:
@@ -12,17 +12,11 @@ import re
12
12
  import sys
13
13
  from pathlib import Path
14
14
 
15
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
16
+ if _hooks_dir not in sys.path:
17
+ sys.path.insert(0, _hooks_dir)
15
18
 
16
- def _insert_hooks_tree_for_imports() -> None:
17
- hooks_tree = Path(__file__).resolve().parent.parent
18
- hooks_tree_string = str(hooks_tree)
19
- if hooks_tree_string not in sys.path:
20
- sys.path.insert(0, hooks_tree_string)
21
-
22
-
23
- _insert_hooks_tree_for_imports()
24
-
25
- from config.messages import USER_FACING_NOTICE
19
+ from hooks_constants.messages import USER_FACING_NOTICE # noqa: E402
26
20
 
27
21
  PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28
22