claude-dev-env 2.2.1 → 2.3.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.
- package/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
- package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/code-verifier.md +1 -1
- package/hooks/blocking/config/verified_commit_constants.py +11 -0
- package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
- package/hooks/blocking/test_verification_verdict_store.py +93 -0
- package/hooks/blocking/verification_verdict_store.py +91 -0
- package/package.json +1 -1
- package/scripts/CLAUDE.md +2 -1
- package/scripts/claude-chain.example.json +15 -3
- package/scripts/claude_chain_runner.py +130 -27
- package/scripts/claude_chain_usage.py +346 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +13 -1
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +1 -1
- package/scripts/dev_env_scripts_constants/timing.py +1 -1
- package/scripts/test_claude_chain_runner.py +412 -6
- package/scripts/test_claude_chain_usage.py +534 -0
- package/skills/orchestrator/SKILL.md +1 -20
- package/skills/orchestrator-refresh/SKILL.md +1 -1
- package/skills/pr-converge/SKILL.md +3 -3
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +3 -3
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/team-advisor/SKILL.md +3 -2
|
@@ -30,7 +30,7 @@ A third-party (non-Claude) harness cannot spawn a Claude `session-advisor` throu
|
|
|
30
30
|
python "$HOME/.claude/scripts/claude_chain_runner.py" -- -p --model <alias> --effort <effort> --output-format json
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Use `--model fable --effort high` on Fable; use `--model opus --effort max` on Opus. The chain runner
|
|
33
|
+
Use `--model fable --effort high` on Fable; use `--model opus --effort max` on Opus. The chain runner probes weekly remaining via `claude_chain_usage` / the usage-pause OAuth probe, ranks accounts in `~/.claude/claude-chain.json` highest remaining first, and fails over only on a usage-limit signature.
|
|
34
34
|
4. Stop at the first successful bind. Record `{tier, result: "cli"}` and set `selected_tier` to that tier. Persist `session_id` from the JSON events (any event carries it; reply text is the `type == "result"` event's `.result` field). Run every bind and every later consult with cwd set to the repo root the work is for — Claude sessions are project-scoped by working directory.
|
|
35
35
|
5. **Fail closed:** when every candidate fails (chain exhausted or model unavailable), set `selected_tier = null` and a `fallback_reason`, report that the advisor is unreachable, and **stop**. Do **not** answer ENDORSE / CORRECTION / PLAN / STOP as this third-party session. Do **not** self-endorse.
|
|
36
36
|
6. Paste the **Third-party host** Advisor block into every executor spawn prompt — never the Claude SendMessage block. Executors report to the orchestrating session; that session consults the bound Claude CLI advisor and relays the four-signal reply.
|
|
@@ -122,7 +122,7 @@ The orchestrating session owns the Claude CLI advisor bind for the whole run —
|
|
|
122
122
|
|
|
123
123
|
## CLI chain
|
|
124
124
|
|
|
125
|
-
The shared runner is `python "$HOME/.claude/scripts/claude_chain_runner.py" -- <claude args...>`. It
|
|
125
|
+
The shared runner is `python "$HOME/.claude/scripts/claude_chain_runner.py" -- <claude args...>`. It ranks accounts in `~/.claude/claude-chain.json` by weekly remaining (via `claude_chain_usage` / the usage-pause OAuth probe) and fails over to the next ranked binary only on a usage-limit signature, so a usage-limited first try still gets served.
|
|
126
126
|
|
|
127
127
|
**Third-party host:** this runner is the **primary** advisor bind and consult path (see **Host profiles → Third-party host**). Map each walk attempt to `--model <alias>` and the effort flags there. When the walk exhausts, fail closed.
|
|
128
128
|
|
|
@@ -11,6 +11,7 @@ Python scripts invoked at runtime by the PR-loop skills. Each script is a standa
|
|
|
11
11
|
| `post_audit_thread.py` | Posts an audit review (APPROVE / REQUEST_CHANGES) to a draft PR via the GitHub reviews API; reads the body skeleton from `audit-reply-template.md` at runtime |
|
|
12
12
|
| `grant_project_claude_permissions.py` | Writes idempotent allow-rules and `additionalDirectories` entries into `~/.claude/settings.json` so subagents can edit the project's `.claude/` tree without prompting |
|
|
13
13
|
| `revoke_project_claude_permissions.py` | Removes the allow-rules and entries that `grant_project_claude_permissions.py` wrote; safe to run when no prior grant exists |
|
|
14
|
+
| `stale_worktree_rule_sweep.py` | Drops allow/deny rules pointing at deleted `~/.claude/worktrees/<repo>/<worktree>` directories and deduplicates the rule arrays; run by the grant and revoke flows before they write |
|
|
14
15
|
| `code_rules_gate.py` | Pre-commit gate that runs `code_rules_enforcer` checks on staged Python files before a fix commit lands, and the terminology sweep over the staged diff |
|
|
15
16
|
| `terminology_sweep.py` | Flags a prose term that near-misses an identifier introduced on added code lines of a unified diff (shared leading word, divergent tail) |
|
|
16
17
|
| `reviews_disabled.py` | Shared helper for the reviewer opt-out and opt-in gates; parses `CLAUDE_REVIEWS_DISABLED` tokens `bugteam`, `bugbot`, `copilot`, and `codex`, plus `CLAUDE_REVIEWS_ENABLED` for bugbot opt-in (bugbot is off by default and runs only when `CLAUDE_REVIEWS_ENABLED` lists it) |
|
|
@@ -1,284 +1,306 @@
|
|
|
1
|
-
"""Grant Edit/
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
all_permission_rules
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
environment_entry =
|
|
230
|
-
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
print(f"
|
|
263
|
-
print(f"
|
|
264
|
-
print(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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))
|
|
@@ -8,6 +8,7 @@ Named constants for every script in `_shared/pr-loop/scripts/`. Each module owns
|
|
|
8
8
|
|---|---|
|
|
9
9
|
| `claude_permissions_constants.py` | `grant_project_claude_permissions.py` and `revoke_project_claude_permissions.py` — permission rule strings and settings.json keys |
|
|
10
10
|
| `claude_settings_keys_constants.py` | Top-level `~/.claude/settings.json` key names used across the permission helpers |
|
|
11
|
+
| `stale_worktree_rule_sweep_constants.py` | `stale_worktree_rule_sweep.py` — the worktrees subdirectory name, the worktree path segment count, the rule-string delimiters, the `.claude` home path marker, and `get_claude_worktrees_root` |
|
|
11
12
|
| `code_rules_gate_constants.py` | `code_rules_gate.py` — file extensions, git diff subcommands, test filename patterns |
|
|
12
13
|
| `inline_duplicate_body_span_constants.py` | `code_rules_gate.py` — regex and capture-group indices for the same-file inline-duplicate message, which carries both the helper and the enclosing span the gate reconstructs the union from |
|
|
13
14
|
| `fix_hookspath_constants.py` | `fix_hookspath.py` — verification suffix and related strings |
|