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
@@ -0,0 +1,385 @@
1
+ """Behavior tests for the agent-config carve-out and stale-trust-entry purge.
2
+
3
+ Covers two Bugbot findings on PR #467:
4
+ - Deny rules must be written to permissions.deny so agent-config edits
5
+ require explicit per-edit user approval.
6
+ - Trust entries in autoMode.environment must be purged on grant
7
+ (preventing accumulation across template revisions) and removed on
8
+ revoke regardless of the exact template wording.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+ from types import ModuleType
18
+ from typing import Any
19
+
20
+ import pytest
21
+
22
+
23
+ def _load_module_from_path(module_name: str, module_path: Path) -> ModuleType:
24
+ specification = importlib.util.spec_from_file_location(module_name, module_path)
25
+ assert specification is not None
26
+ assert specification.loader is not None
27
+ module = importlib.util.module_from_spec(specification)
28
+ specification.loader.exec_module(module)
29
+ return module
30
+
31
+
32
+ def _scripts_directory() -> Path:
33
+ return Path(__file__).parent.parent
34
+
35
+
36
+ def _load_common_module() -> ModuleType:
37
+ scripts_directory = _scripts_directory()
38
+ scripts_directory_str = str(scripts_directory.resolve())
39
+ if scripts_directory_str not in sys.path:
40
+ sys.path.insert(0, scripts_directory_str)
41
+ return _load_module_from_path(
42
+ "_claude_permissions_common",
43
+ scripts_directory / "_claude_permissions_common.py",
44
+ )
45
+
46
+
47
+ def _load_grant_module() -> ModuleType:
48
+ scripts_directory = _scripts_directory()
49
+ scripts_directory_str = str(scripts_directory.resolve())
50
+ if scripts_directory_str not in sys.path:
51
+ sys.path.insert(0, scripts_directory_str)
52
+ return _load_module_from_path(
53
+ "grant_project_claude_permissions",
54
+ scripts_directory / "grant_project_claude_permissions.py",
55
+ )
56
+
57
+
58
+ def _load_revoke_module() -> ModuleType:
59
+ scripts_directory = _scripts_directory()
60
+ scripts_directory_str = str(scripts_directory.resolve())
61
+ if scripts_directory_str not in sys.path:
62
+ sys.path.insert(0, scripts_directory_str)
63
+ return _load_module_from_path(
64
+ "revoke_project_claude_permissions",
65
+ scripts_directory / "revoke_project_claude_permissions.py",
66
+ )
67
+
68
+
69
+ def _load_constants_module() -> ModuleType:
70
+ return _load_module_from_path(
71
+ "pr_loop_shared_constants.claude_permissions_constants",
72
+ _scripts_directory()
73
+ / "pr_loop_shared_constants"
74
+ / "claude_permissions_constants.py",
75
+ )
76
+
77
+
78
+ def _seed_grant_then_run(
79
+ fake_settings_path: Path,
80
+ fake_project_root: Path,
81
+ monkeypatch: pytest.MonkeyPatch,
82
+ pre_existing_settings: dict[str, Any],
83
+ ) -> None:
84
+ fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
85
+ grant_module = _load_grant_module()
86
+ monkeypatch.setattr(
87
+ grant_module,
88
+ "get_claude_user_settings_path",
89
+ lambda: fake_settings_path,
90
+ )
91
+ monkeypatch.chdir(fake_project_root)
92
+ grant_module.grant_permissions_for_current_directory()
93
+
94
+
95
+ def _seed_revoke_then_run(
96
+ fake_settings_path: Path,
97
+ fake_project_root: Path,
98
+ monkeypatch: pytest.MonkeyPatch,
99
+ pre_existing_settings: dict[str, Any],
100
+ ) -> None:
101
+ fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
102
+ revoke_module = _load_revoke_module()
103
+ monkeypatch.setattr(
104
+ revoke_module,
105
+ "get_claude_user_settings_path",
106
+ lambda: fake_settings_path,
107
+ )
108
+ monkeypatch.chdir(fake_project_root)
109
+ revoke_module.revoke_permissions_for_current_directory()
110
+
111
+
112
+ def _make_fake_project(tmp_path: Path) -> Path:
113
+ fake_project_root = tmp_path / "fake_project"
114
+ (fake_project_root / ".claude").mkdir(parents=True)
115
+ return fake_project_root
116
+
117
+
118
+ def _project_path_as_posix(fake_project_root: Path) -> str:
119
+ return str(fake_project_root).replace("\\", "/")
120
+
121
+
122
+ def test_grant_writes_deny_rules_for_every_tool_and_pattern(
123
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
124
+ ) -> None:
125
+ fake_project_root = _make_fake_project(tmp_path)
126
+ fake_settings_path = tmp_path / "settings.json"
127
+ constants_module = _load_constants_module()
128
+ _seed_grant_then_run(
129
+ fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
130
+ )
131
+ capsys.readouterr()
132
+ written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
133
+ deny_list = written_settings["permissions"]["deny"]
134
+ project_path_posix = _project_path_as_posix(fake_project_root)
135
+ for each_tool in constants_module.ALL_AGENT_CONFIG_DENY_TOOLS:
136
+ for each_pattern in constants_module.ALL_AGENT_CONFIG_PATH_PATTERNS:
137
+ expected_rule = f"{each_tool}({project_path_posix}/.claude/{each_pattern})"
138
+ assert expected_rule in deny_list, (
139
+ f"deny list missing expected rule {expected_rule!r}"
140
+ )
141
+
142
+
143
+ def test_grant_writes_glob_deny_rules_for_every_agent_config_pattern(
144
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
145
+ ) -> None:
146
+ """Glob must be in the deny tuple so agent-config paths require approval.
147
+
148
+ The AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE promises Edit/Write/Read/Glob
149
+ trust EXCEPT for agent-config files. Glob deny rules are how the EXCEPT
150
+ clause is honored for the Glob tool.
151
+ """
152
+ fake_project_root = _make_fake_project(tmp_path)
153
+ fake_settings_path = tmp_path / "settings.json"
154
+ constants_module = _load_constants_module()
155
+ _seed_grant_then_run(
156
+ fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
157
+ )
158
+ capsys.readouterr()
159
+ written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
160
+ deny_list = written_settings["permissions"]["deny"]
161
+ project_path_posix = _project_path_as_posix(fake_project_root)
162
+ assert "Glob" in constants_module.ALL_AGENT_CONFIG_DENY_TOOLS
163
+ assert "Glob" not in constants_module.ALL_PERMISSION_ALLOW_TOOLS
164
+ for each_pattern in constants_module.ALL_AGENT_CONFIG_PATH_PATTERNS:
165
+ expected_glob_rule = f"Glob({project_path_posix}/.claude/{each_pattern})"
166
+ assert expected_glob_rule in deny_list, (
167
+ f"deny list missing expected Glob rule {expected_glob_rule!r}"
168
+ )
169
+
170
+
171
+ def test_grant_purges_stale_trust_entries_then_writes_current_template(
172
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
173
+ ) -> None:
174
+ fake_project_root = _make_fake_project(tmp_path)
175
+ fake_settings_path = tmp_path / "settings.json"
176
+ project_path_posix = _project_path_as_posix(fake_project_root)
177
+ stale_entry_a = (
178
+ f"Trusted local workspace: {project_path_posix}/.claude/** old wording form A"
179
+ )
180
+ stale_entry_b = (
181
+ f"Trusted local workspace: {project_path_posix}/.claude/** "
182
+ f"different earlier wording"
183
+ )
184
+ unrelated_entry = "Some unrelated environment hint"
185
+ pre_existing_settings: dict[str, Any] = {
186
+ "autoMode": {
187
+ "environment": [stale_entry_a, stale_entry_b, unrelated_entry],
188
+ },
189
+ }
190
+ _seed_grant_then_run(
191
+ fake_settings_path,
192
+ fake_project_root,
193
+ monkeypatch,
194
+ pre_existing_settings=pre_existing_settings,
195
+ )
196
+ captured = capsys.readouterr()
197
+ written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
198
+ environment_list = written_settings["autoMode"]["environment"]
199
+ assert stale_entry_a not in environment_list
200
+ assert stale_entry_b not in environment_list
201
+ assert unrelated_entry in environment_list
202
+ matching_trust_entries = [
203
+ each_entry
204
+ for each_entry in environment_list
205
+ if isinstance(each_entry, str)
206
+ and each_entry.startswith("Trusted local workspace:")
207
+ and f"{project_path_posix}/.claude/**" in each_entry
208
+ ]
209
+ assert len(matching_trust_entries) == 1
210
+ assert "Stale auto-mode environment entries purged" in captured.out
211
+
212
+
213
+ def test_revoke_removes_deny_rules(
214
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
215
+ ) -> None:
216
+ fake_project_root = _make_fake_project(tmp_path)
217
+ fake_settings_path = tmp_path / "settings.json"
218
+ common_module = _load_common_module()
219
+ constants_module = _load_constants_module()
220
+ project_path_posix = _project_path_as_posix(fake_project_root)
221
+ all_deny_rules = common_module.build_agent_config_deny_rules(
222
+ project_path_posix,
223
+ constants_module.ALL_AGENT_CONFIG_DENY_TOOLS,
224
+ constants_module.ALL_AGENT_CONFIG_PATH_PATTERNS,
225
+ )
226
+ pre_existing_settings: dict[str, Any] = {
227
+ "permissions": {
228
+ "deny": list(all_deny_rules),
229
+ },
230
+ }
231
+ _seed_revoke_then_run(
232
+ fake_settings_path,
233
+ fake_project_root,
234
+ monkeypatch,
235
+ pre_existing_settings=pre_existing_settings,
236
+ )
237
+ capsys.readouterr()
238
+ written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
239
+ permissions_section = written_settings.get("permissions", {})
240
+ remaining_deny_list = permissions_section.get("deny", [])
241
+ for each_rule in all_deny_rules:
242
+ assert each_rule not in remaining_deny_list
243
+
244
+
245
+ def test_revoke_removes_every_legacy_trust_entry_for_project(
246
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
247
+ ) -> None:
248
+ fake_project_root = _make_fake_project(tmp_path)
249
+ fake_settings_path = tmp_path / "settings.json"
250
+ project_path_posix = _project_path_as_posix(fake_project_root)
251
+ legacy_entry_a = (
252
+ f"Trusted local workspace: {project_path_posix}/.claude/** template revision A"
253
+ )
254
+ legacy_entry_b = (
255
+ f"Trusted local workspace: {project_path_posix}/.claude/** template revision B"
256
+ )
257
+ unrelated_other_project_entry = (
258
+ "Trusted local workspace: /some/other/project/.claude/** still valid"
259
+ )
260
+ pre_existing_settings: dict[str, Any] = {
261
+ "autoMode": {
262
+ "environment": [
263
+ legacy_entry_a,
264
+ legacy_entry_b,
265
+ unrelated_other_project_entry,
266
+ ],
267
+ },
268
+ }
269
+ _seed_revoke_then_run(
270
+ fake_settings_path,
271
+ fake_project_root,
272
+ monkeypatch,
273
+ pre_existing_settings=pre_existing_settings,
274
+ )
275
+ written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
276
+ environment_list = written_settings.get("autoMode", {}).get("environment", [])
277
+ assert legacy_entry_a not in environment_list
278
+ assert legacy_entry_b not in environment_list
279
+ assert unrelated_other_project_entry in environment_list
280
+
281
+
282
+ def test_template_constant_documents_agent_config_carveout() -> None:
283
+ constants_module = _load_constants_module()
284
+ template_text = constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
285
+ assert "agent-config files always require explicit per-edit user approval" in (
286
+ template_text
287
+ )
288
+
289
+
290
+ def test_is_trust_entry_for_project_predicate_filters_by_prefix_and_project_path() -> (
291
+ None
292
+ ):
293
+ common_module = _load_common_module()
294
+ project_path_posix = "/fake/proj"
295
+ trust_prefix = "Trusted local workspace:"
296
+ non_string_value: object = 42
297
+ assert (
298
+ common_module.is_trust_entry_for_project(
299
+ non_string_value, project_path_posix, trust_prefix
300
+ )
301
+ is False
302
+ )
303
+ wrong_prefix_entry = (
304
+ f"Something else: {project_path_posix}/.claude/** with marker token"
305
+ )
306
+ assert (
307
+ common_module.is_trust_entry_for_project(
308
+ wrong_prefix_entry, project_path_posix, trust_prefix
309
+ )
310
+ is False
311
+ )
312
+ different_project_entry = (
313
+ "Trusted local workspace: /other/project/.claude/** unrelated"
314
+ )
315
+ assert (
316
+ common_module.is_trust_entry_for_project(
317
+ different_project_entry, project_path_posix, trust_prefix
318
+ )
319
+ is False
320
+ )
321
+ matching_entry = (
322
+ f"Trusted local workspace: {project_path_posix}/.claude/** any wording form"
323
+ )
324
+ assert (
325
+ common_module.is_trust_entry_for_project(
326
+ matching_entry, project_path_posix, trust_prefix
327
+ )
328
+ is True
329
+ )
330
+
331
+
332
+ def test_is_trust_entry_rejects_cross_project_path_suffix_collision() -> None:
333
+ """When the project_path is a path suffix of an unrelated entry's path,
334
+ the predicate must reject the unrelated entry (the boundary anchor case)."""
335
+ common_module = _load_common_module()
336
+ short_project_path = "/projects/foo"
337
+ trust_prefix = "Trusted local workspace:"
338
+ longer_unrelated_path_entry = (
339
+ "Trusted local workspace: /Users/jon/projects/foo/.claude/** unrelated path"
340
+ )
341
+ assert (
342
+ common_module.is_trust_entry_for_project(
343
+ longer_unrelated_path_entry, short_project_path, trust_prefix
344
+ )
345
+ is False
346
+ )
347
+ quoted_matching_entry = (
348
+ f'Trusted local workspace: "{short_project_path}/.claude/**" quoted form'
349
+ )
350
+ assert (
351
+ common_module.is_trust_entry_for_project(
352
+ quoted_matching_entry, short_project_path, trust_prefix
353
+ )
354
+ is True
355
+ )
356
+
357
+
358
+ def test_second_grant_is_idempotent_when_no_other_settings_changed(
359
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
360
+ ) -> None:
361
+ """Running grant twice in a row must perform zero changes the second time.
362
+
363
+ On the second call the existing trust entry is byte-identical to the
364
+ freshly-formatted current entry, so purge_stale_trust_entries treats it as
365
+ protected and does not remove it; add_auto_mode_environment_entry then
366
+ no-ops because the entry is already present.
367
+ """
368
+ fake_project_root = _make_fake_project(tmp_path)
369
+ fake_settings_path = tmp_path / "settings.json"
370
+ _seed_grant_then_run(
371
+ fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
372
+ )
373
+ first_run_output = capsys.readouterr()
374
+ assert "No changes needed" not in first_run_output.out
375
+ grant_module = _load_grant_module()
376
+ monkeypatch.setattr(
377
+ grant_module,
378
+ "get_claude_user_settings_path",
379
+ lambda: fake_settings_path,
380
+ )
381
+ monkeypatch.chdir(fake_project_root)
382
+ grant_module.grant_permissions_for_current_directory()
383
+ second_run_output = capsys.readouterr()
384
+ assert "No changes needed; settings file left untouched." in second_run_output.out
385
+ assert "Stale auto-mode environment entries purged" not in second_run_output.out
@@ -138,10 +138,12 @@ def test_save_settings_temp_suffix_includes_pid_and_random_token(
138
138
 
139
139
  def test_text_file_encoding_sourced_from_config() -> None:
140
140
  config_module_path = (
141
- Path(__file__).parent.parent / "config" / "claude_permissions_constants.py"
141
+ Path(__file__).parent.parent
142
+ / "pr_loop_shared_constants"
143
+ / "claude_permissions_constants.py"
142
144
  )
143
145
  specification = importlib.util.spec_from_file_location(
144
- "config.claude_permissions_constants", config_module_path
146
+ "pr_loop_shared_constants.claude_permissions_constants", config_module_path
145
147
  )
146
148
  assert specification is not None
147
149
  assert specification.loader is not None
@@ -7,10 +7,12 @@ from types import ModuleType
7
7
 
8
8
  def _load_constants_module() -> ModuleType:
9
9
  module_path = (
10
- Path(__file__).parent.parent / "config" / "claude_permissions_constants.py"
10
+ Path(__file__).parent.parent
11
+ / "pr_loop_shared_constants"
12
+ / "claude_permissions_constants.py"
11
13
  )
12
14
  specification = importlib.util.spec_from_file_location(
13
- "config.claude_permissions_constants", module_path
15
+ "pr_loop_shared_constants.claude_permissions_constants", module_path
14
16
  )
15
17
  assert specification is not None
16
18
  assert specification.loader is not None
@@ -26,6 +28,16 @@ def test_exposes_all_permission_allow_tools_tuple() -> None:
26
28
  assert constants_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "Write", "Read")
27
29
 
28
30
 
31
+ def test_exposes_all_agent_config_deny_tools_tuple_with_glob() -> None:
32
+ assert constants_module.ALL_AGENT_CONFIG_DENY_TOOLS == (
33
+ "Edit",
34
+ "Write",
35
+ "Read",
36
+ "Glob",
37
+ )
38
+ assert "Glob" not in constants_module.ALL_PERMISSION_ALLOW_TOOLS
39
+
40
+
29
41
  def test_auto_mode_environment_entry_template_is_format_string() -> None:
30
42
  rendered_template_text = (
31
43
  constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
@@ -36,6 +48,29 @@ def test_auto_mode_environment_entry_template_is_format_string() -> None:
36
48
  assert ".claude/**" in rendered_template_text
37
49
 
38
50
 
51
+ def test_template_derives_human_readable_pattern_list_from_pattern_tuple() -> None:
52
+ """Every pattern in ALL_AGENT_CONFIG_PATH_PATTERNS must surface in the
53
+ rendered template through its derived human-readable form, and the
54
+ template must still expose the {project_path} placeholder for .format()
55
+ substitution at runtime."""
56
+ template_text: str = constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
57
+ assert "{project_path}" in template_text
58
+ for each_pattern in constants_module.ALL_AGENT_CONFIG_PATH_PATTERNS:
59
+ if each_pattern.endswith("/**"):
60
+ directory_name = each_pattern[: -len("/**")]
61
+ expected_phrase = f"anything under {directory_name}/"
62
+ elif each_pattern == "mcp.json":
63
+ expected_phrase = "the mcp.json file"
64
+ else:
65
+ expected_phrase = each_pattern
66
+ assert expected_phrase in template_text, (
67
+ f"template missing derived phrase for pattern {each_pattern!r}: "
68
+ f"expected {expected_phrase!r}"
69
+ )
70
+ rendered_template_text = template_text.format(project_path="/tmp/x")
71
+ assert "/tmp/x" in rendered_template_text
72
+
73
+
39
74
  def test_get_claude_user_settings_path_ends_in_settings_json() -> None:
40
75
  resolved_settings_path = constants_module.get_claude_user_settings_path()
41
76
  assert resolved_settings_path.name == constants_module.CLAUDE_SETTINGS_FILENAME
@@ -7,10 +7,12 @@ from types import ModuleType
7
7
 
8
8
  def _load_constants_module() -> ModuleType:
9
9
  module_path = (
10
- Path(__file__).parent.parent / "config" / "claude_settings_keys_constants.py"
10
+ Path(__file__).parent.parent
11
+ / "pr_loop_shared_constants"
12
+ / "claude_settings_keys_constants.py"
11
13
  )
12
14
  specification = importlib.util.spec_from_file_location(
13
- "config.claude_settings_keys_constants", module_path
15
+ "pr_loop_shared_constants.claude_settings_keys_constants", module_path
14
16
  )
15
17
  assert specification is not None
16
18
  assert specification.loader is not None
@@ -7,10 +7,12 @@ from types import ModuleType
7
7
 
8
8
  def _load_constants_module() -> ModuleType:
9
9
  module_path = (
10
- Path(__file__).parent.parent / "config" / "code_rules_gate_constants.py"
10
+ Path(__file__).parent.parent
11
+ / "pr_loop_shared_constants"
12
+ / "code_rules_gate_constants.py"
11
13
  )
12
14
  specification = importlib.util.spec_from_file_location(
13
- "config.code_rules_gate_constants", module_path
15
+ "pr_loop_shared_constants.code_rules_gate_constants", module_path
14
16
  )
15
17
  assert specification is not None
16
18
  assert specification.loader is not None
@@ -11,9 +11,13 @@ from types import ModuleType
11
11
 
12
12
 
13
13
  def _load_constants_module() -> ModuleType:
14
- module_path = Path(__file__).parent.parent / "config" / "fix_hookspath_constants.py"
14
+ module_path = (
15
+ Path(__file__).parent.parent
16
+ / "pr_loop_shared_constants"
17
+ / "fix_hookspath_constants.py"
18
+ )
15
19
  specification = importlib.util.spec_from_file_location(
16
- "config.fix_hookspath_constants", module_path
20
+ "pr_loop_shared_constants.fix_hookspath_constants", module_path
17
21
  )
18
22
  assert specification is not None
19
23
  assert specification.loader is not None
@@ -1,7 +1,8 @@
1
1
  """Smoke tests for grant_project_claude_permissions wiring.
