claude-dev-env 2.4.0 → 2.7.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 (309) hide show
  1. package/CLAUDE.md +26 -59
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  13. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  16. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  18. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  19. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  20. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  21. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  22. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  23. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  24. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  25. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  26. package/_shared/pr-loop/worker-spawn.md +1 -1
  27. package/agents/CLAUDE.md +3 -2
  28. package/agents/caveman.md +0 -1
  29. package/agents/clasp-deployment-orchestrator.md +0 -1
  30. package/agents/clean-coder.md +0 -1
  31. package/agents/code-advisor.md +0 -1
  32. package/agents/code-quality-agent.md +1 -2
  33. package/agents/code-verifier.md +36 -8
  34. package/agents/deep-research.md +0 -1
  35. package/agents/docs-agent.md +0 -1
  36. package/agents/git-commit-crafter.md +0 -1
  37. package/agents/issue-tracker.md +42 -0
  38. package/agents/plan-packet-validator.md +0 -1
  39. package/agents/pr-description-writer.md +0 -1
  40. package/agents/test_agent_frontmatter.py +67 -18
  41. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  42. package/bin/CLAUDE.md +68 -5
  43. package/bin/codex-compat.mjs +104 -0
  44. package/bin/codex-compat.test.mjs +51 -0
  45. package/bin/ever-shipped-skills.mjs +1 -0
  46. package/bin/install-constants.mjs +88 -0
  47. package/bin/install.mjs +1138 -114
  48. package/bin/install.prune.test.mjs +869 -19
  49. package/bin/install.test.mjs +906 -2
  50. package/codex-capability-map.json +13 -0
  51. package/commands/implement.md +1 -1
  52. package/commands/right-size.md +1 -1
  53. package/docs/CLAUDE.md +1 -0
  54. package/docs/CODE_RULES.md +2 -0
  55. package/docs/codex-compatibility.md +25 -0
  56. package/docs/host-pool-health-monitor.md +102 -0
  57. package/docs/nas-ssh-invocation.md +96 -12
  58. package/docs/references/CLAUDE.md +4 -2
  59. package/docs/references/advisor-tool.md +13 -0
  60. package/docs/references/code-review-enforcement.md +35 -0
  61. package/docs/references/team-advisor-skill.md +14 -0
  62. package/hooks/blocking/CLAUDE.md +4 -0
  63. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  64. package/hooks/blocking/code_review_push_gate.py +9 -4
  65. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  66. package/hooks/blocking/config/__init__.py +5 -5
  67. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
  68. package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
  69. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  70. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  71. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  72. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  73. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  74. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  75. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  76. package/hooks/blocking/state_description_blocker.py +1 -1
  77. package/hooks/blocking/stop_dispatcher.py +1 -1
  78. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  79. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  80. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  81. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  82. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  83. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  84. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  85. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  86. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  87. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  88. package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
  89. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  90. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  91. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  92. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  93. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  94. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  95. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  96. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  97. package/hooks/git-hooks/CLAUDE.md +3 -0
  98. package/hooks/git-hooks/conftest.py +30 -0
  99. package/hooks/git-hooks/gate_utils.py +2 -2
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  101. package/hooks/git-hooks/pre_push.py +75 -4
  102. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  103. package/hooks/git-hooks/test_config.py +0 -15
  104. package/hooks/git-hooks/test_gate_utils.py +3 -15
  105. package/hooks/git-hooks/test_pre_commit.py +1 -15
  106. package/hooks/git-hooks/test_pre_push.py +257 -23
  107. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  108. package/hooks/hooks.json +10 -12
  109. package/hooks/hooks_constants/CLAUDE.md +7 -2
  110. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  111. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  112. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  113. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  114. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  115. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  116. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  117. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  118. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  119. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  120. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  121. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  122. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  123. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  124. package/hooks/pyproject.toml +1 -0
  125. package/hooks/validators/CLAUDE.md +1 -0
  126. package/hooks/validators/config/__init__.py +0 -0
  127. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  128. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  129. package/hooks/validators/conftest.py +4 -0
  130. package/hooks/validators/ruff_integration.py +49 -5
  131. package/hooks/validators/run_all_validators.py +206 -9
  132. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +92 -1
  135. package/hooks/validators/test_run_all_validators.py +115 -68
  136. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  137. package/package.json +13 -3
  138. package/rules/CLAUDE.md +17 -22
  139. package/rules/agent-spawn-protocol.md +6 -6
  140. package/rules/anti-corollary-tests.md +1 -1
  141. package/rules/bdd.md +1 -1
  142. package/rules/cleanup-temp-files.md +10 -4
  143. package/rules/code-standards.md +7 -0
  144. package/rules/conservative-action.md +1 -5
  145. package/rules/context7.md +0 -4
  146. package/rules/destructive-commands.md +47 -0
  147. package/rules/doc-inventory-integrity.md +48 -0
  148. package/rules/doc-prose-cuts.md +58 -0
  149. package/rules/docstring-prose-matches-implementation.md +53 -44
  150. package/rules/durable-post-artifacts.md +0 -4
  151. package/rules/eli11-replies.md +31 -0
  152. package/rules/explore-thoroughly.md +4 -4
  153. package/rules/falsify-before-green.md +68 -0
  154. package/rules/file-global-constants.md +1 -1
  155. package/rules/filesystem-search.md +51 -0
  156. package/rules/gh-cli-conventions.md +27 -0
  157. package/rules/git-workflow.md +26 -0
  158. package/rules/hedging-claims.md +9 -0
  159. package/rules/long-horizon-autonomy.md +0 -4
  160. package/rules/measurement-denominators.md +48 -0
  161. package/rules/nas-ssh-invocation.md +23 -5
  162. package/rules/parallel-tools.md +2 -2
  163. package/rules/plain-illustrative-docstrings.md +3 -7
  164. package/rules/plain-language.md +2 -0
  165. package/rules/proof-of-work-pr-comments.md +0 -4
  166. package/rules/re-stage-before-commit.md +2 -0
  167. package/rules/research-mode.md +10 -0
  168. package/rules/shell-invocation.md +21 -0
  169. package/rules/testing.md +4 -0
  170. package/rules/verified-commit-gate-skip.md +3 -27
  171. package/rules/verify-before-asking.md +5 -0
  172. package/rules/windows-filesystem-safe.md +1 -1
  173. package/rules/workers-done-before-complete.md +4 -0
  174. package/scripts/CLAUDE.md +1 -0
  175. package/scripts/Capture-PoolHealth.ps1 +410 -0
  176. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  177. package/scripts/_code_review_test_support.py +404 -0
  178. package/scripts/claude_chain_runner.py +141 -1
  179. package/scripts/codex_capability_bridge.py +171 -0
  180. package/scripts/codex_compat_materializer.py +1087 -0
  181. package/scripts/codex_compat_watcher.py +502 -0
  182. package/scripts/conftest.py +16 -1
  183. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  184. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  185. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  186. package/scripts/invoke_code_review.py +11 -4
  187. package/scripts/resolve_worker_spawn.py +626 -626
  188. package/scripts/spawn_grok_batch.py +672 -672
  189. package/scripts/sync_to_cursor/rules.py +0 -10
  190. package/scripts/test_claude_chain_runner.py +131 -0
  191. package/scripts/test_invoke_code_review.py +85 -908
  192. package/scripts/test_invoke_code_review_chain.py +70 -0
  193. package/scripts/test_invoke_code_review_cli.py +192 -0
  194. package/scripts/test_invoke_code_review_contract.py +256 -0
  195. package/scripts/test_invoke_code_review_git.py +123 -0
  196. package/scripts/test_invoke_code_review_mode.py +99 -0
  197. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  198. package/scripts/tests/test_code_review_constants.py +80 -0
  199. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  200. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  201. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  202. package/scripts/tests/test_sync_to_cursor.py +0 -1
  203. package/skills/CLAUDE.md +2 -0
  204. package/skills/auditing-claude-config/SKILL.md +114 -114
  205. package/skills/autoconverge/SKILL.md +427 -427
  206. package/skills/autoconverge/reference/convergence.md +24 -3
  207. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  208. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  209. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  210. package/skills/autoconverge/workflow/converge.mjs +168 -1
  211. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  212. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  213. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  214. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  215. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  216. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  217. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  218. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  219. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  220. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  221. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  222. package/skills/closeout/SKILL.md +33 -50
  223. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  224. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  225. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  226. package/skills/condensing-instructions/SKILL.md +72 -0
  227. package/skills/copilot-review/SKILL.md +119 -119
  228. package/skills/e-code-review/SKILL.md +52 -0
  229. package/skills/e-code-review/reference/fix.md +54 -0
  230. package/skills/e-code-review/reference/loop.md +43 -0
  231. package/skills/e-code-review/reference/low.md +57 -0
  232. package/skills/e-code-review/reference/medium.md +153 -0
  233. package/skills/e-code-review/reference/xhigh.md +182 -0
  234. package/skills/e-simplify/SKILL.md +97 -0
  235. package/skills/fresh-branch/CLAUDE.md +1 -1
  236. package/skills/fresh-branch/SKILL.md +5 -6
  237. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  238. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  239. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  240. package/skills/issue-tracker/SKILL.md +92 -0
  241. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  242. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  243. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  244. package/skills/orchestrator/SKILL.md +177 -22
  245. package/skills/orchestrator/scripts/status_gate.py +625 -0
  246. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  247. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  248. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  249. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  250. package/skills/orchestrator-refresh/SKILL.md +129 -35
  251. package/skills/plan-to-pr/SKILL.md +155 -0
  252. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  253. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  254. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  255. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  256. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  257. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  258. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  259. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  260. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  261. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  262. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  263. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  264. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  265. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  266. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  267. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  268. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  269. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  270. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  271. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  272. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  273. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  274. package/skills/pr-converge/SKILL.md +472 -469
  275. package/skills/pr-converge/reference/examples.md +3 -3
  276. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  277. package/skills/pr-converge/reference/ground-rules.md +7 -4
  278. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  279. package/skills/pr-converge/reference/per-tick.md +5 -5
  280. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  281. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  282. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  283. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  284. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  285. package/skills/pr-fix-protocol/SKILL.md +1 -1
  286. package/skills/privacy-hygiene/SKILL.md +68 -68
  287. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  288. package/skills/prototype/workflows/promotion.md +1 -1
  289. package/skills/release-notes-html/SKILL.md +164 -0
  290. package/skills/session-log/SKILL.md +1 -1
  291. package/skills/task-build/CLAUDE.md +8 -7
  292. package/skills/task-build/SKILL.md +16 -8
  293. package/skills/task-build/reference/tool-routing.md +19 -0
  294. package/rules/claude-md-orphan-file.md +0 -28
  295. package/rules/cleanup-command-forms.md +0 -23
  296. package/rules/code-reviews.md +0 -11
  297. package/rules/env-var-table-code-drift.md +0 -10
  298. package/rules/gh-body-file.md +0 -5
  299. package/rules/gh-paginate.md +0 -3
  300. package/rules/hook-prose-matches-detector.md +0 -15
  301. package/rules/no-historical-clutter.md +0 -26
  302. package/rules/no-inline-destructive-literals.md +0 -9
  303. package/rules/no-justification-noise.md +0 -61
  304. package/rules/package-inventory-stale-entry.md +0 -25
  305. package/rules/right-sized-engineering.md +0 -28
  306. package/rules/self-contained-docs.md +0 -17
  307. package/rules/shell-invocation-policy.md +0 -5
  308. package/rules/tdd.md +0 -7
  309. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -7,15 +7,22 @@ from pr_loop_shared_constants.preflight_constants import GIT_DIRECTORY_NAME
7
7
  __all__ = (
8
8
  "ALL_AGENT_CONFIG_DENY_TOOLS",
9
9
  "ALL_AGENT_CONFIG_PATH_PATTERNS",
10
+ "ALL_INERT_FILE_PERMISSION_TOOLS",
11
+ "ALL_LEGACY_PERMISSION_REAP_TOOLS",
10
12
  "ALL_PERMISSION_ALLOW_TOOLS",
11
13
  "ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS",
12
14
  "AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX",
13
15
  "AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE",
14
16
  "CLAUDE_SETTINGS_DIRECTORY_NAME",
15
17
  "CLAUDE_SETTINGS_FILENAME",
18
+ "CLAUDE_SETTINGS_LOCAL_FILENAME",
16
19
  "GIT_DIRECTORY_NAME",
20
+ "HOME_PROJECT_PATH_ALIAS",
21
+ "INERT_RULES_REMOVED_LOG_PREFIX",
22
+ "PROJECT_LOCAL_INERT_RULES_REMOVED_LOG_PREFIX",
17
23
  "TEXT_FILE_ENCODING",
18
24
  "UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH",
25
+ "get_claude_project_local_settings_path",
19
26
  "get_claude_user_settings_path",
20
27
  )
21
28
 
@@ -24,6 +31,20 @@ ALL_PERMISSION_ALLOW_TOOLS: tuple[str, ...] = ("Edit", "Read")
24
31
 
