claude-dev-env 1.36.2 → 1.37.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 (76) hide show
  1. package/_shared/pr-loop/scripts/config/preflight_constants.py +29 -8
  2. package/_shared/pr-loop/scripts/preflight.py +242 -20
  3. package/_shared/pr-loop/scripts/tests/test_preflight.py +362 -25
  4. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +9 -14
  5. package/hooks/blocking/code_rules_enforcer.py +269 -23
  6. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
  7. package/hooks/config/test_unused_module_import_constants.py +48 -0
  8. package/hooks/config/unused_module_import_constants.py +41 -0
  9. package/package.json +1 -1
  10. package/rules/gh-paginate.md +4 -50
  11. package/rules/no-historical-clutter.md +36 -0
  12. package/skills/bg-agent/SKILL.md +69 -0
  13. package/skills/bugteam/CONSTRAINTS.md +10 -19
  14. package/skills/bugteam/PROMPTS.md +21 -14
  15. package/skills/bugteam/SKILL.md +122 -208
  16. package/skills/bugteam/SKILL_EVALS.md +75 -114
  17. package/skills/bugteam/reference/README.md +2 -4
  18. package/skills/bugteam/reference/audit-and-teammates.md +21 -48
  19. package/skills/bugteam/reference/audit-contract.md +7 -7
  20. package/skills/bugteam/reference/design-rationale.md +3 -8
  21. package/skills/bugteam/reference/team-setup.md +11 -19
  22. package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
  23. package/skills/bugteam/scripts/config/__init__.py +0 -0
  24. package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
  25. package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
  26. package/skills/bugteam/sources.md +1 -25
  27. package/skills/bugteam/test_skill_additions.py +4 -13
  28. package/skills/fresh-branch/SKILL.md +71 -0
  29. package/skills/gotcha/SKILL.md +73 -0
  30. package/skills/monitor-open-prs/SKILL.md +4 -37
  31. package/skills/monitor-open-prs/test_skill_contract.py +0 -5
  32. package/skills/pr-converge/SKILL.md +60 -1298
  33. package/skills/pr-converge/reference/convergence-gates.md +122 -0
  34. package/skills/pr-converge/reference/examples.md +76 -0
  35. package/skills/pr-converge/reference/fix-protocol.md +56 -0
  36. package/skills/pr-converge/reference/ground-rules.md +13 -0
  37. package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
  38. package/skills/pr-converge/reference/per-tick.md +204 -0
  39. package/skills/pr-converge/reference/state-schema.md +19 -0
  40. package/skills/pr-converge/reference/stop-conditions.md +26 -0
  41. package/skills/pr-converge/scripts/README.md +36 -9
  42. package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
  43. package/skills/pr-converge/scripts/config/pr_converge_constants.py +74 -5
  44. package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
  45. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
  46. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
  47. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
  48. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
  49. package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
  50. package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
  51. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
  52. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
  53. package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
  54. package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
  55. package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
  56. package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
  57. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
  58. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
  59. package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
  60. package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
  61. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
  62. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
  63. package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
  64. package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
  65. package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
  66. package/skills/pr-converge/scripts/test_view_pr_context.py +44 -0
  67. package/skills/pr-converge/scripts/view_pr_context.py +35 -4
  68. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
  69. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
  70. package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
  71. package/skills/bugteam/test_team_lifecycle.py +0 -103
  72. package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
  73. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
  74. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
  75. package/skills/pr-converge/test_team_lifecycle.py +0 -56
  76. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +0 -108
@@ -8,8 +8,6 @@ GIT_DIRECTORY_NAME: str = ".git"
8
8
 
9
9
  CLAUDE_DIRECTORY_NAME: str = ".claude"
10
10
 
11
- VENV_DIRECTORY_NAME: str = ".venv"
12
-
13
11
  PYTEST_INI_FILENAME: str = "pytest.ini"
14
12
 
15
13
  PYPROJECT_TOML_FILENAME: str = "pyproject.toml"
@@ -18,13 +16,16 @@ PRE_COMMIT_CONFIG_YAML_FILENAME: str = ".pre-commit-config.yaml"
18
16
 
19
17
  PYTEST_TOML_TABLE_PREFIX: str = "[tool.pytest"
20
18
 
