claude-dev-env 1.38.0 → 1.39.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 (271) hide show
  1. package/CLAUDE.md +10 -36
  2. package/_shared/pr-loop/audit-reply-template.md +147 -0
  3. package/_shared/pr-loop/fix-protocol.md +25 -4
  4. package/_shared/pr-loop/gh-payloads.md +37 -50
  5. package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
  6. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +189 -0
  7. package/_shared/pr-loop/scripts/post_audit_thread.py +947 -0
  8. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  9. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +923 -0
  10. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  11. package/_shared/pr-loop/state-schema.md +1 -1
  12. package/agents/clean-coder.md +2 -2
  13. package/bin/install.mjs +6 -7
  14. package/bin/install.test.mjs +8 -0
  15. package/commands/doc-gist.md +16 -0
  16. package/commands/plan.md +0 -2
  17. package/commands/review-plan.md +1 -1
  18. package/docs/CODE_RULES.md +122 -2
  19. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  20. package/hooks/blocking/code_rules_enforcer.py +1236 -161
  21. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  22. package/hooks/blocking/destructive_command_blocker.py +74 -0
  23. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  24. package/hooks/blocking/md_to_html_blocker.py +119 -0
  25. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  26. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  27. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  28. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  29. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  30. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  31. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  32. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  33. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  34. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  36. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  37. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  38. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  39. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  40. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  41. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  42. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +158 -0
  43. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  44. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  45. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  46. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  47. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  48. package/hooks/config/any_type_config.py +7 -0
  49. package/hooks/config/banned_identifiers_constants.py +11 -0
  50. package/hooks/config/blocking_check_limits.py +38 -0
  51. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  52. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  53. package/hooks/config/convergence_branch_constants.py +9 -0
  54. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  55. package/hooks/config/html_companion_constants.py +20 -0
  56. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  57. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  58. package/hooks/hooks.json +28 -20
  59. package/hooks/pyproject.toml +69 -0
  60. package/hooks/validators/mypy_integration.py +47 -1
  61. package/hooks/validators/run_all_validators.py +3 -3
  62. package/hooks/validators/test_mypy_integration.py +50 -1
  63. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  64. package/hooks/workflow/md_to_html_companion.py +365 -0
  65. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  66. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  67. package/package.json +1 -1
  68. package/rules/gh-body-file.md +2 -0
  69. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  70. package/scripts/check.ps1 +106 -0
  71. package/scripts/config/timing.py +11 -0
  72. package/scripts/sweep_empty_dirs.py +138 -0
  73. package/scripts/sync_to_cursor/rules.py +1 -1
  74. package/scripts/test_sweep_empty_dirs.py +183 -0
  75. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  76. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  77. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  78. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  79. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  80. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  81. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  82. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  83. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  84. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  85. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  86. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  87. package/skills/bugteam/CONSTRAINTS.md +21 -22
  88. package/skills/bugteam/EXAMPLES.md +3 -3
  89. package/skills/bugteam/PROMPTS.md +227 -67
  90. package/skills/bugteam/SKILL.md +114 -455
  91. package/skills/bugteam/reference/README.md +1 -1
  92. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  93. package/skills/bugteam/reference/audit-contract.md +4 -22
  94. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  95. package/skills/bugteam/reference/design-rationale.md +2 -2
  96. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  97. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  98. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  99. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  100. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  101. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  102. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  103. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  104. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  105. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  106. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  107. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  108. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  109. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  113. package/skills/bugteam/reference/team-setup.md +106 -9
  114. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  115. package/skills/bugteam/scripts/README.md +60 -0
  116. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  117. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  118. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  119. package/skills/bugteam/scripts/bugteam_preflight.py +294 -0
  120. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  121. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  122. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  123. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  124. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  125. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  126. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  127. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  128. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  129. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  130. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  131. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  132. package/skills/bugteam/scripts/test_bugteam_preflight.py +268 -0
  133. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  134. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  135. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  136. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  137. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  138. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  139. package/skills/bugteam/test_skill_additions.py +1 -11
  140. package/skills/code/SKILL.md +176 -0
  141. package/skills/doc-gist/SKILL.md +99 -0
  142. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  143. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  144. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  145. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  146. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  147. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  148. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  149. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  150. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  151. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  152. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  153. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  154. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  155. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  156. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  157. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  158. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  159. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  160. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  161. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  162. package/skills/doc-gist/references/examples/README.md +5 -0
  163. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  164. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  165. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  166. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  167. package/skills/findbugs/SKILL.md +68 -2
  168. package/skills/monitor-open-prs/SKILL.md +13 -32
  169. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  170. package/skills/pr-consistency-audit/SKILL.md +112 -0
  171. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  172. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  173. package/skills/pr-converge/SKILL.md +227 -23
  174. package/skills/pr-converge/config/__init__.py +0 -0
  175. package/skills/pr-converge/config/constants.py +62 -0
  176. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  177. package/skills/pr-converge/reference/examples.md +43 -11
  178. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  179. package/skills/pr-converge/reference/ground-rules.md +5 -3
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  181. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  182. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  183. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  184. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  185. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  186. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  187. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  188. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  189. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  190. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  191. package/skills/pr-converge/reference/per-tick.md +90 -31
  192. package/skills/pr-converge/reference/state-schema.md +22 -1
  193. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  194. package/skills/pr-converge/scripts/README.md +34 -46
  195. package/skills/pr-converge/scripts/check_bugbot_ci.py +174 -0
  196. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  197. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  198. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  199. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  200. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  201. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  202. package/skills/qbug/SKILL.md +132 -27
  203. package/skills/session-log/SKILL.md +216 -114
  204. package/skills/session-tidy/SKILL.md +1 -1
  205. package/skills/skill-builder/SKILL.md +138 -56
  206. package/skills/skill-builder/references/delegation-map.md +72 -113
  207. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  208. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  209. package/skills/skill-builder/references/skill-types.md +228 -0
  210. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  211. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  212. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  213. package/skills/skill-builder/workflows/new-skill.md +80 -168
  214. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  215. package/skills/structure-prompt/SKILL.md +50 -0
  216. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  217. package/skills/structure-prompt/reference/block-classification.md +27 -0
  218. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  219. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  220. package/skills/structure-prompt/reference/cleanup.md +33 -0
  221. package/skills/structure-prompt/reference/constraints.md +33 -0
  222. package/skills/structure-prompt/reference/directives.md +37 -0
  223. package/skills/structure-prompt/reference/examples.md +72 -0
  224. package/skills/structure-prompt/reference/instantiation.md +51 -0
  225. package/skills/structure-prompt/reference/output-contract.md +72 -0
  226. package/skills/structure-prompt/reference/per-category.md +23 -0
  227. package/skills/structure-prompt/reference/persona.md +38 -0
  228. package/skills/structure-prompt/reference/research.md +33 -0
  229. package/skills/structure-prompt/reference/structure.md +28 -0
  230. package/agents/code-standards-agent.md +0 -93
  231. package/agents/groq-coder.md +0 -113
  232. package/agents/plan-executor.md +0 -226
  233. package/agents/project-docs-analyzer.md +0 -53
  234. package/agents/project-structure-organizer-agent.md +0 -72
  235. package/agents/skill-to-agent-converter.md +0 -370
  236. package/agents/skill-writer-agent.md +0 -470
  237. package/agents/user-docs-writer.md +0 -67
  238. package/agents/workflow-visual-documenter.md +0 -82
  239. package/commands/readability-review.md +0 -20
  240. package/hooks/mypy.ini +0 -2
  241. package/hooks/notification/attention_needed_notify.py +0 -71
  242. package/hooks/notification/claude_notification_handler.py +0 -67
  243. package/hooks/notification/notification_utils.py +0 -267
  244. package/hooks/notification/subagent_complete_notify.py +0 -381
  245. package/hooks/notification/test_attention_needed_notify.py +0 -47
  246. package/hooks/notification/test_claude_notification_handler.py +0 -54
  247. package/hooks/notification/test_notification_utils.py +0 -91
  248. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  249. package/scripts/config/groq_bugteam_config.py +0 -230
  250. package/scripts/config/test_groq_bugteam_config.py +0 -83
  251. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  252. package/scripts/groq_bugteam.README.md +0 -131
  253. package/scripts/groq_bugteam.py +0 -647
  254. package/scripts/groq_bugteam_dotenv.py +0 -40
  255. package/scripts/groq_bugteam_spec.py +0 -226
  256. package/scripts/test_groq_bugteam.py +0 -529
  257. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  258. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  259. package/scripts/test_groq_bugteam_spec.py +0 -338
  260. package/skills/bugteam/SKILL_EVALS.md +0 -309
  261. package/skills/dream/SKILL.md +0 -118
  262. package/skills/ingest/SKILL.md +0 -40
  263. package/skills/npm-creator/SKILL.md +0 -187
  264. package/skills/readability-review/SKILL.md +0 -127
  265. package/skills/resume-review/SKILL.md +0 -261
  266. package/skills/rule-audit/SKILL.md +0 -307
  267. package/skills/rule-creator/SKILL.md +0 -150
  268. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  269. package/skills/skill-writer/REFERENCE.md +0 -284
  270. package/skills/skill-writer/SKILL.md +0 -222
  271. package/skills/tdd-team/SKILL.md +0 -128
