claude-dev-env 2.4.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 (195) hide show
  1. package/CLAUDE.md +53 -49
  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 +2 -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 +0 -1
  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/test_agent_frontmatter.py +67 -18
  31. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  32. package/bin/CLAUDE.md +68 -5
  33. package/bin/ever-shipped-skills.mjs +1 -0
  34. package/bin/install-constants.mjs +88 -0
  35. package/bin/install.mjs +1138 -114
  36. package/bin/install.prune.test.mjs +869 -19
  37. package/bin/install.test.mjs +906 -2
  38. package/commands/implement.md +1 -1
  39. package/commands/right-size.md +1 -1
  40. package/docs/CLAUDE.md +1 -0
  41. package/docs/host-pool-health-monitor.md +102 -0
  42. package/docs/references/CLAUDE.md +4 -2
  43. package/docs/references/advisor-tool.md +13 -0
  44. package/docs/references/code-review-enforcement.md +10 -0
  45. package/docs/references/team-advisor-skill.md +14 -0
  46. package/hooks/blocking/CLAUDE.md +1 -0
  47. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  48. package/hooks/blocking/code_review_push_gate.py +9 -4
  49. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  50. package/hooks/blocking/config/__init__.py +5 -5
  51. package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
  52. package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
  53. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  54. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  55. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  56. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  57. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  58. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  59. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  60. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  61. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  62. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  63. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  64. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  65. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  66. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  67. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  68. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  69. package/hooks/git-hooks/test_pre_push.py +25 -0
  70. package/hooks/hooks.json +10 -0
  71. package/hooks/hooks_constants/CLAUDE.md +2 -1
  72. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  73. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  74. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  75. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  76. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  77. package/hooks/pyproject.toml +1 -0
  78. package/hooks/validators/CLAUDE.md +1 -0
  79. package/hooks/validators/config/__init__.py +0 -0
  80. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  81. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  82. package/hooks/validators/conftest.py +4 -0
  83. package/hooks/validators/ruff_integration.py +49 -5
  84. package/hooks/validators/run_all_validators.py +206 -9
  85. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  86. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  87. package/hooks/validators/test_ruff_integration.py +92 -1
  88. package/hooks/validators/test_run_all_validators.py +115 -68
  89. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  90. package/package.json +10 -2
  91. package/rules/CLAUDE.md +1 -0
  92. package/rules/docstring-prose-matches-implementation.md +45 -44
  93. package/rules/state-what-is.md +25 -0
  94. package/rules/verified-commit-gate-skip.md +1 -1
  95. package/scripts/CLAUDE.md +1 -0
  96. package/scripts/Capture-PoolHealth.ps1 +410 -0
  97. package/scripts/_code_review_test_support.py +404 -0
  98. package/scripts/claude_chain_runner.py +141 -1
  99. package/scripts/conftest.py +16 -1
  100. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  101. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  102. package/scripts/resolve_worker_spawn.py +626 -626
  103. package/scripts/spawn_grok_batch.py +672 -672
  104. package/scripts/test_claude_chain_runner.py +131 -0
  105. package/scripts/test_invoke_code_review_chain.py +70 -0
  106. package/scripts/test_invoke_code_review_cli.py +192 -0
  107. package/scripts/test_invoke_code_review_contract.py +256 -0
  108. package/scripts/test_invoke_code_review_git.py +123 -0
  109. package/scripts/test_invoke_code_review_mode.py +99 -0
  110. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  111. package/skills/CLAUDE.md +2 -0
  112. package/skills/auditing-claude-config/SKILL.md +114 -114
  113. package/skills/autoconverge/SKILL.md +427 -427
  114. package/skills/autoconverge/reference/convergence.md +24 -3
  115. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  116. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  117. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  118. package/skills/autoconverge/workflow/converge.mjs +167 -0
  119. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  120. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  121. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  122. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  123. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  124. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  125. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  126. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  127. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  128. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  129. package/skills/closeout/SKILL.md +33 -50
  130. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  131. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  132. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  133. package/skills/condensing-instructions/SKILL.md +81 -0
  134. package/skills/copilot-review/SKILL.md +119 -119
  135. package/skills/e-code-review/SKILL.md +52 -0
  136. package/skills/e-code-review/reference/fix.md +54 -0
  137. package/skills/e-code-review/reference/loop.md +43 -0
  138. package/skills/e-code-review/reference/low.md +57 -0
  139. package/skills/e-code-review/reference/medium.md +153 -0
  140. package/skills/e-code-review/reference/xhigh.md +182 -0
  141. package/skills/e-simplify/SKILL.md +97 -0
  142. package/skills/issue-tracker/SKILL.md +92 -0
  143. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  144. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  145. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  146. package/skills/orchestrator/SKILL.md +162 -21
  147. package/skills/orchestrator/scripts/status_gate.py +625 -0
  148. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  149. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  150. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  151. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  152. package/skills/orchestrator-refresh/SKILL.md +110 -35
  153. package/skills/plan-to-pr/SKILL.md +155 -0
  154. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  155. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  156. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  157. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  158. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  159. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  160. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  161. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  162. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  163. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  164. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  165. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  166. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  167. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  168. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  169. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  170. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  171. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  172. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  173. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  174. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  175. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  176. package/skills/pr-converge/SKILL.md +472 -469
  177. package/skills/pr-converge/reference/examples.md +3 -3
  178. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  179. package/skills/pr-converge/reference/ground-rules.md +7 -4
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  181. package/skills/pr-converge/reference/per-tick.md +5 -5
  182. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  183. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  184. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  185. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  186. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  187. package/skills/pr-fix-protocol/SKILL.md +1 -1
  188. package/skills/privacy-hygiene/SKILL.md +68 -68
  189. package/skills/prototype/workflows/promotion.md +1 -1
  190. package/skills/release-notes-html/SKILL.md +164 -0
  191. package/skills/task-build/CLAUDE.md +8 -7
  192. package/skills/task-build/SKILL.md +16 -8
  193. package/skills/task-build/reference/tool-routing.md +19 -0
  194. package/scripts/test_invoke_code_review.py +0 -966
  195. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -1,306 +1,306 @@
