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
@@ -8,16 +8,31 @@ pr_loop_shared_constants/claude_settings_keys_constants.py.
8
8
  from __future__ import annotations
9
9
 
10
10
  import importlib.util
11
+ import json
11
12
  import sys
12
13
  from pathlib import Path
13
14
  from types import ModuleType
14
15
 
16
+ import pytest
17
+
15
18
 
16
19
  def _load_revoke_module() -> ModuleType:
17
20
  scripts_directory = Path(__file__).parent.parent
18
21
  parent_directory = str(scripts_directory.resolve())
19
- if parent_directory not in sys.path:
20
- sys.path.insert(0, parent_directory)
22
+ if parent_directory in sys.path:
23
+ sys.path.remove(parent_directory)
24
+ sys.path.insert(0, parent_directory)
25
+ for each_module_name in list(sys.modules):
26
+ if each_module_name == "pr_loop_shared_constants" or each_module_name.startswith(
27
+ "pr_loop_shared_constants."
28
+ ):
29
+ del sys.modules[each_module_name]
30
+ if each_module_name in {
31
+ "_claude_permissions_common",
32
+ "revoke_project_claude_permissions",
33
+ "stale_worktree_rule_sweep",
34
+ }:
35
+ del sys.modules[each_module_name]
21
36
  module_path = scripts_directory / "revoke_project_claude_permissions.py"