@@ -0,0 +1,384 @@
1
+ """Tests for bugteam_fix_hookspath auto-remediation.
2
+
3
+ Covers:
4
+ - removes a local-scope core.hooksPath override and re-runs preflight
5
+ - sets global core.hooksPath when missing
6
+ - idempotent: second invocation produces the same final state with no errors
7
+ - no-op when no override exists and global is already canonical
8
+ - exits non-zero with a clear message when canonical hooks dir is missing
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import os
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+ from types import ModuleType
19
+ from unittest.mock import patch
20
+
21
+ import pytest
22
+
23
+
24
+ def _load_fix_module() -> ModuleType:
25
+ module_path = Path(__file__).parent / "bugteam_fix_hookspath.py"
26
+ spec = importlib.util.spec_from_file_location("bugteam_fix_hookspath", module_path)
27
+ assert spec is not None
28
+ assert spec.loader is not None
29
+ module = importlib.util.module_from_spec(spec)
30
+ spec.loader.exec_module(module)
31
+ return module
32
+
33
+
34
+ bugteam_fix_hookspath = _load_fix_module()
35
+
36
+
37
+ def _make_isolated_git_environment(home_directory: Path) -> dict[str, str]:
38
+ """Build an env dict that pins git's HOME and XDG paths into a tmp directory.
39
+
40
+ Without this, real `git config --global` reads/writes hit the developer's
41
+ actual ~/.gitconfig — which would corrupt the host machine and make tests
42
+ depend on global state. Pointing HOME, USERPROFILE, and XDG_CONFIG_HOME
43
+ at a temp directory isolates the test fully on every supported git
44
+ version. GIT_CONFIG_GLOBAL would tighten the binding but requires
45
+ git >= 2.32 (August 2021); HOME/USERPROFILE already isolate on older git.
46
+ """
47
+ isolated_environment = os.environ.copy()
48
+ isolated_environment["HOME"] = str(home_directory)
49
+ isolated_environment["USERPROFILE"] = str(home_directory)
50
+ isolated_environment["XDG_CONFIG_HOME"] = str(home_directory / ".config")
51
+ isolated_environment["GIT_CONFIG_NOSYSTEM"] = "1"
52
+ return isolated_environment
53
+
54
+
55
+ def _initialize_repository(repository_path: Path, environment: dict[str, str]) -> None:
56
+ repository_path.mkdir(parents=True, exist_ok=True)
57
+ subprocess.run(
58
+ ["git", "init", "--quiet", str(repository_path)],
59
+ check=True,
60
+ env=environment,
61
+ )
62
+
63
+
64
+ def _set_local_hooks_path(
65
+ repository_path: Path,
66
+ hooks_path_value: str,
67
+ environment: dict[str, str],
68
+ ) -> None:
69
+ subprocess.run(
70
+ [
71
+ "git",
72
+ "-C",
73
+ str(repository_path),
74
+ "config",
75
+ "--local",
76
+ "core.hooksPath",
77
+ hooks_path_value,
78
+ ],
79
+ check=True,
80
+ env=environment,
81
+ )
82
+
83
+
84
+ def _set_global_hooks_path(hooks_path_value: str, environment: dict[str, str]) -> None:
85
+ subprocess.run(
86
+ ["git", "config", "--global", "core.hooksPath", hooks_path_value],
87
+ check=True,
88
+ env=environment,
89
+ )
90
+
91
+
92
+ def _read_local_hooks_path(repository_path: Path, environment: dict[str, str]) -> str:
93
+ completed_process = subprocess.run(
94
+ [
95
+ "git",
96
+ "-C",
97
+ str(repository_path),
98
+ "config",
99
+ "--local",
100
+ "--get",
101
+ "core.hooksPath",
102
+ ],
103
+ capture_output=True,
104
+ text=True,
105
+ check=False,
106
+ env=environment,
107
+ )
108
+ return completed_process.stdout.strip()
109
+
110
+
111
+ def _read_global_hooks_path(environment: dict[str, str]) -> str:
112
+ completed_process = subprocess.run(
113
+ ["git", "config", "--global", "--get", "core.hooksPath"],
114
+ capture_output=True,
115
+ text=True,
116
+ check=False,
117
+ env=environment,
118
+ )
119
+ return completed_process.stdout.strip()
120
+
121
+
122
+ def _create_canonical_hooks_directory(home_directory: Path) -> Path:
123
+ canonical_hooks_directory = home_directory / ".claude" / "hooks" / "git-hooks"
124
+ canonical_hooks_directory.mkdir(parents=True)
125
+ return canonical_hooks_directory
126
+
127
+
128
+ def test_should_remove_local_override_and_pass_preflight(tmp_path: Path) -> None:
129
+ home_directory = tmp_path / "home"
130
+ home_directory.mkdir()
131
+ environment = _make_isolated_git_environment(home_directory)
132
+ canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
133
+ _set_global_hooks_path(str(canonical_hooks_directory), environment)
134
+ repository_path = tmp_path / "synthetic-repo"
135
+ _initialize_repository(repository_path, environment)
136
+ stale_local_value = str(repository_path / ".git" / "hooks")
137
+ _set_local_hooks_path(repository_path, stale_local_value, environment)
138
+
139
+ exit_code = bugteam_fix_hookspath.main(
140
+ ["--repo-root", str(repository_path)],
141
+ all_environment_overrides=environment,
142
+ )
143
+
144
+ assert exit_code == 0, (
145
+ "fix script must succeed when canonical global hooks dir exists"
146
+ )
147
+ assert _read_local_hooks_path(repository_path, environment) == "", (
148
+ "local core.hooksPath override must be removed"
149
+ )
150
+
151
+
152
+ def test_should_set_global_hooks_path_when_missing(tmp_path: Path) -> None:
153
+ home_directory = tmp_path / "home"
154
+ home_directory.mkdir()
155
+ environment = _make_isolated_git_environment(home_directory)
156
+ canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
157
+ repository_path = tmp_path / "synthetic-repo"
158
+ _initialize_repository(repository_path, environment)
159
+ stale_local_value = str(repository_path / ".git" / "hooks")
160
+ _set_local_hooks_path(repository_path, stale_local_value, environment)
161
+
162
+ exit_code = bugteam_fix_hookspath.main(
163
+ ["--repo-root", str(repository_path)],
164
+ all_environment_overrides=environment,
165
+ )
166
+
167
+ assert exit_code == 0
168
+ global_value_after_fix = _read_global_hooks_path(environment)
169
+ assert (
170
+ global_value_after_fix.replace("\\", "/")
171
+ .rstrip("/")
172
+ .endswith("hooks/git-hooks")
173
+ ), (
174
+ "fix script must set canonical global core.hooksPath when missing; "
175
+ f"got '{global_value_after_fix}'"
176
+ )
177
+
178
+
179
+ def test_should_be_idempotent(tmp_path: Path) -> None:
180
+ home_directory = tmp_path / "home"
181
+ home_directory.mkdir()
182
+ environment = _make_isolated_git_environment(home_directory)
183
+ canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
184
+ _set_global_hooks_path(str(canonical_hooks_directory), environment)
185
+ repository_path = tmp_path / "synthetic-repo"
186
+ _initialize_repository(repository_path, environment)
187
+ stale_local_value = str(repository_path / ".git" / "hooks")
188
+ _set_local_hooks_path(repository_path, stale_local_value, environment)
189
+
190
+ first_exit_code = bugteam_fix_hookspath.main(
191
+ ["--repo-root", str(repository_path)],
192
+ all_environment_overrides=environment,
193
+ )
194
+ second_exit_code = bugteam_fix_hookspath.main(
195
+ ["--repo-root", str(repository_path)],
196
+ all_environment_overrides=environment,
197
+ )
198
+
199
+ assert first_exit_code == 0
200
+ assert second_exit_code == 0, "second invocation must succeed without errors"
201
+ assert _read_local_hooks_path(repository_path, environment) == ""
202
+
203
+
204
+ def test_should_no_op_when_already_clean(tmp_path: Path) -> None:
205
+ home_directory = tmp_path / "home"
206
+ home_directory.mkdir()
207
+ environment = _make_isolated_git_environment(home_directory)
208
+ canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
209
+ _set_global_hooks_path(str(canonical_hooks_directory), environment)
210
+ repository_path = tmp_path / "synthetic-repo"
211
+ _initialize_repository(repository_path, environment)
212
+
213
+ exit_code = bugteam_fix_hookspath.main(
214
+ ["--repo-root", str(repository_path)],
215
+ all_environment_overrides=environment,
216
+ )
217
+
218
+ assert exit_code == 0
219
+ assert _read_local_hooks_path(repository_path, environment) == ""
220
+ assert (
221
+ _read_global_hooks_path(environment)
222
+ .replace("\\", "/")
223
+ .rstrip("/")
224
+ .endswith("hooks/git-hooks")
225
+ )
226
+
227
+
228
+ def test_should_exit_nonzero_when_canonical_hooks_directory_missing(
229
+ tmp_path: Path,
230
+ capsys: pytest.CaptureFixture[str],
231
+ ) -> None:
232
+ home_directory = tmp_path / "home"
233
+ home_directory.mkdir()
234
+ environment = _make_isolated_git_environment(home_directory)
235
+ repository_path = tmp_path / "synthetic-repo"
236
+ _initialize_repository(repository_path, environment)
237
+ stale_local_value = str(repository_path / ".git" / "hooks")
238
+ _set_local_hooks_path(repository_path, stale_local_value, environment)
239
+
240
+ exit_code = bugteam_fix_hookspath.main(
241
+ ["--repo-root", str(repository_path)],
242
+ all_environment_overrides=environment,
243
+ )
244
+
245
+ assert exit_code != 0, (
246
+ "fix script must fail clearly when ~/.claude/hooks/git-hooks does not exist "
247
+ "so the user knows to run `npx claude-dev-env .`"
248
+ )
249
+ captured_streams = capsys.readouterr()
250
+ assert "hooks/git-hooks" in captured_streams.err.replace("\\", "/")
251
+
252
+
253
+ def test_should_handle_paths_with_spaces(tmp_path: Path) -> None:
254
+ home_directory = tmp_path / "home with space"
255
+ home_directory.mkdir()
256
+ environment = _make_isolated_git_environment(home_directory)
257
+ canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
258
+ _set_global_hooks_path(str(canonical_hooks_directory), environment)
259
+ repository_path = tmp_path / "repo with space"
260
+ _initialize_repository(repository_path, environment)
261
+ stale_local_value = str(repository_path / ".git" / "hooks")
262
+ _set_local_hooks_path(repository_path, stale_local_value, environment)
263
+
264
+ exit_code = bugteam_fix_hookspath.main(
265
+ ["--repo-root", str(repository_path)],
266
+ all_environment_overrides=environment,
267
+ )
268
+
269
+ assert exit_code == 0
270
+ assert _read_local_hooks_path(repository_path, environment) == ""
271
+
272
+
273
+ def test_list_local_core_hooks_path_values_raises_on_unexpected_git_failure(
274
+ tmp_path: Path,
275
+ ) -> None:
276
+ """A non-empty stderr from git config must propagate as an error.
277
+
278
+ Regression for loop1-5: returning [] on every non-zero git exit collapses
279
+ "key unset" with "git failed for some other reason" — the caller then
280
+ skips the unset call, leaving a stale local override in place.
281
+ """
282
+ repository_path = tmp_path / "repo"
283
+ repository_path.mkdir()
284
+
285
+ def fake_run(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess:
286
+ return subprocess.CompletedProcess(
287
+ args=[],
288
+ returncode=128,
289
+ stdout="",
290
+ stderr="fatal: unable to read config file: permission denied\n",
291
+ )
292
+
293
+ with patch.object(subprocess, "run", fake_run):
294
+ with pytest.raises(RuntimeError):
295
+ bugteam_fix_hookspath.list_local_core_hooks_path_values(
296
+ repository_path,
297
+ None,
298
+ )
299
+
300
+
301
+ def test_read_global_core_hooks_path_raises_on_unexpected_git_failure() -> None:
302
+ """A non-empty stderr from git config must propagate as an error.
303
+
304
+ Regression for loop1-6: returning "" on every non-zero git exit conflates
305
+ "global hooksPath unset" with "git failed for some other reason" — the
306
+ caller then overwrites global git config based on a non-truthful read.
307
+ """
308
+
309
+ def fake_run(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess:
310
+ return subprocess.CompletedProcess(
311
+ args=[],
312
+ returncode=128,
313
+ stdout="",
314
+ stderr="fatal: bad config line\n",
315
+ )
316
+
317
+ with patch.object(subprocess, "run", fake_run):
318
+ with pytest.raises(RuntimeError):
319
+ bugteam_fix_hookspath.read_global_core_hooks_path(None)
320
+
321
+
322
+ def test_list_local_core_hooks_path_values_returns_empty_when_key_unset(
323
+ tmp_path: Path,
324
+ ) -> None:
325
+ """Genuine key-unset (exit 1 + empty stderr) must continue to return []."""
326
+ repository_path = tmp_path / "repo"
327
+ repository_path.mkdir()
328
+
329
+ def fake_run(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess:
330
+ return subprocess.CompletedProcess(
331
+ args=[],
332
+ returncode=1,
333
+ stdout="",
334
+ stderr="",
335
+ )
336
+
337
+ with patch.object(subprocess, "run", fake_run):
338
+ result = bugteam_fix_hookspath.list_local_core_hooks_path_values(
339
+ repository_path,
340
+ None,
341
+ )
342
+ assert result == []
343
+
344
+
345
+ def test_read_global_core_hooks_path_returns_empty_when_key_unset() -> None:
346
+ """Genuine key-unset (exit 1 + empty stderr) must continue to return ''."""
347
+
348
+ def fake_run(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess:
349
+ return subprocess.CompletedProcess(
350
+ args=[],
351
+ returncode=1,
352
+ stdout="",
353
+ stderr="",
354
+ )
355
+
356
+ with patch.object(subprocess, "run", fake_run):
357
+ result = bugteam_fix_hookspath.read_global_core_hooks_path(None)
358
+ assert result == ""
359
+
360
+
361
+ def test_module_import_evicts_cached_config_submodules() -> None:
362
+ """Importing bugteam_fix_hookspath must evict cached `config.*` submodules.
363
+
364
+ Regression for loop1-1: without a defensive cache pop above sys.path.insert,
365
+ a previously-cached `config` package shadows scripts/config/ and the
366
+ from-import raises ModuleNotFoundError.
367
+ """
368
+ fake_submodule_name = "config.bugteam_fix_hookspath_constants"
369
+ fake_parent_name = "config"
370
+ sentinel_module_a = ModuleType(fake_parent_name)
371
+ sentinel_module_b = ModuleType(fake_submodule_name)
372
+ sys.modules[fake_parent_name] = sentinel_module_a
373
+ sys.modules[fake_submodule_name] = sentinel_module_b
374
+ try:
375
+ _load_fix_module()
376
+ finally:
377
+ sys.modules.pop(fake_parent_name, None)
378
+ sys.modules.pop(fake_submodule_name, None)
379
+ assert sys.modules.get(fake_parent_name) is not sentinel_module_a, (
380
+ "parent `config` cache entry must be evicted on module import"
381
+ )
382
+ assert sys.modules.get(fake_submodule_name) is not sentinel_module_b, (
383
+ "cached `config.<submodule>` entries must be evicted on module import"
384
+ )
@@ -0,0 +1,268 @@
1
+ """Tests for bugteam_preflight git hooks path verification.
2
+
3
+ Covers:
4
+ - core.hooksPath unset: exits non-zero with correction message
5
+ - core.hooksPath pointing to the correct claude hooks dir: exits zero
6
+ - core.hooksPath pointing elsewhere (husky override): exits non-zero
7
+ - core.hooksPath with trailing slash: must still pass after normalization
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib.util
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+ from unittest.mock import MagicMock, patch
18
+
19
+ import pytest
20
+
21
+
22
+ def _load_preflight_module() -> ModuleType:
23
+ module_path = Path(__file__).parent / "bugteam_preflight.py"
24
+ spec = importlib.util.spec_from_file_location("bugteam_preflight", module_path)
25
+ assert spec is not None
26
+ assert spec.loader is not None
27
+ module = importlib.util.module_from_spec(spec)
28
+ spec.loader.exec_module(module)
29
+ return module
30
+
31
+
32
+ bugteam_preflight = _load_preflight_module()
33
+
34
+
35
+ def _make_completed_process(
36
+ stdout: str, returncode: int
37
+ ) -> subprocess.CompletedProcess:
38
+ process = MagicMock(spec=subprocess.CompletedProcess)
39
+ process.stdout = stdout
40
+ process.returncode = returncode
41
+ return process
42
+
43
+
44
+ def test_should_exit_nonzero_when_core_hooks_path_unset(capsys: pytest.CaptureFixture[str]) -> None:
45
+ with patch("subprocess.run") as mock_run:
46
+ mock_run.return_value = _make_completed_process("", returncode=1)
47
+ exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
48
+ assert exit_code != 0
49
+ captured = capsys.readouterr()
50
+ assert "core.hooksPath" in captured.err
51
+ assert "npx claude-dev-env" in captured.err or "git config" in captured.err
52
+
53
+
54
+ def test_should_exit_zero_when_core_hooks_path_points_to_claude_hooks(tmp_path: Path) -> None:
55
+ claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
56
+ claude_hooks_path.mkdir(parents=True)
57
+ with patch("subprocess.run") as mock_run:
58
+ mock_run.return_value = _make_completed_process(
59
+ str(claude_hooks_path) + "\n", returncode=0
60
+ )
61
+ exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
62
+ assert exit_code == 0
63
+
64
+
65
+ def test_should_exit_nonzero_when_core_hooks_path_points_elsewhere(capsys: pytest.CaptureFixture[str]) -> None:
66
+ with patch("subprocess.run") as mock_run:
67
+ mock_run.return_value = _make_completed_process(
68
+ "/some/other/path/.husky\n", returncode=0
69
+ )
70
+ exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
71
+ assert exit_code != 0
72
+ captured = capsys.readouterr()
73
+ assert "core.hooksPath" in captured.err
74
+
75
+
76
+ def test_should_include_correction_commands_in_error_message(capsys: pytest.CaptureFixture[str]) -> None:
77
+ with patch("subprocess.run") as mock_run:
78
+ mock_run.return_value = _make_completed_process("", returncode=1)
79
+ bugteam_preflight.verify_git_hooks_path(Path("."))
80
+ captured = capsys.readouterr()
81
+ assert (
82
+ "npx claude-dev-env" in captured.err
83
+ or "git config --global core.hooksPath" in captured.err
84
+ )
85
+
86
+
87
+ def test_main_should_exit_nonzero_when_hooks_path_unset() -> None:
88
+ with patch("subprocess.run") as mock_run:
89
+ mock_run.return_value = _make_completed_process("", returncode=1)
90
+ exit_code = bugteam_preflight.main(["--no-pytest"])
91
+ assert exit_code != 0
92
+
93
+
94
+ def test_main_should_continue_when_hooks_path_valid(tmp_path: Path) -> None:
95
+ claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
96
+ claude_hooks_path.mkdir(parents=True)
97
+ with patch("subprocess.run") as mock_run:
98
+ mock_run.return_value = _make_completed_process(
99
+ str(claude_hooks_path) + "\n", returncode=0
100
+ )
101
+ exit_code = bugteam_preflight.main(["--no-pytest"])
102
+ assert exit_code == 0
103
+
104
+
105
+ def test_should_accept_hooks_path_with_trailing_slash() -> None:
106
+ with patch("subprocess.run") as mock_run:
107
+ mock_run.return_value = _make_completed_process(
108
+ "/home/user/.claude/hooks/git-hooks/\n", returncode=0
109
+ )
110
+ exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
111
+ assert exit_code == 0, (
112
+ "hooksPath with trailing slash must pass verification after normalization"
113
+ )
114
+
115
+
116
+ def test_should_exit_zero_when_hooks_path_set_at_repo_scope(tmp_path: Path) -> None:
117
+ claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
118
+ claude_hooks_path.mkdir(parents=True)
119
+ repo_root = tmp_path / "my-repo"
120
+ repo_root.mkdir()
121
+ with patch("subprocess.run") as mock_run:
122
+ mock_run.return_value = _make_completed_process(
123
+ str(claude_hooks_path) + "\n", returncode=0
124
+ )
125
+ exit_code = bugteam_preflight.verify_git_hooks_path(repo_root)
126
+ assert exit_code == 0, (
127
+ "verify_git_hooks_path must accept a valid path returned by effective "
128
+ "config query (not restricted to --global scope)"
129
+ )
130
+ called_command = mock_run.call_args[0][0]
131
+ assert "--global" not in called_command, (
132
+ "verify_git_hooks_path must query effective config, not --global only"
133
+ )
134
+ assert "-C" in called_command, (
135
+ "verify_git_hooks_path must use git -C <repo_root> for repo-effective config"
136
+ )
137
+ dash_c_index = called_command.index("-C")
138
+ assert called_command[dash_c_index + 1] == str(repo_root), (
139
+ "git -C must receive the resolved repository root path"
140
+ )
141
+
142
+
143
+ def test_verify_git_hooks_path_accepts_none_repository_root(tmp_path: Path) -> None:
144
+ """When repository_root is None, the call must use git's cwd-effective config.
145
+
146
+ Binds the documented optional contract: passing None must not raise and must
147
+ omit the `-C <root>` arguments so git falls back to the working directory.
148
+ """
149
+ claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
150
+ claude_hooks_path.mkdir(parents=True)
151
+ with patch("subprocess.run") as mock_run:
152
+ mock_run.return_value = _make_completed_process(
153
+ str(claude_hooks_path) + "\n", returncode=0
154
+ )
155
+ exit_code = bugteam_preflight.verify_git_hooks_path(None)
156
+ assert exit_code == 0
157
+ called_command = mock_run.call_args[0][0]
158
+ assert "-C" not in called_command, (
159
+ "verify_git_hooks_path(None) must omit -C so git uses cwd-effective config"
160
+ )
161
+
162
+
163
+ def test_should_accept_hooks_path_with_backslash_and_trailing_slash() -> None:
164
+ with patch("subprocess.run") as mock_run:
165
+ mock_run.return_value = _make_completed_process(
166
+ "C:\\Users\\user\\.claude\\hooks\\git-hooks\\\n", returncode=0
167
+ )
168
+ exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
169
+ assert exit_code == 0, (
170
+ "Windows hooksPath with trailing backslash must pass after normalization"
171
+ )
172
+
173
+
174
+ def test_should_exit_nonzero_when_git_executable_not_found(
175
+ capsys: pytest.CaptureFixture[str],
176
+ ) -> None:
177
+ """Preflight must not crash with a traceback when git is missing from PATH."""
178
+ with patch("subprocess.run", side_effect=FileNotFoundError()):
179
+ exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
180
+ assert exit_code != 0, (
181
+ "FileNotFoundError from subprocess.run must produce a non-zero exit, "
182
+ "not a propagated traceback"
183
+ )
184
+ captured = capsys.readouterr()
185
+ assert "git" in captured.err.lower(), (
186
+ "Error message must mention git so the user knows what is missing"
187
+ )
188
+ assert (
189
+ "npx claude-dev-env" in captured.err
190
+ or "git config --global core.hooksPath" in captured.err
191
+ ), "Error message must include the enforcement-absent remediation hints"
192
+
193
+
194
+ def test_should_exit_nonzero_when_subprocess_run_raises_os_error(
195
+ capsys: pytest.CaptureFixture[str],
196
+ ) -> None:
197
+ """Preflight must surface a clean error for other OS-level git launch failures."""
198
+ with patch("subprocess.run", side_effect=OSError("permission denied")):
199
+ exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
200
+ assert exit_code != 0, (
201
+ "OSError from subprocess.run must produce a non-zero exit, "
202
+ "not a propagated traceback"
203
+ )
204
+ captured = capsys.readouterr()
205
+ assert "bugteam_preflight" in captured.err, (
206
+ "Error message must be prefixed with the preflight tool name for context"
207
+ )
208
+ assert "permission denied" in captured.err, (
209
+ "Error message must include the underlying OSError detail for diagnosis"
210
+ )
211
+
212
+
213
+ def test_module_import_evicts_cached_config_submodules() -> None:
214
+ """Importing bugteam_preflight must evict cached `config.*` submodules.
215
+
216
+ Regression for loop1-4: a single `sys.modules.pop("config", None)` only
217
+ removes the parent key, leaving stale `config.<submodule>` entries that
218
+ satisfy the next from-import with the wrong bindings.
219
+ """
220
+ fake_submodule_name = "config.bugteam_preflight_constants"
221
+ fake_parent_name = "config"
222
+ sentinel_module_a = ModuleType(fake_parent_name)
223
+ sentinel_module_b = ModuleType(fake_submodule_name)
224
+ sys.modules[fake_parent_name] = sentinel_module_a
225
+ sys.modules[fake_submodule_name] = sentinel_module_b
226
+ try:
227
+ _load_preflight_module()
228
+ finally:
229
+ sys.modules.pop(fake_parent_name, None)
230
+ sys.modules.pop(fake_submodule_name, None)
231
+ assert sys.modules.get(fake_parent_name) is not sentinel_module_a, (
232
+ "parent `config` cache entry must be evicted on module import"
233
+ )
234
+ assert sys.modules.get(fake_submodule_name) is not sentinel_module_b, (
235
+ "cached `config.<submodule>` entries must be evicted on module import"
236
+ )
237
+
238
+
239
+ def test_has_pytest_configuration_finds_pytest_ini(tmp_path: Path) -> None:
240
+ """has_pytest_configuration must detect pytest.ini at the repo root.
241
+
242
+ Regression for loop1-17/loop1-18: the literals "pytest.ini",
243
+ "pyproject.toml", and "[tool.pytest" were inlined in production function
244
+ bodies; centralizing them in config and importing here pins the contract.
245
+ """
246
+ repository_root = tmp_path / "repo"
247
+ repository_root.mkdir()
248
+ (repository_root / "pytest.ini").write_text("[pytest]\n", encoding="utf-8")
249
+ assert bugteam_preflight.has_pytest_configuration(repository_root) is True
250
+
251
+
252
+ def test_has_pytest_configuration_finds_pyproject_pytest_section(
253
+ tmp_path: Path,
254
+ ) -> None:
255
+ repository_root = tmp_path / "repo"
256
+ repository_root.mkdir()
257
+ (repository_root / "pyproject.toml").write_text(
258
+ "[tool.pytest.ini_options]\nminversion = '6.0'\n", encoding="utf-8"
259
+ )
260
+ assert bugteam_preflight.has_pytest_configuration(repository_root) is True
261
+
262
+
263
+ def test_has_pytest_configuration_returns_false_without_either_file(
264
+ tmp_path: Path,
265
+ ) -> None:
266
+ repository_root = tmp_path / "repo"
267
+ repository_root.mkdir()
268
+ assert bugteam_preflight.has_pytest_configuration(repository_root) is False