claude-dev-env 1.28.1 → 1.29.1

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 +352 -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 -13
  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,532 @@
1
+ """Tests for setup_project_paths — one-time bootstrap script."""
2
+
3
+ import inspect
4
+ import json
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from unittest.mock import patch
9
+
10
+ import pytest
11
+
12
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
13
+ _HOOKS_DIR = _SCRIPTS_DIR.parent / "hooks"
14
+ _SESSION_HOOKS_PACKAGE_DIR = _SCRIPTS_DIR.parent / "hooks" / "session"
15
+ for each_sys_path_entry in (
16
+ str(_SCRIPTS_DIR),
17
+ str(_HOOKS_DIR),
18
+ str(_SESSION_HOOKS_PACKAGE_DIR),
19
+ ):
20
+ if each_sys_path_entry not in sys.path:
21
+ sys.path.insert(0, each_sys_path_entry)
22
+
23
+ import setup_project_paths as setup
24
+ import untracked_repo_detector as detector_module
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_FOLDERS_ONLY_QUERY_ARGUMENTS,
30
+ WROTE_ENTRIES_STATUS_TEMPLATE,
31
+ )
32
+
33
+
34
+ class TestFinalSegmentFilter:
35
+ def test_retains_dot_git_directory(self) -> None:
36
+ all_paths = ["C:\\Projects\\my-repo\\.git"]
37
+ retained = setup.filter_to_git_roots(all_paths)
38
+ assert retained == ["C:\\Projects\\my-repo"]
39
+
40
+ def test_rejects_dot_gitignore(self) -> None:
41
+ all_paths = ["C:\\Projects\\my-repo\\.gitignore"]
42
+ retained = setup.filter_to_git_roots(all_paths)
43
+ assert retained == []
44
+
45
+ def test_rejects_dot_github(self) -> None:
46
+ all_paths = ["C:\\Projects\\my-repo\\.github"]
47
+ retained = setup.filter_to_git_roots(all_paths)
48
+ assert retained == []
49
+
50
+ def test_accepts_dot_git_with_forward_slashes(self) -> None:
51
+ all_paths = ["C:/Projects/my-repo/.git"]
52
+ retained = setup.filter_to_git_roots(all_paths)
53
+ assert retained == ["C:/Projects/my-repo"]
54
+
55
+ def test_retains_multiple_valid_git_roots(self) -> None:
56
+ all_paths = [
57
+ "C:\\Projects\\alpha\\.git",
58
+ "D:\\Work\\beta\\.git",
59
+ ]
60
+ retained = setup.filter_to_git_roots(all_paths)
61
+ assert "C:\\Projects\\alpha" in retained
62
+ assert "D:\\Work\\beta" in retained
63
+
64
+ def test_rejects_dot_git_attributes(self) -> None:
65
+ all_paths = ["C:\\Projects\\my-repo\\.gitattributes"]
66
+ retained = setup.filter_to_git_roots(all_paths)
67
+ assert retained == []
68
+
69
+
70
+ class TestExclusionFilter:
71
+ def test_drops_path_with_temp_segment(self) -> None:
72
+ all_candidates = ["C:\\temp\\my-repo"]
73
+ filtered = setup.apply_exclusion_filter(all_candidates)
74
+ assert filtered == []
75
+
76
+ def test_drops_path_with_tmp_segment(self) -> None:
77
+ all_candidates = ["C:\\tmp\\my-repo"]
78
+ filtered = setup.apply_exclusion_filter(all_candidates)
79
+ assert filtered == []
80
+
81
+ def test_drops_path_with_worktree_segment(self) -> None:
82
+ all_candidates = ["C:\\Projects\\main\\worktree\\feature"]
83
+ filtered = setup.apply_exclusion_filter(all_candidates)
84
+ assert filtered == []
85
+
86
+ def test_drops_path_with_node_modules_segment(self) -> None:
87
+ all_candidates = ["C:\\Projects\\app\\node_modules\\pkg"]
88
+ filtered = setup.apply_exclusion_filter(all_candidates)
89
+ assert filtered == []
90
+
91
+ def test_drops_path_with_dot_cache_segment(self) -> None:
92
+ all_candidates = ["C:\\Users\\jon\\.cache\\build"]
93
+ filtered = setup.apply_exclusion_filter(all_candidates)
94
+ assert filtered == []
95
+
96
+ def test_drops_path_with_recycle_bin_segment(self) -> None:
97
+ all_candidates = ["C:\\$Recycle.Bin\\S-1-5\\my-repo"]
98
+ filtered = setup.apply_exclusion_filter(all_candidates)
99
+ assert filtered == []
100
+
101
+ def test_preserves_path_with_template_segment(self) -> None:
102
+ all_candidates = ["C:\\Projects\\template"]
103
+ filtered = setup.apply_exclusion_filter(all_candidates)
104
+ assert filtered == ["C:\\Projects\\template"]
105
+
106
+ def test_preserves_legitimate_project_path(self) -> None:
107
+ all_candidates = ["Y:\\Projects\\my-app"]
108
+ filtered = setup.apply_exclusion_filter(all_candidates)
109
+ assert filtered == ["Y:\\Projects\\my-app"]
110
+
111
+ def test_whole_segment_match_does_not_drop_template(self) -> None:
112
+ all_candidates = ["C:\\Projects\\my-templates\\repo"]
113
+ filtered = setup.apply_exclusion_filter(all_candidates)
114
+ assert filtered == ["C:\\Projects\\my-templates\\repo"]
115
+
116
+
117
+ class TestMergeRegistries:
118
+ def test_merge_preserves_pre_existing_entries(self) -> None:
119
+ existing_registry = {
120
+ "_meta": {"schema_version": 1, "last_scan": "2026-01-01T00:00:00Z"},
121
+ "old-repo": "C:\\Old\\old-repo",
122
+ }
123
+ new_path_by_name = {"new-repo": "D:\\New\\new-repo"}
124
+ merged = setup.merge_registries(existing_registry, new_path_by_name)
125
+ assert merged["old-repo"] == "C:\\Old\\old-repo"
126
+ assert merged["new-repo"] == "D:\\New\\new-repo"
127
+
128
+ def test_merge_updates_meta_last_scan(self) -> None:
129
+ existing_registry: dict = {}
130
+ new_path_by_name = {"alpha": "C:\\alpha"}
131
+ merged = setup.merge_registries(existing_registry, new_path_by_name)
132
+ assert "_meta" in merged
133
+ assert "last_scan" in merged["_meta"]
134
+
135
+ def test_merge_new_entry_wins_on_name_collision(self) -> None:
136
+ existing_registry = {"my-repo": "C:\\Old\\path"}
137
+ new_path_by_name = {"my-repo": "D:\\New\\path"}
138
+ merged = setup.merge_registries(existing_registry, new_path_by_name)
139
+ assert merged["my-repo"] == "D:\\New\\path"
140
+
141
+
142
+ class TestAtomicWrite:
143
+ def test_write_creates_file_with_correct_content(self, tmp_path: Path) -> None:
144
+ target_file = tmp_path / "project-paths.json"
145
+ registry_to_write = {"_meta": {"schema_version": 1}, "repo": "C:\\repo"}
146
+ setup.write_registry_atomically(registry_to_write, target_file)
147
+ written_content = json.loads(target_file.read_text(encoding="utf-8"))
148
+ assert written_content["repo"] == "C:\\repo"
149
+
150
+ def test_write_leaves_no_temp_file_on_success(self, tmp_path: Path) -> None:
151
+ target_file = tmp_path / "project-paths.json"
152
+ registry_to_write = {"_meta": {"schema_version": 1}}
153
+ setup.write_registry_atomically(registry_to_write, target_file)
154
+ all_files = list(tmp_path.iterdir())
155
+ assert all_files == [target_file]
156
+
157
+ def test_write_overwrites_without_schema_check(self, tmp_path: Path) -> None:
158
+ target_file = tmp_path / "project-paths.json"
159
+ target_file.write_text(
160
+ json.dumps({"_meta": {"schema_version": 99}}), encoding="utf-8"
161
+ )
162
+ registry_to_write = {"_meta": {"schema_version": 1}, "repo": "C:\\repo"}
163
+ setup.write_registry_atomically(registry_to_write, target_file)
164
+ written_content = json.loads(target_file.read_text(encoding="utf-8"))
165
+ assert written_content["repo"] == "C:\\repo"
166
+
167
+
168
+ class TestEsExeQueryArguments:
169
+ def test_arguments_do_not_include_name_flag(self) -> None:
170
+ assert "-name" not in ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS
171
+
172
+ def test_arguments_include_folders_only_flag(self) -> None:
173
+ assert "/ad" in ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS
174
+
175
+ def test_arguments_include_git_folder_query(self) -> None:
176
+ assert "folder:.git" in ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS
177
+
178
+ def test_filter_to_git_roots_processes_full_absolute_paths(self) -> None:
179
+ all_raw_paths = [
180
+ "C:\\Projects\\my-repo\\.git",
181
+ "D:\\Work\\other-repo\\.git",
182
+ ]
183
+ all_roots = setup.filter_to_git_roots(all_raw_paths)
184
+ assert "C:\\Projects\\my-repo" in all_roots
185
+ assert "D:\\Work\\other-repo" in all_roots
186
+
187
+
188
+ class TestUserRejection:
189
+ def test_user_rejection_at_final_prompt_writes_nothing(
190
+ self, tmp_path: Path
191
+ ) -> None:
192
+ target_file = tmp_path / "project-paths.json"
193
+ assert not target_file.exists()
194
+ with patch("builtins.input", return_value="no"):
195
+ setup.prompt_and_write(
196
+ path_by_name={"my-repo": "C:\\my-repo"},
197
+ save_path=target_file,
198
+ )
199
+ assert not target_file.exists()
200
+
201
+
202
+ class TestDuplicateLeafName:
203
+ def test_duplicate_leaf_name_keeps_first_seen_entry(
204
+ self, capsys: pytest.CaptureFixture
205
+ ) -> None:
206
+ all_roots = sorted(["Y:\\A\\foo", "Y:\\B\\foo"])
207
+ path_by_name = setup._build_path_by_name_from_roots(all_roots)
208
+ assert len(path_by_name) == 1
209
+ assert path_by_name["foo"] == "Y:\\A\\foo"
210
+
211
+ def test_duplicate_leaf_name_prints_collision_warning(
212
+ self, capsys: pytest.CaptureFixture
213
+ ) -> None:
214
+ all_roots = sorted(["Y:\\A\\foo", "Y:\\B\\foo"])
215
+ setup._build_path_by_name_from_roots(all_roots)
216
+ captured = capsys.readouterr()
217
+ assert "Duplicate leaf name 'foo'" in captured.out
218
+ assert "Y:\\A\\foo" in captured.out
219
+ assert "Y:\\B\\foo" in captured.out
220
+
221
+
222
+ class TestMapNamingConvention:
223
+ def test_merge_registries_signature_uses_path_by_name(self) -> None:
224
+ """Pin PR #230 round 3 rename: X_by_Y means X indexed by Y.
225
+
226
+ The map's keys are repo names and values are paths, so the correct
227
+ name is `path_by_name` (path indexed by name). The old inverted
228
+ name `name_by_path` must not reappear.
229
+ """
230
+ merge_signature = inspect.signature(setup.merge_registries)
231
+ assert "new_path_by_name" in merge_signature.parameters
232
+ assert "new_name_by_path" not in merge_signature.parameters
233
+
234
+ def test_build_helper_is_named_path_by_name(self) -> None:
235
+ assert hasattr(setup, "_build_path_by_name_from_roots")
236
+ assert not hasattr(setup, "_build_name_by_path_from_roots")
237
+
238
+
239
+ class TestRegistryReadError:
240
+ def test_missing_file_returns_empty_dict(self, tmp_path: Path) -> None:
241
+ missing_file = tmp_path / "nonexistent.json"
242
+ result = setup._read_existing_registry(missing_file)
243
+ assert result == {}
244
+
245
+ def test_malformed_json_raises_registry_read_error(self, tmp_path: Path) -> None:
246
+ corrupt_file = tmp_path / "project-paths.json"
247
+ corrupt_file.write_text("{ not valid json", encoding="utf-8")
248
+ with pytest.raises(setup.RegistryReadError):
249
+ setup._read_existing_registry(corrupt_file)
250
+
251
+ def test_non_dict_top_level_raises_registry_read_error(self, tmp_path: Path) -> None:
252
+ non_dict_file = tmp_path / "project-paths.json"
253
+ non_dict_file.write_text(json.dumps(["a", "b"]), encoding="utf-8")
254
+ with pytest.raises(setup.RegistryReadError):
255
+ setup._read_existing_registry(non_dict_file)
256
+
257
+ def test_oserror_raises_registry_read_error(self, tmp_path: Path) -> None:
258
+ existing_file = tmp_path / "project-paths.json"
259
+ existing_file.write_text("{}", encoding="utf-8")
260
+ with patch.object(
261
+ type(existing_file),
262
+ "read_text",
263
+ side_effect=OSError("permission denied"),
264
+ ):
265
+ with pytest.raises(setup.RegistryReadError):
266
+ setup._read_existing_registry(existing_file)
267
+
268
+ def test_registry_read_error_in_prompt_and_write_exits_nonzero(
269
+ self, tmp_path: Path
270
+ ) -> None:
271
+ corrupt_file = tmp_path / "project-paths.json"
272
+ corrupt_file.write_text("{ not valid json", encoding="utf-8")
273
+ with patch("builtins.input", return_value="yes"):
274
+ with pytest.raises(SystemExit) as raised_exit:
275
+ setup.prompt_and_write(
276
+ path_by_name={"my-repo": "C:\\my-repo"},
277
+ save_path=corrupt_file,
278
+ )
279
+ assert raised_exit.value.code != 0
280
+
281
+ def test_registry_read_error_does_not_overwrite_file(
282
+ self, tmp_path: Path, capsys: pytest.CaptureFixture
283
+ ) -> None:
284
+ corrupt_file = tmp_path / "project-paths.json"
285
+ original_content = "{ not valid json"
286
+ corrupt_file.write_text(original_content, encoding="utf-8")
287
+ with patch("builtins.input", return_value="yes"):
288
+ with pytest.raises(SystemExit):
289
+ setup.prompt_and_write(
290
+ path_by_name={"my-repo": "C:\\my-repo"},
291
+ save_path=corrupt_file,
292
+ )
293
+ assert corrupt_file.read_text(encoding="utf-8") == original_content
294
+
295
+
296
+ class TestEarlyRegistryValidation:
297
+ def test_malformed_registry_exits_before_prompting(self, tmp_path: Path) -> None:
298
+ corrupt_file = tmp_path / "project-paths.json"
299
+ corrupt_file.write_text("{ not valid json", encoding="utf-8")
300
+ prompt_call_count = 0
301
+
302
+ def counting_input(prompt_text: str) -> str:
303
+ nonlocal prompt_call_count
304
+ prompt_call_count += 1
305
+ return "yes"
306
+
307
+ with patch("builtins.input", side_effect=counting_input):
308
+ with pytest.raises(SystemExit) as raised_exit:
309
+ setup.prompt_and_write(
310
+ path_by_name={"my-repo": "C:\\my-repo"},
311
+ save_path=corrupt_file,
312
+ )
313
+ assert raised_exit.value.code != 0
314
+ assert prompt_call_count == 0
315
+
316
+ def test_malformed_registry_leaves_file_untouched(self, tmp_path: Path) -> None:
317
+ corrupt_file = tmp_path / "project-paths.json"
318
+ original_content = "{ not valid json"
319
+ corrupt_file.write_text(original_content, encoding="utf-8")
320
+ with patch("builtins.input", return_value="yes"):
321
+ with pytest.raises(SystemExit):
322
+ setup.prompt_and_write(
323
+ path_by_name={"my-repo": "C:\\my-repo"},
324
+ save_path=corrupt_file,
325
+ )
326
+ assert corrupt_file.read_text(encoding="utf-8") == original_content
327
+
328
+ def test_schema_mismatch_exits_before_prompting(self, tmp_path: Path) -> None:
329
+ target_file = tmp_path / "project-paths.json"
330
+ target_file.write_text(
331
+ json.dumps({"_meta": {"schema_version": 99}}), encoding="utf-8"
332
+ )
333
+ prompt_call_count = 0
334
+
335
+ def counting_input(prompt_text: str) -> str:
336
+ nonlocal prompt_call_count
337
+ prompt_call_count += 1
338
+ return "yes"
339
+
340
+ with patch("builtins.input", side_effect=counting_input):
341
+ with pytest.raises(SystemExit) as raised_exit:
342
+ setup.prompt_and_write(
343
+ path_by_name={"my-repo": "C:\\my-repo"},
344
+ save_path=target_file,
345
+ )
346
+ assert raised_exit.value.code != 0
347
+ assert prompt_call_count == 0
348
+
349
+ def test_schema_mismatch_leaves_file_untouched(self, tmp_path: Path) -> None:
350
+ target_file = tmp_path / "project-paths.json"
351
+ original_registry = {"_meta": {"schema_version": 99}}
352
+ target_file.write_text(json.dumps(original_registry), encoding="utf-8")
353
+ with patch("builtins.input", return_value="yes"):
354
+ with pytest.raises(SystemExit):
355
+ setup.prompt_and_write(
356
+ path_by_name={"my-repo": "C:\\my-repo"},
357
+ save_path=target_file,
358
+ )
359
+ written_back = json.loads(target_file.read_text(encoding="utf-8"))
360
+ assert written_back == original_registry
361
+
362
+ def test_registry_read_exactly_once_across_full_write_path(
363
+ self, tmp_path: Path
364
+ ) -> None:
365
+ target_file = tmp_path / "project-paths.json"
366
+ target_file.write_text(
367
+ json.dumps({"_meta": {"schema_version": 1}, "existing-repo": "C:\\existing"}),
368
+ encoding="utf-8",
369
+ )
370
+ read_call_count = 0
371
+ original_read = setup._read_existing_registry
372
+
373
+ def counting_read(target: Path) -> dict:
374
+ nonlocal read_call_count
375
+ read_call_count += 1
376
+ return original_read(target)
377
+
378
+ with patch("setup_project_paths._read_existing_registry", side_effect=counting_read):
379
+ with patch("builtins.input", return_value="yes"):
380
+ setup.prompt_and_write(
381
+ path_by_name={"my-repo": "C:\\my-repo"},
382
+ save_path=target_file,
383
+ )
384
+ assert read_call_count == 1
385
+
386
+
387
+ class TestDiscoverRepoRootsDedup:
388
+ def test_discover_returns_sorted_unique_paths(self) -> None:
389
+ all_paths = [
390
+ "C:\\Projects\\beta\\.git",
391
+ "C:\\Projects\\alpha\\.git",
392
+ "C:\\Projects\\alpha\\.git",
393
+ ]
394
+ with patch("setup_project_paths._run_es_exe_folders_query", return_value=all_paths):
395
+ all_roots = setup.discover_repo_roots_via_everything()
396
+ assert all_roots == sorted(set(all_roots))
397
+ assert len(all_roots) == len(set(all_roots))
398
+
399
+
400
+ class TestPromptAndWriteUsesConstants:
401
+ def test_confirmation_prompt_text_comes_from_constants(self) -> None:
402
+ captured_prompts: list[str] = []
403
+
404
+ def capturing_input(prompt_text: str) -> str:
405
+ captured_prompts.append(prompt_text)
406
+ return "no"
407
+
408
+ with patch("builtins.input", side_effect=capturing_input):
409
+ setup.prompt_and_write(
410
+ path_by_name={"my-repo": "C:\\my-repo"},
411
+ save_path=Path("/nonexistent/path.json"),
412
+ )
413
+ assert len(captured_prompts) == 1
414
+ assert captured_prompts[0] == CONFIRMATION_PROMPT_TEXT
415
+
416
+ def test_abort_message_comes_from_constants(
417
+ self, capsys: pytest.CaptureFixture
418
+ ) -> None:
419
+ with patch("builtins.input", return_value="no"):
420
+ setup.prompt_and_write(
421
+ path_by_name={"my-repo": "C:\\my-repo"},
422
+ save_path=Path("/nonexistent/path.json"),
423
+ )
424
+ captured = capsys.readouterr()
425
+ assert ABORTED_NOTHING_WRITTEN_MESSAGE in captured.out
426
+
427
+ def test_wrote_entries_status_uses_constants_template(
428
+ self, tmp_path: Path, capsys: pytest.CaptureFixture
429
+ ) -> None:
430
+ target_file = tmp_path / "project-paths.json"
431
+ with patch("builtins.input", return_value="yes"):
432
+ setup.prompt_and_write(
433
+ path_by_name={"my-repo": "C:\\my-repo"},
434
+ save_path=target_file,
435
+ )
436
+ captured = capsys.readouterr()
437
+ expected_message = WROTE_ENTRIES_STATUS_TEMPLATE.format(
438
+ entry_count=1, save_path=target_file
439
+ )
440
+ assert expected_message in captured.out
441
+
442
+
443
+ class TestEverythingScanError:
444
+ def test_nonzero_return_code_raises_everything_scan_error(self) -> None:
445
+ failed_completion = subprocess.CompletedProcess(
446
+ args=["es.exe"],
447
+ returncode=1,
448
+ stdout="",
449
+ stderr="service not running",
450
+ )
451
+ with patch("subprocess.run", return_value=failed_completion):
452
+ with pytest.raises(setup.EverythingScanError) as raised_error:
453
+ setup._run_es_exe_folders_query()
454
+ assert "service not running" in str(raised_error.value)
455
+
456
+ def test_nonzero_return_code_includes_return_code_in_message(self) -> None:
457
+ failed_completion = subprocess.CompletedProcess(
458
+ args=["es.exe"],
459
+ returncode=2,
460
+ stdout="",
461
+ stderr="access denied",
462
+ )
463
+ with patch("subprocess.run", return_value=failed_completion):
464
+ with pytest.raises(setup.EverythingScanError) as raised_error:
465
+ setup._run_es_exe_folders_query()
466
+ assert "2" in str(raised_error.value)
467
+
468
+ def test_zero_return_code_returns_parsed_paths(self) -> None:
469
+ successful_completion = subprocess.CompletedProcess(
470
+ args=["es.exe"],
471
+ returncode=0,
472
+ stdout="C:\\Projects\\alpha\\.git\nD:\\Work\\beta\\.git\n",
473
+ stderr="",
474
+ )
475
+ with patch("subprocess.run", return_value=successful_completion):
476
+ all_paths = setup._run_es_exe_folders_query()
477
+ assert all_paths == ["C:\\Projects\\alpha\\.git", "D:\\Work\\beta\\.git"]
478
+
479
+ def test_main_catches_everything_scan_error_and_exits_nonzero(self) -> None:
480
+ with (
481
+ patch(
482
+ "setup_project_paths._everything_binary_is_available",
483
+ return_value=True,
484
+ ),
485
+ patch(
486
+ "setup_project_paths._run_es_exe_folders_query",
487
+ side_effect=setup.EverythingScanError("service not running: exit 1"),
488
+ ),
489
+ pytest.raises(SystemExit) as raised_exit,
490
+ ):
491
+ setup.main()
492
+ assert raised_exit.value.code != 0
493
+
494
+ def test_main_prints_clear_error_to_stderr_on_everything_scan_error(
495
+ self, capsys: pytest.CaptureFixture
496
+ ) -> None:
497
+ with (
498
+ patch(
499
+ "setup_project_paths._everything_binary_is_available",
500
+ return_value=True,
501
+ ),
502
+ patch(
503
+ "setup_project_paths._run_es_exe_folders_query",
504
+ side_effect=setup.EverythingScanError("service not running: exit 1"),
505
+ ),
506
+ pytest.raises(SystemExit),
507
+ ):
508
+ setup.main()
509
+ captured = capsys.readouterr()
510
+ assert "Everything scan failed" in captured.err
511
+ assert "service" in captured.err.lower()
512
+
513
+
514
+ class TestSharedConfigPath:
515
+ def test_default_user_config_path_matches_project_paths_reader(self) -> None:
516
+ assert setup._default_user_config_path() == registry_file_path()
517
+
518
+ def test_untracked_repo_detector_config_path_matches_project_paths_reader(
519
+ self,
520
+ ) -> None:
521
+ shared_path = registry_file_path()
522
+ assert str(shared_path) in detector_module._build_confirm_instruction(
523
+ str(shared_path.parent)
524
+ ) or "project-paths.json" in detector_module._build_confirm_instruction(
525
+ str(shared_path.parent)
526
+ )
527
+
528
+ def test_all_three_modules_resolve_identical_config_path(self) -> None:
529
+ shared_path = registry_file_path()
530
+ assert shared_path == setup._default_user_config_path()
531
+ assert shared_path.name == "project-paths.json"
532
+ assert shared_path.parent.name == ".claude"
@@ -0,0 +1,6 @@
1
+ """This module is intentionally empty.
2
+
3
+ Constant-contract coverage for setup_project_paths is consolidated in
4
+ packages/claude-dev-env/hooks/config/test_setup_project_paths_constants.py
5
+ to avoid maintaining duplicate assertions across two files.
6
+ """
@@ -12,7 +12,7 @@
12
12
  - **Code rules gate before every AUDIT.** Run `scripts/bugteam_code_rules_gate.py` until exit **0** before spawning **bugfind**. Same `validate_content` logic as `hooks/blocking/code_rules_enforcer.py`.
13
13
  - **Clean-room audits, every loop.** Each bugfind teammate's spawn prompt contains only the PR scope, audit rubric, and the current loop number. Prior loop history stays in the lead.
14
14
  - **Targeted fixes.** Each fix teammate sees ONLY the most recent audit's findings. Prior loops are invisible to the fix teammate.
15
- - **Sonnet for both teammates.** Predictable cost, fits-purpose for code work.
15
+ - **Opus 4.7 at xhigh effort for both teammates.** Both `Agent(...)` spawns pass `model="opus"`, which resolves to Opus 4.7 on the Anthropic API. Opus 4.7's default effort level in Claude Code is `xhigh` (https://code.claude.com/docs/en/model-config — *"On Opus 4.7, the default effort is `xhigh` for all plans and providers."*), so no `effort` override is needed at spawn time. Effort is set per-subagent in YAML frontmatter, not via the `Agent` tool's parameters; `code-quality-agent` and `clean-coder` rely on the model default. The trade vs Sonnet is higher per-loop cost in exchange for deeper audit recall and stronger fix correctness on bug-hunting work, which the per-PR loop economics tolerate (10-loop hard cap bounds total spend).
16
16
  - **Fix teammate receives the latest audit as its input contract.** Passing the audit's findings to the fix teammate is the input contract — each loop's fix run operates on the current audit's output and only that.
17
17
  - **One commit per fix action.** Loops produce one commit per loop, not one per bug.
18
18
  - **Linear branch, fixed PR base.** Every loop appends one forward-only commit; existing commits and the PR base stay intact throughout the cycle.
@@ -29,7 +29,7 @@ cd into `<worktree_path>` before any git, gh, or file operation.
29
29
  B. Selector / query / engine compatibility
30
30
  C. Resource cleanup and lifecycle (file handles, connections, processes, locks)
31
31
  D. Variable scoping, ordering, and unbound references
32
- E. Dead code and unused imports
32
+ E. Dead code: dead parameters, dead locals, dead imports, dead branches, dead returns, and unused imports
33
33
  F. Silent failures (catch-all excepts, unconditional success returns, missing error propagation)
34
34
  G. Off-by-one, bounds, and integer overflow
35
35
  H. Security boundaries (injection, path traversal, auth bypass, secret leakage)
@@ -2,8 +2,8 @@
2
2
  name: bugteam
3
3
  description: >-
4
4
  Claude Code agent team on the open pull request: run the CODE_RULES gate,
5
- spawn a fresh clean-room audit (code-quality-agent, sonnet) and a fix pass
6
- (clean-coder, sonnet), post per-loop GitHub review threads from teammates,
5
+ spawn a fresh clean-room audit (code-quality-agent, opus) and a fix pass
6
+ (clean-coder, opus), post per-loop GitHub review threads from teammates,
7
7
  stop at zero findings or a 10-audit safety cap. Grants then revokes
8
8
  `.claude/**` edit permission around the run. SKILL.md is the orchestration
9
9
  checklist; `reference/` holds expanded prose by domain; CONSTRAINTS,
@@ -122,7 +122,7 @@ TeamCreate(
122
122
 
123
123
  **`<team_temp_dir>`:** `Path(tempfile.gettempdir()) / team_name` (lead resolves once to an absolute path; every shell gets that literal string).
124
124
 
125
- **Roles (spawned per loop, not here):** bugfind → `code-quality-agent` sonnet; bugfix → `clean-coder` sonnet. **Display:** inherit `teammateMode` from `~/.claude.json`. Reference subagent types by name when spawning teammates ([`sources.md`](sources.md) § Referencing subagent types when spawning teammates).
125
+ **Roles (spawned per loop, not here):** bugfind → `code-quality-agent` opus (4.7) at xhigh effort; bugfix → `clean-coder` opus (4.7) at xhigh effort. `model="opus"` resolves to Opus 4.7 on the Anthropic API and runs at the model's default `xhigh` effort level — see [`CONSTRAINTS.md`](CONSTRAINTS.md) § **Opus 4.7 at xhigh effort for both teammates** for rationale. **Display:** inherit `teammateMode` from `~/.claude.json`. Reference subagent types by name when spawning teammates ([`sources.md`](sources.md) § Referencing subagent types when spawning teammates).
126
126
 
127
127
  **Loop state (lead; not a single script):**
128
128
 
@@ -231,7 +231,7 @@ Agent(
231
231
  subagent_type="code-quality-agent",
232
232
  name="bugfind-pr<N>-loop<L>",
233
233
  team_name="<team_name>",
234
- model="sonnet",
234
+ model="opus",
235
235
  description="Bugfind audit PR <N> loop <L>",
236
236
  prompt="<audit XML; see PROMPTS.md>"
237
237
  )
@@ -261,7 +261,7 @@ Agent(
261
261
  subagent_type="clean-coder",
262
262
  name="bugfix-pr<N>-loop<L>",
263
263
  team_name="<team_name>",
264
- model="sonnet",
264
+ model="opus",
265
265
  description="Bugfix PR <N> loop <L>",
266
266
  prompt="<fix XML; see PROMPTS.md>"
267
267
  )
@@ -23,7 +23,7 @@ Each invariant cites the normative section or companion file it derives from.
23
23
  | I-3 | Exactly one `TeamCreate` and exactly one `TeamDelete` per invocation. | `SKILL.md` § Step 2; § Step 4 |
24
24
  | I-4 | Before `TeamDelete`, no teammate remains active without cleanup: either the teammate self-terminated after `Agent` returned, or the lead sent a matching `SendMessage(..., shutdown_request)` (including parallel-auditor shutdowns). No orphaned teammates when `TeamDelete` runs. | `SKILL.md` § AUDIT action (**Shutdown**); § FIX action (**Shutdown**); § Step 4 |
25
25
  | I-5 | `Agent` calls are fresh per loop — the same `name` is never reused across loops without an intervening shutdown. | `CONSTRAINTS.md` — **Fresh teammate per loop** |
26
- | I-6 | Both audit and fix `Agent` calls pass `model="sonnet"`. | `SKILL.md` § Step 2 (**Roles**); `CONSTRAINTS.md` — **Sonnet for both teammates** |
26
+ | I-6 | Both audit and fix `Agent` calls pass `model="opus"` (resolves to Opus 4.7 via the Anthropic API alias; effort remains the Claude Code/model-config default `xhigh`). | `SKILL.md` § Step 2 (**Roles**); `CONSTRAINTS.md` — **Opus 4.7 at xhigh effort for both teammates** |
27
27
  | I-7 | `TeamDelete()` is called with no arguments. | TeamDelete schema: no required params, no properties |
28
28
  | I-8 | Loop count ≤ 10 audits. 11th audit never fires. | `SKILL.md` YAML `description` (10-loop cap); § Step 3 (**Pre-audit** / **FIX** increment rules) |
29
29
  | I-9 | From loop 4 onward without convergence, the audit phase emits three parallel `Agent` calls in a single assistant message with names `bugfind-loop-<N>-a/b/c`. | `SKILL.md` § AUDIT action (**Parallel auditors**); `reference/audit-and-teammates.md` § **Parallel auditors** |
@@ -111,10 +111,10 @@ The harness does not yet exist; this document defines its contract.
111
111
  | 4 | `TeamCreate(team_name="bugteam-pr-42-<ts>", description=..., agent_type="team-lead")` | `SKILL.md` § Step 2 |
112
112
  | 5 | `Bash("mkdir -p <team_temp_dir>")` | `SKILL.md` § AUDIT action |
113
113
  | 6 | `Bash("gh pr diff 42 -R ... > <team_temp_dir>/loop-1.patch")` | `SKILL.md` § AUDIT action |
114
- | 7 | `Agent(subagent_type="code-quality-agent", name="bugfind", team_name=..., model="sonnet", description=..., prompt=<audit XML loop 1>)` | `SKILL.md` § AUDIT action |
114
+ | 7 | `Agent(subagent_type="code-quality-agent", name="bugfind", team_name=..., model="opus", description=..., prompt=<audit XML loop 1>)` | `SKILL.md` § AUDIT action |
115
115
  | 8 | `Read(".bugteam-loop-1.outcomes.xml")` | `SKILL.md` § AUDIT action |
116
116
  | 9 | `SendMessage(to="bugfind", message={type: "shutdown_request", reason: "audit loop 1 complete; outcome XML captured"})` | `SKILL.md` § AUDIT action (**Shutdown** fallback) |
117
- | 10 | `Agent(subagent_type="clean-coder", name="bugfix", team_name=..., model="sonnet", description=..., prompt=<fix XML loop 1>)` | `SKILL.md` § FIX action |
117
+ | 10 | `Agent(subagent_type="clean-coder", name="bugfix", team_name=..., model="opus", description=..., prompt=<fix XML loop 1>)` | `SKILL.md` § FIX action |
118
118
  | 11 | `Read(".bugteam-loop-1.outcomes.xml")` — bugfix outcome XML overwrites same filename | `SKILL.md` § FIX action |
119
119
  | 12 | `Bash("git rev-parse HEAD")` → verify HEAD advanced | `SKILL.md` § FIX action (**Verify**) |
120
120
  | 13 | `Bash("git fetch origin <branch> && git rev-parse origin/<branch>")` → verify push landed | `SKILL.md` § FIX action (**Verify**) |
@@ -302,7 +302,7 @@ Agent(
302
302
  subagent_type="code-quality-agent",
303
303
  name="bugfind-adjacent",
304
304
  team_name="<lead_team_name>", // same team as bugfind/bugfix
305
- model="sonnet",
305
+ model="opus",
306
306
  description="Supplementary audit of adjacent infrastructure",
307
307
  prompt=<brief naming the specific adjacent files + observed symptom>
308
308
  )
@@ -343,4 +343,4 @@ A minimal Python harness under `packages/claude-dev-env/skills/bugteam/evals/`:
343
343
 
344
344
  1. **GitHub REST review-POST payload shape.** Eval 9 and Eval 10 depend on the exact body shape of `POST /pulls/<number>/reviews`. The `jq -n --rawfile ... --argjson ... | gh api ... --input -` fence lives in `SKILL.md` § Step 2.5 (**Review POST**); expanded copy in `reference/github-pr-reviews.md` § **Per-loop review**. Before running Eval 9/10 for real, fetch the current GitHub REST reference to confirm the request schema (fields `commit_id`, `event`, `body`, `comments[]`) and the multi-line anchor `{path, start_line, start_side, line, side, body}` shape still apply. Record the confirmed version and URL here.
345
345
  2. **`SendMessage` shutdown origination — RESOLVED.** `SendMessage` tool docs include the line "Don't originate `shutdown_request` unless asked." `TeamCreate` tool docs explicitly direct the lead to originate `{type: "shutdown_request"}` for teammate cleanup. Real-run observation (loop 1 of eval run 2026-04-18) resolved the contradiction: teammates self-terminate when their task is complete — the `Agent` call returns and the teammate's session ends without any `SendMessage`. The cycle proceeded correctly without the lead ever needing to originate a `shutdown_request`. `SKILL.md` § AUDIT / FIX actions document self-termination as the expected path and lead-originated `SendMessage(shutdown_request)` as a fallback; `reference/audit-and-teammates.md` carries the longer shutdown narrative. Layer A **I-4** encodes “no orphaned teammates,” not “always send SendMessage.”
346
- 3. **Model override redundancy.** `code-quality-agent` and `clean-coder` may already pin `model` in their agent definitions. The explicit `model="sonnet"` in every spawn is insurance, but on the first real run confirm no conflict between the lead-passed model and the agent-frontmatter model.
346
+ 3. **Model override redundancy.** `clean-coder` pins `model: opus` in its agent definition, while `code-quality-agent` currently uses `model: inherit`. The explicit `model="opus"` in every spawn is insurance against frontmatter drift; on the first real run, confirm the resolved model is `claude-opus-4-7` and that effort defaults to `xhigh` (Claude Code shows the active effort next to the spinner per the model-config docs). If a teammate's frontmatter ever pins a non-default `effort:` value, that frontmatter overrides the model default for that subagent (https://code.claude.com/docs/en/model-config — *"Frontmatter effort applies when that skill or subagent is active, overriding the session level but not the environment variable."*).
@@ -76,9 +76,9 @@ The teammate replies with `{type: "shutdown_response", approve: true}`. If `appr
76
76
  The pre-audit gate must pass immediately before this step. After three full audit/fix rounds without convergence, issue three `Agent` calls in **one** assistant message so they run in parallel:
77
77
 
78
78
  ```
79
- Agent(subagent_type="code-quality-agent", name="bugfind-loop-<N>-a", team_name="<team_name>", model="sonnet", description="Bugfind audit loop <N> variant a", prompt="<audit XML; write outcome to .bugteam-loop-<N>.outcomes.xml; post the per-loop review; read and merge b/c outcomes from <team_temp_dir>/loop-<N>-b.outcomes.xml and <team_temp_dir>/loop-<N>-c.outcomes.xml>")
80
- Agent(subagent_type="code-quality-agent", name="bugfind-loop-<N>-b", team_name="<team_name>", model="sonnet", description="Bugfind audit loop <N> variant b", prompt="<audit XML; write outcome to <team_temp_dir>/loop-<N>-b.outcomes.xml; skip PR posting>")
81
- Agent(subagent_type="code-quality-agent", name="bugfind-loop-<N>-c", team_name="<team_name>", model="sonnet", description="Bugfind audit loop <N> variant c", prompt="<audit XML; write outcome to <team_temp_dir>/loop-<N>-c.outcomes.xml; skip PR posting>")
79
+ Agent(subagent_type="code-quality-agent", name="bugfind-loop-<N>-a", team_name="<team_name>", model="opus", description="Bugfind audit loop <N> variant a", prompt="<audit XML; write outcome to .bugteam-loop-<N>.outcomes.xml; post the per-loop review; read and merge b/c outcomes from <team_temp_dir>/loop-<N>-b.outcomes.xml and <team_temp_dir>/loop-<N>-c.outcomes.xml>")
80
+ Agent(subagent_type="code-quality-agent", name="bugfind-loop-<N>-b", team_name="<team_name>", model="opus", description="Bugfind audit loop <N> variant b", prompt="<audit XML; write outcome to <team_temp_dir>/loop-<N>-b.outcomes.xml; skip PR posting>")
81
+ Agent(subagent_type="code-quality-agent", name="bugfind-loop-<N>-c", team_name="<team_name>", model="opus", description="Bugfind audit loop <N> variant c", prompt="<audit XML; write outcome to <team_temp_dir>/loop-<N>-c.outcomes.xml; skip PR posting>")
82
82
  ```
83
83
 
84
84
  Teammate `-a` is the post-owner: read all three outcome XML files at explicit absolute paths (`.bugteam-loop-<N>.outcomes.xml` in cwd, plus sibling paths under `<team_temp_dir>`), merge findings by `(file, line, category_letter)` (collapse duplicates, keep longest description and highest severity), re-assign merged IDs as `loopN-K`, post the single per-loop review. The `-a` prompt must embed sibling paths as literal absolutes so `Read` works without discovery.