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.
Files changed (44) hide show
  1. package/agents/code-quality-agent.md +279 -24
  2. package/agents/groq-coder.md +111 -0
  3. package/commands/plan.md +4 -5
  4. package/hooks/blocking/code_rules_enforcer.py +775 -8
  5. package/hooks/blocking/destructive_command_blocker.py +149 -12
  6. package/hooks/blocking/test_code_rules_enforcer.py +751 -0
  7. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +130 -0
  8. package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +134 -0
  9. package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +150 -0
  10. package/hooks/blocking/test_destructive_command_blocker.py +281 -4
  11. package/hooks/blocking/test_pr_description_enforcer.py +9 -8
  12. package/hooks/git-hooks/test_config.py +9 -3
  13. package/hooks/git-hooks/test_gate_utils.py +9 -3
  14. package/hooks/git-hooks/test_pre_commit.py +9 -3
  15. package/hooks/git-hooks/test_pre_push.py +9 -3
  16. package/hooks/validators/run_all_validators.py +76 -3
  17. package/hooks/validators/test_files/skip_decorators/conftest.py +9 -0
  18. package/hooks/validators/test_output_formatter.py +4 -16
  19. package/hooks/validators/test_run_all_validators.py +22 -0
  20. package/hooks/validators/test_run_all_validators_integration.py +2 -11
  21. package/package.json +1 -1
  22. package/scripts/config/groq_bugteam_config.py +104 -0
  23. package/scripts/config/test_groq_bugteam_config.py +11 -0
  24. package/scripts/config/test_spec_implementer_prompt.py +36 -0
  25. package/scripts/groq_bugteam.README.md +2 -0
  26. package/scripts/groq_bugteam.py +74 -15
  27. package/scripts/groq_bugteam_dotenv.py +40 -0
  28. package/scripts/groq_bugteam_spec.py +226 -0
  29. package/scripts/test_groq_bugteam.py +143 -5
  30. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +426 -0
  31. package/scripts/test_groq_bugteam_dotenv.py +66 -0
  32. package/scripts/test_groq_bugteam_spec.py +346 -0
  33. package/scripts/tests/test_sync_to_cursor.py +36 -70
  34. package/skills/bugteam/SKILL.md +4 -0
  35. package/skills/bugteam/reference/README.md +16 -0
  36. package/skills/bugteam/test_skill_additions.py +30 -0
  37. package/skills/monitor-open-prs/SKILL.md +104 -0
  38. package/skills/monitor-open-prs/scripts/discover_open_prs.py +69 -0
  39. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +149 -0
  40. package/skills/monitor-open-prs/test_skill_contract.py +43 -0
  41. package/skills/pr-review-responder/SKILL.md +10 -8
  42. package/hooks/github-action/pre-push-review.yml +0 -27
  43. package/hooks/github-action/test_workflow.py +0 -33
  44. 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
- # Allow git reset --hard in explicitly approved projects (case-insensitive for Windows drive letters)
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
- current_working_directory = os.getcwd()
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 = {