21
- ALL_TEST_FILE_PATTERNS_FOR_DISCOVERY: tuple[str, str] = (
22
- "test_*.py",
23
- "*_test.py",
24
- )
19
+ PYTEST_FAILED_FIRST_FLAG: str = "--ff"
25
20
 
26
- ALL_TESTS_DIRECTORY_IGNORE_PARTS: frozenset[str] = frozenset(
27
- {"site-packages", VENV_DIRECTORY_NAME, "venv", "node_modules"}
21
+ ALL_GIT_LS_FILES_TEST_DISCOVERY_SUBCOMMAND: tuple[str, ...] = (
22
+ "ls-files",
23
+ "--cached",
24
+ "--others",
25
+ "--exclude-standard",
26
+ "--",
27
+ "**/test_*.py",
28
+ "**/*_test.py",
28
29
  )
29
30
 
30
31
  ALL_REPOSITORY_ROOT_MARKER_FILENAMES: tuple[str, str] = (
@@ -44,4 +45,24 @@ ALL_PRE_COMMIT_RUN_ALL_FILES_COMMAND: tuple[str, str, str] = (
44
45
  "--all-files",
45
46
  )
46
47
 
48
+ ALL_GIT_DIFF_NAME_ONLY_SUBCOMMAND: tuple[str, str] = (
49
+ "diff",
50
+ "--name-only",
51
+ )
52
+
53
+ PYTEST_SCOPE_ALL: str = "all"
54
+
55
+ PYTEST_SCOPE_CHANGED: str = "changed"
56
+
57
+ ALL_PYTEST_SCOPE_CHOICES: tuple[str, str] = (PYTEST_SCOPE_ALL, PYTEST_SCOPE_CHANGED)
58
+
59
+
60
+ PYTHON_FILE_SUFFIX: str = ".py"
61
+
62
+ PYTEST_TEST_FILENAME_PREFIX: str = "test_"
63
+
64
+ PYTEST_TEST_FILENAME_SUFFIX: str = "_test"
65
+
47
66
  PYTEST_NO_TESTS_COLLECTED_EXIT_CODE: int = 5
67
+
68
+ TESTS_DIRECTORY_NAME: str = "tests"
@@ -5,23 +5,57 @@ import sys
5
5
  from pathlib import Path
6
6
 
7
7
  sys.modules.pop("config", None)
8
- if str(Path(__file__).resolve().parent) not in sys.path:
9
- sys.path.insert(0, str(Path(__file__).resolve().parent))
8
+ _script_directory_resolved = Path(__file__).resolve().parent
9
+ _script_directory_absolute = Path(__file__).absolute().parent
10
+
11
+
12
+ def _entry_points_at_preflight_script_directory(each_path_entry: str) -> bool:
13
+ if each_path_entry in (
14
+ str(_script_directory_resolved),
15
+ str(_script_directory_absolute),
16
+ ):
17
+ return True
18
+ try:
19
+ candidate_path = Path(each_path_entry)
20
+ except (OSError, ValueError):
21
+ return False
22
+ if candidate_path.exists():
23
+ try:
24
+ return os.path.samefile(candidate_path, _script_directory_resolved)
25
+ except OSError:
26
+ return False
27
+ return False
28
+
29
+
30
+ for each_index in range(len(sys.path) - 1, -1, -1):
31
+ if _entry_points_at_preflight_script_directory(sys.path[each_index]):
32
+ sys.path.pop(each_index)
33
+ _preflight_scripts_path_entry = str(_script_directory_absolute)
34
+ if _preflight_scripts_path_entry not in sys.path:
35
+ sys.path.insert(0, _preflight_scripts_path_entry)
10
36
 
11
37
  from config.fix_hookspath_constants import HOOKS_PATH_VERIFICATION_SUFFIX
12
38
  from config.preflight_constants import (
13
39
  ALL_GIT_CONFIG_GET_CORE_HOOKS_PATH_SUBCOMMAND,
40
+ ALL_GIT_DIFF_NAME_ONLY_SUBCOMMAND,
41
+ ALL_GIT_LS_FILES_TEST_DISCOVERY_SUBCOMMAND,
14
42
  ALL_PRE_COMMIT_RUN_ALL_FILES_COMMAND,
15
- ALL_TEST_FILE_PATTERNS_FOR_DISCOVERY,
16
- ALL_TESTS_DIRECTORY_IGNORE_PARTS,
17
43
  BUGTEAM_PREFLIGHT_SKIP_ENABLED_VALUE,
18
44
  BUGTEAM_PREFLIGHT_SKIP_ENV_VAR_NAME,
19
45
  GIT_DIRECTORY_NAME,
20
46
  PRE_COMMIT_CONFIG_YAML_FILENAME,
21
47
  PYPROJECT_TOML_FILENAME,
48
+ PYTEST_FAILED_FIRST_FLAG,
22
49
  PYTEST_INI_FILENAME,
50
+ ALL_PYTEST_SCOPE_CHOICES,
23
51
  PYTEST_NO_TESTS_COLLECTED_EXIT_CODE,
52
+ PYTEST_SCOPE_ALL,
53
+ PYTEST_SCOPE_CHANGED,
54
+ PYTEST_TEST_FILENAME_PREFIX,
55
+ PYTEST_TEST_FILENAME_SUFFIX,
24
56
  PYTEST_TOML_TABLE_PREFIX,
57
+ PYTHON_FILE_SUFFIX,
58
+ TESTS_DIRECTORY_NAME,
25
59
  )
26
60
 
27
61
 
@@ -111,18 +145,41 @@ def has_pytest_configuration(root: Path) -> bool:
111
145
  return PYTEST_TOML_TABLE_PREFIX in text
112
146
 
113
147
 
114
- def has_discoverable_tests(root: Path) -> bool:
115
- all_ignored_parts = ALL_TESTS_DIRECTORY_IGNORE_PARTS
116
- test_filename_glob, test_suffix_glob = ALL_TEST_FILE_PATTERNS_FOR_DISCOVERY
117
- for each_path in root.rglob(test_filename_glob):
118
- if any(each_part in all_ignored_parts for each_part in each_path.parts):
119
- continue
148
+ def has_discoverable_tests(root: Path) -> bool | None:
149
+ git_marker = root / GIT_DIRECTORY_NAME
150
+ if not (git_marker.is_dir() or git_marker.is_file()):
120
151
  return True
121
- for each_path in root.rglob(test_suffix_glob):
122
- if any(each_part in all_ignored_parts for each_part in each_path.parts):
123
- continue
124
- return True
125
- return False
152
+ command = ["git", "-C", str(root), *ALL_GIT_LS_FILES_TEST_DISCOVERY_SUBCOMMAND]
153
+ try:
154
+ completed = subprocess.run(
155
+ command,
156
+ capture_output=True,
157
+ text=True,
158
+ encoding="utf-8",
159
+ errors="replace",
160
+ check=True,
161
+ )
162
+ except FileNotFoundError:
163
+ print(
164
+ "bugteam_preflight: git is not installed or not available on PATH.",
165
+ file=sys.stderr,
166
+ )
167
+ return None
168
+ except subprocess.CalledProcessError as error:
169
+ error_detail = (error.stderr or "").strip()
170
+ print(
171
+ f"bugteam_preflight: git ls-files failed (exit {error.returncode}):"
172
+ + (f"\n{error_detail}" if error_detail else ""),
173
+ file=sys.stderr,
174
+ )
175
+ return None
176
+ except OSError as error:
177
+ print(
178
+ f"bugteam_preflight: failed to run git ls-files: {error}",
179
+ file=sys.stderr,
180
+ )
181
+ return None
182
+ return bool(completed.stdout.strip())
126
183
 
127
184
 
128
185
  def _pytest_exit_code_no_tests_collected() -> int:
@@ -130,10 +187,17 @@ def _pytest_exit_code_no_tests_collected() -> int:
130
187
  return pytest_no_tests_collected_exit_code
131
188
 
132
189
 
133
- def run_pytest(repository_root: Path, verbose: bool) -> int:
134
- command = [sys.executable, "-m", "pytest"]
190
+ def run_pytest(
191
+ repository_root: Path,
192
+ verbose: bool,
193
+ all_test_paths: list[Path] | None = None,
194
+ ) -> int:
195
+ command = [sys.executable, "-m", "pytest", PYTEST_FAILED_FIRST_FLAG]
135
196
  if not verbose:
136
197
  command.append("-q")
198
+ if all_test_paths is not None:
199
+ command.append("--")
200
+ command.extend(str(each_path) for each_path in all_test_paths)
137
201
  completed = subprocess.run(
138
202
  command,
139
203
  cwd=str(repository_root),
@@ -144,6 +208,99 @@ def run_pytest(repository_root: Path, verbose: bool) -> int:
144
208
  return completed.returncode
145
209
 
146
210
 
211
+ def get_changed_files(repository_root: Path, base_ref: str) -> list[Path] | None:
212
+ if base_ref.startswith("-"):
213
+ print(
214
+ f"bugteam_preflight: invalid base_ref '{base_ref}' starts "
215
+ f"with hyphen; falling back to full suite.",
216
+ file=sys.stderr,
217
+ )
218
+ return None
219
+ command = [
220
+ "git",
221
+ *ALL_GIT_DIFF_NAME_ONLY_SUBCOMMAND,
222
+ f"{base_ref}...HEAD",
223
+ ]
224
+ try:
225
+ completed = subprocess.run(
226
+ command,
227
+ cwd=str(repository_root),
228
+ capture_output=True,
229
+ text=True,
230
+ encoding="utf-8",
231
+ errors="replace",
232
+ check=False,
233
+ )
234
+ except FileNotFoundError:
235
+ print(
236
+ "bugteam_preflight: git is not installed or not available on PATH.\n"
237
+ f"bugteam_preflight: cannot determine changed files against "
238
+ f"{base_ref}; falling back to full suite.",
239
+ file=sys.stderr,
240
+ )
241
+ return None
242
+ except OSError as os_error:
243
+ print(
244
+ f"bugteam_preflight: failed to run git: {os_error}\n"
245
+ f"bugteam_preflight: cannot determine changed files against "
246
+ f"{base_ref}; falling back to full suite.",
247
+ file=sys.stderr,
248
+ )
249
+ return None
250
+ if completed.returncode != 0:
251
+ print(
252
+ f"bugteam_preflight: git diff against {base_ref} failed "
253
+ f"(exit {completed.returncode}); falling back to full suite.\n"
254
+ f"{completed.stderr.strip()}",
255
+ file=sys.stderr,
256
+ )
257
+ return None
258
+ return [
259
+ Path(each_line.strip())
260
+ for each_line in completed.stdout.splitlines()
261
+ if each_line.strip()
262
+ ]
263
+
264
+
265
+ def _find_related_test_files(changed_path: Path, repository_root: Path) -> list[Path]:
266
+ if changed_path.suffix != PYTHON_FILE_SUFFIX:
267
+ return []
268
+ stem = changed_path.stem
269
+ test_prefix = PYTEST_TEST_FILENAME_PREFIX
270
+ test_suffix = PYTEST_TEST_FILENAME_SUFFIX
271
+ if (stem.startswith(test_prefix) or stem.endswith(test_suffix)) and (
272
+ repository_root / changed_path
273
+ ).is_file():
274
+ return [repository_root / changed_path]
275
+ full_path = repository_root / changed_path
276
+ parent = full_path.parent
277
+ adjacent_tests = parent / TESTS_DIRECTORY_NAME
278
+ top_tests = repository_root / TESTS_DIRECTORY_NAME
279
+ relative_parent = changed_path.parent
280
+ python_suffix = PYTHON_FILE_SUFFIX
281
+ all_candidates = [
282
+ parent / f"{test_prefix}{stem}{python_suffix}",
283
+ parent / f"{stem}{test_suffix}{python_suffix}",
284
+ adjacent_tests / f"{test_prefix}{stem}{python_suffix}",
285
+ adjacent_tests / f"{stem}{test_suffix}{python_suffix}",
286
+ ]
287
+ if relative_parent != Path("."):
288
+ all_candidates.extend([
289
+ top_tests / relative_parent / f"{test_prefix}{stem}{python_suffix}",
290
+ top_tests / relative_parent / f"{stem}{test_suffix}{python_suffix}",
291
+ ])
292
+ return sorted({each_candidate for each_candidate in all_candidates if each_candidate.is_file()})
293
+
294
+
295
+ def discover_related_tests(
296
+ all_changed_files: list[Path], repository_root: Path
297
+ ) -> list[Path]:
298
+ related: set[Path] = set()
299
+ for each_file in all_changed_files:
300
+ related.update(_find_related_test_files(each_file, repository_root))
301
+ return sorted(related)
302
+
303
+
147
304
  def run_pre_commit(repository_root: Path) -> int:
148
305
  completed = subprocess.run(
149
306
  list(ALL_PRE_COMMIT_RUN_ALL_FILES_COMMAND),
@@ -179,6 +336,26 @@ def parse_arguments(all_arguments: list[str]) -> argparse.Namespace:
179
336
  action="store_true",
180
337
  help="Verbose pytest output.",
181
338
  )
339
+ parser.add_argument(
340
+ "--base-ref",
341
+ type=str,
342
+ default=None,
343
+ help=(
344
+ "Git base ref for scoped test selection (e.g., origin/main). "
345
+ "When set, only tests related to files changed vs this ref are run."
346
+ ),
347
+ )
348
+ parser.add_argument(
349
+ "--scope",
350
+ type=str,
351
+ choices=list(ALL_PYTEST_SCOPE_CHOICES),
352
+ default=None,
353
+ help=(
354
+ "Test selection scope. 'all' runs the full suite. "
355
+ "'changed' runs only tests related to changed files (requires --base-ref). "
356
+ "Defaults to 'changed' when --base-ref is provided, 'all' otherwise."
357
+ ),
358
+ )
182
359
  return parser.parse_args(all_arguments)
183
360
 
184
361
 
@@ -202,13 +379,58 @@ def main(all_arguments: list[str]) -> int:
202
379
  if hooks_path_exit_code != 0:
203
380
  return hooks_path_exit_code
204
381
  if not arguments.no_pytest and has_pytest_configuration(repository_root):
205
- if not has_discoverable_tests(repository_root):
382
+ discovery_result = has_discoverable_tests(repository_root)
383
+ if discovery_result is None:
384
+ print(
385
+ "bugteam_preflight: test discovery failed; running full suite anyway.",
386
+ file=sys.stderr,
387
+ )
388
+ elif not discovery_result:
206
389
  print(
207
390
  "bugteam_preflight: pytest configured but no tests found; skipping pytest.",
208
391
  file=sys.stderr,
209
392
  )
210
- else:
211
- exit_code = run_pytest(repository_root, arguments.verbose)
393
+ if discovery_result is not False:
394
+ effective_scope = arguments.scope
395
+ if discovery_result is None:
396
+ effective_scope = PYTEST_SCOPE_ALL
397
+ if effective_scope is None:
398
+ effective_scope = (
399
+ PYTEST_SCOPE_CHANGED
400
+ if arguments.base_ref is not None
401
+ else PYTEST_SCOPE_ALL
402
+ )
403
+ if effective_scope == PYTEST_SCOPE_CHANGED and arguments.base_ref is None:
404
+ print(
405
+ "bugteam_preflight: --scope changed requires --base-ref; "
406
+ "falling back to full suite.",
407
+ file=sys.stderr,
408
+ )
409
+ effective_scope = PYTEST_SCOPE_ALL
410
+ if effective_scope == PYTEST_SCOPE_CHANGED and arguments.base_ref is not None:
411
+ all_changed = get_changed_files(repository_root, arguments.base_ref)
412
+ if all_changed is None:
413
+ exit_code = run_pytest(repository_root, arguments.verbose)
414
+ else:
415
+ all_related = discover_related_tests(all_changed, repository_root)
416
+ if all_related:
417
+ print(
418
+ f"bugteam_preflight: running {len(all_related)} test(s) "
419
+ f"related to changed files (scope=changed).",
420
+ file=sys.stderr,
421
+ )
422
+ exit_code = run_pytest(
423
+ repository_root, arguments.verbose, all_related
424
+ )
425
+ else:
426
+ print(
427
+ "bugteam_preflight: no related tests found; "
428
+ "running full suite.",
429
+ file=sys.stderr,
430
+ )
431
+ exit_code = run_pytest(repository_root, arguments.verbose)
432
+ else:
433
+ exit_code = run_pytest(repository_root, arguments.verbose)
212
434
  if exit_code != 0:
213
435
  return exit_code
214
436
  elif not arguments.no_pytest: