claude-dev-env 1.28.1 → 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.
- package/agents/caveman.md +74 -0
- package/hooks/blocking/code_rules_enforcer.py +82 -7
- package/hooks/blocking/code_rules_path_utils.py +31 -0
- package/hooks/blocking/es_exe_path_rewriter.py +159 -0
- package/hooks/blocking/hedging_language_blocker.py +12 -2
- package/hooks/blocking/test_code_rules_enforcer.py +148 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +123 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/hooks/blocking/test_code_rules_path_utils.py +52 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +369 -0
- package/hooks/blocking/test_hedging_language_blocker.py +7 -6
- package/hooks/config/dynamic_stderr_handler.py +22 -0
- package/hooks/config/path_rewriter_constants.py +13 -0
- package/hooks/config/project_paths_reader.py +78 -0
- package/hooks/config/setup_project_paths_constants.py +41 -0
- package/hooks/config/test_dynamic_stderr_handler.py +48 -0
- package/hooks/config/test_messages.py +5 -1
- package/hooks/config/test_path_rewriter_constants.py +57 -0
- package/hooks/config/test_project_paths_reader.py +149 -0
- package/hooks/config/test_setup_project_paths_constants.py +74 -0
- package/hooks/git-hooks/test_config.py +1 -0
- package/hooks/git-hooks/test_gate_utils.py +1 -0
- package/hooks/git-hooks/test_pre_commit.py +1 -0
- package/hooks/git-hooks/test_pre_push.py +1 -0
- package/hooks/hooks.json +10 -0
- package/hooks/session/test_untracked_repo_detector.py +192 -0
- package/hooks/session/untracked_repo_detector.py +103 -0
- package/hooks/validators/exempt_paths.py +17 -14
- package/hooks/validators/test_exempt_paths.py +65 -0
- package/hooks/validators/test_git_checks.py +17 -17
- package/package.json +1 -1
- package/scripts/config/__init__.py +1 -0
- package/scripts/config/groq_bugteam_config.py +118 -0
- package/scripts/config/test_groq_bugteam_config.py +72 -0
- package/scripts/groq_bugteam.README.md +129 -0
- package/scripts/groq_bugteam.py +586 -0
- package/scripts/setup_project_paths.py +347 -0
- package/scripts/test_groq_bugteam.py +391 -0
- package/scripts/test_setup_project_paths.py +532 -0
- package/scripts/test_setup_project_paths_config.py +6 -0
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/PROMPTS.md +1 -1
- package/skills/bugteam/SKILL.md +5 -5
- package/skills/bugteam/SKILL_EVALS.md +5 -5
- package/skills/bugteam/reference/audit-and-teammates.md +3 -3
- package/skills/bugteam/reference/audit-contract.md +159 -0
- package/skills/bugteam/reference/team-setup.md +2 -2
- package/skills/bugteam/scripts/bugteam_preflight.py +66 -0
- package/skills/bugteam/scripts/test_bugteam_preflight.py +189 -0
- package/skills/copilot-review/SKILL.md +145 -0
- package/skills/findbugs/SKILL.md +14 -22
- package/skills/qbug/SKILL.md +56 -13
- package/skills/qbug/test_qbug_skill_audit_schema.py +156 -0
- 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())
|