2
2
 
3
3
  Confirms the module imports cleanly with the constants now sourced from
4
- config/claude_permissions_constants.py and config/claude_settings_keys_constants.py.
4
+ pr_loop_shared_constants/claude_permissions_constants.py and
5
+ pr_loop_shared_constants/claude_settings_keys_constants.py.
5
6
  """
6
7
 
7
8
  from __future__ import annotations
@@ -17,7 +18,6 @@ def _load_grant_module() -> ModuleType:
17
18
  parent_directory = str(scripts_directory.resolve())
18
19
  if parent_directory not in sys.path:
19
20
  sys.path.insert(0, parent_directory)
20
- sys.modules.pop("config", None)
21
21
  module_path = scripts_directory / "grant_project_claude_permissions.py"
22
22
  specification = importlib.util.spec_from_file_location(
23
23
  "grant_project_claude_permissions", module_path
@@ -36,11 +36,10 @@ from typing import Any
36
36
  THIS_FILE_DIRECTORY = Path(__file__).resolve().parent
37
37
  SCRIPT_DIRECTORY = THIS_FILE_DIRECTORY.parent
38
38
 
39
- sys.modules.pop("config", None)
40
39
  if str(SCRIPT_DIRECTORY) not in sys.path:
41
40
  sys.path.insert(0, str(SCRIPT_DIRECTORY))
42
41
 
43
- from config.post_audit_thread_constants import ( # noqa: E402
42
+ from pr_loop_shared_constants.post_audit_thread_constants import ( # noqa: E402
44
43
  ALL_GH_AUTH_TOKEN_COMMAND_PARTS,
45
44
  ALL_RETRY_BACKOFF_SECONDS,
46
45
  BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME,
@@ -7,10 +7,12 @@ from types import ModuleType
7
7
 
8
8
  def _load_constants_module() -> ModuleType:
9
9
  module_path = (
10
- Path(__file__).parent.parent / "config" / "post_audit_thread_constants.py"
10
+ Path(__file__).parent.parent
11
+ / "pr_loop_shared_constants"
12
+ / "post_audit_thread_constants.py"
11
13
  )
12
14
  specification = importlib.util.spec_from_file_location(
13
- "config.post_audit_thread_constants", module_path
15
+ "pr_loop_shared_constants.post_audit_thread_constants", module_path
14
16
  )
15
17
  assert specification is not None
16
18
  assert specification.loader is not None
@@ -31,7 +31,7 @@ def _load_preflight_module() -> ModuleType:
31
31
 
32
32
  preflight = _load_preflight_module()
33
33
 
34
- from config.preflight_constants import ( # noqa: E402
34
+ from pr_loop_shared_constants.preflight_constants import ( # noqa: E402
35
35
  PYTEST_INI_FILENAME,
36
36
  PYTEST_NO_TESTS_COLLECTED_EXIT_CODE,
37
37
  )
@@ -204,16 +204,19 @@ def test_should_exit_nonzero_when_subprocess_run_raises_os_error(
204
204
 
205
205
 
206
206
  def test_preflight_uses_shared_hooks_path_suffix_constant() -> None:
207
- """Preflight's expected suffix must come from config.fix_hookspath_constants
208
- so the canonical hooks directory is defined in exactly one place."""
207
+ """Preflight's expected suffix must come from
208
+ pr_loop_shared_constants.fix_hookspath_constants so the canonical hooks
209
+ directory is defined in exactly one place."""
209
210
  scripts_directory = str(Path(__file__).parent.parent.resolve())
210
211
  if scripts_directory not in sys.path:
211
212
  sys.path.insert(0, scripts_directory)
212
213
  constants_module_path = (
213
- Path(__file__).parent.parent / "config" / "fix_hookspath_constants.py"
214
+ Path(__file__).parent.parent
215
+ / "pr_loop_shared_constants"
216
+ / "fix_hookspath_constants.py"
214
217
  )
215
218
  constants_specification = importlib.util.spec_from_file_location(
216
- "config.fix_hookspath_constants",
219
+ "pr_loop_shared_constants.fix_hookspath_constants",
217
220
  constants_module_path,
218
221
  )
219
222
  assert constants_specification is not None
@@ -234,15 +237,18 @@ def test_preflight_skip_uses_shared_env_var_constant(
234
237
  capsys: pytest.CaptureFixture[str],
235
238
  monkeypatch: pytest.MonkeyPatch,
236
239
  ) -> None:
237
- """The preflight skip env-var name must come from config/preflight_constants.py."""
240
+ """The preflight skip env-var name must come from
241
+ pr_loop_shared_constants/preflight_constants.py."""
238
242
  scripts_directory = str(Path(__file__).parent.parent.resolve())
239
243
  if scripts_directory not in sys.path:
240
244
  sys.path.insert(0, scripts_directory)
241
245
  constants_module_path = (
242
- Path(__file__).parent.parent / "config" / "preflight_constants.py"
246
+ Path(__file__).parent.parent
247
+ / "pr_loop_shared_constants"
248
+ / "preflight_constants.py"
243
249
  )
244
250
  constants_specification = importlib.util.spec_from_file_location(
245
- "config.preflight_constants",
251
+ "pr_loop_shared_constants.preflight_constants",
246
252
  constants_module_path,
247
253
  )
248
254
  assert constants_specification is not None
@@ -306,8 +312,9 @@ def test_preflight_does_not_import_unused_repository_root_marker_constant() -> N
306
312
 
307
313
  def test_pytest_no_tests_collected_helper_returns_named_constant() -> None:
308
314
  """The pytest "no tests collected" exit code must be sourced from the