22
37
  specification = importlib.util.spec_from_file_location(
23
38
  "revoke_project_claude_permissions", module_path
@@ -29,6 +44,172 @@ def _load_revoke_module() -> ModuleType:
29
44
  return module
30
45
 
31
46
 
47
+ def _write_legacy_form_settings_file(
48
+ settings_path: Path, revoke_module: ModuleType, project_path: str
49
+ ) -> None:
50
+ """Seed a settings file with only pre-#158 legacy Write()/Glob() rules.
51
+
52
+ One Write(/.claude/**) allow rule, and a Write(/.claude/<pattern>) plus
53
+ a Glob(/.claude/<pattern>) deny rule for every agent-config path
54
+ pattern -- the exact stranded-rule shapes issue #233 describes."""
55
+ legacy_allow_rule = f"Write({project_path}/.claude/**)"
56
+ legacy_deny_rules = [
57
+ f"Write({project_path}/.claude/{each_pattern})"
58
+ for each_pattern in revoke_module.ALL_AGENT_CONFIG_PATH_PATTERNS
59
+ ] + [
60
+ f"Glob({project_path}/.claude/{each_pattern})"
61
+ for each_pattern in revoke_module.ALL_AGENT_CONFIG_PATH_PATTERNS
62
+ ]
63
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
64
+ settings_path.write_text(
65
+ json.dumps(
66
+ {
67
+ "permissions": {
68
+ "allow": [legacy_allow_rule],
69
+ "deny": legacy_deny_rules,
70
+ }
71
+ }
72
+ ),
73
+ encoding="utf-8",
74
+ )
75
+
76
+
77
+ def test_revoke_reaps_legacy_write_and_glob_rules_leaving_no_dead_structure(
78
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
79
+ ) -> None:
80
+ """A revoke run reaps the legacy Write()/Glob() rules an older grant left.
81
+
82
+ No current-form Edit/Read rule is present, so a revoke run that reaps
83
+ only the mint set would remove nothing and leave every legacy rule
84
+ stranded. After revoke runs, the permissions section is gone entirely,
85
+ matching the module's "no dead structure" contract."""
86
+ revoke_module = _load_revoke_module()
87
+ project_directory = tmp_path / "project"
88
+ project_directory.mkdir()
89
+ (project_directory / ".git").mkdir()
90
+ settings_path = tmp_path / "home" / ".claude" / "settings.json"
91
+ project_path = str(project_directory).replace("\\", "/")
92
+ _write_legacy_form_settings_file(settings_path, revoke_module, project_path)
93
+
94
+ monkeypatch.setattr(
95
+ revoke_module, "get_claude_user_settings_path", lambda: settings_path
96
+ )
97
+ monkeypatch.chdir(project_directory)
98
+ revoke_module.revoke_permissions_for_current_directory()
99
+
100
+ saved_settings = json.loads(settings_path.read_text(encoding="utf-8"))
101
+ assert "permissions" not in saved_settings
102
+
103
+
104
+ def test_revoke_strips_inert_write_glob_notebookedit_on_foreign_worktree_paths(
105
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
106
+ ) -> None:
107
+ """Global inert strip removes Write/Glob/NotebookEdit for any path.
108
+
109
+ Foreign worktree rules are outside the cwd project's exact path, so
110
+ project-scoped #315 reap alone leaves them; revoke must strip by tool."""
111
+ revoke_module = _load_revoke_module()
112
+ project_directory = tmp_path / "operator_project"
113
+ project_directory.mkdir()
114
+ (project_directory / ".git").mkdir()
115
+ settings_path = tmp_path / "home" / ".claude" / "settings.json"
116
+ foreign_worktree = (
117
+ "c:/Users/jon/.claude/worktrees/claude-dev-env-702a0c2055d2/other-wt"
118
+ )
119
+ live_other_project = "C:/dev/live-other-project"
120
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
121
+ settings_path.write_text(
122
+ json.dumps(
123
+ {
124
+ "permissions": {
125
+ "allow": [
126
+ f"Write({foreign_worktree}/**)",
127
+ f"NotebookEdit(**/.claude/worktrees/**)",
128
+ f"Edit({live_other_project}/.claude/**)",
129
+ f"Read({live_other_project}/.claude/**)",
130
+ "Bash(echo hi)",
131
+ ],
132
+ "deny": [
133
+ f"Glob({foreign_worktree}/.claude/hooks/**)",
134
+ f"Write({foreign_worktree}/.claude/skills/**)",
135
+ f"Edit({live_other_project}/.claude/hooks/**)",
136
+ ],
137
+ }
138
+ }
139
+ ),
140
+ encoding="utf-8",
141
+ )
142
+ monkeypatch.setattr(
143
+ revoke_module, "get_claude_user_settings_path", lambda: settings_path
144
+ )
145
+ monkeypatch.chdir(project_directory)
146
+ revoke_module.revoke_permissions_for_current_directory()
147
+
148
+ saved_settings = json.loads(settings_path.read_text(encoding="utf-8"))
149
+ permissions_section = saved_settings["permissions"]
150
+ allow_rules = permissions_section["allow"]
151
+ deny_rules = permissions_section["deny"]
152
+ assert allow_rules == [
153
+ f"Edit({live_other_project}/.claude/**)",
154
+ f"Read({live_other_project}/.claude/**)",
155
+ "Bash(echo hi)",
156
+ ]
157
+ assert deny_rules == [f"Edit({live_other_project}/.claude/hooks/**)"]
158
+ for each_rule in allow_rules + deny_rules:
159
+ tool_name = each_rule.split("(", 1)[0]
160
+ assert tool_name not in ("Write", "Glob", "NotebookEdit")
161
+
162
+
163
+ def test_revoke_from_home_reaps_dollar_home_grant_shapes(
164
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
165
+ ) -> None:
166
+ """When cwd is the user home, Edit($HOME/.claude/**) shapes are reaped."""
167
+ revoke_module = _load_revoke_module()
168
+ home_directory = tmp_path / "fake_home"
169
+ home_directory.mkdir()
170
+ (home_directory / ".git").mkdir()
171
+ (home_directory / ".claude").mkdir()
172
+ settings_path = tmp_path / "settings_home" / ".claude" / "settings.json"
173
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
174
+ live_other_project = "C:/dev/unrelated-live"
175
+ settings_path.write_text(
176
+ json.dumps(
177
+ {
178
+ "permissions": {
179
+ "allow": [
180
+ "Edit($HOME/.claude/**)",
181
+ "Write($HOME/.claude/**)",
182
+ "Read($HOME/.claude/**)",
183
+ f"Edit({live_other_project}/.claude/**)",
184
+ ],
185
+ "deny": [
186
+ "Edit($HOME/.claude/hooks/**)",
187
+ "Glob($HOME/.claude/skills/**)",
188
+ f"Read({live_other_project}/.claude/hooks/**)",
189
+ ],
190
+ }
191
+ }
192
+ ),
193
+ encoding="utf-8",
194
+ )
195
+ monkeypatch.setattr(
196
+ revoke_module, "get_claude_user_settings_path", lambda: settings_path
197
+ )
198
+ monkeypatch.setattr(
199
+ Path, "home", classmethod(lambda _cls: home_directory), raising=True
200
+ )
201
+ monkeypatch.chdir(home_directory)
202
+ revoke_module.revoke_permissions_for_current_directory()
203
+
204
+ saved_settings = json.loads(settings_path.read_text(encoding="utf-8"))
205
+ permissions_section = saved_settings["permissions"]
206
+ allow_rules = permissions_section["allow"]
207
+ deny_rules = permissions_section["deny"]
208
+ assert allow_rules == [f"Edit({live_other_project}/.claude/**)"]
209
+ assert deny_rules == [f"Read({live_other_project}/.claude/hooks/**)"]
210
+ assert not any("$HOME" in each_rule for each_rule in allow_rules + deny_rules)
211
+
212
+
32
213
  def test_module_imports_constants_from_config_modules() -> None:
33
214
  revoke_module = _load_revoke_module()
34
215
  assert revoke_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "Read")
@@ -38,6 +219,134 @@ def test_module_imports_constants_from_config_modules() -> None:
38
219
  assert revoke_module.CLAUDE_SETTINGS_PERMISSIONS_KEY == "permissions"
39
220
 
40
221
 
222
+ def test_remove_inert_file_permission_rules_strips_only_inert_tools() -> None:
223
+ revoke_module = _load_revoke_module()
224
+ settings: dict[str, object] = {
225
+ "permissions": {
226
+ "allow": [
227
+ "Write(/foreign/.claude/**)",
228
+ "NotebookEdit(**/.claude/worktrees/**)",
229
+ "Edit(/live/.claude/**)",
230
+ ],
231
+ "deny": ["Glob(/foreign/.claude/hooks/**)", "Read(/live/.claude/hooks/**)"],
232
+ }
233
+ }
234
+ removed_count = revoke_module.remove_inert_file_permission_rules(settings)
235
+ assert removed_count == 3
236
+ permissions_section = _section_of(settings, "permissions")
237
+ assert permissions_section["allow"] == ["Edit(/live/.claude/**)"]
238
+ assert permissions_section["deny"] == ["Read(/live/.claude/hooks/**)"]
239
+
240
+
241
+ def test_strip_inert_rules_from_settings_file_missing_path_is_noop(
242
+ tmp_path: Path,
243
+ ) -> None:
244
+ revoke_module = _load_revoke_module()
245
+ missing_path = revoke_module.get_claude_project_local_settings_path(tmp_path)
246
+ assert not missing_path.is_file()
247
+ assert revoke_module.strip_inert_rules_from_settings_file(missing_path) == 0
248
+ assert not missing_path.is_file()
249
+
250
+
251
+ def test_strip_inert_rules_from_settings_file_updates_project_local_file(
252
+ tmp_path: Path,
253
+ ) -> None:
254
+ revoke_module = _load_revoke_module()
255
+ local_settings_path = revoke_module.get_claude_project_local_settings_path(tmp_path)
256
+ local_settings_path.parent.mkdir(parents=True, exist_ok=True)
257
+ local_settings_path.write_text(
258
+ json.dumps(
259
+ {
260
+ "permissions": {
261
+ "allow": [
262
+ "Write(//**/.claude/**)",
263
+ r"Write(C:\\**\\.claude\\**)",
264
+ "Edit(C:/dev/live/.claude/**)",
265
+ "Bash(echo hi)",
266
+ ]
267
+ }
268
+ }
269
+ ),
270
+ encoding="utf-8",
271
+ )
272
+ removed_count = revoke_module.strip_inert_rules_from_settings_file(
273
+ local_settings_path
274
+ )
275
+ assert removed_count == 2
276
+ saved_settings = json.loads(local_settings_path.read_text(encoding="utf-8"))
277
+ allow_rules = saved_settings["permissions"]["allow"]
278
+ assert allow_rules == ["Edit(C:/dev/live/.claude/**)", "Bash(echo hi)"]
279
+
280
+
281
+ def test_revoke_strips_inert_rules_from_project_settings_local_json(
282
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
283
+ ) -> None:
284
+ """User settings may already be clean; project settings.local still warns.
285
+
286
+ Claude loads .claude/settings.local.json for the cwd project; revoke must
287
+ strip inert Write/Glob/NotebookEdit there too."""
288
+ revoke_module = _load_revoke_module()
289
+ project_directory = tmp_path / "project"
290
+ project_directory.mkdir()
291
+ (project_directory / ".git").mkdir()
292
+ user_settings_path = tmp_path / "home" / ".claude" / "settings.json"
293
+ user_settings_path.parent.mkdir(parents=True, exist_ok=True)
294
+ user_settings_path.write_text(json.dumps({"permissions": {"allow": []}}), encoding="utf-8")
295
+ local_settings_path = revoke_module.get_claude_project_local_settings_path(
296
+ project_directory
297
+ )
298
+ local_settings_path.parent.mkdir(parents=True, exist_ok=True)
299
+ local_settings_path.write_text(
300
+ json.dumps(
301
+ {
302
+ "permissions": {
303
+ "allow": [
304
+ "Write(//**/.claude/**)",
305
+ r"Write(C:\\**\\.claude\\**)",
306
+ "Read(C:/other/.claude/**)",
307
+ ]
308
+ }
309
+ }
310
+ ),
311
+ encoding="utf-8",
312
+ )
313
+ monkeypatch.setattr(
314
+ revoke_module, "get_claude_user_settings_path", lambda: user_settings_path
315
+ )
316
+ monkeypatch.chdir(project_directory)
317
+ revoke_module.revoke_permissions_for_current_directory()
318
+
319
+ local_settings = json.loads(local_settings_path.read_text(encoding="utf-8"))
320
+ allow_rules = local_settings["permissions"]["allow"]
321
+ assert allow_rules == ["Read(C:/other/.claude/**)"]
322
+ assert not any(
323
+ each_rule.split("(", 1)[0] in ("Write", "Glob", "NotebookEdit")
324
+ for each_rule in allow_rules
325
+ if isinstance(each_rule, str) and "(" in each_rule
326
+ )
327
+
328
+
329
+ def test_build_project_scoped_allow_rules_for_reap_includes_legacy_tools() -> None:
330
+ revoke_module = _load_revoke_module()
331
+ all_allow_rules = revoke_module.build_project_scoped_allow_rules_for_reap(
332
+ "/repo/project"
333
+ )
334
+ assert "Edit(/repo/project/.claude/**)" in all_allow_rules
335
+ assert "Write(/repo/project/.claude/**)" in all_allow_rules
336
+ assert "Read(/repo/project/.claude/**)" in all_allow_rules
337
+ assert "Glob(/repo/project/.claude/**)" in all_allow_rules
338
+
339
+
340
+ def test_build_project_scoped_deny_rules_for_reap_includes_legacy_tools() -> None:
341
+ revoke_module = _load_revoke_module()
342
+ all_deny_rules = revoke_module.build_project_scoped_deny_rules_for_reap(
343
+ "/repo/project"
344
+ )
345
+ assert "Edit(/repo/project/.claude/hooks/**)" in all_deny_rules
346
+ assert "Write(/repo/project/.claude/hooks/**)" in all_deny_rules
347
+ assert "Glob(/repo/project/.claude/skills/**)" in all_deny_rules
348
+
349
+
41
350
  def test_revoke_module_guards_sys_path_insert_against_duplicates() -> None:
42
351
  """revoke_project_claude_permissions.py must guard its sys.path.insert with a
43
352
  membership check so re-imports under test harnesses do not push duplicate