claude-dev-env 1.26.1 → 1.26.3
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/bin/install.mjs +2 -1
- package/hooks/hooks.json +0 -42
- package/hooks/lifecycle/config_change_guard.py +1 -0
- package/hooks/validators/git_checks.py +4 -1
- package/hooks/validators/test_output_formatter.py +7 -2
- package/package.json +1 -1
- package/skills/bugteam/SKILL.md +143 -309
- package/skills/bugteam/SKILL_EVALS.md +46 -46
- package/skills/bugteam/reference/README.md +13 -0
- package/skills/bugteam/reference/audit-and-teammates.md +127 -0
- package/skills/bugteam/reference/design-rationale.md +28 -0
- package/skills/bugteam/reference/github-pr-reviews.md +86 -0
- package/skills/bugteam/reference/team-setup.md +51 -0
- package/skills/bugteam/reference/teardown-publish-permissions.md +70 -0
- package/skills/bugteam/scripts/README.md +4 -0
- package/skills/bugteam/{_claude_permissions_common.py → scripts/_claude_permissions_common.py} +37 -33
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +55 -0
- package/skills/bugteam/{grant_project_claude_permissions.py → scripts/grant_project_claude_permissions.py} +10 -11
- package/skills/bugteam/{revoke_project_claude_permissions.py → scripts/revoke_project_claude_permissions.py} +13 -14
- package/skills/bugteam/sources.md +93 -0
- /package/hooks/validators/{config.py → validator_defaults.py} +0 -0
- /package/skills/bugteam/{test_claude_permissions_common.py → scripts/test_claude_permissions_common.py} +0 -0
package/skills/bugteam/{_claude_permissions_common.py → scripts/_claude_permissions_common.py}
RENAMED
|
@@ -13,28 +13,12 @@ import os
|
|
|
13
13
|
import stat
|
|
14
14
|
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import
|
|
16
|
+
from typing import NoReturn
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
TEXT_FILE_ENCODING: str = "utf-8"
|
|
20
|
-
GLOB_METACHARACTERS_IN_PATH: tuple[str, ...] = (
|
|
21
|
-
"*",
|
|
22
|
-
"?",
|
|
23
|
-
"[",
|
|
24
|
-
"]",
|
|
25
|
-
"(",
|
|
26
|
-
")",
|
|
27
|
-
"{",
|
|
28
|
-
"}",
|
|
29
|
-
",",
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
JSON_INDENT_SPACES: int = 2
|
|
33
20
|
PERMISSION_ALLOW_TOOLS: tuple[str, ...] = ("Edit", "Write", "Read")
|
|
34
21
|
|
|
35
|
-
DEFAULT_SETTINGS_FILE_MODE: int = 0o600
|
|
36
|
-
ATOMIC_WRITE_TEMPORARY_SUFFIX: str = ".tmp"
|
|
37
|
-
|
|
38
22
|
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE: str = (
|
|
39
23
|
"Trusted local workspace: {project_path}/.claude/** is the user's "
|
|
40
24
|
"project Claude Code config tree; edits inside are routine"
|
|
@@ -47,9 +31,20 @@ def exit_with_error(message: str) -> NoReturn:
|
|
|
47
31
|
|
|
48
32
|
|
|
49
33
|
def path_contains_glob_metacharacters(candidate_path: str) -> bool:
|
|
34
|
+
glob_metacharacters_in_path: tuple[str, ...] = (
|
|
35
|
+
"*",
|
|
36
|
+
"?",
|
|
37
|
+
"[",
|
|
38
|
+
"]",
|
|
39
|
+
"(",
|
|
40
|
+
")",
|
|
41
|
+
"{",
|
|
42
|
+
"}",
|
|
43
|
+
",",
|
|
44
|
+
)
|
|
50
45
|
return any(
|
|
51
46
|
each_character in candidate_path
|
|
52
|
-
for each_character in
|
|
47
|
+
for each_character in glob_metacharacters_in_path
|
|
53
48
|
)
|
|
54
49
|
|
|
55
50
|
|
|
@@ -76,10 +71,10 @@ def build_permission_rules(
|
|
|
76
71
|
]
|
|
77
72
|
|
|
78
73
|
|
|
79
|
-
def load_settings(settings_path: Path) -> dict[str,
|
|
74
|
+
def load_settings(settings_path: Path) -> dict[str, object]:
|
|
80
75
|
if not settings_path.exists():
|
|
81
76
|
return {}
|
|
82
|
-
parsed_settings: dict[str,
|
|
77
|
+
parsed_settings: dict[str, object] = {}
|
|
83
78
|
try:
|
|
84
79
|
raw_text = settings_path.read_text(encoding=TEXT_FILE_ENCODING)
|
|
85
80
|
except OSError as read_error:
|
|
@@ -100,11 +95,21 @@ def load_settings(settings_path: Path) -> dict[str, Any]:
|
|
|
100
95
|
return parsed_settings
|
|
101
96
|
|
|
102
97
|
|
|
98
|
+
def serialize_settings_to_json_text(settings: dict[str, object]) -> str:
|
|
99
|
+
json_indent_width_columns: int = len(" ")
|
|
100
|
+
return json.dumps(
|
|
101
|
+
settings,
|
|
102
|
+
indent=json_indent_width_columns,
|
|
103
|
+
sort_keys=True,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
103
107
|
def get_mode_to_preserve(settings_path: Path) -> int:
|
|
108
|
+
default_settings_file_mode: int = 0o600
|
|
104
109
|
try:
|
|
105
110
|
stat_result = os.stat(settings_path)
|
|
106
111
|
except FileNotFoundError:
|
|
107
|
-
return
|
|
112
|
+
return default_settings_file_mode
|
|
108
113
|
except OSError as stat_error:
|
|
109
114
|
exit_with_error(f"Failed to stat {settings_path}: {stat_error}")
|
|
110
115
|
return stat.S_IMODE(stat_result.st_mode)
|
|
@@ -122,13 +127,12 @@ def write_atomically_with_mode(
|
|
|
122
127
|
writer.write(serialized_content)
|
|
123
128
|
|
|
124
129
|
|
|
125
|
-
def save_settings(settings_path: Path, settings: dict[str,
|
|
130
|
+
def save_settings(settings_path: Path, settings: dict[str, object]) -> None:
|
|
131
|
+
atomic_write_temporary_suffix: str = ".tmp"
|
|
126
132
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
127
|
-
serialized_settings =
|
|
128
|
-
settings, indent=JSON_INDENT_SPACES, sort_keys=True
|
|
129
|
-
)
|
|
133
|
+
serialized_settings = serialize_settings_to_json_text(settings)
|
|
130
134
|
temporary_path = settings_path.with_suffix(
|
|
131
|
-
settings_path.suffix +
|
|
135
|
+
settings_path.suffix + atomic_write_temporary_suffix
|
|
132
136
|
)
|
|
133
137
|
mode_to_preserve = get_mode_to_preserve(settings_path)
|
|
134
138
|
try:
|
|
@@ -149,7 +153,7 @@ def save_settings(settings_path: Path, settings: dict[str, Any]) -> None:
|
|
|
149
153
|
pass
|
|
150
154
|
|
|
151
155
|
|
|
152
|
-
def append_if_missing(target_list: list[
|
|
156
|
+
def append_if_missing(target_list: list[object], new_value: str) -> bool:
|
|
153
157
|
if new_value in target_list:
|
|
154
158
|
return False
|
|
155
159
|
target_list.append(new_value)
|
|
@@ -157,8 +161,8 @@ def append_if_missing(target_list: list[str], new_value: str) -> bool:
|
|
|
157
161
|
|
|
158
162
|
|
|
159
163
|
def ensure_dict_section(
|
|
160
|
-
settings: dict[str,
|
|
161
|
-
) -> dict[str,
|
|
164
|
+
settings: dict[str, object], section_name: str
|
|
165
|
+
) -> dict[str, object]:
|
|
162
166
|
"""Return an existing dict section or create an empty one if absent.
|
|
163
167
|
|
|
164
168
|
A missing key and an explicit JSON null are treated identically: both
|
|
@@ -168,7 +172,7 @@ def ensure_dict_section(
|
|
|
168
172
|
"""
|
|
169
173
|
existing_section = settings.get(section_name)
|
|
170
174
|
if existing_section is None:
|
|
171
|
-
replacement_section: dict[str,
|
|
175
|
+
replacement_section: dict[str, object] = {}
|
|
172
176
|
settings[section_name] = replacement_section
|
|
173
177
|
return replacement_section
|
|
174
178
|
if not isinstance(existing_section, dict):
|
|
@@ -180,7 +184,7 @@ def ensure_dict_section(
|
|
|
180
184
|
return existing_section
|
|
181
185
|
|
|
182
186
|
|
|
183
|
-
def ensure_list_entry(section: dict[str,
|
|
187
|
+
def ensure_list_entry(section: dict[str, object], entry_name: str) -> list[object]:
|
|
184
188
|
"""Return an existing list entry or create an empty one if absent.
|
|
185
189
|
|
|
186
190
|
A missing key and an explicit JSON null are treated identically: both
|
|
@@ -190,7 +194,7 @@ def ensure_list_entry(section: dict[str, Any], entry_name: str) -> list[Any]:
|
|
|
190
194
|
"""
|
|
191
195
|
existing_entry = section.get(entry_name)
|
|
192
196
|
if existing_entry is None:
|
|
193
|
-
replacement_entry: list[
|
|
197
|
+
replacement_entry: list[object] = []
|
|
194
198
|
section[entry_name] = replacement_entry
|
|
195
199
|
return replacement_entry
|
|
196
200
|
if not isinstance(existing_entry, list):
|
|
@@ -203,7 +207,7 @@ def ensure_list_entry(section: dict[str, Any], entry_name: str) -> list[Any]:
|
|
|
203
207
|
|
|
204
208
|
|
|
205
209
|
def prune_empty_list_then_empty_section(
|
|
206
|
-
settings: dict[str,
|
|
210
|
+
settings: dict[str, object], section_key: str, list_key: str
|
|
207
211
|
) -> None:
|
|
208
212
|
section = settings.get(section_key)
|
|
209
213
|
if not isinstance(section, dict):
|
|
@@ -46,6 +46,8 @@ def resolve_merge_base(repository_root: Path, base_reference: str) -> str:
|
|
|
46
46
|
cwd=str(repository_root),
|
|
47
47
|
capture_output=True,
|
|
48
48
|
text=True,
|
|
49
|
+
encoding="utf-8",
|
|
50
|
+
errors="replace",
|
|
49
51
|
check=False,
|
|
50
52
|
)
|
|
51
53
|
if merge_result.returncode != 0:
|
|
@@ -58,6 +60,35 @@ def resolve_merge_base(repository_root: Path, base_reference: str) -> str:
|
|
|
58
60
|
return merge_result.stdout.strip()
|
|
59
61
|
|
|
60
62
|
|
|
63
|
+
def filter_paths_under_prefixes(
|
|
64
|
+
file_paths: list[Path],
|
|
65
|
+
repository_root: Path,
|
|
66
|
+
prefix_list: list[str],
|
|
67
|
+
) -> list[Path]:
|
|
68
|
+
if not prefix_list:
|
|
69
|
+
return file_paths
|
|
70
|
+
normalized_prefixes = [
|
|
71
|
+
each.strip().replace("\\", "/").rstrip("/")
|
|
72
|
+
for each in prefix_list
|
|
73
|
+
if each.strip()
|
|
74
|
+
]
|
|
75
|
+
if not normalized_prefixes:
|
|
76
|
+
return file_paths
|
|
77
|
+
resolved_root = repository_root.resolve()
|
|
78
|
+
filtered: list[Path] = []
|
|
79
|
+
for each_path in file_paths:
|
|
80
|
+
try:
|
|
81
|
+
relative_posix = each_path.resolve().relative_to(resolved_root).as_posix()
|
|
82
|
+
except ValueError:
|
|
83
|
+
continue
|
|
84
|
+
if any(
|
|
85
|
+
relative_posix == prefix or relative_posix.startswith(prefix + "/")
|
|
86
|
+
for prefix in normalized_prefixes
|
|
87
|
+
):
|
|
88
|
+
filtered.append(each_path)
|
|
89
|
+
return filtered
|
|
90
|
+
|
|
91
|
+
|
|
61
92
|
def paths_from_git_diff(repository_root: Path, base_reference: str) -> list[Path]:
|
|
62
93
|
merge_base = resolve_merge_base(repository_root, base_reference)
|
|
63
94
|
name_result = subprocess.run(
|
|
@@ -65,6 +96,8 @@ def paths_from_git_diff(repository_root: Path, base_reference: str) -> list[Path
|
|
|
65
96
|
cwd=str(repository_root),
|
|
66
97
|
capture_output=True,
|
|
67
98
|
text=True,
|
|
99
|
+
encoding="utf-8",
|
|
100
|
+
errors="replace",
|
|
68
101
|
check=False,
|
|
69
102
|
)
|
|
70
103
|
if name_result.returncode != 0:
|
|
@@ -109,6 +142,8 @@ def is_file_new_at_base(
|
|
|
109
142
|
cwd=str(repository_root),
|
|
110
143
|
capture_output=True,
|
|
111
144
|
text=True,
|
|
145
|
+
encoding="utf-8",
|
|
146
|
+
errors="replace",
|
|
112
147
|
check=False,
|
|
113
148
|
)
|
|
114
149
|
return cat_result.returncode != 0
|
|
@@ -124,6 +159,8 @@ def added_lines_for_file(
|
|
|
124
159
|
cwd=str(repository_root),
|
|
125
160
|
capture_output=True,
|
|
126
161
|
text=True,
|
|
162
|
+
encoding="utf-8",
|
|
163
|
+
errors="replace",
|
|
127
164
|
check=False,
|
|
128
165
|
)
|
|
129
166
|
if diff_result.returncode != 0:
|
|
@@ -300,6 +337,17 @@ def parse_arguments(argv: list[str]) -> argparse.Namespace:
|
|
|
300
337
|
default="origin/main",
|
|
301
338
|
help="Merge-base ref for git diff (default: origin/main).",
|
|
302
339
|
)
|
|
340
|
+
parser.add_argument(
|
|
341
|
+
"--only-under",
|
|
342
|
+
action="append",
|
|
343
|
+
default=[],
|
|
344
|
+
dest="only_under",
|
|
345
|
+
metavar="PREFIX",
|
|
346
|
+
help=(
|
|
347
|
+
"After resolving the merge-base diff, keep only files whose repo-relative path "
|
|
348
|
+
"uses POSIX slashes and starts with PREFIX or equals PREFIX (repeatable)."
|
|
349
|
+
),
|
|
350
|
+
)
|
|
303
351
|
parser.add_argument(
|
|
304
352
|
"paths",
|
|
305
353
|
nargs="*",
|
|
@@ -321,6 +369,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
321
369
|
file_paths = [repository_root / path for path in arguments.paths]
|
|
322
370
|
return run_gate(validate_content, file_paths, repository_root, added_lines_map=None)
|
|
323
371
|
file_paths = paths_from_git_diff(repository_root, arguments.base)
|
|
372
|
+
file_paths = filter_paths_under_prefixes(
|
|
373
|
+
file_paths,
|
|
374
|
+
repository_root,
|
|
375
|
+
arguments.only_under,
|
|
376
|
+
)
|
|
377
|
+
if not file_paths:
|
|
378
|
+
return 0
|
|
324
379
|
scoped_added_lines = added_lines_by_file(repository_root, arguments.base, file_paths)
|
|
325
380
|
return run_gate(
|
|
326
381
|
validate_content,
|
|
@@ -8,7 +8,6 @@ the changes applied. No-op when the entries already exist.
|
|
|
8
8
|
|
|
9
9
|
import sys
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
12
11
|
|
|
13
12
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
14
13
|
|
|
@@ -26,16 +25,13 @@ from _claude_permissions_common import ( # noqa: E402
|
|
|
26
25
|
)
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
CLAUDE_USER_SETTINGS_PATH: Path = Path.home() / ".claude" / "settings.json"
|
|
30
|
-
|
|
31
|
-
|
|
32
28
|
def is_valid_project_root(candidate_path: Path) -> bool:
|
|
33
29
|
git_marker_path = candidate_path / ".git"
|
|
34
30
|
claude_marker_path = candidate_path / ".claude"
|
|
35
31
|
return git_marker_path.exists() or claude_marker_path.exists()
|
|
36
32
|
|
|
37
33
|
|
|
38
|
-
def add_rules_to_allow_list(settings: dict[str,
|
|
34
|
+
def add_rules_to_allow_list(settings: dict[str, object], rules_to_add: list[str]) -> int:
|
|
39
35
|
permissions_section = ensure_dict_section(settings, "permissions")
|
|
40
36
|
existing_allow_list = ensure_list_entry(permissions_section, "allow")
|
|
41
37
|
return sum(
|
|
@@ -46,7 +42,7 @@ def add_rules_to_allow_list(settings: dict[str, Any], rules_to_add: list[str]) -
|
|
|
46
42
|
|
|
47
43
|
|
|
48
44
|
def add_directory_to_additional_directories(
|
|
49
|
-
settings: dict[str,
|
|
45
|
+
settings: dict[str, object], directory_path: str
|
|
50
46
|
) -> int:
|
|
51
47
|
permissions_section = ensure_dict_section(settings, "permissions")
|
|
52
48
|
existing_directories = ensure_list_entry(
|
|
@@ -57,7 +53,9 @@ def add_directory_to_additional_directories(
|
|
|
57
53
|
return 0
|
|
58
54
|
|
|
59
55
|
|
|
60
|
-
def add_auto_mode_environment_entry(
|
|
56
|
+
def add_auto_mode_environment_entry(
|
|
57
|
+
settings: dict[str, object], entry_text: str
|
|
58
|
+
) -> int:
|
|
61
59
|
auto_mode_section = ensure_dict_section(settings, "autoMode")
|
|
62
60
|
existing_environment = ensure_list_entry(auto_mode_section, "environment")
|
|
63
61
|
if append_if_missing(existing_environment, entry_text):
|
|
@@ -66,6 +64,7 @@ def add_auto_mode_environment_entry(settings: dict[str, Any], entry_text: str) -
|
|
|
66
64
|
|
|
67
65
|
|
|
68
66
|
def grant_permissions_for_current_directory() -> None:
|
|
67
|
+
claude_user_settings_path: Path = Path.home() / ".claude" / "settings.json"
|
|
69
68
|
project_root_path = Path.cwd()
|
|
70
69
|
if not is_valid_project_root(project_root_path):
|
|
71
70
|
print(
|
|
@@ -79,7 +78,7 @@ def grant_permissions_for_current_directory() -> None:
|
|
|
79
78
|
environment_entry = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
|
|
80
79
|
project_path=project_path
|
|
81
80
|
)
|
|
82
|
-
settings = load_settings(
|
|
81
|
+
settings = load_settings(claude_user_settings_path)
|
|
83
82
|
rules_added_count = add_rules_to_allow_list(settings, permission_rules)
|
|
84
83
|
directories_added_count = add_directory_to_additional_directories(
|
|
85
84
|
settings, project_path
|
|
@@ -92,12 +91,12 @@ def grant_permissions_for_current_directory() -> None:
|
|
|
92
91
|
)
|
|
93
92
|
if total_changes_count == 0:
|
|
94
93
|
print(f"Project path: {project_path}")
|
|
95
|
-
print(f"Settings file: {
|
|
94
|
+
print(f"Settings file: {claude_user_settings_path}")
|
|
96
95
|
print("No changes needed; settings file left untouched.")
|
|
97
96
|
return
|
|
98
|
-
save_settings(
|
|
97
|
+
save_settings(claude_user_settings_path, settings)
|
|
99
98
|
print(f"Project path: {project_path}")
|
|
100
|
-
print(f"Settings file: {
|
|
99
|
+
print(f"Settings file: {claude_user_settings_path}")
|
|
101
100
|
print(f"Allow rules added: {rules_added_count} of {len(permission_rules)}")
|
|
102
101
|
print(f"Additional directories added: {directories_added_count}")
|
|
103
102
|
print(f"Auto-mode environment entries added: {environment_entries_added_count}")
|
|
@@ -9,7 +9,6 @@ autoMode sections so repeated grant/revoke cycles leave no dead structure.
|
|
|
9
9
|
|
|
10
10
|
import sys
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
13
12
|
|
|
14
13
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
15
14
|
|
|
@@ -25,25 +24,24 @@ from _claude_permissions_common import ( # noqa: E402
|
|
|
25
24
|
)
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
CLAUDE_USER_SETTINGS_PATH: Path = Path.home() / ".claude" / "settings.json"
|
|
29
|
-
|
|
30
|
-
|
|
31
27
|
def is_valid_project_root(candidate_path: Path) -> bool:
|
|
32
28
|
git_marker_path = candidate_path / ".git"
|
|
33
29
|
claude_marker_path = candidate_path / ".claude"
|
|
34
30
|
return git_marker_path.exists() or claude_marker_path.exists()
|
|
35
31
|
|
|
36
32
|
|
|
37
|
-
def remove_values_from_list(target_list: list[
|
|
33
|
+
def remove_values_from_list(target_list: list[object], values_to_remove: set[str]) -> int:
|
|
38
34
|
original_length = len(target_list)
|
|
39
35
|
target_list[:] = [
|
|
40
|
-
each_value
|
|
36
|
+
each_value
|
|
37
|
+
for each_value in target_list
|
|
38
|
+
if not (isinstance(each_value, str) and each_value in values_to_remove)
|
|
41
39
|
]
|
|
42
40
|
return original_length - len(target_list)
|
|
43
41
|
|
|
44
42
|
|
|
45
43
|
def remove_rules_from_allow_list(
|
|
46
|
-
settings: dict[str,
|
|
44
|
+
settings: dict[str, object], rules_to_remove: list[str]
|
|
47
45
|
) -> int:
|
|
48
46
|
permissions_section = settings.get("permissions")
|
|
49
47
|
if not isinstance(permissions_section, dict):
|
|
@@ -55,7 +53,7 @@ def remove_rules_from_allow_list(
|
|
|
55
53
|
|
|
56
54
|
|
|
57
55
|
def remove_directory_from_additional_directories(
|
|
58
|
-
settings: dict[str,
|
|
56
|
+
settings: dict[str, object], directory_path: str
|
|
59
57
|
) -> int:
|
|
60
58
|
permissions_section = settings.get("permissions")
|
|
61
59
|
if not isinstance(permissions_section, dict):
|
|
@@ -67,7 +65,7 @@ def remove_directory_from_additional_directories(
|
|
|
67
65
|
|
|
68
66
|
|
|
69
67
|
def remove_auto_mode_environment_entry(
|
|
70
|
-
settings: dict[str,
|
|
68
|
+
settings: dict[str, object], entry_text: str
|
|
71
69
|
) -> int:
|
|
72
70
|
auto_mode_section = settings.get("autoMode")
|
|
73
71
|
if not isinstance(auto_mode_section, dict):
|
|
@@ -78,7 +76,7 @@ def remove_auto_mode_environment_entry(
|
|
|
78
76
|
return remove_values_from_list(existing_environment, {entry_text})
|
|
79
77
|
|
|
80
78
|
|
|
81
|
-
def prune_settings_after_revoke(settings: dict[str,
|
|
79
|
+
def prune_settings_after_revoke(settings: dict[str, object]) -> None:
|
|
82
80
|
prune_empty_list_then_empty_section(settings, "permissions", "allow")
|
|
83
81
|
prune_empty_list_then_empty_section(
|
|
84
82
|
settings, "permissions", "additionalDirectories"
|
|
@@ -87,6 +85,7 @@ def prune_settings_after_revoke(settings: dict[str, Any]) -> None:
|
|
|
87
85
|
|
|
88
86
|
|
|
89
87
|
def revoke_permissions_for_current_directory() -> None:
|
|
88
|
+
claude_user_settings_path: Path = Path.home() / ".claude" / "settings.json"
|
|
90
89
|
project_root_path = Path.cwd()
|
|
91
90
|
if not is_valid_project_root(project_root_path):
|
|
92
91
|
print(
|
|
@@ -100,7 +99,7 @@ def revoke_permissions_for_current_directory() -> None:
|
|
|
100
99
|
environment_entry = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
|
|
101
100
|
project_path=project_path
|
|
102
101
|
)
|
|
103
|
-
settings = load_settings(
|
|
102
|
+
settings = load_settings(claude_user_settings_path)
|
|
104
103
|
rules_removed_count = remove_rules_from_allow_list(settings, permission_rules)
|
|
105
104
|
directories_removed_count = remove_directory_from_additional_directories(
|
|
106
105
|
settings, project_path
|
|
@@ -115,13 +114,13 @@ def revoke_permissions_for_current_directory() -> None:
|
|
|
115
114
|
)
|
|
116
115
|
if total_changes_count == 0:
|
|
117
116
|
print(f"Project path: {project_path}")
|
|
118
|
-
print(f"Settings file: {
|
|
117
|
+
print(f"Settings file: {claude_user_settings_path}")
|
|
119
118
|
print("No changes to revoke; settings file left untouched.")
|
|
120
119
|
return
|
|
121
120
|
prune_settings_after_revoke(settings)
|
|
122
|
-
save_settings(
|
|
121
|
+
save_settings(claude_user_settings_path, settings)
|
|
123
122
|
print(f"Project path: {project_path}")
|
|
124
|
-
print(f"Settings file: {
|
|
123
|
+
print(f"Settings file: {claude_user_settings_path}")
|
|
125
124
|
print(f"Allow rules removed: {rules_removed_count} of {len(permission_rules)}")
|
|
126
125
|
print(f"Additional directories removed: {directories_removed_count}")
|
|
127
126
|
print(
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Bugteam skill — sources and citations
|
|
2
|
+
|
|
3
|
+
Canonical URLs and verbatim quotes that `SKILL.md` relies on. Load this file when verifying wording against upstream documentation or when expanding citations. Domain-oriented narrative (without duplicating these quotes) lives under [`reference/README.md`](reference/README.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Claude Code — Agent teams
|
|
8
|
+
|
|
9
|
+
**URL:** [Orchestrate teams of Claude Code sessions](https://code.claude.com/docs/en/agent-teams)
|
|
10
|
+
|
|
11
|
+
**Enable flag (operational link used in refusal copy):** [Enable agent teams](https://code.claude.com/docs/en/agent-teams#enable-agent-teams)
|
|
12
|
+
|
|
13
|
+
### Teammate context isolation (clean-room basis)
|
|
14
|
+
|
|
15
|
+
Direct quote:
|
|
16
|
+
|
|
17
|
+
> "Each teammate has its own context window. When spawned, a teammate loads the same project context as a regular session: CLAUDE.md, MCP servers, and skills. It also receives the spawn prompt from the lead. The lead's conversation history does not carry over."
|
|
18
|
+
|
|
19
|
+
**Skill use:** Independent context per teammate enforces the clean-room audit property; the same sentence is cited again where per-loop `Agent` spawns are justified.
|
|
20
|
+
|
|
21
|
+
### Subagents vs agent teams
|
|
22
|
+
|
|
23
|
+
Direct quote:
|
|
24
|
+
|
|
25
|
+
> "Use subagents when you need quick, focused workers that report back. Use agent teams when teammates need to share findings, challenge each other, and coordinate on their own."
|
|
26
|
+
|
|
27
|
+
**Skill use:** Subagents return into the lead context (accumulates across loops); agent-team teammates do not pollute the lead. This skill needs the independent-context property.
|
|
28
|
+
|
|
29
|
+
### Team creation in natural language
|
|
30
|
+
|
|
31
|
+
Direct quote:
|
|
32
|
+
|
|
33
|
+
> "tell Claude to create an agent team and describe the task and the team structure you want in natural language. Claude creates the team, spawns teammates, and coordinates work based on your prompt."
|
|
34
|
+
|
|
35
|
+
**Skill use:** Maps to the `TeamCreate` tool step in the process section.
|
|
36
|
+
|
|
37
|
+
### Referencing subagent types when spawning teammates
|
|
38
|
+
|
|
39
|
+
Direct quote:
|
|
40
|
+
|
|
41
|
+
> "When spawning a teammate, you can reference a subagent type from any subagent scope: project, user, plugin, or CLI-defined. This lets you define a role once... and reuse it both as a delegated subagent and as an agent team teammate."
|
|
42
|
+
|
|
43
|
+
**Skill use:** Bugfind / bugfix roles reference `code-quality-agent` and `clean-coder` by subagent type name.
|
|
44
|
+
|
|
45
|
+
### Lead cleanup and active teammates
|
|
46
|
+
|
|
47
|
+
Direct quote:
|
|
48
|
+
|
|
49
|
+
> "When the lead runs cleanup, it checks for active teammates and fails if any are still running, so shut them down first."
|
|
50
|
+
|
|
51
|
+
**Skill use:** Step 4 shutdown sequence before `TeamDelete`.
|
|
52
|
+
|
|
53
|
+
### Ending the team
|
|
54
|
+
|
|
55
|
+
Direct quote:
|
|
56
|
+
|
|
57
|
+
> "When you're done, ask the lead to clean up: 'Clean up the team'."
|
|
58
|
+
|
|
59
|
+
**Skill use:** Maps to calling `TeamDelete()` after shutdown messages.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Claude — Agent skills best practices
|
|
64
|
+
|
|
65
|
+
**Base URL:** [Agent Skills best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)
|
|
66
|
+
|
|
67
|
+
### Table of contents for long skills
|
|
68
|
+
|
|
69
|
+
**URL:** [Structure longer reference files with table of contents](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#structure-longer-reference-files-with-table-of-contents)
|
|
70
|
+
|
|
71
|
+
**Skill use:** Justifies the top-of-file Contents section so partial reads still expose scope.
|
|
72
|
+
|
|
73
|
+
### Progressive disclosure and utility scripts
|
|
74
|
+
|
|
75
|
+
**URL:** [Progressive disclosure patterns](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#progressive-disclosure-patterns)
|
|
76
|
+
|
|
77
|
+
**Skill use:** Shell helpers live under `scripts/` and are executed, not loaded as primary context.
|
|
78
|
+
|
|
79
|
+
### Concise is key
|
|
80
|
+
|
|
81
|
+
**URL:** [Concise is key](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#concise-is-key)
|
|
82
|
+
|
|
83
|
+
Direct quotes:
|
|
84
|
+
|
|
85
|
+
> "However, being concise in SKILL.md still matters: once Claude loads it, every token competes with conversation history and other context."
|
|
86
|
+
|
|
87
|
+
Heading and following line (section uses the heading as its own emphasis):
|
|
88
|
+
|
|
89
|
+
> Default assumption: Claude is already very smart
|
|
90
|
+
|
|
91
|
+
> "Only add context Claude doesn't already have. Challenge each piece of information:"
|
|
92
|
+
|
|
93
|
+
**Skill use:** `SKILL.md` revisions that drop redundant narration and trust the reader’s prior knowledge.
|
|
File without changes
|
|
File without changes
|