309
- named constant in config/preflight_constants.py rather than the bare
310
- literal 5 inside the function body (CODE_RULES magic-values rule)."""
315
+ named constant in pr_loop_shared_constants/preflight_constants.py rather
316
+ than the bare literal 5 inside the function body (CODE_RULES magic-values
317
+ rule)."""
311
318
  assert preflight._pytest_exit_code_no_tests_collected() == (
312
319
  PYTEST_NO_TESTS_COLLECTED_EXIT_CODE
313
320
  )
@@ -317,30 +324,6 @@ def test_pytest_no_tests_collected_helper_returns_named_constant() -> None:
317
324
  )
318
325
 
319
326
 
320
- def test_preflight_bootstrap_moves_script_directory_to_front() -> None:
321
- """Import bootstrap keeps exactly one script directory entry at the front."""
322
- module_path = Path(__file__).parent.parent / "preflight.py"
323
- script_directory_resolved = str(module_path.parent.resolve())
324
- script_directory_absolute = str(module_path.parent.absolute())
325
- original_sys_path = list(sys.path)
326
- try:
327
- sys.path.insert(0, script_directory_resolved)
328
- sys.path.insert(0, script_directory_resolved)
329
- sys.path.insert(0, str(module_path.parents[4]))
330
- _load_preflight_module()
331
- assert os.path.samefile(sys.path[0], script_directory_resolved)
332
- equivalent_count = sum(
333
- 1
334
- for each_entry in sys.path
335
- if os.path.exists(each_entry)
336
- and os.path.samefile(each_entry, script_directory_resolved)
337
- )
338
- assert equivalent_count == 1
339
- assert sys.path[0] == script_directory_absolute
340
- finally:
341
- sys.path[:] = original_sys_path
342
-
343
-
344
327
  def test_main_uses_correct_changed_files_function_name() -> None:
345
328
  """main() must call get_changed_files, not the undefined get_all_changed_files."""
346
329
  main_source = inspect.getsource(preflight.main)
@@ -528,24 +511,6 @@ def test_explicit_scope_all_with_base_ref_should_not_call_get_changed_files(
528
511
  mock_get_changed.assert_not_called()
529
512
 
530
513
 
531
- def test_preflight_bootstrap_matches_code_rules_sys_path_pattern() -> None:
532
- """Bootstrap must clear duplicate script_directory entries, then guard insert."""
533
- module_path = Path(__file__).parent.parent / "preflight.py"
534
- source = module_path.read_text(encoding="utf-8")
535
- assert "_entry_points_at_preflight_script_directory" in source, (
536
- "Bootstrap must remove script_directory entries using path equivalence"
537
- )
538
- assert "for each_index in range(len(sys.path) - 1, -1, -1):" in source, (
539
- "Bootstrap must walk sys.path to drop duplicate script directory entries"
540
- )
541
- assert "_preflight_scripts_path_entry not in sys.path:" in source, (
542
- "Bootstrap insert must be guarded for code_rules_gate compliance"
543
- )
544
- assert "sys.path.insert(0, _preflight_scripts_path_entry)" in source, (
545
- "Bootstrap must insert the absolute script directory at index 0"
546
- )
547
-
548
-
549
514
  def test_has_discoverable_tests_should_include_untracked_test_files(
550
515
  tmp_path: Path,
551
516
  ) -> None: