claude-dev-env 1.29.2 → 1.30.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/agents/code-quality-agent.md +279 -24
- package/agents/groq-coder.md +111 -0
- package/commands/plan.md +4 -5
- package/hooks/blocking/code_rules_enforcer.py +775 -8
- package/hooks/blocking/destructive_command_blocker.py +149 -12
- package/hooks/blocking/test_code_rules_enforcer.py +751 -0
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +130 -0
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +134 -0
- package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +150 -0
- package/hooks/blocking/test_destructive_command_blocker.py +281 -4
- package/hooks/blocking/test_pr_description_enforcer.py +9 -8
- package/hooks/git-hooks/test_config.py +9 -3
- package/hooks/git-hooks/test_gate_utils.py +9 -3
- package/hooks/git-hooks/test_pre_commit.py +9 -3
- package/hooks/git-hooks/test_pre_push.py +9 -3
- package/hooks/validators/run_all_validators.py +76 -3
- package/hooks/validators/test_files/skip_decorators/conftest.py +9 -0
- package/hooks/validators/test_output_formatter.py +4 -16
- package/hooks/validators/test_run_all_validators.py +22 -0
- package/hooks/validators/test_run_all_validators_integration.py +2 -11
- package/package.json +1 -1
- package/scripts/config/groq_bugteam_config.py +104 -0
- package/scripts/config/test_groq_bugteam_config.py +11 -0
- package/scripts/config/test_spec_implementer_prompt.py +36 -0
- package/scripts/groq_bugteam.README.md +2 -0
- package/scripts/groq_bugteam.py +74 -15
- package/scripts/groq_bugteam_dotenv.py +40 -0
- package/scripts/groq_bugteam_spec.py +226 -0
- package/scripts/test_groq_bugteam.py +143 -5
- package/scripts/test_groq_bugteam_apply_fix_from_spec.py +426 -0
- package/scripts/test_groq_bugteam_dotenv.py +66 -0
- package/scripts/test_groq_bugteam_spec.py +346 -0
- package/scripts/tests/test_sync_to_cursor.py +36 -70
- package/skills/bugteam/SKILL.md +4 -0
- package/skills/bugteam/reference/README.md +16 -0
- package/skills/bugteam/test_skill_additions.py +30 -0
- package/skills/monitor-open-prs/SKILL.md +104 -0
- package/skills/monitor-open-prs/scripts/discover_open_prs.py +69 -0
- package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +149 -0
- package/skills/monitor-open-prs/test_skill_contract.py +43 -0
- package/skills/pr-review-responder/SKILL.md +10 -8
- package/hooks/github-action/pre-push-review.yml +0 -27
- package/hooks/github-action/test_workflow.py +0 -33
- package/skills/pr-review-responder/update_skill.py +0 -297
|
@@ -3,6 +3,7 @@ import datetime
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
|
+
import shlex
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
@@ -139,6 +140,132 @@ def _build_silent_gh_deny_response(matched_description: str) -> dict:
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
|
|
143
|
+
def _path_is_bare_ephemeral_root(resolved_path: str) -> bool:
|
|
144
|
+
leading_repeated_slash_pattern = re.compile(r"^/{2,}")
|
|
145
|
+
forward_slash_normalized_path = leading_repeated_slash_pattern.sub(
|
|
146
|
+
"/",
|
|
147
|
+
resolved_path.replace("\\", "/").lower().rstrip("/"),
|
|
148
|
+
)
|
|
149
|
+
system_temporary_root = leading_repeated_slash_pattern.sub(
|
|
150
|
+
"/",
|
|
151
|
+
os.path.normpath(tempfile.gettempdir()).replace("\\", "/").lower().rstrip("/"),
|
|
152
|
+
)
|
|
153
|
+
forbidden_bare_ephemeral_roots = {"/tmp", "/temp", "/worktrees", "/worktree", system_temporary_root}
|
|
154
|
+
return forward_slash_normalized_path in forbidden_bare_ephemeral_roots
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _path_is_bare_named_worktrees_container(resolved_path: str) -> bool:
|
|
158
|
+
return Path(resolved_path).name.lower() in ("worktrees", "worktree")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _path_basename_is_shell_glob_wildcard(resolved_path: str) -> bool:
|
|
162
|
+
bracket_class_empty_length = len("[]")
|
|
163
|
+
basename = Path(resolved_path).name
|
|
164
|
+
if not basename:
|
|
165
|
+
return False
|
|
166
|
+
if basename in ("*", "?"):
|
|
167
|
+
return True
|
|
168
|
+
if basename.startswith("[") and basename.endswith("]") and len(basename) > bracket_class_empty_length:
|
|
169
|
+
return True
|
|
170
|
+
if "*" in basename or "?" in basename:
|
|
171
|
+
return True
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _command_contains_windows_style_path(command: str) -> bool:
|
|
176
|
+
windows_drive_path_pattern = re.compile(r"(?<![A-Za-z0-9_])[A-Za-z]:\\")
|
|
177
|
+
windows_unc_path_pattern = re.compile(r"(?<!\S)\\\\[^\s\\]+\\[^\s\\]+")
|
|
178
|
+
return bool(
|
|
179
|
+
windows_drive_path_pattern.search(command)
|
|
180
|
+
or windows_unc_path_pattern.search(command)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _split_command_preserving_windows_backslashes(command: str) -> list[str]:
|
|
185
|
+
if "\\" in command and (
|
|
186
|
+
os.name == "nt" or _command_contains_windows_style_path(command)
|
|
187
|
+
):
|
|
188
|
+
forward_slash_normalized_command = command.replace("\\", "/")
|
|
189
|
+
return shlex.split(forward_slash_normalized_command)
|
|
190
|
+
return shlex.split(command)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _rm_flags_before_double_dash_are_unsafe(tokens_after_rm: list[str]) -> bool:
|
|
194
|
+
safe_long_options = frozenset({
|
|
195
|
+
"--recursive",
|
|
196
|
+
"--force",
|
|
197
|
+
"--verbose",
|
|
198
|
+
"--interactive",
|
|
199
|
+
"--dir",
|
|
200
|
+
})
|
|
201
|
+
for each_token in tokens_after_rm:
|
|
202
|
+
if each_token == "--":
|
|
203
|
+
return False
|
|
204
|
+
if not each_token.startswith("-"):
|
|
205
|
+
continue
|
|
206
|
+
if "=" in each_token:
|
|
207
|
+
return True
|
|
208
|
+
if each_token.startswith("--"):
|
|
209
|
+
if each_token not in safe_long_options:
|
|
210
|
+
return True
|
|
211
|
+
continue
|
|
212
|
+
short_rest = each_token[1:]
|
|
213
|
+
if not short_rest or not all(c in "rfRvidI" for c in short_rest):
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _collect_rm_target_tokens(tokens_after_rm: list[str]) -> list[str]:
|
|
219
|
+
targets: list[str] = []
|
|
220
|
+
has_seen_end_of_options = False
|
|
221
|
+
for each_token in tokens_after_rm:
|
|
222
|
+
if not has_seen_end_of_options and each_token == "--":
|
|
223
|
+
has_seen_end_of_options = True
|
|
224
|
+
continue
|
|
225
|
+
if not has_seen_end_of_options and each_token.startswith("-"):
|
|
226
|
+
continue
|
|
227
|
+
targets.append(each_token)
|
|
228
|
+
return targets
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def rm_targets_only_ephemeral_paths(command: str) -> bool:
|
|
232
|
+
"""Return True when command is a single rm invocation whose every target is inside an ephemeral directory.
|
|
233
|
+
|
|
234
|
+
Refuses compound commands so operators like && / || / ; / | / backticks /
|
|
235
|
+
$(...) cannot piggy-back non-rm work on the ephemeral auto-allow. Rejects
|
|
236
|
+
bare ephemeral roots (/tmp, system temp dir) and bare directories named
|
|
237
|
+
worktrees/worktree so we never auto-approve wiping those roots. Only
|
|
238
|
+
allows common short flags and a small set of long options before ``--``;
|
|
239
|
+
tokens with ``=`` or unknown long options disable auto-allow.
|
|
240
|
+
"""
|
|
241
|
+
compound_shell_operator_pattern = re.compile(r'(?:&&|\|\||;|\||`|\$\()')
|
|
242
|
+
if compound_shell_operator_pattern.search(command):
|
|
243
|
+
return False
|
|
244
|
+
try:
|
|
245
|
+
all_command_tokens = _split_command_preserving_windows_backslashes(command)
|
|
246
|
+
except ValueError:
|
|
247
|
+
return False
|
|
248
|
+
if len(all_command_tokens) < 2 or all_command_tokens[0] != "rm":
|
|
249
|
+
return False
|
|
250
|
+
tokens_after_rm = all_command_tokens[1:]
|
|
251
|
+
if _rm_flags_before_double_dash_are_unsafe(tokens_after_rm):
|
|
252
|
+
return False
|
|
253
|
+
all_target_tokens = _collect_rm_target_tokens(tokens_after_rm)
|
|
254
|
+
if not all_target_tokens:
|
|
255
|
+
return False
|
|
256
|
+
for each_target_token in all_target_tokens:
|
|
257
|
+
each_resolved_path = os.path.normpath(os.path.expanduser(each_target_token))
|
|
258
|
+
if _path_basename_is_shell_glob_wildcard(each_resolved_path):
|
|
259
|
+
return False
|
|
260
|
+
if _path_is_bare_ephemeral_root(each_resolved_path):
|
|
261
|
+
return False
|
|
262
|
+
if _path_is_bare_named_worktrees_container(each_resolved_path):
|
|
263
|
+
return False
|
|
264
|
+
if not directory_is_ephemeral(each_resolved_path):
|
|
265
|
+
return False
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
|
|
142
269
|
def targets_only_claude_directory(command: str) -> bool:
|
|
143
270
|
"""Check if rm command targets only paths under ~/.claude/."""
|
|
144
271
|
all_rm_target_paths = re.findall(
|
|
@@ -160,6 +287,24 @@ def targets_only_claude_directory(command: str) -> bool:
|
|
|
160
287
|
return True
|
|
161
288
|
|
|
162
289
|
|
|
290
|
+
def _ephemeral_recursive_rm_auto_allow_granted(command: str, matched_description: str) -> bool:
|
|
291
|
+
return matched_description.startswith(("rm -rf", "rm --recursive")) and rm_targets_only_ephemeral_paths(command)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _git_reset_hard_allowed_for_command(command: str, current_working_directory: str) -> bool:
|
|
295
|
+
if directory_is_ephemeral(current_working_directory):
|
|
296
|
+
return True
|
|
297
|
+
current_working_directory_lowercased = os.path.normpath(current_working_directory).lower()
|
|
298
|
+
for allowed_project in load_allow_git_reset_hard_projects():
|
|
299
|
+
allowed_project_lowercased = os.path.normpath(allowed_project).lower()
|
|
300
|
+
if current_working_directory_lowercased.startswith(allowed_project_lowercased):
|
|
301
|
+
return True
|
|
302
|
+
for path_match in re.findall(r'cd\s+"([^"]+)"', command):
|
|
303
|
+
if os.path.normpath(path_match).lower().startswith(allowed_project_lowercased):
|
|
304
|
+
return True
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
|
|
163
308
|
def main() -> None:
|
|
164
309
|
try:
|
|
165
310
|
hook_input = json.load(sys.stdin)
|
|
@@ -185,20 +330,12 @@ def main() -> None:
|
|
|
185
330
|
if matched_description is not None and targets_only_claude_directory(command):
|
|
186
331
|
sys.exit(0)
|
|
187
332
|
|
|
188
|
-
|
|
333
|
+
if matched_description is not None and _ephemeral_recursive_rm_auto_allow_granted(command, matched_description):
|
|
334
|
+
sys.exit(0)
|
|
335
|
+
|
|
189
336
|
if matched_description is not None and "git reset --hard" in matched_description:
|
|
190
|
-
|
|
191
|
-
if directory_is_ephemeral(current_working_directory):
|
|
337
|
+
if _git_reset_hard_allowed_for_command(command, os.getcwd()):
|
|
192
338
|
sys.exit(0)
|
|
193
|
-
current_working_directory_lowercased = os.path.normpath(current_working_directory).lower()
|
|
194
|
-
for allowed_project in load_allow_git_reset_hard_projects():
|
|
195
|
-
allowed_project_lowercased = os.path.normpath(allowed_project).lower()
|
|
196
|
-
if current_working_directory_lowercased.startswith(allowed_project_lowercased):
|
|
197
|
-
sys.exit(0)
|
|
198
|
-
# Also check the cd target in the command itself
|
|
199
|
-
for path_match in re.findall(r'cd\s+"([^"]+)"', command):
|
|
200
|
-
if os.path.normpath(path_match).lower().startswith(allowed_project_lowercased):
|
|
201
|
-
sys.exit(0)
|
|
202
339
|
|
|
203
340
|
if matched_description is not None:
|
|
204
341
|
ask_response = {
|