claude-dev-env 1.41.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 (214) hide show
  1. package/CLAUDE.md +8 -0
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +232 -8
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +293 -8
  4. package/_shared/pr-loop/scripts/fix_hookspath.py +96 -5
  5. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +124 -20
  6. package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +90 -0
  8. package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +2 -0
  9. package/_shared/pr-loop/scripts/preflight.py +13 -31
  10. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -16
  11. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +76 -33
  12. package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
  13. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
  14. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
  15. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +37 -2
  16. package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +4 -2
  17. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +4 -2
  18. package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +6 -2
  19. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +2 -2
  20. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1 -2
  21. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +4 -2
  22. package/_shared/pr-loop/scripts/tests/test_preflight.py +17 -52
  23. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +6 -2
  24. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +5 -3
  25. package/agents/pr-description-writer.md +50 -140
  26. package/docs/PR_DESCRIPTION_GUIDE.md +101 -102
  27. package/hooks/_gh_pr_author_swap_utils.py +1 -1
  28. package/hooks/blocking/bot_mention_comment_blocker.py +4 -10
  29. package/hooks/blocking/code_rules_enforcer.py +217 -99
  30. package/hooks/blocking/code_rules_path_utils.py +8 -1
  31. package/hooks/blocking/destructive_command_blocker.py +1 -1
  32. package/hooks/blocking/es_exe_path_rewriter.py +7 -13
  33. package/hooks/blocking/gh_body_arg_blocker.py +6 -1
  34. package/hooks/blocking/gh_pr_author_enforcer.py +5 -5
  35. package/hooks/blocking/gh_pr_author_restore.py +5 -5
  36. package/hooks/blocking/hedging_language_blocker.py +4 -10
  37. package/hooks/blocking/md_path_exemptions.py +205 -0
  38. package/hooks/blocking/md_to_html_blocker.py +48 -20
  39. package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -11
  40. package/hooks/blocking/pr_description_enforcer.py +626 -41
  41. package/hooks/blocking/question_to_user_enforcer.py +4 -10
  42. package/hooks/blocking/state_description_blocker.py +6 -12
  43. package/hooks/blocking/tdd_enforcer.py +1 -1
  44. package/hooks/blocking/test_bot_mention_comment_blocker.py +1 -1
  45. package/hooks/blocking/test_code_rules_enforcer.py +3 -3
  46. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +1 -1
  47. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -2
  48. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +184 -0
  49. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +82 -0
  50. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +29 -29
  51. package/hooks/blocking/test_gh_body_arg_blocker.py +7 -8
  52. package/hooks/blocking/test_gh_pr_author_enforcer.py +1 -1
  53. package/hooks/blocking/test_gh_pr_author_restore.py +1 -1
  54. package/hooks/blocking/test_hedging_language_blocker.py +2 -2
  55. package/hooks/blocking/test_md_to_html_blocker.py +463 -8
  56. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +1 -1
  57. package/hooks/blocking/test_pr_description_enforcer.py +1210 -13
  58. package/hooks/blocking/test_question_to_user_enforcer.py +1 -1
  59. package/hooks/blocking/windows_rmtree_blocker.py +5 -11
  60. package/hooks/diagnostic/hook_log_extractor.py +1 -1
  61. package/hooks/diagnostic/hook_log_init.py +1 -1
  62. package/hooks/diagnostic/hook_log_stop_wrapper.py +1 -1
  63. package/hooks/diagnostic/test_hook_log_extractor.py +1 -1
  64. package/hooks/diagnostic/test_hook_log_init.py +2 -2
  65. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +1 -1
  66. package/hooks/git-hooks/gate_utils.py +1 -1
  67. package/hooks/git-hooks/pre_commit.py +1 -1
  68. package/hooks/git-hooks/pre_push.py +1 -1
  69. package/hooks/git-hooks/test_config.py +5 -5
  70. package/hooks/git-hooks/test_pre_push.py +6 -6
  71. package/hooks/{config → hooks_constants}/code_rules_enforcer_constants.py +37 -0
  72. package/hooks/hooks_constants/code_rules_path_utils_constants.py +28 -0
  73. package/hooks/hooks_constants/md_to_html_blocker_constants.py +82 -0
  74. package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_state.py +1 -1
  75. package/hooks/hooks_constants/pr_description_enforcer_constants.py +154 -0
  76. package/hooks/{config → hooks_constants}/pre_tool_use_stdin.py +1 -1
  77. package/hooks/{config → hooks_constants}/project_paths_reader.py +2 -2
  78. package/hooks/{config → hooks_constants}/test_banned_identifiers_constants.py +1 -1
  79. package/hooks/{config → hooks_constants}/test_dynamic_stderr_handler.py +1 -1
  80. package/hooks/{config → hooks_constants}/test_hardcoded_user_path_constants.py +1 -1
  81. package/hooks/{config → hooks_constants}/test_hook_log_extractor_constants.py +2 -2
  82. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +110 -0
  83. package/hooks/{config → hooks_constants}/test_messages.py +2 -6
  84. package/hooks/{config → hooks_constants}/test_path_rewriter_constants.py +1 -1
  85. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +292 -0
  86. package/hooks/{config → hooks_constants}/test_pre_tool_use_stdin.py +2 -2
  87. package/hooks/{config → hooks_constants}/test_project_paths_reader.py +3 -3
  88. package/hooks/{config → hooks_constants}/test_session_env_cleanup_constants.py +1 -1
  89. package/hooks/{config → hooks_constants}/test_setup_project_paths_constants.py +2 -2
  90. package/hooks/{config → hooks_constants}/test_unused_module_import_constants.py +1 -1
  91. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +5 -11
  92. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +1 -1
  93. package/hooks/session/gh_pr_author_session_cleanup.py +5 -6
  94. package/hooks/session/session_env_cleanup.py +4 -10
  95. package/hooks/session/test_gh_pr_author_session_cleanup.py +1 -1
  96. package/hooks/session/test_untracked_repo_detector.py +2 -2
  97. package/hooks/session/untracked_repo_detector.py +6 -12
  98. package/hooks/test__gh_pr_author_swap_utils.py +1 -1
  99. package/hooks/validators/run_all_validators.py +16 -5
  100. package/hooks/validators/test_output_formatter.py +46 -0
  101. package/hooks/workflow/doc_gist_auto_publish.py +1 -1
  102. package/hooks/workflow/md_to_html_companion.py +8 -15
  103. package/hooks/workflow/test_md_to_html_companion.py +184 -23
  104. package/package.json +1 -1
  105. package/rules/ask-user-question-required.md +1 -1
  106. package/rules/vault-context.md +1 -1
  107. package/scripts/{config → dev_env_scripts_constants}/timing.py +1 -1
  108. package/scripts/setup_project_paths.py +49 -11
  109. package/scripts/sweep_empty_dirs.py +10 -1
  110. package/scripts/test_setup_project_paths.py +2 -2
  111. package/scripts/test_sweep_empty_dirs.py +2 -6
  112. package/skills/_shared/pr-loop/scripts/_path_resolver.py +1 -1
  113. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  114. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  115. package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -1
  116. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -1
  117. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  118. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  119. package/skills/bugteam/PROMPTS.md +1 -1
  120. package/skills/bugteam/SKILL.md +1 -1
  121. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  122. package/skills/bugteam/scripts/{_claude_permissions_common.py → _bugteam_permissions_common.py} +110 -13
  123. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +1 -13
  124. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +1 -16
  125. package/skills/bugteam/scripts/bugteam_preflight.py +1 -13
  126. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +69 -0
  127. package/skills/bugteam/scripts/grant_project_claude_permissions.py +117 -12
  128. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +1 -1
  129. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  130. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +71 -25
  131. package/skills/bugteam/scripts/{test__claude_permissions_common.py → test__bugteam_permissions_common.py} +4 -4
  132. package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
  133. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -26
  134. package/skills/bugteam/scripts/{test_claude_permissions_common.py → test_bugteam_permissions_common.py} +3 -66
  135. package/skills/bugteam/scripts/test_bugteam_preflight.py +2 -27
  136. package/skills/bugteam/scripts/windows_safe_rmtree.py +1 -1
  137. package/skills/doc-gist/SKILL.md +1 -1
  138. package/skills/doc-gist/scripts/gist_upload.py +1 -1
  139. package/skills/implement/SKILL.md +66 -0
  140. package/skills/implement/scripts/append_note.py +133 -0
  141. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  142. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +12 -0
  143. package/skills/implement/scripts/test_append_note.py +191 -0
  144. package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
  145. package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +6 -1
  146. package/skills/pr-converge/scripts/check_bugbot_ci.py +2 -2
  147. package/skills/pr-converge/scripts/check_convergence.py +175 -29
  148. package/skills/pr-converge/scripts/check_pending_reviews.py +2 -2
  149. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +2 -2
  150. package/skills/pr-converge/scripts/post_fix_reply.py +2 -2
  151. package/skills/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
  152. package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py +1 -1
  153. package/skills/pr-converge/scripts/reflow_skill_md.py +90 -16
  154. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
  155. package/skills/pr-converge/scripts/test_check_convergence.py +324 -0
  156. package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
  157. package/skills/refine/SKILL.md +257 -0
  158. package/skills/refine/templates/implementation-notes-template.html +56 -0
  159. package/skills/refine/templates/plan-template.md +60 -0
  160. package/skills/session-log/SKILL.md +98 -233
  161. package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +0 -36
  162. package/hooks/config/pr_description_enforcer_constants.py +0 -19
  163. package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
  164. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +0 -20
  165. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
  166. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
  167. package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
  168. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
  169. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
  170. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
  171. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
  172. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
  173. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
  174. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
  175. /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
  176. /package/hooks/{config → hooks_constants}/__init__.py +0 -0
  177. /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
  178. /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
  179. /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
  180. /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
  181. /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
  182. /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
  183. /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
  184. /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
  185. /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
  186. /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
  187. /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
  188. /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
  189. /package/hooks/{config → hooks_constants}/messages.py +0 -0
  190. /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
  191. /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
  192. /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
  193. /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
  194. /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
  195. /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
  196. /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
  197. /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
  198. /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
  199. /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
  200. /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
  201. /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
  202. /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
  203. /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
  204. /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
  205. /package/skills/{pr-converge/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
  206. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
  207. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
  208. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
  209. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
  210. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
  211. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
  212. /package/skills/{pr-converge/scripts/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
  213. /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
  214. /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
@@ -9,43 +9,37 @@ the changes applied. No-op when the entries already exist.
9
9
  import sys
10
10
  from pathlib import Path
11
11
 
12
- parent_directory = str(Path(__file__).absolute().parent)
13
- try:
14
- sys.path.remove(parent_directory)
15
- except ValueError:
16
- pass
12
+ parent_directory = str(Path(__file__).resolve().parent)
17
13
  if parent_directory not in sys.path:
18
14
  sys.path.insert(0, parent_directory)
19
15
 
20
- for each_cached_module_name in [
21
- each_module_key
22
- for each_module_key in list(sys.modules)
23
- if each_module_key == "config"
24
- or each_module_key.startswith("config.")
25
- or each_module_key == "_claude_permissions_common"
26
- ]:
27
- sys.modules.pop(each_cached_module_name, None)
28
-
29
16
  from _claude_permissions_common import ( # noqa: E402
30
17
  append_if_missing,
18
+ build_agent_config_deny_rules,
31
19
  build_permission_rules,
32
20
  ensure_dict_section,
33
21
  ensure_list_entry,
34
22
  exit_with_error,
35
23
  get_current_project_path,
24
+ is_trust_entry_for_project,
36
25
  is_valid_project_root,
37
26
  load_settings,
27
+ remove_matching_entries_from_list,
38
28
  save_settings,
39
29
  )
40
- from config.claude_permissions_constants import ( # noqa: E402
30
+ from pr_loop_shared_constants.claude_permissions_constants import (
31
+ ALL_AGENT_CONFIG_DENY_TOOLS,
32
+ ALL_AGENT_CONFIG_PATH_PATTERNS,
41
33
  ALL_PERMISSION_ALLOW_TOOLS,
34
+ AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
42
35
  AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE,
43
36
  get_claude_user_settings_path,
44
37
  )
45
- from config.claude_settings_keys_constants import ( # noqa: E402
38
+ from pr_loop_shared_constants.claude_settings_keys_constants import (
46
39
  CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY,
47
40
  CLAUDE_SETTINGS_ALLOW_KEY,
48
41
  CLAUDE_SETTINGS_AUTO_MODE_KEY,
42
+ CLAUDE_SETTINGS_DENY_KEY,
49
43
  CLAUDE_SETTINGS_ENVIRONMENT_KEY,
50
44
  CLAUDE_SETTINGS_PERMISSIONS_KEY,
51
45
  )
@@ -76,6 +70,35 @@ def add_rules_to_allow_list(
76
70
  )
77
71
 
78
72
 
73
+ def add_rules_to_deny_list(
74
+ all_settings: dict[str, object], all_rules_to_add: list[str]
75
+ ) -> int:
76
+ """Add permission rules to the settings deny list.
77
+
78
+ Deny rules take precedence over allow rules in Claude Code's permission
79
+ matching, so writing agent-config paths into the deny list forces a
80
+ per-edit user approval even when a broader allow rule would cover them.
81
+
82
+ Args:
83
+ all_settings: The parsed settings dictionary.
84
+ all_rules_to_add: Permission rule strings to append.
85
+
86
+ Returns:
87
+ Number of rules actually added (new entries).
88
+ """
89
+ permissions_section = ensure_dict_section(
90
+ all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
91
+ )
92
+ existing_deny_list = ensure_list_entry(
93
+ permissions_section, CLAUDE_SETTINGS_DENY_KEY
94
+ )
95
+ return sum(
96
+ 1
97
+ for each_rule in all_rules_to_add
98
+ if append_if_missing(existing_deny_list, each_rule)
99
+ )
100
+
101
+
79
102
  def add_directory_to_additional_directories(
80
103
  all_settings: dict[str, object], directory_path: str
81
104
  ) -> int:
@@ -122,11 +145,63 @@ def add_auto_mode_environment_entry(
122
145
  return 0
123
146
 
124
147
 
148
+ def purge_stale_trust_entries(
149
+ all_settings: dict[str, object],
150
+ project_path: str,
151
+ prefix: str,
152
+ protected_entry: str | None = None,
153
+ ) -> int:
154
+ """Remove every prior trust entry for the project from autoMode.environment.
155
+
156
+ A trust entry is any string in autoMode.environment whose prefix matches
157
+ the trust-entry marker and that contains the project's .claude/** path.
158
+ Purging stale entries before adding the current template prevents
159
+ accumulation across template revisions. The optional protected_entry
160
+ survives the purge so an entry byte-identical to the one about to be
161
+ re-added is not removed and re-added on every invocation, preserving the
162
+ idempotency contract documented on grant_permissions_for_current_directory.
163
+
164
+ Args:
165
+ all_settings: The parsed settings dictionary.
166
+ project_path: The POSIX-style project root path.
167
+ prefix: The literal prefix that marks a trust entry.
168
+ protected_entry: Optional entry text that, when byte-equal to a
169
+ candidate, prevents removal. Pass the freshly-formatted current
170
+ template entry from grant to preserve idempotency. Revoke passes
171
+ None so every matching entry is removed.
172
+
173
+ Returns:
174
+ Number of stale entries removed.
175
+ """
176
+ auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
177
+ if not isinstance(auto_mode_section, dict):
178
+ return 0
179
+ existing_environment = auto_mode_section.get(CLAUDE_SETTINGS_ENVIRONMENT_KEY)
180
+ if not isinstance(existing_environment, list):
181
+ return 0
182
+
183
+ def _should_purge_candidate(candidate_entry: object) -> bool:
184
+ if not is_trust_entry_for_project(candidate_entry, project_path, prefix):
185
+ return False
186
+ if protected_entry is not None and candidate_entry == protected_entry:
187
+ return False
188
+ return True
189
+
190
+ return remove_matching_entries_from_list(
191
+ existing_environment,
192
+ _should_purge_candidate,
193
+ )
194
+
195
+
125
196
  def grant_permissions_for_current_directory() -> None:
126
197
  """Grant Edit/Write/Read permissions for the current project directory.
127
198
 
128
199
  Reads the current project path, constructs permission rules from config
129
- constants, and writes them to ~/.claude/settings.json atomically.
200
+ constants, and writes them to ~/.claude/settings.json atomically. Adds
201
+ deny rules for agent-config paths so edits to settings, hooks, commands,
202
+ agents, skills, mcp.json, and CLAUDE.md still require per-edit user
203
+ approval. Purges any prior trust entries for this project before writing
204
+ the current template to prevent accumulation across template revisions.
130
205
 
131
206
  Raises:
132
207
  SystemExit: When the current directory is not a valid project root.
@@ -146,19 +221,37 @@ def grant_permissions_for_current_directory() -> None:
146
221
  all_permission_rules = build_permission_rules(
147
222
  project_path, ALL_PERMISSION_ALLOW_TOOLS
148
223
  )
224
+ all_agent_config_deny_rules = build_agent_config_deny_rules(
225
+ project_path,
226
+ ALL_AGENT_CONFIG_DENY_TOOLS,
227
+ ALL_AGENT_CONFIG_PATH_PATTERNS,
228
+ )
149
229
  environment_entry = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
150
230
  project_path=project_path
151
231
  )
152
232
  settings = load_settings(claude_user_settings_path)
153
- rules_added_count = add_rules_to_allow_list(settings, all_permission_rules)
233
+ allow_rules_added_count = add_rules_to_allow_list(settings, all_permission_rules)
234
+ deny_rules_added_count = add_rules_to_deny_list(
235
+ settings, all_agent_config_deny_rules
236
+ )
154
237
  directories_added_count = add_directory_to_additional_directories(
155
238
  settings, project_path
156
239
  )
240
+ stale_trust_entries_purged_count = purge_stale_trust_entries(
241
+ settings,
242
+ project_path,
243
+ AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
244
+ protected_entry=environment_entry,
245
+ )
157
246
  environment_entries_added_count = add_auto_mode_environment_entry(
158
247
  settings, environment_entry
159
248
  )
160
249
  total_changes_count = (
161
- rules_added_count + directories_added_count + environment_entries_added_count
250
+ allow_rules_added_count
251
+ + deny_rules_added_count
252
+ + directories_added_count
253
+ + stale_trust_entries_purged_count
254
+ + environment_entries_added_count
162
255
  )
163
256
  if total_changes_count == 0:
164
257
  print(f"Project path: {project_path}")
@@ -168,8 +261,19 @@ def grant_permissions_for_current_directory() -> None:
168
261
  save_settings(claude_user_settings_path, settings)
169
262
  print(f"Project path: {project_path}")
170
263
  print(f"Settings file: {claude_user_settings_path}")
171
- print(f"Allow rules added: {rules_added_count} of {len(all_permission_rules)}")
264
+ print(
265
+ f"Allow rules added: {allow_rules_added_count} of {len(all_permission_rules)}"
266
+ )
267
+ print(
268
+ f"Deny rules added: {deny_rules_added_count} of "
269
+ f"{len(all_agent_config_deny_rules)}"
270
+ )
172
271
  print(f"Additional directories added: {directories_added_count}")
272
+ if stale_trust_entries_purged_count > 0:
273
+ print(
274
+ f"Stale auto-mode environment entries purged: "
275
+ f"{stale_trust_entries_purged_count}"
276
+ )
173
277
  print(f"Auto-mode environment entries added: {environment_entries_added_count}")
174
278
 
175
279
 
@@ -32,11 +32,11 @@ import urllib.request
32
32
  from pathlib import Path
33
33
  from typing import NoReturn
34
34
 
35
- sys.modules.pop("config", None)
36
- if str(Path(__file__).absolute().parent) not in sys.path:
37
- sys.path.insert(0, str(Path(__file__).absolute().parent))
35
+ parent_directory = str(Path(__file__).resolve().parent)
36
+ if parent_directory not in sys.path:
37
+ sys.path.insert(0, parent_directory)
38
38
 
39
- from config.post_audit_thread_constants import (
39
+ from pr_loop_shared_constants.post_audit_thread_constants import ( # noqa: E402
40
40
  ALL_GH_API_COMMAND_PARTS,
41
41
  ALL_GH_API_USER_COMMAND_PARTS,
42
42
  ALL_GH_AUTH_STATUS_COMMAND_PARTS,
@@ -0,0 +1,90 @@
1
+ """Constants shared by grant_project_claude_permissions and revoke_project_claude_permissions."""
2
+
3
+ from pathlib import Path
4
+
5
+ from pr_loop_shared_constants.preflight_constants import GIT_DIRECTORY_NAME
6
+
7
+ __all__ = (
8
+ "ALL_AGENT_CONFIG_DENY_TOOLS",
9
+ "ALL_AGENT_CONFIG_PATH_PATTERNS",
10
+ "ALL_PERMISSION_ALLOW_TOOLS",
11
+ "ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS",
12
+ "AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX",
13
+ "AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE",
14
+ "CLAUDE_SETTINGS_DIRECTORY_NAME",
15
+ "CLAUDE_SETTINGS_FILENAME",
16
+ "GIT_DIRECTORY_NAME",
17
+ "TEXT_FILE_ENCODING",
18
+ "UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH",
19
+ "get_claude_user_settings_path",
20
+ )
21
+
22
+
23
+ ALL_PERMISSION_ALLOW_TOOLS: tuple[str, ...] = ("Edit", "Write", "Read")
24
+
25
+ ALL_AGENT_CONFIG_DENY_TOOLS: tuple[str, ...] = ("Edit", "Write", "Read", "Glob")
26
+
27
+ ALL_AGENT_CONFIG_PATH_PATTERNS: tuple[str, ...] = (
28
+ "settings*.json",
29
+ "hooks/**",
30
+ "commands/**",
31
+ "agents/**",
32
+ "skills/**",
33
+ "mcp.json",
34
+ "CLAUDE.md",
35
+ )
36
+
37
+ AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX: str = "Trusted local workspace:"
38
+
39
+
40
+ def _describe_agent_config_pattern_for_humans(agent_config_path_pattern: str) -> str:
41
+ glob_suffix_under_directory = "/**"
42
+ file_name_for_special_phrasing = "mcp.json"
43
+ if agent_config_path_pattern.endswith(glob_suffix_under_directory):
44
+ directory_name = agent_config_path_pattern[
45
+ : -len(glob_suffix_under_directory)
46
+ ]
47
+ return f"anything under {directory_name}/"
48
+ if agent_config_path_pattern == file_name_for_special_phrasing:
49
+ return f"the {file_name_for_special_phrasing} file"
50
+ return agent_config_path_pattern
51
+
52
+
53
+ def _build_agent_config_pattern_phrase(
54
+ all_agent_config_path_patterns: tuple[str, ...],
55
+ ) -> str:
56
+ all_described_patterns: list[str] = [
57
+ _describe_agent_config_pattern_for_humans(each_pattern)
58
+ for each_pattern in all_agent_config_path_patterns
59
+ ]
60
+ if len(all_described_patterns) <= 1:
61
+ return ", ".join(all_described_patterns)
62
+ leading_phrase_parts = ", ".join(all_described_patterns[:-1])
63
+ final_phrase_part = all_described_patterns[-1]
64
+ return f"{leading_phrase_parts}, and {final_phrase_part}"
65
+
66
+
67
+ _AGENT_CONFIG_PATTERN_PHRASE: str = _build_agent_config_pattern_phrase(
68
+ ALL_AGENT_CONFIG_PATH_PATTERNS
69
+ )
70
+
71
+ AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE: str = (
72
+ f"Trusted local workspace: Files under {{project_path}}/.claude/** inherit "
73
+ f"the workspace's trust for Edit, Write, Read, and Glob operations EXCEPT "
74
+ f"for agent-config files: {_AGENT_CONFIG_PATTERN_PHRASE}. Edits to those "
75
+ f"agent-config files always require explicit per-edit user approval."
76
+ )
77
+
78
+ ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS: tuple[str, ...] = ('"', "'")
79
+
80
+ CLAUDE_SETTINGS_DIRECTORY_NAME: str = ".claude"
81
+
82
+ CLAUDE_SETTINGS_FILENAME: str = "settings.json"
83
+
84
+ TEXT_FILE_ENCODING: str = "utf-8"
85
+
86
+ UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH: int = 8
87
+
88
+
89
+ def get_claude_user_settings_path() -> Path:
90
+ return Path.home() / CLAUDE_SETTINGS_DIRECTORY_NAME / CLAUDE_SETTINGS_FILENAME
@@ -4,6 +4,8 @@ CLAUDE_SETTINGS_PERMISSIONS_KEY: str = "permissions"
4
4
 
5
5
  CLAUDE_SETTINGS_ALLOW_KEY: str = "allow"
6
6
 
7
+ CLAUDE_SETTINGS_DENY_KEY: str = "deny"
8
+
7
9
  CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY: str = "additionalDirectories"
8
10
 
9
11
  CLAUDE_SETTINGS_AUTO_MODE_KEY: str = "autoMode"
@@ -4,38 +4,20 @@ import subprocess
4
4
  import sys
5
5
  from pathlib import Path
6
6
 
7
- sys.modules.pop("config", None)
8
- _script_directory_resolved = Path(__file__).resolve().parent
9
- _script_directory_absolute = Path(__file__).absolute().parent
10
-
7
+ parent_directory = str(Path(__file__).resolve().parent)
8
+ sys.path[:] = [
9
+ each_existing_entry
10
+ for each_existing_entry in sys.path
11
+ if not (
12
+ os.path.exists(each_existing_entry)
13
+ and os.path.samefile(each_existing_entry, parent_directory)
14
+ )
15
+ ]
16
+ if parent_directory not in sys.path:
17
+ sys.path.insert(0, parent_directory)
11
18
 
12
- def _entry_points_at_preflight_script_directory(each_path_entry: str) -> bool:
13
- if each_path_entry in (
14
- str(_script_directory_resolved),
15
- str(_script_directory_absolute),
16
- ):
17
- return True
18
- try:
19
- candidate_path = Path(each_path_entry)
20
- except (OSError, ValueError):
21
- return False
22
- if candidate_path.exists():
23
- try:
24
- return os.path.samefile(candidate_path, _script_directory_resolved)
25
- except OSError:
26
- return False
27
- return False
28
-
29
-
30
- for each_index in range(len(sys.path) - 1, -1, -1):
31
- if _entry_points_at_preflight_script_directory(sys.path[each_index]):
32
- sys.path.pop(each_index)
33
- _preflight_scripts_path_entry = str(_script_directory_absolute)
34
- if _preflight_scripts_path_entry not in sys.path:
35
- sys.path.insert(0, _preflight_scripts_path_entry)
36
-
37
- from config.fix_hookspath_constants import HOOKS_PATH_VERIFICATION_SUFFIX
38
- from config.preflight_constants import (
19
+ from pr_loop_shared_constants.fix_hookspath_constants import HOOKS_PATH_VERIFICATION_SUFFIX # noqa: E402
20
+ from pr_loop_shared_constants.preflight_constants import (
39
21
  ALL_GIT_CONFIG_GET_CORE_HOOKS_PATH_SUBCOMMAND,
40
22
  ALL_GIT_DIFF_NAME_ONLY_SUBCOMMAND,
41
23
  ALL_GIT_LS_FILES_TEST_DISCOVERY_SUBCOMMAND,
@@ -8,22 +8,8 @@ rules and disabled-token taxonomy live in exactly one place.
8
8
  from __future__ import annotations
9
9
 
10
10
  import os
11
- import sys
12
- from pathlib import Path
13
-
14
- for each_cached_module_name in [
15
- each_module_key
16
- for each_module_key in list(sys.modules)
17
- if each_module_key == "config" or each_module_key.startswith("config.")
18
- ]:
19
- sys.modules.pop(each_cached_module_name, None)
20
- _shared_pr_loop_scripts_directory = str(Path(__file__).absolute().parent)
21
- while _shared_pr_loop_scripts_directory in sys.path:
22
- sys.path.remove(_shared_pr_loop_scripts_directory)
23
- if _shared_pr_loop_scripts_directory not in sys.path:
24
- sys.path.insert(0, _shared_pr_loop_scripts_directory)
25
-
26
- from config.reviews_disabled_constants import (
11
+
12
+ from pr_loop_shared_constants.reviews_disabled_constants import (
27
13
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
28
14
  CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
29
15
  CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR,
@@ -10,41 +10,34 @@ autoMode sections so repeated grant/revoke cycles leave no dead structure.
10
10
  import sys
11
11
  from pathlib import Path
12
12
 
13
- parent_directory = str(Path(__file__).absolute().parent)
14
- try:
15
- sys.path.remove(parent_directory)
16
- except ValueError:
17
- pass
13
+ parent_directory = str(Path(__file__).resolve().parent)
18
14
  if parent_directory not in sys.path:
19
15
  sys.path.insert(0, parent_directory)
20
16
 
21
- for each_cached_module_name in [
22
- each_module_key
23
- for each_module_key in list(sys.modules)
24
- if each_module_key == "config"
25
- or each_module_key.startswith("config.")
26
- or each_module_key == "_claude_permissions_common"
27
- ]:
28
- sys.modules.pop(each_cached_module_name, None)
29
-
30
17
  from _claude_permissions_common import ( # noqa: E402
18
+ build_agent_config_deny_rules,
31
19
  build_permission_rules,
32
20
  exit_with_error,
33
21
  get_current_project_path,
22
+ is_trust_entry_for_project,
34
23
  is_valid_project_root,
35
24
  load_settings,
36
25
  prune_empty_list_then_empty_section,
26
+ remove_matching_entries_from_list,
37
27
  save_settings,
38
28
  )
39
- from config.claude_permissions_constants import ( # noqa: E402
29
+ from pr_loop_shared_constants.claude_permissions_constants import (
30
+ ALL_AGENT_CONFIG_DENY_TOOLS,
31
+ ALL_AGENT_CONFIG_PATH_PATTERNS,
40
32
  ALL_PERMISSION_ALLOW_TOOLS,
41
- AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE,
33
+ AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
42
34
  get_claude_user_settings_path,
43
35
  )
44
- from config.claude_settings_keys_constants import ( # noqa: E402
36
+ from pr_loop_shared_constants.claude_settings_keys_constants import (
45
37
  CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY,
46
38
  CLAUDE_SETTINGS_ALLOW_KEY,
47
39
  CLAUDE_SETTINGS_AUTO_MODE_KEY,
40
+ CLAUDE_SETTINGS_DENY_KEY,
48
41
  CLAUDE_SETTINGS_ENVIRONMENT_KEY,
49
42
  CLAUDE_SETTINGS_PERMISSIONS_KEY,
50
43
  )
@@ -92,6 +85,27 @@ def remove_rules_from_allow_list(
92
85
  return remove_values_from_list(existing_allow_list, set(all_rules_to_remove))
93
86
 
94
87
 
88
+ def remove_rules_from_deny_list(
89
+ all_settings: dict[str, object], all_rules_to_remove: list[str]
90
+ ) -> int:
91
+ """Remove matching permission rules from the settings deny list.
92
+
93
+ Args:
94
+ all_settings: The parsed settings dictionary.
95
+ all_rules_to_remove: Permission rule strings to remove.
96
+
97
+ Returns:
98
+ Number of rules removed.
99
+ """
100
+ permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
101
+ if not isinstance(permissions_section, dict):
102
+ return 0
103
+ existing_deny_list = permissions_section.get(CLAUDE_SETTINGS_DENY_KEY)
104
+ if not isinstance(existing_deny_list, list):
105
+ return 0
106
+ return remove_values_from_list(existing_deny_list, set(all_rules_to_remove))
107
+
108
+
95
109
  def remove_directory_from_additional_directories(
96
110
  all_settings: dict[str, object], directory_path: str
97
111
  ) -> int:
@@ -115,17 +129,23 @@ def remove_directory_from_additional_directories(
115
129
  return remove_values_from_list(existing_directories, {directory_path})
116
130
 
117
131
 
118
- def remove_auto_mode_environment_entry(
119
- all_settings: dict[str, object], entry_text: str
132
+ def remove_trust_entries_for_project(
133
+ all_settings: dict[str, object], project_path: str, prefix: str
120
134
  ) -> int:
121
- """Remove an auto-mode environment entry for the project.
135
+ """Remove every trust entry for the project from autoMode.environment.
136
+
137
+ Matches any string in autoMode.environment whose prefix matches the
138
+ trust-entry marker and that contains the project's .claude/** path.
139
+ The match is wording-agnostic so prior template revisions are removed
140
+ cleanly even when the current template differs.
122
141
 
123
142
  Args:
124
143
  all_settings: The parsed settings dictionary.
125
- entry_text: The environment entry text to remove.
144
+ project_path: The POSIX-style project root path.
145
+ prefix: The literal prefix that marks a trust entry.
126
146
 
127
147
  Returns:
128
- 1 when the entry was removed, 0 when not found.
148
+ Number of entries removed.
129
149
  """
130
150
  auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
131
151
  if not isinstance(auto_mode_section, dict):
@@ -133,7 +153,12 @@ def remove_auto_mode_environment_entry(
133
153
  existing_environment = auto_mode_section.get(CLAUDE_SETTINGS_ENVIRONMENT_KEY)
134
154
  if not isinstance(existing_environment, list):
135
155
  return 0
136
- return remove_values_from_list(existing_environment, {entry_text})
156
+ return remove_matching_entries_from_list(
157
+ existing_environment,
158
+ lambda candidate_entry: is_trust_entry_for_project(
159
+ candidate_entry, project_path, prefix
160
+ ),
161
+ )
137
162
 
138
163
 
139
164
  def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
@@ -147,6 +172,11 @@ def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
147
172
  CLAUDE_SETTINGS_PERMISSIONS_KEY,
148
173
  CLAUDE_SETTINGS_ALLOW_KEY,
149
174
  )
175
+ prune_empty_list_then_empty_section(
176
+ all_settings,
177
+ CLAUDE_SETTINGS_PERMISSIONS_KEY,
178
+ CLAUDE_SETTINGS_DENY_KEY,
179
+ )
150
180
  prune_empty_list_then_empty_section(
151
181
  all_settings,
152
182
  CLAUDE_SETTINGS_PERMISSIONS_KEY,
@@ -162,9 +192,10 @@ def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
162
192
  def revoke_permissions_for_current_directory() -> None:
163
193
  """Revoke permissions previously granted for the current project directory.
164
194
 
165
- Reads the current project path, constructs the matching permission rules,
166
- removes them from ~/.claude/settings.json, and prunes any newly empty
167
- sections.
195
+ Reads the current project path, constructs the matching allow and deny
196
+ permission rules, removes them from ~/.claude/settings.json, removes
197
+ every trust entry for the project from autoMode.environment, and prunes
198
+ any newly empty sections.
168
199
 
169
200
  Raises:
170
201
  SystemExit: When the current directory is not a valid project root.
@@ -182,19 +213,25 @@ def revoke_permissions_for_current_directory() -> None:
182
213
  raise SystemExit(1)
183
214
  project_path = get_current_project_path()
184
215
  permission_rules = build_permission_rules(project_path, ALL_PERMISSION_ALLOW_TOOLS)
185
- environment_entry = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
186
- project_path=project_path
216
+ all_agent_config_deny_rules = build_agent_config_deny_rules(
217
+ project_path,
218
+ ALL_AGENT_CONFIG_DENY_TOOLS,
219
+ ALL_AGENT_CONFIG_PATH_PATTERNS,
187
220
  )
188
221
  settings = load_settings(claude_user_settings_path)
189
- rules_removed_count = remove_rules_from_allow_list(settings, permission_rules)
222
+ allow_rules_removed_count = remove_rules_from_allow_list(settings, permission_rules)
223
+ deny_rules_removed_count = remove_rules_from_deny_list(
224
+ settings, all_agent_config_deny_rules
225
+ )
190
226
  directories_removed_count = remove_directory_from_additional_directories(
191
227
  settings, project_path
192
228
  )
193
- environment_entries_removed_count = remove_auto_mode_environment_entry(
194
- settings, environment_entry
229
+ environment_entries_removed_count = remove_trust_entries_for_project(
230
+ settings, project_path, AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX
195
231
  )
196
232
  total_changes_count = (
197
- rules_removed_count
233
+ allow_rules_removed_count
234
+ + deny_rules_removed_count
198
235
  + directories_removed_count
199
236
  + environment_entries_removed_count
200
237
  )
@@ -207,7 +244,13 @@ def revoke_permissions_for_current_directory() -> None:
207
244
  save_settings(claude_user_settings_path, settings)
208
245
  print(f"Project path: {project_path}")
209
246
  print(f"Settings file: {claude_user_settings_path}")
210
- print(f"Allow rules removed: {rules_removed_count} of {len(permission_rules)}")
247
+ print(
248
+ f"Allow rules removed: {allow_rules_removed_count} of {len(permission_rules)}"
249
+ )
250
+ print(
251
+ f"Deny rules removed: {deny_rules_removed_count} of "
252
+ f"{len(all_agent_config_deny_rules)}"
253
+ )
211
254
  print(f"Additional directories removed: {directories_removed_count}")
212
255
  print(
213
256
  f"Auto-mode environment entries removed: {environment_entries_removed_count}"
@@ -1,51 +1 @@
1
- """Test fixtures for _shared/pr-loop/scripts/.
2
-
3
- Two unrelated Python packages live under the name ``config`` in this repo:
4
- - ``_shared/pr-loop/scripts/config/`` (constants for grant/revoke/gate/preflight scripts)
5
- - ``hooks/config/`` (constants for the code-rules enforcer and other hooks)
6
-
7
- When tests under this directory exercise the gate (which loads
8
- ``hooks/blocking/code_rules_enforcer.py``) and also load the grant/revoke
9
- scripts in the same pytest process, ``sys.modules['config']`` and
10
- ``sys.modules['config.<submodule>']`` cache entries from one package leak
11
- into the other. The next ``from config.<submodule> import ...`` then fails
12
- with ``ModuleNotFoundError`` because the cached parent package does not
13
- expose that submodule.
14
-
15
- Independently, several scripts in this folder do
16
- ``Path(__file__).resolve()`` then prepend the resulting directory to
17
- ``sys.path``. On Windows when the working tree lives under a mapped drive
18
- backed by a UNC share (``Y:`` -> ``\\\\server\\share\\...``), ``.resolve()``
19
- returns the UNC form, and Python's import machinery on this host cannot
20
- locate ``config`` packages from a UNC ``sys.path`` entry. The Y:-form entry
21
- gets pushed to a later index by subsequent inserts, making ``from
22
- config.<submodule> import ...`` fail.
23
-
24
- This autouse fixture restores both invariants before each test:
25
- 1. evict every ``config`` and ``config.*`` entry from ``sys.modules``
26
- 2. prepend the drive-letter (``.absolute()``) form of the scripts
27
- directory to ``sys.path`` so package resolution always has a
28
- non-UNC path to search first
29
- """
30
-
31
- from __future__ import annotations
32
-
33
- import sys
34
- from pathlib import Path
35
-
36
- import pytest
37
-
38
- SCRIPTS_DIRECTORY_DRIVE_LETTER_FORM = str(Path(__file__).absolute().parent.parent)
39
-
40
-
41
- @pytest.fixture(autouse=True)
42
- def _evict_config_namespace_between_tests() -> None:
43
- for each_module_name in [
44
- each_key
45
- for each_key in list(sys.modules)
46
- if each_key == "config" or each_key.startswith("config.")
47
- ]:
48
- sys.modules.pop(each_module_name, None)
49
- if SCRIPTS_DIRECTORY_DRIVE_LETTER_FORM in sys.path:
50
- sys.path.remove(SCRIPTS_DIRECTORY_DRIVE_LETTER_FORM)
51
- sys.path.insert(0, SCRIPTS_DIRECTORY_DRIVE_LETTER_FORM)
1
+ """Test fixtures for _shared/pr-loop/scripts/tests/."""