claude-dev-env 1.40.0 → 1.41.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/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +53 -3
- package/_shared/pr-loop/scripts/post_audit_thread.py +2 -2
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +68 -3
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
- package/hooks/_gh_pr_author_swap_utils.py +1211 -0
- package/hooks/blocking/gh_body_arg_blocker.py +9 -6
- package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
- package/hooks/blocking/gh_pr_author_restore.py +100 -0
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
- package/hooks/blocking/pr_description_enforcer.py +1 -3
- package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
- package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
- package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
- package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
- package/hooks/config/gh_pr_author_swap_constants.py +76 -0
- package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
- package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
- package/hooks/config/pr_description_enforcer_constants.py +5 -0
- package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
- package/hooks/hooks.json +40 -0
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
- package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
- package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
- package/hooks/test__gh_pr_author_swap_utils.py +333 -0
- package/package.json +1 -1
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
- package/skills/bugteam/reference/audit-contract.md +22 -0
- package/skills/bugteam/reference/github-pr-reviews.md +1 -1
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
- package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
- package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
- package/skills/pr-converge/SKILL.md +8 -2
- package/skills/pr-converge/config/constants.py +2 -1
- package/skills/pr-converge/reference/state-schema.md +36 -8
package/CLAUDE.md
CHANGED
|
@@ -5,7 +5,7 @@ The user delegates execution to you and expects zero manual steps unless strictl
|
|
|
5
5
|
## Code Rules
|
|
6
6
|
@~/.claude/docs/CODE_RULES.md
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
ALWAYS call the AskUserQuestion tool if you have a question for the user. Provide content-appropriate default options, with a flag for the recommended one.
|
|
9
9
|
|
|
10
10
|
## GOTCHAS
|
|
11
11
|
When making code changes, make sure you are working in the proper worktree path for the task at hand.
|
|
@@ -9,9 +9,22 @@ the changes applied. No-op when the entries already exist.
|
|
|
9
9
|
import sys
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
sys.path.
|
|
12
|
+
parent_directory = str(Path(__file__).absolute().parent)
|
|
13
|
+
try:
|
|
14
|
+
sys.path.remove(parent_directory)
|
|
15
|
+
except ValueError:
|
|
16
|
+
pass
|
|
17
|
+
if parent_directory not in sys.path:
|
|
18
|
+
sys.path.insert(0, parent_directory)
|
|
19
|
+
|
|
20
|
+
for each_cached_module_name in [
|
|
21
|
+
each_module_key
|
|
22
|
+
for each_module_key in list(sys.modules)
|
|
23
|
+
if each_module_key == "config"
|
|
24
|
+
or each_module_key.startswith("config.")
|
|
25
|
+
or each_module_key == "_claude_permissions_common"
|
|
26
|
+
]:
|
|
27
|
+
sys.modules.pop(each_cached_module_name, None)
|
|
15
28
|
|
|
16
29
|
from _claude_permissions_common import ( # noqa: E402
|
|
17
30
|
append_if_missing,
|
|
@@ -41,6 +54,15 @@ from config.claude_settings_keys_constants import ( # noqa: E402
|
|
|
41
54
|
def add_rules_to_allow_list(
|
|
42
55
|
all_settings: dict[str, object], all_rules_to_add: list[str]
|
|
43
56
|
) -> int:
|
|
57
|
+
"""Add permission rules to the settings allow list.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
all_settings: The parsed settings dictionary.
|
|
61
|
+
all_rules_to_add: Permission rule strings to append.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Number of rules actually added (new entries).
|
|
65
|
+
"""
|
|
44
66
|
permissions_section = ensure_dict_section(
|
|
45
67
|
all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
|
|
46
68
|
)
|
|
@@ -57,6 +79,15 @@ def add_rules_to_allow_list(
|
|
|
57
79
|
def add_directory_to_additional_directories(
|
|
58
80
|
all_settings: dict[str, object], directory_path: str
|
|
59
81
|
) -> int:
|
|
82
|
+
"""Add a project path to the additionalDirectories allow list.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
all_settings: The parsed settings dictionary.
|
|
86
|
+
directory_path: The project directory path to add.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
1 when the entry was added, 0 when it already existed.
|
|
90
|
+
"""
|
|
60
91
|
permissions_section = ensure_dict_section(
|
|
61
92
|
all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
|
|
62
93
|
)
|
|
@@ -71,6 +102,15 @@ def add_directory_to_additional_directories(
|
|
|
71
102
|
def add_auto_mode_environment_entry(
|
|
72
103
|
all_settings: dict[str, object], entry_text: str
|
|
73
104
|
) -> int:
|
|
105
|
+
"""Add an auto-mode environment entry for the project.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
all_settings: The parsed settings dictionary.
|
|
109
|
+
entry_text: The environment entry text to add.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
1 when the entry was added, 0 when it already existed.
|
|
113
|
+
"""
|
|
74
114
|
auto_mode_section = ensure_dict_section(
|
|
75
115
|
all_settings, CLAUDE_SETTINGS_AUTO_MODE_KEY
|
|
76
116
|
)
|
|
@@ -83,6 +123,16 @@ def add_auto_mode_environment_entry(
|
|
|
83
123
|
|
|
84
124
|
|
|
85
125
|
def grant_permissions_for_current_directory() -> None:
|
|
126
|
+
"""Grant Edit/Write/Read permissions for the current project directory.
|
|
127
|
+
|
|
128
|
+
Reads the current project path, constructs permission rules from config
|
|
129
|
+
constants, and writes them to ~/.claude/settings.json atomically.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
SystemExit: When the current directory is not a valid project root.
|
|
133
|
+
ValueError: Propagated from get_current_project_path() when the path
|
|
134
|
+
contains glob metacharacters.
|
|
135
|
+
"""
|
|
86
136
|
claude_user_settings_path: Path = get_claude_user_settings_path()
|
|
87
137
|
project_root_path = Path.cwd()
|
|
88
138
|
if not is_valid_project_root(project_root_path):
|
|
@@ -33,8 +33,8 @@ from pathlib import Path
|
|
|
33
33
|
from typing import NoReturn
|
|
34
34
|
|
|
35
35
|
sys.modules.pop("config", None)
|
|
36
|
-
if str(Path(__file__).
|
|
37
|
-
sys.path.insert(0, str(Path(__file__).
|
|
36
|
+
if str(Path(__file__).absolute().parent) not in sys.path:
|
|
37
|
+
sys.path.insert(0, str(Path(__file__).absolute().parent))
|
|
38
38
|
|
|
39
39
|
from config.post_audit_thread_constants import (
|
|
40
40
|
ALL_GH_API_COMMAND_PARTS,
|
|
@@ -10,9 +10,22 @@ autoMode sections so repeated grant/revoke cycles leave no dead structure.
|
|
|
10
10
|
import sys
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
sys.path.
|
|
13
|
+
parent_directory = str(Path(__file__).absolute().parent)
|
|
14
|
+
try:
|
|
15
|
+
sys.path.remove(parent_directory)
|
|
16
|
+
except ValueError:
|
|
17
|
+
pass
|
|
18
|
+
if parent_directory not in sys.path:
|
|
19
|
+
sys.path.insert(0, parent_directory)
|
|
20
|
+
|
|
21
|
+
for each_cached_module_name in [
|
|
22
|
+
each_module_key
|
|
23
|
+
for each_module_key in list(sys.modules)
|
|
24
|
+
if each_module_key == "config"
|
|
25
|
+
or each_module_key.startswith("config.")
|
|
26
|
+
or each_module_key == "_claude_permissions_common"
|
|
27
|
+
]:
|
|
28
|
+
sys.modules.pop(each_cached_module_name, None)
|
|
16
29
|
|
|
17
30
|
from _claude_permissions_common import ( # noqa: E402
|
|
18
31
|
build_permission_rules,
|
|
@@ -40,6 +53,15 @@ from config.claude_settings_keys_constants import ( # noqa: E402
|
|
|
40
53
|
def remove_values_from_list(
|
|
41
54
|
all_target_list: list[object], all_values_to_remove: set[str]
|
|
42
55
|
) -> int:
|
|
56
|
+
"""Remove matching values from a list in place.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
all_target_list: The list to remove values from.
|
|
60
|
+
all_values_to_remove: Set of string values to remove.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Number of values removed.
|
|
64
|
+
"""
|
|
43
65
|
original_length = len(all_target_list)
|
|
44
66
|
all_target_list[:] = [
|
|
45
67
|
each_value
|
|
@@ -52,6 +74,15 @@ def remove_values_from_list(
|
|
|
52
74
|
def remove_rules_from_allow_list(
|
|
53
75
|
all_settings: dict[str, object], all_rules_to_remove: list[str]
|
|
54
76
|
) -> int:
|
|
77
|
+
"""Remove matching permission rules from the settings allow list.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
all_settings: The parsed settings dictionary.
|
|
81
|
+
all_rules_to_remove: Permission rule strings to remove.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Number of rules removed.
|
|
85
|
+
"""
|
|
55
86
|
permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
|
|
56
87
|
if not isinstance(permissions_section, dict):
|
|
57
88
|
return 0
|
|
@@ -64,6 +95,15 @@ def remove_rules_from_allow_list(
|
|
|
64
95
|
def remove_directory_from_additional_directories(
|
|
65
96
|
all_settings: dict[str, object], directory_path: str
|
|
66
97
|
) -> int:
|
|
98
|
+
"""Remove a project path from the additionalDirectories list.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
all_settings: The parsed settings dictionary.
|
|
102
|
+
directory_path: The project directory path to remove.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
1 when the entry was removed, 0 when not found.
|
|
106
|
+
"""
|
|
67
107
|
permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
|
|
68
108
|
if not isinstance(permissions_section, dict):
|
|
69
109
|
return 0
|
|
@@ -78,6 +118,15 @@ def remove_directory_from_additional_directories(
|
|
|
78
118
|
def remove_auto_mode_environment_entry(
|
|
79
119
|
all_settings: dict[str, object], entry_text: str
|
|
80
120
|
) -> int:
|
|
121
|
+
"""Remove an auto-mode environment entry for the project.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
all_settings: The parsed settings dictionary.
|
|
125
|
+
entry_text: The environment entry text to remove.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
1 when the entry was removed, 0 when not found.
|
|
129
|
+
"""
|
|
81
130
|
auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
|
|
82
131
|
if not isinstance(auto_mode_section, dict):
|
|
83
132
|
return 0
|
|
@@ -88,6 +137,11 @@ def remove_auto_mode_environment_entry(
|
|
|
88
137
|
|
|
89
138
|
|
|
90
139
|
def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
|
|
140
|
+
"""Remove empty lists and their parent sections after revoking entries.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
all_settings: The parsed settings dictionary to prune in place.
|
|
144
|
+
"""
|
|
91
145
|
prune_empty_list_then_empty_section(
|
|
92
146
|
all_settings,
|
|
93
147
|
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
@@ -106,6 +160,17 @@ def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
|
|
|
106
160
|
|
|
107
161
|
|
|
108
162
|
def revoke_permissions_for_current_directory() -> None:
|
|
163
|
+
"""Revoke permissions previously granted for the current project directory.
|
|
164
|
+
|
|
165
|
+
Reads the current project path, constructs the matching permission rules,
|
|
166
|
+
removes them from ~/.claude/settings.json, and prunes any newly empty
|
|
167
|
+
sections.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
SystemExit: When the current directory is not a valid project root.
|
|
171
|
+
ValueError: Propagated from get_current_project_path() when the path
|
|
172
|
+
contains glob metacharacters.
|
|
173
|
+
"""
|
|
109
174
|
claude_user_settings_path: Path = get_claude_user_settings_path()
|
|
110
175
|
project_root_path = Path.cwd()
|
|
111
176
|
if not is_valid_project_root(project_root_path):
|
|
@@ -43,7 +43,7 @@ def test_grant_module_guards_sys_path_insert_against_duplicates() -> None:
|
|
|
43
43
|
module_source = (
|
|
44
44
|
Path(__file__).parent.parent / "grant_project_claude_permissions.py"
|
|
45
45
|
).read_text(encoding="utf-8")
|
|
46
|
-
assert "if
|
|
46
|
+
assert "if parent_directory not in sys.path:" in module_source, (
|
|
47
47
|
"grant_project_claude_permissions.py must guard sys.path.insert against "
|
|
48
48
|
"duplicate entries on reload (consistent with sibling modules)"
|
|
49
49
|
)
|
|
@@ -43,7 +43,7 @@ def test_revoke_module_guards_sys_path_insert_against_duplicates() -> None:
|
|
|
43
43
|
module_source = (
|
|
44
44
|
Path(__file__).parent.parent / "revoke_project_claude_permissions.py"
|
|
45
45
|
).read_text(encoding="utf-8")
|
|
46
|
-
assert "if
|
|
46
|
+
assert "if parent_directory not in sys.path:" in module_source, (
|
|
47
47
|
"revoke_project_claude_permissions.py must guard sys.path.insert against "
|
|
48
48
|
"duplicate entries on reload (consistent with sibling modules)"
|
|
49
49
|
)
|