claude-dev-env 2.3.0 → 2.5.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 (290) hide show
  1. package/CLAUDE.md +53 -48
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +3 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +3 -4
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/skill-writer-agent.md +84 -0
  31. package/agents/test_agent_frontmatter.py +67 -18
  32. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
  33. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  34. package/bin/CLAUDE.md +68 -5
  35. package/bin/ever-shipped-skills.mjs +1 -0
  36. package/bin/install-constants.mjs +88 -0
  37. package/bin/install.mjs +1138 -114
  38. package/bin/install.prune.test.mjs +869 -19
  39. package/bin/install.test.mjs +906 -2
  40. package/commands/implement.md +1 -1
  41. package/commands/right-size.md +1 -1
  42. package/docs/CLAUDE.md +2 -0
  43. package/docs/host-pool-health-monitor.md +102 -0
  44. package/docs/references/CLAUDE.md +5 -2
  45. package/docs/references/advisor-tool.md +13 -0
  46. package/docs/references/code-review-enforcement.md +107 -0
  47. package/docs/references/team-advisor-skill.md +14 -0
  48. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  49. package/hooks/blocking/CLAUDE.md +9 -1
  50. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  51. package/hooks/blocking/code_review_gate_deny.py +74 -0
  52. package/hooks/blocking/code_review_pr_create_gate.py +198 -0
  53. package/hooks/blocking/code_review_push_gate.py +145 -0
  54. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
  55. package/hooks/blocking/code_review_stamp_store.py +233 -0
  56. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  57. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  58. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  59. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  60. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  61. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  62. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  63. package/hooks/blocking/config/__init__.py +5 -5
  64. package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
  65. package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
  66. package/hooks/blocking/config/verified_commit_constants.py +160 -155
  67. package/hooks/blocking/conftest.py +2 -0
  68. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  69. package/hooks/blocking/destructive_command_blocker.py +19 -6
  70. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  71. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  72. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  73. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  74. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  75. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  76. package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
  77. package/hooks/blocking/test_code_review_push_gate.py +205 -0
  78. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
  79. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  80. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  81. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  82. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  83. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  84. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  85. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  86. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
  87. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  88. package/hooks/blocking/test_verification_verdict_store.py +974 -903
  89. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  90. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  91. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  92. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  93. package/hooks/blocking/verification_verdict_store.py +1039 -1014
  94. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  95. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  96. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  97. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  98. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  99. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  100. package/hooks/git-hooks/pre_push.py +89 -2
  101. package/hooks/git-hooks/test_pre_push.py +128 -0
  102. package/hooks/hooks.json +26 -1
  103. package/hooks/hooks_constants/CLAUDE.md +3 -1
  104. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  105. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  106. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  107. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  108. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  109. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  110. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  111. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  112. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  113. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  114. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  115. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  116. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  117. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  118. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  119. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  120. package/hooks/pyproject.toml +1 -0
  121. package/hooks/validators/CLAUDE.md +2 -0
  122. package/hooks/validators/config/__init__.py +0 -0
  123. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  124. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  125. package/hooks/validators/conftest.py +4 -0
  126. package/hooks/validators/mypy_integration.py +63 -50
  127. package/hooks/validators/pyproject_config_discovery.py +101 -0
  128. package/hooks/validators/ruff_integration.py +257 -25
  129. package/hooks/validators/run_all_validators.py +223 -19
  130. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  131. package/hooks/validators/test_mypy_integration.py +32 -0
  132. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +160 -2
  135. package/hooks/validators/test_run_all_validators.py +140 -68
  136. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  137. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  138. package/package.json +10 -2
  139. package/rules/CLAUDE.md +1 -0
  140. package/rules/docstring-prose-matches-implementation.md +45 -67
  141. package/rules/durable-post-artifacts.md +7 -0
  142. package/rules/state-what-is.md +25 -0
  143. package/rules/verified-commit-gate-skip.md +1 -1
  144. package/scripts/CLAUDE.md +1 -0
  145. package/scripts/Capture-PoolHealth.ps1 +410 -0
  146. package/scripts/_code_review_test_support.py +404 -0
  147. package/scripts/claude_chain_runner.py +141 -1
  148. package/scripts/codec_forwarding_test_support.py +83 -0
  149. package/scripts/conftest.py +23 -0
  150. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
  151. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
  152. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  153. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  154. package/scripts/invoke_code_review.py +550 -38
  155. package/scripts/resolve_worker_spawn.py +626 -619
  156. package/scripts/spawn_grok_batch.py +672 -672
  157. package/scripts/test_claude_chain_runner.py +131 -0
  158. package/scripts/test_invoke_code_review_chain.py +70 -0
  159. package/scripts/test_invoke_code_review_cli.py +192 -0
  160. package/scripts/test_invoke_code_review_codec.py +77 -0
  161. package/scripts/test_invoke_code_review_contract.py +256 -0
  162. package/scripts/test_invoke_code_review_git.py +123 -0
  163. package/scripts/test_invoke_code_review_mode.py +99 -0
  164. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  165. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  166. package/skills/CLAUDE.md +2 -0
  167. package/skills/auditing-claude-config/SKILL.md +114 -114
  168. package/skills/autoconverge/SKILL.md +427 -421
  169. package/skills/autoconverge/reference/convergence.md +26 -4
  170. package/skills/autoconverge/reference/multi-pr.md +6 -1
  171. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  172. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  173. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  174. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  175. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
  176. package/skills/autoconverge/workflow/converge.mjs +191 -8
  177. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  178. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  179. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  180. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  181. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  182. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  183. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  184. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  185. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  186. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  187. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  188. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  189. package/skills/closeout/SKILL.md +33 -50
  190. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  191. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  192. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  193. package/skills/condensing-instructions/SKILL.md +81 -0
  194. package/skills/copilot-review/SKILL.md +119 -119
  195. package/skills/e-code-review/SKILL.md +52 -0
  196. package/skills/e-code-review/reference/fix.md +54 -0
  197. package/skills/e-code-review/reference/loop.md +43 -0
  198. package/skills/e-code-review/reference/low.md +57 -0
  199. package/skills/e-code-review/reference/medium.md +153 -0
  200. package/skills/e-code-review/reference/xhigh.md +182 -0
  201. package/skills/e-simplify/SKILL.md +97 -0
  202. package/skills/fresh-branch/CLAUDE.md +2 -0
  203. package/skills/fresh-branch/SKILL.md +2 -0
  204. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  205. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  206. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  207. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  208. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  209. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  210. package/skills/issue-tracker/SKILL.md +92 -0
  211. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  212. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  213. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  214. package/skills/orchestrator/SKILL.md +162 -21
  215. package/skills/orchestrator/scripts/status_gate.py +625 -0
  216. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  217. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  218. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  219. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  220. package/skills/orchestrator-refresh/SKILL.md +110 -35
  221. package/skills/plan-to-pr/SKILL.md +155 -0
  222. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  223. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  224. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  225. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  226. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  227. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  228. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  229. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  230. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  231. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  232. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  233. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  234. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  235. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  236. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  237. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  238. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  239. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  240. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  241. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  242. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  243. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  244. package/skills/pr-converge/SKILL.md +472 -469
  245. package/skills/pr-converge/reference/examples.md +3 -3
  246. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  247. package/skills/pr-converge/reference/ground-rules.md +7 -4
  248. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  249. package/skills/pr-converge/reference/per-tick.md +5 -5
  250. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  251. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  252. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  253. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  254. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  255. package/skills/pr-fix-protocol/SKILL.md +1 -1
  256. package/skills/privacy-hygiene/SKILL.md +68 -68
  257. package/skills/prototype/SKILL.md +86 -0
  258. package/skills/prototype/reference/honest-limitations.md +23 -0
  259. package/skills/prototype/reference/promotion-tasks.md +23 -0
  260. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  261. package/skills/prototype/scripts/conftest.py +15 -0
  262. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  263. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  264. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  265. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  266. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  267. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  268. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  269. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  270. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  271. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  272. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  273. package/skills/prototype/workflows/promotion.md +27 -0
  274. package/skills/prototype/workflows/sandbox.md +35 -0
  275. package/skills/release-notes-html/SKILL.md +164 -0
  276. package/skills/skill-builder/CLAUDE.md +3 -3
  277. package/skills/skill-builder/SKILL.md +5 -5
  278. package/skills/skill-builder/references/CLAUDE.md +1 -1
  279. package/skills/skill-builder/references/delegation-map.md +3 -3
  280. package/skills/skill-builder/references/description-field.md +1 -1
  281. package/skills/skill-builder/references/skill-modularity.md +2 -3
  282. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  283. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  284. package/skills/skill-builder/workflows/new-skill.md +2 -2
  285. package/skills/task-build/CLAUDE.md +8 -7
  286. package/skills/task-build/SKILL.md +16 -8
  287. package/skills/task-build/reference/tool-routing.md +19 -0
  288. package/skills/team-advisor/SKILL.md +2 -2
  289. package/scripts/test_invoke_code_review.py +0 -672
  290. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -1,178 +1,185 @@
1
- """Unit tests for the enter_worktree_origin_prefetch PreToolUse hook.
2
-
3
- Uses real git repositories (a "remote" repo cloned into a "local" one) so the
4
- fetch behavior is exercised against actual git plumbing, not mocked calls.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import importlib.util
10
- import io
11
- import json
12
- import pathlib
13
- import subprocess
14
- import sys
15
-
16
- import pytest
17
-
18
- _HOOK_DIR = pathlib.Path(__file__).parent
19
- _HOOKS_TREE = _HOOK_DIR.parent
20
- for each_path in (str(_HOOK_DIR), str(_HOOKS_TREE)):
21
- if each_path not in sys.path:
22
- sys.path.insert(0, each_path)
23
-
24
- hook_spec = importlib.util.spec_from_file_location(
25
- "enter_worktree_origin_prefetch",
26
- _HOOK_DIR / "enter_worktree_origin_prefetch.py",
27
- )
28
- assert hook_spec is not None
29
- assert hook_spec.loader is not None
30
- hook_module = importlib.util.module_from_spec(hook_spec)
31
- hook_spec.loader.exec_module(hook_module)
32
-
33
-
34
- def _run_git(repo_directory: pathlib.Path, *args: str) -> subprocess.CompletedProcess[str]:
35
- return subprocess.run(
36
- ["git", *args],
37
- cwd=repo_directory,
38
- capture_output=True,
39
- text=True,
40
- check=True,
41
- )
42
-
43
-
44
- def _init_repo_with_commit(repo_directory: pathlib.Path) -> None:
45
- repo_directory.mkdir(parents=True, exist_ok=True)
46
- _run_git(repo_directory, "init", "--quiet", "--initial-branch=main")
47
- _run_git(repo_directory, "config", "user.email", "test@example.com")
48
- _run_git(repo_directory, "config", "user.name", "Test")
49
- _run_git(repo_directory, "commit", "--allow-empty", "--quiet", "-m", "init")
50
-
51
-
52
- def _clone_as_local(remote_directory: pathlib.Path, local_directory: pathlib.Path) -> None:
53
- subprocess.run(
54
- ["git", "clone", "--quiet", str(remote_directory), str(local_directory)],
55
- capture_output=True,
56
- text=True,
57
- check=True,
58
- )
59
- _run_git(local_directory, "config", "user.email", "test@example.com")
60
- _run_git(local_directory, "config", "user.name", "Test")
61
-
62
-
63
- def test_is_enter_worktree_creation_true_when_no_path_given() -> None:
64
- payload = {"tool_name": "EnterWorktree", "tool_input": {}}
65
- assert hook_module.is_enter_worktree_creation(payload) is True
66
-
67
-
68
- def test_is_enter_worktree_creation_true_with_name_only() -> None:
69
- payload = {"tool_name": "EnterWorktree", "tool_input": {"name": "my-branch"}}
70
- assert hook_module.is_enter_worktree_creation(payload) is True
71
-
72
-
73
- def test_is_enter_worktree_creation_false_when_path_given() -> None:
74
- payload = {"tool_name": "EnterWorktree", "tool_input": {"path": "/some/worktree"}}
75
- assert hook_module.is_enter_worktree_creation(payload) is False
76
-
77
-
78
- def test_is_enter_worktree_creation_false_for_other_tool() -> None:
79
- payload = {"tool_name": "Bash", "tool_input": {}}
80
- assert hook_module.is_enter_worktree_creation(payload) is False
81
-
82
-
83
- def test_resolve_origin_default_branch_reads_cloned_head(tmp_path: pathlib.Path) -> None:
84
- remote_directory = tmp_path / "remote"
85
- local_directory = tmp_path / "local"
86
- _init_repo_with_commit(remote_directory)
87
- _clone_as_local(remote_directory, local_directory)
88
-
89
- resolved_branch = hook_module.resolve_origin_default_branch(str(local_directory))
90
-
91
- assert resolved_branch == "main"
92
-
93
-
94
- def test_resolve_origin_default_branch_returns_none_without_origin(
95
- tmp_path: pathlib.Path,
96
- ) -> None:
97
- solo_directory = tmp_path / "solo"
98
- _init_repo_with_commit(solo_directory)
99
-
100
- resolved_branch = hook_module.resolve_origin_default_branch(str(solo_directory))
101
-
102
- assert resolved_branch is None
103
-
104
-
105
- def test_fetch_origin_branch_updates_stale_local_ref(tmp_path: pathlib.Path) -> None:
106
- remote_directory = tmp_path / "remote"
107
- local_directory = tmp_path / "local"
108
- _init_repo_with_commit(remote_directory)
109
- _clone_as_local(remote_directory, local_directory)
110
-
111
- _run_git(remote_directory, "commit", "--allow-empty", "--quiet", "-m", "second")
112
- remote_head_sha = _run_git(remote_directory, "rev-parse", "HEAD").stdout.strip()
113
- local_cached_sha_before = _run_git(
114
- local_directory, "rev-parse", "refs/remotes/origin/main"
115
- ).stdout.strip()
116
- assert local_cached_sha_before != remote_head_sha
117
-
118
- hook_module.fetch_origin_branch(str(local_directory), "main")
119
-
120
- local_cached_sha_after = _run_git(
121
- local_directory, "rev-parse", "refs/remotes/origin/main"
122
- ).stdout.strip()
123
- assert local_cached_sha_after == remote_head_sha
124
-
125
-
126
- def test_fetch_origin_branch_never_raises_when_remote_missing(tmp_path: pathlib.Path) -> None:
127
- solo_directory = tmp_path / "solo"
128
- _init_repo_with_commit(solo_directory)
129
-
130
- hook_module.fetch_origin_branch(str(solo_directory), "main")
131
-
132
-
133
- def test_main_fetches_stale_ref_and_exits_zero(
134
- tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
135
- ) -> None:
136
- remote_directory = tmp_path / "remote"
137
- local_directory = tmp_path / "local"
138
- _init_repo_with_commit(remote_directory)
139
- _clone_as_local(remote_directory, local_directory)
140
- _run_git(remote_directory, "commit", "--allow-empty", "--quiet", "-m", "second")
141
- remote_head_sha = _run_git(remote_directory, "rev-parse", "HEAD").stdout.strip()
142
-
143
- payload = {
144
- "tool_name": "EnterWorktree",
145
- "tool_input": {},
146
- "cwd": str(local_directory),
147
- }
148
- monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
149
-
150
- with pytest.raises(SystemExit) as exit_info:
151
- hook_module.main()
152
-
153
- assert exit_info.value.code == 0
154
- local_cached_sha_after = _run_git(
155
- local_directory, "rev-parse", "refs/remotes/origin/main"
156
- ).stdout.strip()
157
- assert local_cached_sha_after == remote_head_sha
158
-
159
-
160
- def test_main_exits_zero_for_non_enter_worktree_tool(
161
- monkeypatch: pytest.MonkeyPatch,
162
- ) -> None:
163
- payload = {"tool_name": "Bash", "tool_input": {"command": "ls"}, "cwd": "/tmp"}
164
- monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
165
-
166
- with pytest.raises(SystemExit) as exit_info:
167
- hook_module.main()
168
-
169
- assert exit_info.value.code == 0
170
-
171
-
172
- def test_main_exits_zero_on_malformed_json(monkeypatch: pytest.MonkeyPatch) -> None:
173
- monkeypatch.setattr(sys, "stdin", io.StringIO("not json"))
174
-
175
- with pytest.raises(SystemExit) as exit_info:
176
- hook_module.main()
177
-
178
- assert exit_info.value.code == 0
1
+ """Unit tests for the enter_worktree_origin_prefetch PreToolUse hook.
2
+
3
+ Uses real git repositories (a "remote" repo cloned into a "local" one) so the
4
+ fetch behavior is exercised against actual git plumbing, not mocked calls.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib.util
10
+ import io
11
+ import json
12
+ import pathlib
13
+ import subprocess
14
+ import sys
15
+
16
+ import pytest
17
+
18
+ _HOOK_DIR = pathlib.Path(__file__).parent
19
+ _HOOKS_TREE = _HOOK_DIR.parent
20
+ for each_path in (str(_HOOK_DIR), str(_HOOKS_TREE)):
21
+ if each_path not in sys.path:
22
+ sys.path.insert(0, each_path)
23
+
24
+ hook_spec = importlib.util.spec_from_file_location(
25
+ "enter_worktree_origin_prefetch",
26
+ _HOOK_DIR / "enter_worktree_origin_prefetch.py",
27
+ )
28
+ assert hook_spec is not None
29
+ assert hook_spec.loader is not None
30
+ hook_module = importlib.util.module_from_spec(hook_spec)
31
+ hook_spec.loader.exec_module(hook_module)
32
+
33
+
34
+ def _run_git(repo_directory: pathlib.Path, *args: str) -> subprocess.CompletedProcess[str]:
35
+ return subprocess.run(
36
+ ["git", *args],
37
+ cwd=repo_directory,
38
+ capture_output=True,
39
+ text=True,
40
+ check=True,
41
+ )
42
+
43
+
44
+ def _init_repo_with_commit(repo_directory: pathlib.Path) -> None:
45
+ repo_directory.mkdir(parents=True, exist_ok=True)
46
+ _run_git(repo_directory, "init", "--quiet", "--initial-branch=main")
47
+ _run_git(repo_directory, "config", "user.email", "test@example.com")
48
+ _run_git(repo_directory, "config", "user.name", "Test")
49
+ _run_git(repo_directory, "commit", "--allow-empty", "--quiet", "-m", "init")
50
+
51
+
52
+ def _clone_as_local(remote_directory: pathlib.Path, local_directory: pathlib.Path) -> None:
53
+ subprocess.run(
54
+ ["git", "clone", "--quiet", str(remote_directory), str(local_directory)],
55
+ capture_output=True,
56
+ text=True,
57
+ check=True,
58
+ )
59
+ _run_git(local_directory, "config", "user.email", "test@example.com")
60
+ _run_git(local_directory, "config", "user.name", "Test")
61
+
62
+
63
+ def test_is_enter_worktree_creation_true_when_no_path_given() -> None:
64
+ payload = {"tool_name": "EnterWorktree", "tool_input": {}}
65
+ assert hook_module.is_enter_worktree_creation(payload) is True
66
+
67
+
68
+ def test_is_enter_worktree_creation_true_with_name_only() -> None:
69
+ payload = {"tool_name": "EnterWorktree", "tool_input": {"name": "my-branch"}}
70
+ assert hook_module.is_enter_worktree_creation(payload) is True
71
+
72
+
73
+ def test_is_enter_worktree_creation_false_when_path_given() -> None:
74
+ payload = {"tool_name": "EnterWorktree", "tool_input": {"path": "/some/worktree"}}
75
+ assert hook_module.is_enter_worktree_creation(payload) is False
76
+
77
+
78
+ def test_is_enter_worktree_creation_false_for_other_tool() -> None:
79
+ payload = {"tool_name": "Bash", "tool_input": {}}
80
+ assert hook_module.is_enter_worktree_creation(payload) is False
81
+
82
+
83
+ def test_resolve_origin_default_branch_reads_cloned_head(tmp_path: pathlib.Path) -> None:
84
+ remote_directory = tmp_path / "remote"
85
+ local_directory = tmp_path / "local"
86
+ _init_repo_with_commit(remote_directory)
87
+ _clone_as_local(remote_directory, local_directory)
88
+
89
+ resolved_branch = hook_module.resolve_origin_default_branch(str(local_directory))
90
+
91
+ assert resolved_branch == "main"
92
+
93
+
94
+ def test_resolve_origin_default_branch_returns_none_without_origin(
95
+ tmp_path: pathlib.Path,
96
+ ) -> None:
97
+ solo_directory = tmp_path / "solo"
98
+ _init_repo_with_commit(solo_directory)
99
+
100
+ resolved_branch = hook_module.resolve_origin_default_branch(str(solo_directory))
101
+
102
+ assert resolved_branch is None
103
+
104
+
105
+ def test_fetch_origin_branch_updates_stale_local_ref(tmp_path: pathlib.Path) -> None:
106
+ remote_directory = tmp_path / "remote"
107
+ local_directory = tmp_path / "local"
108
+ _init_repo_with_commit(remote_directory)
109
+ _clone_as_local(remote_directory, local_directory)
110
+
111
+ _run_git(remote_directory, "commit", "--allow-empty", "--quiet", "-m", "second")
112
+ remote_head_sha = _run_git(remote_directory, "rev-parse", "HEAD").stdout.strip()
113
+ local_cached_sha_before = _run_git(
114
+ local_directory, "rev-parse", "refs/remotes/origin/main"
115
+ ).stdout.strip()
116
+ assert local_cached_sha_before != remote_head_sha
117
+
118
+ hook_module.fetch_origin_branch(str(local_directory), "main")
119
+
120
+ local_cached_sha_after = _run_git(
121
+ local_directory, "rev-parse", "refs/remotes/origin/main"
122
+ ).stdout.strip()
123
+ assert local_cached_sha_after == remote_head_sha
124
+
125
+
126
+ def test_fetch_origin_branch_never_raises_when_remote_missing(tmp_path: pathlib.Path) -> None:
127
+ solo_directory = tmp_path / "solo"
128
+ _init_repo_with_commit(solo_directory)
129
+
130
+ hook_module.fetch_origin_branch(str(solo_directory), "main")
131
+
132
+
133
+ def test_main_fetches_stale_ref_and_exits_zero(
134
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
135
+ ) -> None:
136
+ remote_directory = tmp_path / "remote"
137
+ local_directory = tmp_path / "local"
138
+ _init_repo_with_commit(remote_directory)
139
+ _clone_as_local(remote_directory, local_directory)
140
+ _run_git(remote_directory, "commit", "--allow-empty", "--quiet", "-m", "second")
141
+ remote_head_sha = _run_git(remote_directory, "rev-parse", "HEAD").stdout.strip()
142
+
143
+ payload = {
144
+ "tool_name": "EnterWorktree",
145
+ "tool_input": {},
146
+ "cwd": str(local_directory),
147
+ }
148
+ monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
149
+
150
+ with pytest.raises(SystemExit) as exit_info:
151
+ hook_module.main()
152
+
153
+ assert exit_info.value.code == 0
154
+ local_cached_sha_after = _run_git(
155
+ local_directory, "rev-parse", "refs/remotes/origin/main"
156
+ ).stdout.strip()
157
+ assert local_cached_sha_after == remote_head_sha
158
+
159
+
160
+ def test_main_exits_zero_for_non_enter_worktree_tool(
161
+ monkeypatch: pytest.MonkeyPatch,
162
+ ) -> None:
163
+ payload = {"tool_name": "Bash", "tool_input": {"command": "ls"}, "cwd": "/tmp"}
164
+ monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
165
+
166
+ with pytest.raises(SystemExit) as exit_info:
167
+ hook_module.main()
168
+
169
+ assert exit_info.value.code == 0
170
+
171
+
172
+ @pytest.mark.parametrize(
173
+ "stdin_text",
174
+ ["not json", "", " \n\t ", "[]", "42"],
175
+ ids=["malformed-json", "empty", "whitespace-only", "json-array-root", "json-scalar-root"],
176
+ )
177
+ def test_main_exits_zero_on_fail_soft_stdin(
178
+ stdin_text: str, monkeypatch: pytest.MonkeyPatch
179
+ ) -> None:
180
+ monkeypatch.setattr(sys, "stdin", io.StringIO(stdin_text))
181
+
182
+ with pytest.raises(SystemExit) as exit_info:
183
+ hook_module.main()
184
+
185
+ assert exit_info.value.code == 0
@@ -73,6 +73,7 @@ ignore = [
73
73
  "PLR0912",
74
74
  "PLR0913",
75
75
  "PLR0915",
76
+ "PLR0917",
76
77
  "PLR1714",
77
78
  "PLR2004",
78
79
  "PLR5501",
@@ -12,6 +12,7 @@ A library of check modules used by the validation hooks. Each module focuses on
12
12
  | `output_formatter.py` | Formats `Violation` lists into human-readable output |
13
13
  | `run_all_validators.py` | Entry point — runs every check module and aggregates results |
14
14
  | `health_check.py` | Verifies that all validator dependencies (ruff, mypy) are reachable |
15
+ | `pyproject_config_discovery.py` | Shared walk-up primitive that resolves a tool's pyproject.toml config from an original target path, matching the `[tool.<name>]` table the tool owns |
15
16
  | `python_style_helpers.py` | Shared source-line splitting and function-discovery helpers imported by `python_style_checks.py` |
16
17
 
17
18
  ## Check modules
@@ -39,6 +40,7 @@ A library of check modules used by the validation hooks. Each module focuses on
39
40
 
40
41
  | Directory | Role |
41
42
  |---|---|
43
+ | `config/` | Constants module (`directory_exemption_constants.py`) holding the exemption directory-segment names that `validate_proposed_file` staging keeps |
42
44
  | `test_files/` | Fixture files used by the validator tests — not checked-in test code |
43
45
 
44
46
  ## Conventions
File without changes
@@ -0,0 +1,183 @@
1
+ """Directory-segment signals that anchor validator temp-path staging.
2
+
3
+ ``validate_proposed_file`` keeps the target's path from the first directory
4
+ segment that either matches an exemption directory name exactly or contains a
5
+ separator-free substring pattern (``test_``, ``conftest``, ``.spec.``, and
6
+ siblings). Exact names come from the authoritative path-exemption pattern sets
7
+ and CLI markers, unioned with the config-directory allowlist. Substring
8
+ patterns come from the separator-free members of those same pattern sets so a
9
+ directory such as ``pkg/test_helpers/`` stages with the same test exemption as
10
+ the real path.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Iterable
16
+
17
+ from hooks_constants.code_rules_enforcer_constants import ALL_CLI_FILE_PATH_MARKERS
18
+ from hooks_constants.code_rules_path_utils_constants import ALL_CONFIG_DIRECTORY_NAMES
19
+
20
+ from ..exempt_paths import (
21
+ HOOK_INFRASTRUCTURE_PATTERNS,
22
+ MIGRATION_PATH_PATTERNS,
23
+ TEST_PATH_PATTERNS,
24
+ WORKFLOW_REGISTRY_PATTERNS,
25
+ )
26
+
27
+ POSIX_DIRECTORY_SEPARATOR = "/"
28
+ WINDOWS_DIRECTORY_SEPARATOR = "\\"
29
+
30
+ ALL_SYSTEM_TEMPORARY_ROOT_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
31
+ {
32
+ "TEMP",
33
+ "TMP",
34
+ "TMPDIR",
35
+ "RUNNER_TEMP",
36
+ }
37
+ )
38
+
39
+ ALL_AUTHORITATIVE_DIRECTORY_PATH_PATTERN_SETS: tuple[Iterable[str], ...] = (
40
+ TEST_PATH_PATTERNS,
41
+ WORKFLOW_REGISTRY_PATTERNS,
42
+ MIGRATION_PATH_PATTERNS,
43
+ HOOK_INFRASTRUCTURE_PATTERNS,
44
+ ALL_CLI_FILE_PATH_MARKERS,
45
+ )
46
+
47
+
48
+ def is_filename_like_path_segment(path_segment: str) -> bool:
49
+ """Return True when *path_segment* looks like a file name, not a directory.
50
+
51
+ ::
52
+
53
+ is_filename_like_path_segment("states.py") # True
54
+ is_filename_like_path_segment(".claude") # False
55
+ is_filename_like_path_segment("tests") # False
56
+
57
+ A leading-dot name is treated as a directory (``.claude``). Any other
58
+ segment that contains a dot is treated as a filename (``states.py``).
59
+
60
+ Args:
61
+ path_segment: One path component with no separators.
62
+
63
+ Returns:
64
+ True when the segment should be ignored as a directory token.
65
+ """
66
+ if path_segment.startswith("."):
67
+ return False
68
+ return "." in path_segment
69
+
70
+
71
+ def all_directory_segments_in_path_pattern(path_pattern: str) -> list[str]:
72
+ """Return lowercased directory tokens from one path-exemption pattern.
73
+
74
+ ::
75
+
76
+ all_directory_segments_in_path_pattern("/.claude/hooks/")
77
+ # ['.claude', 'hooks']
78
+
79
+ all_directory_segments_in_path_pattern("/tests.py")
80
+ # []
81
+
82
+ all_directory_segments_in_path_pattern("conftest")
83
+ # []
84
+
85
+ Separators normalize to POSIX. Filename-like segments and empty splits
86
+ are dropped so only directory tokens remain.
87
+
88
+ Args:
89
+ path_pattern: One entry from an exemption or CLI marker pattern set.
90
+
91
+ Returns:
92
+ Lowercased directory segment names in path order.
93
+ """
94
+ normalized_pattern = path_pattern.replace(
95
+ WINDOWS_DIRECTORY_SEPARATOR, POSIX_DIRECTORY_SEPARATOR
96
+ )
97
+ if POSIX_DIRECTORY_SEPARATOR not in normalized_pattern:
98
+ return []
99
+ all_directory_segments: list[str] = []
100
+ for each_segment in normalized_pattern.split(POSIX_DIRECTORY_SEPARATOR):
101
+ if not each_segment:
102
+ continue
103
+ if is_filename_like_path_segment(each_segment):
104
+ continue
105
+ all_directory_segments.append(each_segment.lower())
106
+ return all_directory_segments
107
+
108
+
109
+ def directory_segment_names_from_path_patterns(
110
+ all_path_patterns: Iterable[str],
111
+ ) -> frozenset[str]:
112
+ """Collect unique directory segment names from path-exemption patterns.
113
+
114
+ ::
115
+
116
+ directory_segment_names_from_path_patterns({"/tests/", "\\workflow\\", "/cli.py"})
117
+ # frozenset({'tests', 'workflow'})
118
+
119
+ Basename markers without a directory token contribute nothing. Adding a
120
+ new ``/foo/`` pattern to an authoritative set surfaces ``foo`` here
121
+ automatically.
122
+
123
+ Args:
124
+ all_path_patterns: Path substrings used by exemption or CLI checks.
125
+
126
+ Returns:
127
+ Lowercased directory segment names present in the patterns.
128
+ """
129
+ all_segment_names: set[str] = set()
130
+ for each_pattern in all_path_patterns:
131
+ all_segment_names.update(all_directory_segments_in_path_pattern(each_pattern))
132
+ return frozenset(all_segment_names)
133
+
134
+
135
+ def substring_patterns_from_path_patterns(
136
+ all_path_patterns: Iterable[str],
137
+ ) -> frozenset[str]:
138
+ """Collect separator-free substring patterns from path-exemption patterns.
139
+
140
+ ::
141
+
142
+ substring_patterns_from_path_patterns({"test_", "/tests/", "conftest"})
143
+ # frozenset({'test_', 'conftest'})
144
+
145
+ ``is_test_file`` and its siblings match these fragments anywhere in a path,
146
+ including inside a directory segment name such as ``pkg/test_helpers/``.
147
+ Patterns carrying a separator are directory tokens handled by
148
+ ``directory_segment_names_from_path_patterns`` and are skipped here.
149
+
150
+ Args:
151
+ all_path_patterns: Path substrings used by exemption or CLI checks.
152
+
153
+ Returns:
154
+ Lowercased separator-free substring patterns.
155
+ """
156
+ all_substring_patterns: set[str] = set()
157
+ for each_pattern in all_path_patterns:
158
+ if (
159
+ POSIX_DIRECTORY_SEPARATOR in each_pattern
160
+ or WINDOWS_DIRECTORY_SEPARATOR in each_pattern
161
+ ):
162
+ continue
163
+ all_substring_patterns.add(each_pattern.lower())
164
+ return frozenset(all_substring_patterns)
165
+
166
+
167
+ ALL_DIRECTORY_SEGMENTS_FROM_PATH_PATTERNS: frozenset[str] = frozenset().union(
168
+ *(
169
+ directory_segment_names_from_path_patterns(each_pattern_set)
170
+ for each_pattern_set in ALL_AUTHORITATIVE_DIRECTORY_PATH_PATTERN_SETS
171
+ )
172
+ )
173
+
174
+ ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES: frozenset[str] = (
175
+ ALL_CONFIG_DIRECTORY_NAMES | ALL_DIRECTORY_SEGMENTS_FROM_PATH_PATTERNS
176
+ )
177
+
178
+ ALL_DIRECTORY_EXEMPTION_SUBSTRING_PATTERNS: frozenset[str] = frozenset().union(
179
+ *(
180
+ substring_patterns_from_path_patterns(each_pattern_set)
181
+ for each_pattern_set in ALL_AUTHORITATIVE_DIRECTORY_PATH_PATTERN_SETS
182
+ )
183
+ )
@@ -0,0 +1,21 @@
1
+ """Tests for the derived directory-exemption segment set."""
2
+
3
+ from hooks_constants.code_rules_path_utils_constants import ALL_CONFIG_DIRECTORY_NAMES
4
+
5
+ from .directory_exemption_constants import ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
6
+
7
+
8
+ def test_exemption_set_covers_every_config_directory_name() -> None:
9
+ missing_names = ALL_CONFIG_DIRECTORY_NAMES - ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
10
+ assert missing_names == frozenset()
11
+
12
+
13
+ def test_exemption_set_covers_constants_package_directories() -> None:
14
+ assert "hooks_constants" in ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
15
+ assert "pr_loop_shared_constants" in ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
16
+ assert "git_hooks_constants" in ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
17
+
18
+
19
+ def test_exemption_set_keeps_path_marker_segments() -> None:
20
+ for each_marker in ("scripts", "tests", "migrations", "workflow", "hooks"):
21
+ assert each_marker in ALL_DIRECTORY_EXEMPTION_SEGMENT_NAMES
@@ -5,6 +5,10 @@ from pathlib import Path
5
5
 
6
6
 
7
7
  VALIDATORS_DIRECTORY = Path(__file__).resolve().parent
8
+ HOOKS_DIRECTORY = VALIDATORS_DIRECTORY.parent
8
9
 
9
10
  if str(VALIDATORS_DIRECTORY) not in sys.path:
10
11
  sys.path.insert(0, str(VALIDATORS_DIRECTORY))
12
+
13
+ if str(HOOKS_DIRECTORY) not in sys.path:
14
+ sys.path.insert(0, str(HOOKS_DIRECTORY))