25
32
  ALL_AGENT_CONFIG_DENY_TOOLS: tuple[str, ...] = ("Edit", "Read")
26
33
 
34
+ ALL_LEGACY_PERMISSION_REAP_TOOLS: tuple[str, ...] = ("Write", "Glob")
35
+
36
+ ALL_INERT_FILE_PERMISSION_TOOLS: tuple[str, ...] = ("Write", "Glob", "NotebookEdit")
37
+
38
+ HOME_PROJECT_PATH_ALIAS: str = "$HOME"
39
+
40
+ INERT_RULES_REMOVED_LOG_PREFIX: str = (
41
+ "Inert Write/Glob/NotebookEdit rules removed: "
42
+ )
43
+
44
+ PROJECT_LOCAL_INERT_RULES_REMOVED_LOG_PREFIX: str = (
45
+ "Project-local inert Write/Glob/NotebookEdit rules removed: "
46
+ )
47
+
27
48
  ALL_AGENT_CONFIG_PATH_PATTERNS: tuple[str, ...] = (
28
49
  "settings*.json",
29
50
  "hooks/**",
@@ -81,6 +102,8 @@ CLAUDE_SETTINGS_DIRECTORY_NAME: str = ".claude"
81
102
 
82
103
  CLAUDE_SETTINGS_FILENAME: str = "settings.json"
83
104
 
105
+ CLAUDE_SETTINGS_LOCAL_FILENAME: str = "settings.local.json"
106
+
84
107
  TEXT_FILE_ENCODING: str = "utf-8"
85
108
 
86
109
  UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH: int = 8
@@ -88,3 +111,24 @@ UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH: int = 8
88
111
 
89
112
  def get_claude_user_settings_path() -> Path:
90
113
  return Path.home() / CLAUDE_SETTINGS_DIRECTORY_NAME / CLAUDE_SETTINGS_FILENAME
114
+
115
+
116
+ def get_claude_project_local_settings_path(
117
+ project_root_path: Path | None = None,
118
+ ) -> Path:
119
+ """Return ``<project>/.claude/settings.local.json`` for the project root.
120
+
121
+ Args:
122
+ project_root_path: Project root; defaults to the process cwd.
123
+
124
+ Returns:
125
+ Path to the project-local Claude settings file (may not exist yet).
126
+ """
127
+ resolved_project_root_path = (
128
+ project_root_path if project_root_path is not None else Path.cwd()
129
+ )
130
+ return (
131
+ resolved_project_root_path
132
+ / CLAUDE_SETTINGS_DIRECTORY_NAME
133
+ / CLAUDE_SETTINGS_LOCAL_FILENAME
134
+ )
@@ -146,3 +146,205 @@ STAGED_TEST_GROUP_FAILURE_MESSAGE: str = (
146
146
  MINIMUM_STAGED_PYTEST_PYTHON_MAJOR: int = 3
147
147
 
148
148
  MINIMUM_STAGED_PYTEST_PYTHON_MINOR: int = 12
149
+
150
+ JUNIT_XML_FLAG_PREFIX: str = "--junitxml="
151
+
152
+ JUNIT_XML_TESTCASE_TAG: str = "testcase"
153
+
154
+ JUNIT_XML_FAILURE_TAG: str = "failure"
155
+
156
+ JUNIT_XML_ERROR_TAG: str = "error"
157
+
158
+ JUNIT_XML_CLASSNAME_ATTRIBUTE: str = "classname"
159
+
160
+ JUNIT_XML_NAME_ATTRIBUTE: str = "name"
161
+
162
+ JUNIT_XML_MISSING_ATTRIBUTE_FALLBACK: str = ""
163
+
164
+ REGRESSION_JUNIT_TEMP_DIRECTORY_PREFIX: str = "code_rules_gate_junit_"
165
+
166
+ REGRESSION_STAGED_JUNIT_SUBDIRECTORY_NAME: str = "staged"
167
+
168
+ REGRESSION_BASELINE_JUNIT_SUBDIRECTORY_NAME: str = "baseline"
169
+
170
+ REGRESSION_BASELINE_WORKTREE_TEMP_DIRECTORY_PREFIX: str = "code_rules_gate_baseline_"
171
+
172
+ REGRESSION_BASELINE_WORKTREE_DIRECTORY_NAME: str = "tree"
173
+
174
+ GIT_HEAD_REVISION: str = "HEAD"
175
+
176
+ ALL_GIT_HEAD_EXISTS_ARGS: tuple[str, ...] = ("rev-parse", "--verify", "HEAD")
177
+
178
+ ALL_GIT_WORKTREE_ADD_DETACH_ARGS: tuple[str, ...] = (
179
+ "worktree",
180
+ "add",
181
+ "--detach",
182
+ "--quiet",
183
+ )
184
+
185
+ ALL_GIT_WORKTREE_REMOVE_FORCE_ARGS: tuple[str, ...] = ("worktree", "remove", "--force")
186
+
187
+ ALL_GIT_WORKTREE_PRUNE_ARGS: tuple[str, ...] = ("worktree", "prune")
188
+
189
+ REGRESSION_NO_BASELINE_MESSAGE: str = (
190
+ "code_rules_gate: no prior commit to compare against (first commit on this branch); "
191
+ "every staged test failure blocks."
192
+ )
193
+
194
+ REGRESSION_WORKTREE_ADD_FAILED_MESSAGE: str = (
195
+ "code_rules_gate: could not open a detached HEAD worktree for the pre-staged baseline "
196
+ "(git worktree add --detach failed); falling back to blocking on every staged test "
197
+ "failure."
198
+ )
199
+
200
+ REGRESSION_WORKTREE_REMOVE_FAILED_MESSAGE: str = (
201
+ "code_rules_gate: git worktree remove --force failed after the baseline check — your "
202
+ "staged changes are untouched in this worktree. Run 'git worktree list' and "
203
+ "'git worktree prune' if a temporary baseline tree remains registered."
204
+ )
205
+
206
+ REGRESSION_PRE_EXISTING_FAILURE_BYPASSED_MESSAGE: str = (
207
+ "code_rules_gate: staged test group rooted at {group_root} has {count} failure(s) "
208
+ "already present before this change (not caused by it); not blocking."
209
+ )
210
+
211
+ REGRESSION_GROUP_FAILURE_MESSAGE: str = (
212
+ "code_rules_gate: staged test group rooted at {group_root} has {count} failure(s) "
213
+ "this change introduces; commit blocked."
214
+ )
215
+
216
+ BASELINE_LEAK_PLUGIN_DIRECTORY_NAME: str = "baseline_import_plugin"
217
+
218
+ BASELINE_LEAK_PLUGIN_MODULE_NAME: str = "code_rules_gate_baseline_import_plugin"
219
+
220
+ BASELINE_LEAK_REPORT_FILENAME: str = "imported_from_primary_tree.json"
221
+
222
+ BASELINE_PRIMARY_ROOT_ENV_VAR: str = "CODE_RULES_GATE_BASELINE_PRIMARY_ROOT"
223
+
224
+ BASELINE_LEAK_REPORT_ENV_VAR: str = "CODE_RULES_GATE_BASELINE_LEAK_REPORT"
225
+
226
+ PYTEST_PLUGINS_ENV_VAR: str = "PYTEST_PLUGINS"
227
+
228
+ PYTEST_PLUGINS_SEPARATOR: str = ","
229
+
230
+ BASELINE_IMPORT_PROBE_TIMEOUT_SECONDS: int = 120
231
+
232
+ BASELINE_IMPORT_PROBE_DEGRADED_TAIL: str = (
233
+ "The HEAD baseline runs with its import roots un-rebased; a module that still "
234
+ "resolves into your working tree is reported after that run and blocks the group."
235
+ )
236
+
237
+ BASELINE_IMPORT_PROBE_TIMED_OUT_MESSAGE: str = (
238
+ "code_rules_gate: the import-root probe did not finish within {seconds} seconds. "
239
+ + BASELINE_IMPORT_PROBE_DEGRADED_TAIL
240
+ )
241
+
242
+ BASELINE_IMPORT_PROBE_UNSTARTABLE_MESSAGE: str = (
243
+ "code_rules_gate: the import-root probe did not start ({reason}). "
244
+ + BASELINE_IMPORT_PROBE_DEGRADED_TAIL
245
+ )
246
+
247
+ BASELINE_IMPORT_PROBE_REJECTED_MESSAGE: str = (
248
+ "code_rules_gate: the import-root probe exited with status {status}. "
249
+ + BASELINE_IMPORT_PROBE_DEGRADED_TAIL
250
+ )
251
+
252
+ PYTHON_INTERPRETER_COMMAND_FLAG: str = "-c"
253
+
254
+ BASELINE_IMPORT_ROOT_PROBE_SOURCE: str = (
255
+ "import json\n"
256
+ "import sys\n"
257
+ "from pathlib import Path\n"
258
+ "all_roots = [each_entry for each_entry in sys.path if each_entry]\n"
259
+ "for each_finder in list(sys.meta_path):\n"
260
+ " owning_module = sys.modules.get(getattr(each_finder, '__module__', '') or '')\n"
261
+ " for each_attribute in ('MAPPING', 'NAMESPACES'):\n"
262
+ " mapping = getattr(owning_module, each_attribute, None)\n"
263
+ " if not isinstance(mapping, dict):\n"
264
+ " continue\n"
265
+ " for each_name, each_target in mapping.items():\n"
266
+ " all_targets = each_target if isinstance(each_target, list) else [each_target]\n"
267
+ " for each_package_directory in all_targets:\n"
268
+ " try:\n"
269
+ " all_roots.append(\n"
270
+ " str(Path(each_package_directory).parents[str(each_name).count('.')])\n"
271
+ " )\n"
272
+ " except (IndexError, TypeError, OSError):\n"
273
+ " continue\n"
274
+ "print(json.dumps(all_roots))\n"
275
+ )
276
+
277
+ BASELINE_LEAK_PLUGIN_SOURCE: str = '''"""Report every module the baseline pytest run imported out of the user's own tree."""
278
+
279
+ import json
280
+ import os
281
+ import sys
282
+ import sysconfig
283
+ from pathlib import Path
284
+
285
+ PRIMARY_ROOT_ENV_VAR = "{primary_root_env_var}"
286
+ REPORT_PATH_ENV_VAR = "{report_path_env_var}"
287
+
288
+
289
+ def _resolved(path_text):
290
+ if not path_text:
291
+ return None
292
+ try:
293
+ return Path(path_text).resolve()
294
+ except OSError:
295
+ return None
296
+
297
+
298
+ def _is_under(candidate, root):
299
+ return candidate == root or root in candidate.parents
300
+
301
+
302
+ def _interpreter_roots():
303
+ all_texts = [sys.prefix, sys.base_prefix, *sysconfig.get_paths().values()]
304
+ all_roots = [_resolved(each_text) for each_text in all_texts]
305
+ return [each_root for each_root in all_roots if each_root is not None]
306
+
307
+
308
+ def _already_reported(report_path):
309
+ try:
310
+ return set(json.loads(report_path.read_text(encoding="utf-8")))
311
+ except (OSError, ValueError):
312
+ return set()
313
+
314
+
315
+ def _imported_from_primary_tree(primary_root, report_path):
316
+ all_interpreter_roots = _interpreter_roots()
317
+ all_leaked = _already_reported(report_path)
318
+ for each_module in list(sys.modules.values()):
319
+ module_path = _resolved(getattr(each_module, "__file__", None))
320
+ if module_path is None or not _is_under(module_path, primary_root):
321
+ continue
322
+ if any(_is_under(module_path, each_root) for each_root in all_interpreter_roots):
323
+ continue
324
+ all_leaked.add(str(module_path))
325
+ return sorted(all_leaked)
326
+
327
+
328
+ def pytest_sessionfinish(session, exitstatus):
329
+ primary_root = _resolved(os.environ.get(PRIMARY_ROOT_ENV_VAR))
330
+ report_path = _resolved(os.environ.get(REPORT_PATH_ENV_VAR))
331
+ if primary_root is None or report_path is None:
332
+ return
333
+ report_path.write_text(
334
+ json.dumps(_imported_from_primary_tree(primary_root, report_path)), encoding="utf-8"
335
+ )
336
+ '''
337
+
338
+ REGRESSION_BASELINE_IMPORT_LEAK_MESSAGE: str = (
339
+ "code_rules_gate: the HEAD baseline run for the group rooted at {group_root} imported "
340
+ "{count} module(s) from your working tree, starting with {first_module}. It measured the "
341
+ "staged code, not HEAD, so that baseline is not trusted and every staged failure in this "
342
+ "group blocks. An editable install whose import hook runs ahead of the path scan is the "
343
+ "usual cause."
344
+ )
345
+
346
+ REGRESSION_BASELINE_LEAK_UNREPORTED_MESSAGE: str = (
347
+ "code_rules_gate: the HEAD baseline run for the group rooted at {group_root} wrote no "
348
+ "import-origin report, so whether it loaded your working-tree code is unknown. That "
349
+ "baseline is not trusted and every staged failure in this group blocks."
350
+ )
@@ -1,24 +1,24 @@
1
- """Configuration constants for the Copilot premium-interaction quota pre-check."""
2
-
3
- from __future__ import annotations
4
-
5
- from pathlib import Path
6
-
7
- COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME: str = "COPILOT_QUOTA_ACCOUNT"
8
- GH_TOKEN_ENV_VAR_NAME: str = "GH_TOKEN"
9
- COPILOT_INTERNAL_USER_API_PATH: str = "copilot_internal/user"
10
-
11
- QUOTA_SNAPSHOTS_FIELD_NAME: str = "quota_snapshots"
12
- PREMIUM_INTERACTIONS_FIELD_NAME: str = "premium_interactions"
13
- PREMIUM_UNLIMITED_FIELD_NAME: str = "unlimited"
14
- PREMIUM_REMAINING_FIELD_NAME: str = "remaining"
15
- PREMIUM_OVERAGE_PERMITTED_FIELD_NAME: str = "overage_permitted"
16
- PREMIUM_ENTITLEMENT_FIELD_NAME: str = "entitlement"
17
- PREMIUM_PERCENT_REMAINING_FIELD_NAME: str = "percent_remaining"
18
-
19
- EXIT_CODE_QUOTA_AVAILABLE: int = 0
20
- EXIT_CODE_OUT_OF_QUOTA: int = 1
21
- EXIT_CODE_QUOTA_API_DOWN: int = 2
22
- EXIT_CODE_NO_ACCOUNT_CONFIGURED: int = 3
23
-
24
- COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH: Path = Path(__file__).resolve().parents[4] / ".env"
1
+ """Configuration constants for the Copilot premium-interaction quota pre-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME: str = "COPILOT_QUOTA_ACCOUNT"
8
+ GH_TOKEN_ENV_VAR_NAME: str = "GH_TOKEN"
9
+ COPILOT_INTERNAL_USER_API_PATH: str = "copilot_internal/user"
10
+
11
+ QUOTA_SNAPSHOTS_FIELD_NAME: str = "quota_snapshots"
12
+ PREMIUM_INTERACTIONS_FIELD_NAME: str = "premium_interactions"
13
+ PREMIUM_UNLIMITED_FIELD_NAME: str = "unlimited"
14
+ PREMIUM_REMAINING_FIELD_NAME: str = "remaining"
15
+ PREMIUM_OVERAGE_PERMITTED_FIELD_NAME: str = "overage_permitted"
16
+ PREMIUM_ENTITLEMENT_FIELD_NAME: str = "entitlement"
17
+ PREMIUM_PERCENT_REMAINING_FIELD_NAME: str = "percent_remaining"
18
+
19
+ EXIT_CODE_QUOTA_AVAILABLE: int = 0
20
+ EXIT_CODE_OUT_OF_QUOTA: int = 1
21
+ EXIT_CODE_QUOTA_API_DOWN: int = 2
22
+ EXIT_CODE_NO_ACCOUNT_CONFIGURED: int = 3
23
+
24
+ COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH: Path = Path(__file__).resolve().parents[4] / ".env"
@@ -1,107 +1,107 @@
1
- """Constants and rule-format readers for stale_worktree_rule_sweep.
2
-
3
- The worktrees root is derived from the same ~/.claude home the permission
4
- grant script mints its rules under, so the location is named in exactly one
5
- place. A worktree rule points at a directory one or more segments below that
6
- root (flat or nested layouts)::
7
-
8
- Edit(<claude home>/worktrees/<worktree name>/.claude/**)
9
- ^^^^^^^^^^^^^^^
10
- segment 1 (flat)
11
-
12
- Edit(<claude home>/worktrees/<repository>/<worktree name>/.claude/**)
13
- ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
14
- nested layout
15
-
16
- worktree_directory_for_rule(rule, root) -> project directory under root
17
-
18
- A rule whose target path lies outside the worktrees root reads back as None,
19
- so the sweep leaves it untouched.
20
- """
21
-
22
- from pathlib import Path
23
-
24
- from pr_loop_shared_constants.claude_permissions_constants import (
25
- get_claude_user_settings_path,
26
- )
27
-
28
- WORKTREES_SUBDIRECTORY_NAME: str = "worktrees"
29
-
30
- MINIMUM_WORKTREE_PATH_SEGMENT_COUNT: int = 1
31
-
32
- RULE_PATH_OPEN_DELIMITER: str = "("
33
-
34
- RULE_PATH_CLOSE_DELIMITER: str = ")"
35
-
36
- CLAUDE_HOME_PATH_MARKER: str = "/.claude"
37
-
38
-
39
- def get_claude_worktrees_root() -> Path:
40
- """Return the ~/.claude/worktrees directory the grant rules point under.
41
-
42
- Reuses get_claude_user_settings_path so the ~/.claude home is resolved
43
- in one place: the settings file's parent directory is the home, and the
44
- worktrees live directly beneath it.
45
-
46
- Returns:
47
- The absolute path to the user's worktrees root directory.
48
- """
49
- claude_home_directory = get_claude_user_settings_path().parent
50
- return claude_home_directory / WORKTREES_SUBDIRECTORY_NAME
51
-
52
-
53
- def extract_rule_target_path(rule: str) -> str | None:
54
- """Return the path a permission rule targets, or None when unparsable.
55
-
56
- A rule reads `Tool(<path>)`; the target is the text between the first
57
- open delimiter and the last close delimiter::
58
-
59
- Edit(/repo/wt/.claude/**) -> /repo/wt/.claude/**
60
- not a rule -> None
61
-
62
- Args:
63
- rule: The permission rule string to read.
64
-
65
- Returns:
66
- The target path string, or None when the delimiters are absent.
67
- """
68
- open_index = rule.find(RULE_PATH_OPEN_DELIMITER)
69
- close_index = rule.rfind(RULE_PATH_CLOSE_DELIMITER)
70
- if open_index == -1 or close_index <= open_index:
71
- return None
72
- return rule[open_index + 1 : close_index]
73
-
74
-
75
- def worktree_directory_for_rule(rule: str, worktrees_root: Path) -> Path | None:
76
- """Return the worktree directory a rule targets, or None when it is not one.
77
-
78
- Keeps every path segment below the worktrees root so flat
79
- (`worktrees/<name>`) and nested (`worktrees/<repo>/<name>`) layouts
80
- both resolve to the on-disk project directory::
81
-
82
- Edit(<root>/flat-wt/.claude/**) -> <root>/flat-wt
83
- Edit(<root>/repo/wt/.claude/**) -> <root>/repo/wt
84
- Edit(/elsewhere/project/.claude/**) -> None
85
-
86
- Args:
87
- rule: The permission rule string to read.
88
- worktrees_root: The ~/.claude/worktrees directory rules live under.
89
-
90
- Returns:
91
- The worktree directory, or None when the target lies outside the
92
- worktrees root or has no path segments under it.
93
- """
94
- target_path = extract_rule_target_path(rule)
95
- if target_path is None:
96
- return None
97
- project_portion = target_path.rsplit(CLAUDE_HOME_PATH_MARKER, 1)[0]
98
- if not project_portion:
99
- return None
100
- try:
101
- relative_path = Path(project_portion).relative_to(worktrees_root)
102
- except ValueError:
103
- return None
104
- all_segments = relative_path.parts
105
- if len(all_segments) < MINIMUM_WORKTREE_PATH_SEGMENT_COUNT:
106
- return None
107
- return worktrees_root.joinpath(*all_segments)
1
+ """Constants and rule-format readers for stale_worktree_rule_sweep.
2
+
3
+ The worktrees root is derived from the same ~/.claude home the permission
4
+ grant script mints its rules under, so the location is named in exactly one
5
+ place. A worktree rule points at a directory one or more segments below that
6
+ root (flat or nested layouts)::
7
+
8
+ Edit(<claude home>/worktrees/<worktree name>/.claude/**)
9
+ ^^^^^^^^^^^^^^^
10
+ segment 1 (flat)
11
+
12
+ Edit(<claude home>/worktrees/<repository>/<worktree name>/.claude/**)
13
+ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
14
+ nested layout
15
+
16
+ worktree_directory_for_rule(rule, root) -> project directory under root
17
+
18
+ A rule whose target path lies outside the worktrees root reads back as None,
19
+ so the sweep leaves it untouched.
20
+ """
21
+
22
+ from pathlib import Path
23
+
24
+ from pr_loop_shared_constants.claude_permissions_constants import (
25
+ get_claude_user_settings_path,
26
+ )
27
+
28
+ WORKTREES_SUBDIRECTORY_NAME: str = "worktrees"
29
+
30
+ MINIMUM_WORKTREE_PATH_SEGMENT_COUNT: int = 1
31
+
32
+ RULE_PATH_OPEN_DELIMITER: str = "("
33
+
34
+ RULE_PATH_CLOSE_DELIMITER: str = ")"
35
+
36
+ CLAUDE_HOME_PATH_MARKER: str = "/.claude"
37
+
38
+
39
+ def get_claude_worktrees_root() -> Path:
40
+ """Return the ~/.claude/worktrees directory the grant rules point under.
41
+
42
+ Reuses get_claude_user_settings_path so the ~/.claude home is resolved
43
+ in one place: the settings file's parent directory is the home, and the
44
+ worktrees live directly beneath it.
45
+
46
+ Returns:
47
+ The absolute path to the user's worktrees root directory.
48
+ """
49
+ claude_home_directory = get_claude_user_settings_path().parent
50
+ return claude_home_directory / WORKTREES_SUBDIRECTORY_NAME
51
+
52
+
53
+ def extract_rule_target_path(rule: str) -> str | None:
54
+ """Return the path a permission rule targets, or None when unparsable.
55
+
56
+ A rule reads `Tool(<path>)`; the target is the text between the first
57
+ open delimiter and the last close delimiter::
58
+
59
+ Edit(/repo/wt/.claude/**) -> /repo/wt/.claude/**
60
+ not a rule -> None
61
+
62
+ Args:
63
+ rule: The permission rule string to read.
64
+
65
+ Returns:
66
+ The target path string, or None when the delimiters are absent.
67
+ """
68
+ open_index = rule.find(RULE_PATH_OPEN_DELIMITER)
69
+ close_index = rule.rfind(RULE_PATH_CLOSE_DELIMITER)
70
+ if open_index == -1 or close_index <= open_index:
71
+ return None
72
+ return rule[open_index + 1 : close_index]
73
+
74
+
75
+ def worktree_directory_for_rule(rule: str, worktrees_root: Path) -> Path | None:
76
+ """Return the worktree directory a rule targets, or None when it is not one.
77
+
78
+ Keeps every path segment below the worktrees root so flat
79
+ (`worktrees/<name>`) and nested (`worktrees/<repo>/<name>`) layouts
80
+ both resolve to the on-disk project directory::
81
+
82
+ Edit(<root>/flat-wt/.claude/**) -> <root>/flat-wt
83
+ Edit(<root>/repo/wt/.claude/**) -> <root>/repo/wt
84
+ Edit(/elsewhere/project/.claude/**) -> None
85
+
86
+ Args:
87
+ rule: The permission rule string to read.
88
+ worktrees_root: The ~/.claude/worktrees directory rules live under.
89
+
90
+ Returns:
91
+ The worktree directory, or None when the target lies outside the
92
+ worktrees root or has no path segments under it.
93
+ """
94
+ target_path = extract_rule_target_path(rule)
95
+ if target_path is None:
96
+ return None
97
+ project_portion = target_path.rsplit(CLAUDE_HOME_PATH_MARKER, 1)[0]
98
+ if not project_portion:
99
+ return None
100
+ try:
101
+ relative_path = Path(project_portion).relative_to(worktrees_root)
102
+ except ValueError:
103
+ return None
104
+ all_segments = relative_path.parts
105
+ if len(all_segments) < MINIMUM_WORKTREE_PATH_SEGMENT_COUNT:
106
+ return None
107
+ return worktrees_root.joinpath(*all_segments)