1
- """Grant Edit/Read permissions on the current directory's .claude tree.
2
-
3
- Run from the project root whose .claude/** you want a Claude Code session
4
- (including spawned subagents) to edit without prompting. Writes idempotent
5
- entries into the user-scope settings at ~/.claude/settings.json and prints
6
- the changes applied. No-op when the entries already exist.
7
- """
8
-
9
- import sys
10
- from pathlib import Path
11
-
12
- parent_directory = str(Path(__file__).resolve().parent)
13
- if parent_directory not in sys.path:
14
- sys.path.insert(0, parent_directory)
15
-
16
- from _claude_permissions_common import ( # noqa: E402
17
- append_if_missing,
18
- build_agent_config_deny_rules,
19
- build_permission_rules,
20
- ensure_dict_section,
21
- ensure_list_entry,
22
- exit_with_error,
23
- get_current_project_path,
24
- is_trust_entry_for_project,
25
- is_valid_project_root,
26
- load_settings,
27
- remove_matching_entries_from_list,
28
- save_settings,
29
- )
30
- from pr_loop_shared_constants.claude_permissions_constants import (
31
- ALL_AGENT_CONFIG_DENY_TOOLS,
32
- ALL_AGENT_CONFIG_PATH_PATTERNS,
33
- ALL_PERMISSION_ALLOW_TOOLS,
34
- AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
35
- AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE,
36
- get_claude_user_settings_path,
37
- )
38
- from pr_loop_shared_constants.claude_settings_keys_constants import (
39
- CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY,
40
- CLAUDE_SETTINGS_ALLOW_KEY,
41
- CLAUDE_SETTINGS_AUTO_MODE_KEY,
42
- CLAUDE_SETTINGS_DENY_KEY,
43
- CLAUDE_SETTINGS_ENVIRONMENT_KEY,
44
- CLAUDE_SETTINGS_PERMISSIONS_KEY,
45
- )
46
- from stale_worktree_rule_sweep import ( # noqa: E402
47
- sweep_stale_worktree_rules_from_settings,
48
- )
49
-
50
-
51
- def add_rules_to_allow_list(
52
- all_settings: dict[str, object], all_rules_to_add: list[str]
53
- ) -> int:
54
- """Add permission rules to the settings allow list.
55
-
56
- Args:
57
- all_settings: The parsed settings dictionary.
58
- all_rules_to_add: Permission rule strings to append.
59
-
60
- Returns:
61
- Number of rules actually added (new entries).
62
- """
63
- permissions_section = ensure_dict_section(
64
- all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
65
- )
66
- existing_allow_list = ensure_list_entry(
67
- permissions_section, CLAUDE_SETTINGS_ALLOW_KEY
68
- )
69
- return sum(
70
- 1
71
- for each_rule in all_rules_to_add
72
- if append_if_missing(existing_allow_list, each_rule)
73
- )
74
-
75
-
76
- def add_rules_to_deny_list(
77
- all_settings: dict[str, object], all_rules_to_add: list[str]
78
- ) -> int:
79
- """Add permission rules to the settings deny list.
80
-
81
- Deny rules take precedence over allow rules in Claude Code's permission
82
- matching, so writing agent-config paths into the deny list forces a
83
- per-edit user approval even when a broader allow rule would cover them.
84
-
85
- Args:
86
- all_settings: The parsed settings dictionary.
87
- all_rules_to_add: Permission rule strings to append.
88
-
89
- Returns:
90
- Number of rules actually added (new entries).
91
- """
92
- permissions_section = ensure_dict_section(
93
- all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
94
- )
95
- existing_deny_list = ensure_list_entry(
96
- permissions_section, CLAUDE_SETTINGS_DENY_KEY
97
- )
98
- return sum(
99
- 1
100
- for each_rule in all_rules_to_add
101
- if append_if_missing(existing_deny_list, each_rule)
102
- )
103
-
104
-
105
- def add_directory_to_additional_directories(
106
- all_settings: dict[str, object], directory_path: str
107
- ) -> int:
108
- """Add a project path to the additionalDirectories allow list.
109
-
110
- Args:
111
- all_settings: The parsed settings dictionary.
112
- directory_path: The project directory path to add.
113
-
114
- Returns:
115
- 1 when the entry was added, 0 when it already existed.
116
- """
117
- permissions_section = ensure_dict_section(
118
- all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
119
- )
120
- existing_directories = ensure_list_entry(
121
- permissions_section, CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY
122
- )
123
- if append_if_missing(existing_directories, directory_path):
124
- return 1
125
- return 0
126
-
127
-
128
- def add_auto_mode_environment_entry(
129
- all_settings: dict[str, object], entry_text: str
130
- ) -> int:
131
- """Add an auto-mode environment entry for the project.
132
-
133
- Args:
134
- all_settings: The parsed settings dictionary.
135
- entry_text: The environment entry text to add.
136
-
137
- Returns:
138
- 1 when the entry was added, 0 when it already existed.
139
- """
140
- auto_mode_section = ensure_dict_section(
141
- all_settings, CLAUDE_SETTINGS_AUTO_MODE_KEY
142
- )
143
- existing_environment = ensure_list_entry(
144
- auto_mode_section, CLAUDE_SETTINGS_ENVIRONMENT_KEY
145
- )
146
- if append_if_missing(existing_environment, entry_text):
147
- return 1
148
- return 0
149
-
150
-
151
- def _is_stale_trust_entry(
152
- candidate_entry: object,
153
- project_path: str,
154
- prefix: str,
155
- protected_entry: str | None,
156
- ) -> bool:
157
- if not is_trust_entry_for_project(candidate_entry, project_path, prefix):
158
- return False
159
- if protected_entry is not None and candidate_entry == protected_entry:
160
- return False
161
- return True
162
-
163
-
164
- def purge_stale_trust_entries(
165
- all_settings: dict[str, object],
166
- project_path: str,
167
- prefix: str,
168
- protected_entry: str | None = None,
169
- ) -> int:
170
- """Remove every prior trust entry for the project from autoMode.environment.
171
-
172
- Args:
173
- all_settings: The parsed settings dictionary.
174
- project_path: The POSIX-style project root path.
175
- prefix: The literal prefix that marks a trust entry.
176
- protected_entry: When byte-equal to a candidate, prevents its removal.
177
-
178
- Returns:
179
- Number of stale entries removed.
180
- """
181
- auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
182
- if not isinstance(auto_mode_section, dict):
183
- return 0
184
- existing_environment = auto_mode_section.get(CLAUDE_SETTINGS_ENVIRONMENT_KEY)
185
- if not isinstance(existing_environment, list):
186
- return 0
187
- return remove_matching_entries_from_list(
188
- existing_environment,
189
- lambda candidate_entry: _is_stale_trust_entry(
190
- candidate_entry, project_path, prefix, protected_entry
191
- ),
192
- )
193
-
194
-
195
- def _resolve_project_path_or_exit() -> str:
196
- project_root_path = Path.cwd()
197
- if not is_valid_project_root(project_root_path):
198
- print(
199
- f"ERROR: cwd {project_root_path} is not a project root "
200
- f"(no .git or .claude). Run from a project root.",
201
- file=sys.stderr,
202
- )
203
- raise SystemExit(1)
204
- return get_current_project_path()
205
-
206
-
207
- def _build_grant_rule_sets(
208
- project_path: str,
209
- ) -> tuple[list[str], list[str], str]:
210
- all_permission_rules = build_permission_rules(
211
- project_path, ALL_PERMISSION_ALLOW_TOOLS
212
- )
213
- all_agent_config_deny_rules = build_agent_config_deny_rules(
214
- project_path,
215
- ALL_AGENT_CONFIG_DENY_TOOLS,
216
- ALL_AGENT_CONFIG_PATH_PATTERNS,
217
- )
218
- environment_entry = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
219
- project_path=project_path
220
- )
221
- return (all_permission_rules, all_agent_config_deny_rules, environment_entry)
222
-
223
-
224
- def _apply_grant(
225
- all_settings: dict[str, object],
226
- project_path: str,
227
- all_grant_rule_sets: tuple[list[str], list[str], str],
228
- ) -> tuple[int, int, int, int, int]:
229
- all_permission_rules, all_agent_config_deny_rules, environment_entry = (
230
- all_grant_rule_sets
231
- )
232
- allow_added = add_rules_to_allow_list(all_settings, all_permission_rules)
233
- deny_added = add_rules_to_deny_list(all_settings, all_agent_config_deny_rules)
234
- dirs_added = add_directory_to_additional_directories(all_settings, project_path)
235
- purged = purge_stale_trust_entries(
236
- all_settings,
237
- project_path,
238
- AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
239
- protected_entry=environment_entry,
240
- )
241
- env_added = add_auto_mode_environment_entry(all_settings, environment_entry)
242
- return (allow_added, deny_added, dirs_added, purged, env_added)
243
-
244
-
245
- def _print_no_change_notice(
246
- project_path: str, claude_user_settings_path: Path
247
- ) -> None:
248
- print(f"Project path: {project_path}")
249
- print(f"Settings file: {claude_user_settings_path}")
250
- print("No changes needed; settings file left untouched.")
251
-
252
-
253
- def _print_grant_summary(
254
- project_path: str,
255
- claude_user_settings_path: Path,
256
- all_grant_rule_sets: tuple[list[str], list[str], str],
257
- all_change_counts: tuple[int, int, int, int, int],
258
- ) -> None:
259
- all_permission_rules, all_agent_config_deny_rules, _ = all_grant_rule_sets
260
- allow_added, deny_added, dirs_added, purged, env_added = all_change_counts
261
- print(f"Project path: {project_path}")
262
- print(f"Settings file: {claude_user_settings_path}")
263
- print(f"Allow rules added: {allow_added} of {len(all_permission_rules)}")
264
- print(f"Deny rules added: {deny_added} of {len(all_agent_config_deny_rules)}")
265
- print(f"Additional directories added: {dirs_added}")
266
- if purged > 0:
267
- print(f"Stale auto-mode environment entries purged: {purged}")
268
- print(f"Auto-mode environment entries added: {env_added}")
269
-
270
-
271
- def grant_permissions_for_current_directory() -> None:
272
- """Grant Edit/Read permissions for the current project directory.
273
-
274
- Builds allow and agent-config deny rules from config, writes them to the
275
- user settings, and prints the counts applied. A fresh grant reports::
276
-
277
- Allow rules added: 2 of 2
278
- Deny rules added: 14 of 14
279
- Additional directories added: 1
280
- Auto-mode environment entries added: 1
281
-
282
- Raises:
283
- SystemExit: When the current directory is not a valid project root.
284
- ValueError: Propagated from get_current_project_path() when the path
285
- contains glob metacharacters.
286
- """
287
- claude_user_settings_path: Path = get_claude_user_settings_path()
288
- project_path = _resolve_project_path_or_exit()
289
- grant_rule_sets = _build_grant_rule_sets(project_path)
290
- settings = load_settings(claude_user_settings_path)
291
- worktree_sweep_removed_count = sweep_stale_worktree_rules_from_settings(settings)
292
- change_counts = _apply_grant(settings, project_path, grant_rule_sets)
293
- if sum(change_counts) + worktree_sweep_removed_count == 0:
294
- _print_no_change_notice(project_path, claude_user_settings_path)
295
- return
296
- save_settings(claude_user_settings_path, settings)
297
- _print_grant_summary(
298
- project_path, claude_user_settings_path, grant_rule_sets, change_counts
299
- )
300
-
301
-
302
- if __name__ == "__main__":
303
- try:
304
- grant_permissions_for_current_directory()
305
- except ValueError as path_error:
306
- exit_with_error(str(path_error))
1
+ """Grant Edit/Read permissions on the current directory's .claude tree.
2
+
3
+ Run from the project root whose .claude/** you want a Claude Code session
4
+ (including spawned subagents) to edit without prompting. Writes idempotent
5
+ entries into the user-scope settings at ~/.claude/settings.json and prints
6
+ the changes applied. No-op when the entries already exist.
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ parent_directory = str(Path(__file__).resolve().parent)
13
+ if parent_directory not in sys.path:
14
+ sys.path.insert(0, parent_directory)
15
+
16
+ from _claude_permissions_common import ( # noqa: E402
17
+ append_if_missing,
18
+ build_agent_config_deny_rules,
19
+ build_permission_rules,
20
+ ensure_dict_section,
21
+ ensure_list_entry,
22
+ exit_with_error,
23
+ get_current_project_path,
24
+ is_trust_entry_for_project,
25
+ is_valid_project_root,
26
+ load_settings,
27
+ remove_matching_entries_from_list,
28
+ save_settings,
29
+ )
30
+ from pr_loop_shared_constants.claude_permissions_constants import (
31
+ ALL_AGENT_CONFIG_DENY_TOOLS,
32
+ ALL_AGENT_CONFIG_PATH_PATTERNS,
33
+ ALL_PERMISSION_ALLOW_TOOLS,
34
+ AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
35
+ AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE,
36
+ get_claude_user_settings_path,
37
+ )
38
+ from pr_loop_shared_constants.claude_settings_keys_constants import (
39
+ CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY,
40
+ CLAUDE_SETTINGS_ALLOW_KEY,
41
+ CLAUDE_SETTINGS_AUTO_MODE_KEY,
42
+ CLAUDE_SETTINGS_DENY_KEY,
43
+ CLAUDE_SETTINGS_ENVIRONMENT_KEY,
44
+ CLAUDE_SETTINGS_PERMISSIONS_KEY,
45
+ )
46
+ from stale_worktree_rule_sweep import ( # noqa: E402
47
+ sweep_stale_worktree_rules_from_settings,
48
+ )
49
+
50
+
51
+ def add_rules_to_allow_list(
52
+ all_settings: dict[str, object], all_rules_to_add: list[str]
53
+ ) -> int:
54
+ """Add permission rules to the settings allow list.
55
+
56
+ Args:
57
+ all_settings: The parsed settings dictionary.
58
+ all_rules_to_add: Permission rule strings to append.
59
+
60
+ Returns:
61
+ Number of rules actually added (new entries).
62
+ """
63
+ permissions_section = ensure_dict_section(
64
+ all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
65
+ )
66
+ existing_allow_list = ensure_list_entry(
67
+ permissions_section, CLAUDE_SETTINGS_ALLOW_KEY
68
+ )
69
+ return sum(
70
+ 1
71
+ for each_rule in all_rules_to_add
72
+ if append_if_missing(existing_allow_list, each_rule)
73
+ )
74
+
75
+
76
+ def add_rules_to_deny_list(
77
+ all_settings: dict[str, object], all_rules_to_add: list[str]
78
+ ) -> int:
79
+ """Add permission rules to the settings deny list.
80
+
81
+ Deny rules take precedence over allow rules in Claude Code's permission
82
+ matching, so writing agent-config paths into the deny list forces a
83
+ per-edit user approval even when a broader allow rule would cover them.
84
+
85
+ Args:
86
+ all_settings: The parsed settings dictionary.
87
+ all_rules_to_add: Permission rule strings to append.
88
+
89
+ Returns:
90
+ Number of rules actually added (new entries).
91
+ """
92
+ permissions_section = ensure_dict_section(
93
+ all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
94
+ )
95
+ existing_deny_list = ensure_list_entry(
96
+ permissions_section, CLAUDE_SETTINGS_DENY_KEY
97
+ )
98
+ return sum(
99
+ 1
100
+ for each_rule in all_rules_to_add
101
+ if append_if_missing(existing_deny_list, each_rule)
102
+ )
103
+
104
+
105
+ def add_directory_to_additional_directories(
106
+ all_settings: dict[str, object], directory_path: str
107
+ ) -> int:
108
+ """Add a project path to the additionalDirectories allow list.
109
+
110
+ Args:
111
+ all_settings: The parsed settings dictionary.
112
+ directory_path: The project directory path to add.
113
+
114
+ Returns:
115
+ 1 when the entry was added, 0 when it already existed.
116
+ """
117
+ permissions_section = ensure_dict_section(
118
+ all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
119
+ )
120
+ existing_directories = ensure_list_entry(
121
+ permissions_section, CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY
122
+ )
123
+ if append_if_missing(existing_directories, directory_path):
124
+ return 1
125
+ return 0
126
+
127
+
128
+ def add_auto_mode_environment_entry(
129
+ all_settings: dict[str, object], entry_text: str
130
+ ) -> int:
131
+ """Add an auto-mode environment entry for the project.
132
+
133
+ Args:
134
+ all_settings: The parsed settings dictionary.
135
+ entry_text: The environment entry text to add.
136
+
137
+ Returns:
138
+ 1 when the entry was added, 0 when it already existed.
139
+ """
140
+ auto_mode_section = ensure_dict_section(
141
+ all_settings, CLAUDE_SETTINGS_AUTO_MODE_KEY
142
+ )
143
+ existing_environment = ensure_list_entry(
144
+ auto_mode_section, CLAUDE_SETTINGS_ENVIRONMENT_KEY
145
+ )
146
+ if append_if_missing(existing_environment, entry_text):
147
+ return 1
148
+ return 0
149
+
150
+
151
+ def _is_stale_trust_entry(
152
+ candidate_entry: object,
153
+ project_path: str,
154
+ prefix: str,
155
+ protected_entry: str | None,
156
+ ) -> bool:
157
+ if not is_trust_entry_for_project(candidate_entry, project_path, prefix):
158
+ return False
159
+ if protected_entry is not None and candidate_entry == protected_entry:
160
+ return False
161
+ return True
162
+
163
+
164
+ def purge_stale_trust_entries(
165
+ all_settings: dict[str, object],
166
+ project_path: str,
167
+ prefix: str,
168
+ protected_entry: str | None = None,
169
+ ) -> int:
170
+ """Remove every prior trust entry for the project from autoMode.environment.
171
+
172
+ Args:
173
+ all_settings: The parsed settings dictionary.
174
+ project_path: The POSIX-style project root path.
175
+ prefix: The literal prefix that marks a trust entry.
176
+ protected_entry: When byte-equal to a candidate, prevents its removal.
177
+
178
+ Returns:
179
+ Number of stale entries removed.
180
+ """
181
+ auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
182
+ if not isinstance(auto_mode_section, dict):
183
+ return 0
184
+ existing_environment = auto_mode_section.get(CLAUDE_SETTINGS_ENVIRONMENT_KEY)
185
+ if not isinstance(existing_environment, list):
186
+ return 0
187
+ return remove_matching_entries_from_list(
188
+ existing_environment,
189
+ lambda candidate_entry: _is_stale_trust_entry(
190
+ candidate_entry, project_path, prefix, protected_entry
191
+ ),
192
+ )
193
+
194
+
195
+ def _resolve_project_path_or_exit() -> str:
196
+ project_root_path = Path.cwd()
197
+ if not is_valid_project_root(project_root_path):
198
+ print(
199
+ f"ERROR: cwd {project_root_path} is not a project root "
200
+ f"(no .git or .claude). Run from a project root.",
201
+ file=sys.stderr,
202
+ )
203
+ raise SystemExit(1)
204
+ return get_current_project_path()
205
+
206
+
207
+ def _build_grant_rule_sets(
208
+ project_path: str,
209
+ ) -> tuple[list[str], list[str], str]:
210
+ all_permission_rules = build_permission_rules(
211
+ project_path, ALL_PERMISSION_ALLOW_TOOLS
212
+ )
213
+ all_agent_config_deny_rules = build_agent_config_deny_rules(
214
+ project_path,
215
+ ALL_AGENT_CONFIG_DENY_TOOLS,
216
+ ALL_AGENT_CONFIG_PATH_PATTERNS,
217
+ )
218
+ environment_entry = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
219
+ project_path=project_path
220
+ )
221
+ return (all_permission_rules, all_agent_config_deny_rules, environment_entry)
222
+
223
+
224
+ def _apply_grant(
225
+ all_settings: dict[str, object],
226
+ project_path: str,
227
+ all_grant_rule_sets: tuple[list[str], list[str], str],
228
+ ) -> tuple[int, int, int, int, int]:
229
+ all_permission_rules, all_agent_config_deny_rules, environment_entry = (
230
+ all_grant_rule_sets
231
+ )
232
+ allow_added = add_rules_to_allow_list(all_settings, all_permission_rules)
233
+ deny_added = add_rules_to_deny_list(all_settings, all_agent_config_deny_rules)
234
+ dirs_added = add_directory_to_additional_directories(all_settings, project_path)
235
+ purged = purge_stale_trust_entries(
236
+ all_settings,
237
+ project_path,
238
+ AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
239
+ protected_entry=environment_entry,
240
+ )
241
+ env_added = add_auto_mode_environment_entry(all_settings, environment_entry)
242
+ return (allow_added, deny_added, dirs_added, purged, env_added)
243
+
244
+
245
+ def _print_no_change_notice(
246
+ project_path: str, claude_user_settings_path: Path
247
+ ) -> None:
248
+ print(f"Project path: {project_path}")
249
+ print(f"Settings file: {claude_user_settings_path}")
250
+ print("No changes needed; settings file left untouched.")
251
+
252
+
253
+ def _print_grant_summary(
254
+ project_path: str,
255
+ claude_user_settings_path: Path,
256
+ all_grant_rule_sets: tuple[list[str], list[str], str],
257
+ all_change_counts: tuple[int, int, int, int, int],
258
+ ) -> None:
259
+ all_permission_rules, all_agent_config_deny_rules, _ = all_grant_rule_sets
260
+ allow_added, deny_added, dirs_added, purged, env_added = all_change_counts
261
+ print(f"Project path: {project_path}")
262
+ print(f"Settings file: {claude_user_settings_path}")
263
+ print(f"Allow rules added: {allow_added} of {len(all_permission_rules)}")
264
+ print(f"Deny rules added: {deny_added} of {len(all_agent_config_deny_rules)}")
265
+ print(f"Additional directories added: {dirs_added}")
266
+ if purged > 0:
267
+ print(f"Stale auto-mode environment entries purged: {purged}")
268
+ print(f"Auto-mode environment entries added: {env_added}")
269
+
270
+
271
+ def grant_permissions_for_current_directory() -> None:
272
+ """Grant Edit/Read permissions for the current project directory.
273
+
274
+ Builds allow and agent-config deny rules from config, writes them to the
275
+ user settings, and prints the counts applied. A fresh grant reports::
276
+
277
+ Allow rules added: 2 of 2
278
+ Deny rules added: 14 of 14
279
+ Additional directories added: 1
280
+ Auto-mode environment entries added: 1
281
+
282
+ Raises:
283
+ SystemExit: When the current directory is not a valid project root.
284
+ ValueError: Propagated from get_current_project_path() when the path
285
+ contains glob metacharacters.
286
+ """
287
+ claude_user_settings_path: Path = get_claude_user_settings_path()
288
+ project_path = _resolve_project_path_or_exit()
289
+ grant_rule_sets = _build_grant_rule_sets(project_path)
290
+ settings = load_settings(claude_user_settings_path)
291
+ worktree_sweep_removed_count = sweep_stale_worktree_rules_from_settings(settings)
292
+ change_counts = _apply_grant(settings, project_path, grant_rule_sets)
293
+ if sum(change_counts) + worktree_sweep_removed_count == 0:
294
+ _print_no_change_notice(project_path, claude_user_settings_path)
295
+ return
296
+ save_settings(claude_user_settings_path, settings)
297
+ _print_grant_summary(
298
+ project_path, claude_user_settings_path, grant_rule_sets, change_counts
299
+ )
300
+
301
+
302
+ if __name__ == "__main__":
303
+ try:
304
+ grant_permissions_for_current_directory()
305
+ except ValueError as path_error:
306
+ exit_with_error(str(path_error))