claude-dev-env 1.28.0 → 1.29.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 (54) hide show
  1. package/agents/caveman.md +74 -0
  2. package/hooks/blocking/code_rules_enforcer.py +82 -7
  3. package/hooks/blocking/code_rules_path_utils.py +31 -0
  4. package/hooks/blocking/es_exe_path_rewriter.py +159 -0
  5. package/hooks/blocking/hedging_language_blocker.py +12 -2
  6. package/hooks/blocking/test_code_rules_enforcer.py +148 -0
  7. package/hooks/blocking/test_code_rules_enforcer_config_path.py +123 -0
  8. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  9. package/hooks/blocking/test_code_rules_path_utils.py +52 -0
  10. package/hooks/blocking/test_es_exe_path_rewriter.py +369 -0
  11. package/hooks/blocking/test_hedging_language_blocker.py +7 -6
  12. package/hooks/config/dynamic_stderr_handler.py +22 -0
  13. package/hooks/config/path_rewriter_constants.py +13 -0
  14. package/hooks/config/project_paths_reader.py +78 -0
  15. package/hooks/config/setup_project_paths_constants.py +41 -0
  16. package/hooks/config/test_dynamic_stderr_handler.py +48 -0
  17. package/hooks/config/test_messages.py +5 -1
  18. package/hooks/config/test_path_rewriter_constants.py +57 -0
  19. package/hooks/config/test_project_paths_reader.py +149 -0
  20. package/hooks/config/test_setup_project_paths_constants.py +74 -0
  21. package/hooks/git-hooks/test_config.py +1 -0
  22. package/hooks/git-hooks/test_gate_utils.py +1 -0
  23. package/hooks/git-hooks/test_pre_commit.py +1 -0
  24. package/hooks/git-hooks/test_pre_push.py +1 -0
  25. package/hooks/hooks.json +10 -0
  26. package/hooks/session/test_untracked_repo_detector.py +192 -0
  27. package/hooks/session/untracked_repo_detector.py +103 -0
  28. package/hooks/validators/exempt_paths.py +17 -14
  29. package/hooks/validators/test_exempt_paths.py +65 -0
  30. package/hooks/validators/test_git_checks.py +17 -17
  31. package/package.json +1 -1
  32. package/scripts/config/__init__.py +1 -0
  33. package/scripts/config/groq_bugteam_config.py +118 -0
  34. package/scripts/config/test_groq_bugteam_config.py +72 -0
  35. package/scripts/groq_bugteam.README.md +129 -0
  36. package/scripts/groq_bugteam.py +586 -0
  37. package/scripts/setup_project_paths.py +347 -0
  38. package/scripts/test_groq_bugteam.py +391 -0
  39. package/scripts/test_setup_project_paths.py +532 -0
  40. package/scripts/test_setup_project_paths_config.py +6 -0
  41. package/skills/bugteam/CONSTRAINTS.md +1 -1
  42. package/skills/bugteam/PROMPTS.md +1 -1
  43. package/skills/bugteam/SKILL.md +5 -5
  44. package/skills/bugteam/SKILL_EVALS.md +5 -5
  45. package/skills/bugteam/reference/audit-and-teammates.md +3 -3
  46. package/skills/bugteam/reference/audit-contract.md +159 -0
  47. package/skills/bugteam/reference/team-setup.md +2 -2
  48. package/skills/bugteam/scripts/bugteam_preflight.py +66 -0
  49. package/skills/bugteam/scripts/test_bugteam_preflight.py +189 -0
  50. package/skills/copilot-review/SKILL.md +145 -0
  51. package/skills/findbugs/SKILL.md +14 -22
  52. package/skills/qbug/SKILL.md +56 -12
  53. package/skills/qbug/test_qbug_skill_audit_schema.py +156 -0
  54. package/skills/qbug/test_qbug_skill_post_fix_audit.py +103 -0
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env python3
2
+ """One-time bootstrap: discover git repos via es.exe and write ~/.claude/project-paths.json.
3
+
4
+ Invokes Everything's command-line binary (es.exe) with a folders-only query to
5
+ locate ``.git`` directories across all indexed locations, applies final-segment
6
+ and exclusion filters, presents the discovered mapping to the user, and writes the
7
+ approved entries to the per-user config file. Never hardcodes scan roots —
8
+ discovery runs against whatever es.exe returns on the local machine.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import datetime
14
+ import json
15
+ import os
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ _hooks_dir = Path(__file__).resolve().parent.parent / "hooks"
22
+ if str(_hooks_dir) not in sys.path:
23
+ sys.path.insert(0, str(_hooks_dir))
24
+
25
+ from config.project_paths_reader import registry_file_path
26
+ from config.setup_project_paths_constants import (
27
+ ABORTED_NOTHING_WRITTEN_MESSAGE,
28
+ CONFIRMATION_PROMPT_TEXT,
29
+ ES_EXE_BINARY_NAME,
30
+ ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS,
31
+ EXCLUDED_PATH_SEGMENTS,
32
+ GIT_DIRECTORY_SEGMENT_NAME,
33
+ JSON_INDENT_SPACES,
34
+ META_KEY,
35
+ STDERR_TRUNCATION_LENGTH,
36
+ SUPPORTED_SCHEMA_VERSION,
37
+ UTF8_ENCODING,
38
+ WROTE_ENTRIES_STATUS_TEMPLATE,
39
+ )
40
+
41
+
42
+ class SchemaMismatchError(Exception):
43
+ """Raised when the on-disk config declares a schema newer than this script supports."""
44
+
45
+
46
+ class RegistryReadError(Exception):
47
+ """Raised when an existing registry file is unreadable or corrupt."""
48
+
49
+
50
+ class EverythingScanError(Exception):
51
+ """Raised when es.exe returns a non-zero exit code during the folder scan."""
52
+
53
+
54
+ def _split_path_segments(path_string: str) -> list[str]:
55
+ normalized = path_string.replace("\\", "/")
56
+ return [each_segment for each_segment in normalized.split("/") if each_segment]
57
+
58
+
59
+ def _final_segment(path_string: str) -> str:
60
+ all_segments = _split_path_segments(path_string)
61
+ if not all_segments:
62
+ return ""
63
+ return all_segments[-1]
64
+
65
+
66
+ def _parent_of_git_directory(git_directory_path: str) -> str:
67
+ normalized = git_directory_path.replace("\\", "/").rstrip("/")
68
+ last_slash_index = normalized.rfind("/")
69
+ if last_slash_index < 0:
70
+ return ""
71
+ original_separator_kind = "\\" if "\\" in git_directory_path else "/"
72
+ parent_with_forward_slashes = normalized[:last_slash_index]
73
+ if original_separator_kind == "\\":
74
+ return parent_with_forward_slashes.replace("/", "\\")
75
+ return parent_with_forward_slashes
76
+
77
+
78
+ def filter_to_git_roots(all_es_exe_paths: list[str]) -> list[str]:
79
+ """Return repo-root paths for only those entries whose final segment is exactly ``.git``.
80
+
81
+ Rejects siblings like ``.gitignore``, ``.github``, ``.gitattributes`` that
82
+ share the ``.git`` prefix but are not the canonical git metadata directory.
83
+ """
84
+ all_repo_roots: list[str] = []
85
+ for each_es_path in all_es_exe_paths:
86
+ if _final_segment(each_es_path).lower() != GIT_DIRECTORY_SEGMENT_NAME:
87
+ continue
88
+ parent_repo_root = _parent_of_git_directory(each_es_path)
89
+ if parent_repo_root:
90
+ all_repo_roots.append(parent_repo_root)
91
+ return all_repo_roots
92
+
93
+
94
+ def apply_exclusion_filter(all_candidate_paths: list[str]) -> list[str]:
95
+ """Drop paths whose any whole segment matches an excluded name (case-insensitive).
96
+
97
+ Whole-segment matching preserves legitimate names that merely contain an
98
+ excluded substring (for example ``template`` is retained even though
99
+ ``temp`` is excluded).
100
+ """
101
+ all_retained_paths: list[str] = []
102
+ for each_candidate_path in all_candidate_paths:
103
+ all_lowercased_segments = [
104
+ each_segment.lower()
105
+ for each_segment in _split_path_segments(each_candidate_path)
106
+ ]
107
+ is_excluded = any(
108
+ each_segment in EXCLUDED_PATH_SEGMENTS
109
+ for each_segment in all_lowercased_segments
110
+ )
111
+ if not is_excluded:
112
+ all_retained_paths.append(each_candidate_path)
113
+ return all_retained_paths
114
+
115
+
116
+ def _current_iso_timestamp_utc() -> str:
117
+ now_utc = datetime.datetime.now(datetime.timezone.utc)
118
+ formatted = now_utc.strftime("%Y-%m-%dT%H:%M:%S")
119
+ iso_utc_suffix = "Z"
120
+ return formatted + iso_utc_suffix
121
+
122
+
123
+ def merge_registries(
124
+ existing_registry: dict,
125
+ new_path_by_name: dict[str, str],
126
+ ) -> dict:
127
+ """Merge newly discovered entries into the existing registry.
128
+
129
+ Pre-existing entries not in the new set are preserved. On name collisions
130
+ the newly discovered entry wins. The ``_meta.last_scan`` timestamp is
131
+ refreshed to the current UTC time.
132
+ """
133
+ merged_registry: dict = {
134
+ each_key: each_value
135
+ for each_key, each_value in existing_registry.items()
136
+ if each_key != META_KEY
137
+ }
138
+ for each_name, each_path in new_path_by_name.items():
139
+ merged_registry[each_name] = each_path
140
+ merged_registry[META_KEY] = {
141
+ "schema_version": SUPPORTED_SCHEMA_VERSION,
142
+ "last_scan": _current_iso_timestamp_utc(),
143
+ }
144
+ return merged_registry
145
+
146
+
147
+ def _read_existing_registry(target_file: Path) -> dict:
148
+ if not target_file.is_file():
149
+ return {}
150
+ try:
151
+ raw_text = target_file.read_text(encoding=UTF8_ENCODING)
152
+ except OSError as read_error:
153
+ raise RegistryReadError(
154
+ f"Cannot read registry at {target_file}: {read_error}"
155
+ ) from read_error
156
+ try:
157
+ parsed = json.loads(raw_text)
158
+ except json.JSONDecodeError as decode_error:
159
+ raise RegistryReadError(
160
+ f"Malformed JSON in registry at {target_file}: {decode_error}"
161
+ ) from decode_error
162
+ if not isinstance(parsed, dict):
163
+ raise RegistryReadError(
164
+ f"Registry at {target_file} is not a JSON object."
165
+ )
166
+ return parsed
167
+
168
+
169
+ def _verify_schema_version_is_supported(existing_registry: dict) -> None:
170
+ existing_meta = existing_registry.get(META_KEY)
171
+ if not isinstance(existing_meta, dict):
172
+ return
173
+ existing_schema_version = existing_meta.get("schema_version")
174
+ if not isinstance(existing_schema_version, int):
175
+ return
176
+ if existing_schema_version > SUPPORTED_SCHEMA_VERSION:
177
+ raise SchemaMismatchError(
178
+ f"On-disk schema_version {existing_schema_version} exceeds supported "
179
+ f"version {SUPPORTED_SCHEMA_VERSION}; refusing to overwrite."
180
+ )
181
+
182
+
183
+ def write_registry_atomically(registry_to_write: dict, target_file: Path) -> None:
184
+ """Serialize registry to a temp sibling and rename into place atomically.
185
+
186
+ Caller is responsible for reading the existing registry, verifying the
187
+ schema version, and merging before calling this function. This function
188
+ performs no file reads and no schema checks.
189
+ """
190
+ target_file.parent.mkdir(parents=True, exist_ok=True)
191
+ temp_suffix = ".tmp"
192
+ temp_sibling_path = target_file.with_suffix(target_file.suffix + temp_suffix)
193
+ serialized_text = json.dumps(registry_to_write, indent=JSON_INDENT_SPACES, sort_keys=True)
194
+ try:
195
+ temp_sibling_path.write_text(serialized_text, encoding=UTF8_ENCODING)
196
+ os.replace(temp_sibling_path, target_file)
197
+ finally:
198
+ if temp_sibling_path.exists():
199
+ try:
200
+ temp_sibling_path.unlink()
201
+ except OSError:
202
+ pass
203
+
204
+
205
+ def _everything_binary_is_available() -> bool:
206
+ return shutil.which(ES_EXE_BINARY_NAME) is not None
207
+
208
+
209
+ def _run_es_exe_folders_query() -> list[str]:
210
+ completion = subprocess.run(
211
+ [ES_EXE_BINARY_NAME, *ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS],
212
+ capture_output=True,
213
+ text=True,
214
+ encoding=UTF8_ENCODING,
215
+ check=False,
216
+ )
217
+ if completion.returncode != 0:
218
+ truncated_stderr = completion.stderr[:STDERR_TRUNCATION_LENGTH].strip()
219
+ raise EverythingScanError(
220
+ f"es.exe exited with code {completion.returncode}: {truncated_stderr}"
221
+ )
222
+ return [line.strip() for line in completion.stdout.splitlines() if line.strip()]
223
+
224
+
225
+ def discover_repo_roots_via_everything() -> list[str]:
226
+ """Run es.exe, filter to genuine git roots, deduplicate, and sort."""
227
+ all_raw_paths = _run_es_exe_folders_query()
228
+ all_git_roots = filter_to_git_roots(all_raw_paths)
229
+ all_included = apply_exclusion_filter(all_git_roots)
230
+ return sorted(set(all_included))
231
+
232
+
233
+ def _default_user_config_path() -> Path:
234
+ return registry_file_path()
235
+
236
+
237
+ def _prompt_for_affirmative(prompt_text: str) -> bool:
238
+ affirmative_values = frozenset({"yes", "y"})
239
+ user_response = input(prompt_text).strip().lower()
240
+ return user_response in affirmative_values
241
+
242
+
243
+ def _leaf_name_of(repo_root_path: str) -> str:
244
+ leaf = _final_segment(repo_root_path)
245
+ return leaf if leaf else repo_root_path
246
+
247
+
248
+ def _load_and_validate_registry(save_path: Path) -> dict:
249
+ """Read and validate the existing registry, exiting on fatal errors."""
250
+ try:
251
+ existing_registry = _read_existing_registry(save_path)
252
+ except RegistryReadError as registry_error:
253
+ print(
254
+ f"Existing registry at {save_path} is unreadable: {registry_error}. "
255
+ "Refusing to overwrite. Fix or remove the file manually and re-run.",
256
+ file=sys.stderr,
257
+ )
258
+ raise SystemExit(1) from registry_error
259
+ try:
260
+ _verify_schema_version_is_supported(existing_registry)
261
+ except SchemaMismatchError as schema_error:
262
+ print(
263
+ f"Existing registry at {save_path} cannot be overwritten: {schema_error}. "
264
+ "Upgrade this script before re-running.",
265
+ file=sys.stderr,
266
+ )
267
+ raise SystemExit(1) from schema_error
268
+ return existing_registry
269
+
270
+
271
+ def _display_proposed_mapping(
272
+ path_by_name: dict[str, str], save_path: Path
273
+ ) -> None:
274
+ """Print the proposed name-to-path mapping for user review."""
275
+ print(f"Proposed mapping (save target: {save_path}):")
276
+ for each_name, each_path in sorted(path_by_name.items()):
277
+ print(f" {each_name} -> {each_path}")
278
+ print()
279
+
280
+
281
+ def prompt_and_write(
282
+ path_by_name: dict[str, str],
283
+ save_path: Path,
284
+ ) -> None:
285
+ """Present the mapping to the user and write it only on explicit approval.
286
+
287
+ Reads and validates the existing registry BEFORE prompting so the user
288
+ learns of any schema or read error early. Declining writes nothing.
289
+ """
290
+ existing_registry = _load_and_validate_registry(save_path)
291
+ _display_proposed_mapping(path_by_name, save_path)
292
+ if not _prompt_for_affirmative(CONFIRMATION_PROMPT_TEXT):
293
+ print(ABORTED_NOTHING_WRITTEN_MESSAGE)
294
+ return
295
+ merged = merge_registries(existing_registry, path_by_name)
296
+ write_registry_atomically(merged, save_path)
297
+ print(WROTE_ENTRIES_STATUS_TEMPLATE.format(entry_count=len(path_by_name), save_path=save_path))
298
+
299
+
300
+ def _build_path_by_name_from_roots(all_repo_roots: list[str]) -> dict[str, str]:
301
+ path_by_name: dict[str, str] = {}
302
+ for each_repo_root in sorted(all_repo_roots):
303
+ each_leaf_name = _leaf_name_of(each_repo_root)
304
+ if each_leaf_name in path_by_name:
305
+ kept_path = path_by_name[each_leaf_name]
306
+ print(
307
+ f"Duplicate leaf name '{each_leaf_name}' — keeping {kept_path}, "
308
+ f"skipping {each_repo_root}. Edit {registry_file_path()} "
309
+ "after generation to disambiguate."
310
+ )
311
+ continue
312
+ path_by_name[each_leaf_name] = each_repo_root
313
+ return path_by_name
314
+
315
+
316
+ def main() -> int:
317
+ if not _everything_binary_is_available():
318
+ print(
319
+ f"ERROR: {ES_EXE_BINARY_NAME} not found on PATH. Install Everything "
320
+ "and ensure its command-line binary is available before running this script.",
321
+ file=sys.stderr,
322
+ )
323
+ return 1
324
+ print(
325
+ f"Running Everything folder scan for {GIT_DIRECTORY_SEGMENT_NAME} directories..."
326
+ )
327
+ try:
328
+ all_repo_roots = discover_repo_roots_via_everything()
329
+ except EverythingScanError as scan_error:
330
+ print(
331
+ f"Everything scan failed: {scan_error}. "
332
+ "Ensure the Everything service is running and try again.",
333
+ file=sys.stderr,
334
+ )
335
+ raise SystemExit(1) from scan_error
336
+ if not all_repo_roots:
337
+ print("No candidate git repositories found via es.exe.")
338
+ return 0
339
+ print(f"Found {len(all_repo_roots)} candidate repositories.")
340
+ path_by_name = _build_path_by_name_from_roots(all_repo_roots)
341
+ save_path = _default_user_config_path()
342
+ prompt_and_write(path_by_name=path_by_name, save_path=save_path)
343
+ return 0
344
+
345
+
346
+ if __name__ == "__main__":
347
+ sys.exit(main())