claude-dev-env 1.95.0 → 2.0.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 (240) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/tests/conftest.py +10 -0
  117. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  118. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  119. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  120. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  121. package/hooks/git-hooks/post_commit.py +3 -4
  122. package/hooks/hooks_constants/CLAUDE.md +2 -2
  123. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  124. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  125. package/hooks/hooks_constants/local_identity.py +59 -8
  126. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  127. package/hooks/hooks_constants/test_local_identity.py +105 -3
  128. package/hooks/pyproject.toml +13 -36
  129. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  130. package/hooks/validation/mypy_validator.py +2 -2
  131. package/hooks/validators/health_check.py +1 -0
  132. package/hooks/validators/mypy_integration.py +2 -0
  133. package/hooks/validators/ruff_integration.py +3 -0
  134. package/hooks/workflow/auto_formatter.py +5 -4
  135. package/package.json +1 -1
  136. package/scripts/CLAUDE.md +4 -0
  137. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  138. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  139. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  140. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  141. package/scripts/grok_headless_runner.py +294 -0
  142. package/scripts/grok_worker_preflight.py +410 -0
  143. package/scripts/invoke_code_review.py +463 -0
  144. package/scripts/resolve_worker_spawn.py +619 -0
  145. package/scripts/spawn_grok_batch.py +672 -0
  146. package/scripts/test_grok_headless_runner.py +626 -0
  147. package/scripts/test_grok_worker_preflight.py +1054 -0
  148. package/scripts/test_invoke_code_review.py +672 -0
  149. package/scripts/test_resolve_worker_spawn.py +1014 -0
  150. package/scripts/test_spawn_grok_batch.py +1017 -0
  151. package/skills/CLAUDE.md +5 -3
  152. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  153. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  154. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  155. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  157. package/skills/autoconverge/CLAUDE.md +3 -3
  158. package/skills/autoconverge/SKILL.md +9 -3
  159. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  160. package/skills/autoconverge/reference/convergence.md +33 -11
  161. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  162. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  163. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  164. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  165. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  166. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  167. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  168. package/skills/autoconverge/workflow/converge.mjs +200 -16
  169. package/skills/bugteam/CLAUDE.md +2 -2
  170. package/skills/bugteam/CONSTRAINTS.md +3 -2
  171. package/skills/bugteam/PROMPTS.md +7 -6
  172. package/skills/bugteam/SKILL.md +18 -13
  173. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  174. package/skills/bugteam/reference/design-rationale.md +1 -1
  175. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  176. package/skills/bugteam/reference/team-setup.md +8 -2
  177. package/skills/codex-review/CLAUDE.md +46 -0
  178. package/skills/codex-review/SKILL.md +181 -0
  179. package/skills/codex-review/reference/CLAUDE.md +15 -0
  180. package/skills/codex-review/reference/cli-contract.md +253 -0
  181. package/skills/codex-review/reference/loop-integration.md +118 -0
  182. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  183. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  189. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  190. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  191. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  193. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  194. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  195. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  196. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  197. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  199. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  200. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  201. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  202. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  203. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  204. package/skills/codex-review/test_skill_scaffold.py +192 -0
  205. package/skills/grok-spawn/CLAUDE.md +28 -0
  206. package/skills/grok-spawn/SKILL.md +226 -0
  207. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  208. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  209. package/skills/grokify/SKILL.md +9 -1
  210. package/skills/grokify/capability-claims.test.mjs +28 -0
  211. package/skills/grokify/evals/README.md +72 -0
  212. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  213. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  214. package/skills/orchestrator/SKILL.md +15 -11
  215. package/skills/orchestrator-refresh/SKILL.md +5 -5
  216. package/skills/pr-converge/SKILL.md +34 -13
  217. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  218. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  219. package/skills/pr-converge/reference/ground-rules.md +1 -1
  220. package/skills/pr-converge/reference/per-tick.md +130 -42
  221. package/skills/pr-converge/reference/state-schema.md +10 -0
  222. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  223. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  224. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  225. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  226. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  227. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  228. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  229. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  230. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  231. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  232. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  234. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  235. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  236. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  237. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  238. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  239. package/skills/reviewer-gates/SKILL.md +7 -5
  240. package/skills/team-advisor/SKILL.md +7 -7
@@ -0,0 +1,108 @@
1
+ """Behavioral tests for the commit repository-path resolution helpers.
2
+
3
+ Covers reading the repository a commit command targets, so the staged PII scan
4
+ runs against the named path rather than the session working directory::
5
+
6
+ git -C /a -C sub commit -> /a/sub (multiple -C compose)
7
+ git -C /a/b -C /c/d commit -> /c/d (a later absolute -C resets)
8
+ cd "/my repo" && git commit -> /my repo (quoted cd path, shlex-parsed)
9
+ git commit -> None (runs in the session cwd)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ from pii_prevention_blocker_parts.repository_resolution import (
17
+ compose_command_working_directory,
18
+ expand_user_directory,
19
+ refusal_reason_for_unresolved_repository,
20
+ )
21
+
22
+
23
+ def test_single_absolute_dash_c_is_the_target() -> None:
24
+ assert compose_command_working_directory("git -C /repo commit -m x") == "/repo"
25
+
26
+
27
+ def test_relative_dash_c_composes_onto_a_leading_absolute_dash_c() -> None:
28
+ composed = compose_command_working_directory("git -C /a -C sub commit -m x")
29
+ assert composed is not None
30
+ assert Path(composed) == Path("/a") / "sub"
31
+
32
+
33
+ def test_later_absolute_dash_c_resets_the_composition() -> None:
34
+ assert compose_command_working_directory("git -C /a/b -C /c/d commit -m x") == "/c/d"
35
+
36
+
37
+ def test_quoted_dash_c_with_spaces_is_read_whole() -> None:
38
+ assert compose_command_working_directory('git -C "/my repo" commit -m x') == "/my repo"
39
+
40
+
41
+ def test_leading_cd_supplies_the_target_when_no_dash_c() -> None:
42
+ assert compose_command_working_directory("cd /work/repo && git commit -m x") == "/work/repo"
43
+
44
+
45
+ def test_quoted_cd_path_with_spaces_is_read_whole() -> None:
46
+ assert compose_command_working_directory('cd "/my repo" && git commit -m x') == "/my repo"
47
+
48
+
49
+ def test_plain_commit_names_no_target() -> None:
50
+ assert compose_command_working_directory("git commit -m x") is None
51
+
52
+
53
+ def test_expand_user_directory_expands_tilde() -> None:
54
+ expanded = expand_user_directory("~/repo")
55
+ assert expanded is not None
56
+ assert not expanded.startswith("~")
57
+ assert Path(expanded).name == "repo"
58
+
59
+
60
+ def test_expand_user_directory_passes_none_through() -> None:
61
+ assert expand_user_directory(None) is None
62
+
63
+
64
+ def test_refusal_names_the_attempted_path() -> None:
65
+ reason = refusal_reason_for_unresolved_repository("/gone/repo")
66
+ assert "repository root" in reason
67
+ assert "/gone/repo" in reason
68
+
69
+
70
+ def test_refusal_names_the_session_cwd_when_no_path_given() -> None:
71
+ reason = refusal_reason_for_unresolved_repository(None)
72
+ assert "repository root" in reason
73
+ assert "session working directory" in reason
74
+
75
+
76
+ def test_windows_git_exe_dash_c_is_the_target() -> None:
77
+ assert (
78
+ compose_command_working_directory(r'git.exe -C "/srv/repo" commit -m x')
79
+ == "/srv/repo"
80
+ )
81
+
82
+
83
+ def test_leading_cd_with_git_exe_supplies_the_target() -> None:
84
+ assert (
85
+ compose_command_working_directory("cd /work/repo && git.exe commit -m x")
86
+ == "/work/repo"
87
+ )
88
+
89
+
90
+ def test_leading_pushd_with_git_exe_supplies_the_target() -> None:
91
+ assert (
92
+ compose_command_working_directory("pushd /work/repo; git.exe commit -m x")
93
+ == "/work/repo"
94
+ )
95
+
96
+
97
+ def test_quoted_cd_path_with_git_exe_is_read_whole() -> None:
98
+ assert (
99
+ compose_command_working_directory('cd "/my repo" && git.exe commit -m x')
100
+ == "/my repo"
101
+ )
102
+
103
+
104
+ def test_dash_c_wins_over_leading_cd() -> None:
105
+ assert (
106
+ compose_command_working_directory("cd /ignored && git -C /named commit -m x")
107
+ == "/named"
108
+ )
@@ -309,7 +309,7 @@ class AttachmentRecordIterator:
309
309
 
310
310
  def __iter__(self) -> Iterator[tuple[dict[str, object], int, int]]:
311
311
  try:
312
- jsonl_file_handle = io.open(self._jsonl_file_path, "rb")
312
+ jsonl_file_handle = io.open(self._jsonl_file_path, "rb") # noqa: SIM115
313
313
  except (FileNotFoundError, OSError):
314
314
  self.final_line_number = self._start_line_number
315
315
  self.final_byte_offset = self._start_offset
@@ -419,14 +419,13 @@ def save_offsets(
419
419
  state_file_parent = os.path.dirname(state_file_path)
420
420
  if state_file_parent:
421
421
  os.makedirs(state_file_parent, exist_ok=True)
422
- temporary_file_handle = tempfile.NamedTemporaryFile(
422
+ with tempfile.NamedTemporaryFile(
423
423
  mode="w",
424
424
  encoding="utf-8",
425
425
  dir=state_file_parent or None,
426
426
  delete=False,
427
- )
428
- temporary_file_path = temporary_file_handle.name
429
- try:
427
+ ) as temporary_file_handle:
428
+ temporary_file_path = temporary_file_handle.name
430
429
  try:
431
430
  json.dump(
432
431
  offset_by_jsonl_path,
@@ -436,8 +435,14 @@ def save_offsets(
436
435
  )
437
436
  temporary_file_handle.flush()
438
437
  os.fsync(temporary_file_handle.fileno())
439
- finally:
438
+ except Exception:
440
439
  temporary_file_handle.close()
440
+ try:
441
+ os.unlink(temporary_file_path)
442
+ except OSError:
443
+ pass
444
+ raise
445
+ try:
441
446
  os.replace(temporary_file_path, state_file_path)
442
447
  except Exception:
443
448
  try:
@@ -468,15 +473,12 @@ def _acquire_offsets_lock(state_file_path: str) -> Iterator[None]:
468
473
  lock_parent_directory = os.path.dirname(lock_file_path)
469
474
  if lock_parent_directory:
470
475
  os.makedirs(lock_parent_directory, exist_ok=True)
471
- lock_file_handle = io.open(lock_file_path, "a+", encoding="utf-8")
472
- try:
476
+ with io.open(lock_file_path, "a+", encoding="utf-8") as lock_file_handle:
473
477
  _lock_file_handle_blocking(lock_file_handle)
474
478
  try:
475
479
  yield
476
480
  finally:
477
481
  _unlock_file_handle(lock_file_handle)
478
- finally:
479
- lock_file_handle.close()
480
482
 
481
483
 
482
484
  def _lock_file_handle_blocking(lock_file_handle: IO[str]) -> None:
@@ -10,7 +10,6 @@ When you commit in a submodule, this hook:
10
10
  This prevents the "lost work" issue where submodule commits aren't tracked by parent.
11
11
  """
12
12
 
13
- import os
14
13
  import subprocess
15
14
  import sys
16
15
  from pathlib import Path
@@ -21,7 +20,7 @@ def run_git(*args: str, cwd: Path | None = None) -> str:
21
20
  result = subprocess.run(
22
21
  ["git"] + list(args),
23
22
  cwd=cwd,
24
- capture_output=True,
23
+ check=False, capture_output=True,
25
24
  text=True,
26
25
  )
27
26
  return result.stdout.strip()
@@ -74,7 +73,7 @@ def main() -> int:
74
73
 
75
74
  diff_result = subprocess.run(
76
75
  ["git", "diff", "--cached", "--quiet"],
77
- cwd=parent_repo,
76
+ check=False, cwd=parent_repo,
78
77
  )
79
78
 
80
79
  if diff_result.returncode == 0:
@@ -89,7 +88,7 @@ Co-Authored-By: Claude <noreply@anthropic.com>"""
89
88
 
90
89
  subprocess.run(
91
90
  ["git", "commit", "-m", full_commit_msg],
92
- cwd=parent_repo,
91
+ check=False, cwd=parent_repo,
93
92
  )
94
93
 
95
94
  print("Parent updated successfully.")
@@ -39,7 +39,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
39
39
  | `hosted_hook_runner.py` | `run_hook_capturing_output()` — shared runner that runs one dispatcher-hosted hook in-process via runpy and returns its captured stdout and crash flag |
40
40
  | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
41
41
  | `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
42
- | `local_identity.py` | Loader for local machine values: NAS host, ssh user, and ssh port the ssh enforcer guards (`CLAUDE_NAS_*` / `~/.claude/local-identity.json` with placeholder defaults), plus the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`); also composes the ssh enforcer's two deny messages that quote the NAS values |
42
+ | `local_identity.py` | Loader for local machine values: NAS host, ssh user, and ssh port the ssh enforcer guards (`CLAUDE_NAS_*` / `~/.claude/local-identity.json` with placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`, keyed by owner/repo slug) read from the git-ignored local file whose path `CLAUDE_LOCAL_IDENTITY_PATH` may point elsewhere; also composes the ssh enforcer's two deny messages that quote the NAS values |
43
43
  | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
44
44
  | `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
45
45
  | `nas_ssh_binary_enforcer_constants.py` | Bash tool name, ssh-family basenames, OpenSSH binary path suffixes, launcher-wrapper set, shell control-operator tokens and split pattern, leading-assignment and duration patterns, and the batch-mode pattern for the NAS ssh binary enforcer |
@@ -87,5 +87,5 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
87
87
  - Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
88
88
  - Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
89
89
  - `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, `hosted_hook_runner.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they are shared across many hooks.
90
- - `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` to resolve private NAS values (with committed placeholder defaults) and the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`).
90
+ - `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` (its path overridable via `CLAUDE_LOCAL_IDENTITY_PATH`) to resolve private NAS values (with committed placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`).
91
91
 
@@ -22,7 +22,6 @@ ALL_BANNED_IDENTIFIERS: frozenset[str] = frozenset(
22
22
  "cnt",
23
23
  "btn",
24
24
  "idx",
25
- "tmp",
26
25
  "msg",
27
26
  "elem",
28
27
  "val",
@@ -10,7 +10,6 @@ module.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
-
14
13
  ALL_CONFIG_DIRECTORY_NAMES = frozenset(
15
14
  {
16
15
  "anthropic_plan_scripts_constants",
@@ -23,6 +22,7 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
23
22
  "pr_converge_scripts_constants",
24
23
  "bugteam_scripts_constants",
25
24
  "usage_pause_constants",
25
+ "codex_review_scripts_constants",
26
26
  "dev_env_scripts_constants",
27
27
  }
28
28
  )
@@ -27,9 +27,11 @@ import os
27
27
  from pathlib import Path
28
28
 
29
29
  NAS_HOST_ENV_VAR = "CLAUDE_NAS_HOST"
30
+ LOCAL_IDENTITY_PATH_ENV_VAR = "CLAUDE_LOCAL_IDENTITY_PATH"
30
31
  PII_EXEMPT_REPOS_ENV_VAR = "CLAUDE_PII_EXEMPT_REPOS"
31
32
  PII_EXEMPT_REPOS_JSON_KEY = "pii_exempt_repositories"
32
33
  PII_EXEMPT_REPOS_SEPARATOR = ","
34
+ PII_ALLOWLISTED_VALUES_JSON_KEY = "pii_allowlisted_values"
33
35
  NAS_SSH_USER_ENV_VAR = "CLAUDE_NAS_SSH_USER"
34
36
  NAS_SSH_PORT_ENV_VAR = "CLAUDE_NAS_SSH_PORT"
35
37
  NAS_JSON_KEY = "nas"
@@ -46,6 +48,9 @@ NAS_SSH_RULE_REFERENCE = "~/.claude/rules/nas-ssh-invocation.md"
46
48
 
47
49
 
48
50
  def _local_identity_file_path() -> Path:
51
+ path_override = os.environ.get(LOCAL_IDENTITY_PATH_ENV_VAR)
52
+ if path_override:
53
+ return Path(path_override)
49
54
  return Path.home() / CLAUDE_HOME_DIRECTORY_NAME / LOCAL_IDENTITY_FILE_NAME
50
55
 
51
56
 
@@ -73,13 +78,22 @@ def _normalized_slug_set(all_raw_slugs: list[object]) -> frozenset[str]:
73
78
  )
74
79
 
75
80
 
81
+ def _environment_exempt_slugs() -> frozenset[str]:
82
+ """Return the exempt slugs named by ``CLAUDE_PII_EXEMPT_REPOS``, or empty."""
83
+ slugs_from_environment = os.environ.get(PII_EXEMPT_REPOS_ENV_VAR)
84
+ if slugs_from_environment is None:
85
+ return frozenset()
86
+ return _normalized_slug_set(
87
+ list(slugs_from_environment.split(PII_EXEMPT_REPOS_SEPARATOR))
88
+ )
89
+
90
+
76
91
  def pii_exempt_repository_slugs() -> frozenset[str]:
77
92
  """Return owner/repo slugs whose commits skip the staged PII scan.
78
93
 
79
94
  ::
80
95
 
81
96
  CLAUDE_PII_EXEMPT_REPOS="Owner/repo-a, Other/b" -> {"owner/repo-a", "other/b"}
82
- CLAUDE_PII_EXEMPT_REPOS=" , , " + file lists A -> {"a"} (whitespace falls through)
83
97
  (env unset, file lists ["Owner/repo-a"]) -> {"owner/repo-a"}
84
98
  (env unset, no file) -> frozenset()
85
99
 
@@ -91,13 +105,9 @@ def pii_exempt_repository_slugs() -> frozenset[str]:
91
105
  Returns:
92
106
  Lowercased ``owner/repo`` slugs exempt from staged-commit PII scanning.
93
107
  """
94
- slugs_from_environment = os.environ.get(PII_EXEMPT_REPOS_ENV_VAR)
95
- if slugs_from_environment is not None:
96
- normalized_environment_slugs = _normalized_slug_set(
97
- list(slugs_from_environment.split(PII_EXEMPT_REPOS_SEPARATOR))
98
- )
99
- if normalized_environment_slugs:
100
- return normalized_environment_slugs
108
+ environment_slugs = _environment_exempt_slugs()
109
+ if environment_slugs:
110
+ return environment_slugs
101
111
  stored_slugs = _identity_dictionary_from_local_file().get(
102
112
  PII_EXEMPT_REPOS_JSON_KEY
103
113
  )
@@ -106,6 +116,47 @@ def pii_exempt_repository_slugs() -> frozenset[str]:
106
116
  return _normalized_slug_set(stored_slugs)
107
117
 
108
118
 
119
+ def _normalized_allowlisted_values(all_raw_values: object) -> frozenset[str]:
120
+ """Return the non-empty string members of a raw values list, or empty."""
121
+ if not isinstance(all_raw_values, list):
122
+ return frozenset()
123
+ return frozenset(
124
+ each_value
125
+ for each_value in all_raw_values
126
+ if isinstance(each_value, str) and each_value
127
+ )
128
+
129
+
130
+ def pii_allowlisted_values_by_repository() -> dict[str, frozenset[str]]:
131
+ """Return the exact literal values each repository allows past the PII scan.
132
+
133
+ ::
134
+
135
+ file {"Owner/Repo": ["user@example.com"]} -> {"owner/repo": {"user@example.com"}}
136
+ (env unset, no file, or bad json) -> {}
137
+
138
+ Keyed by lowercased ``owner/repo`` slug so matching is case-insensitive; the
139
+ values stay exact. The mapping lives under ``pii_allowlisted_values`` in the
140
+ git-ignored ``~/.claude/local-identity.json`` so a private repository's real
141
+ fixture addresses stay out of this published tree.
142
+
143
+ Returns:
144
+ Lowercased ``owner/repo`` slug to the frozenset of exact allowed values.
145
+ """
146
+ stored_mapping = _identity_dictionary_from_local_file().get(
147
+ PII_ALLOWLISTED_VALUES_JSON_KEY
148
+ )
149
+ if not isinstance(stored_mapping, dict):
150
+ return {}
151
+ all_values_by_slug: dict[str, frozenset[str]] = {}
152
+ for each_raw_slug, each_raw_values in stored_mapping.items():
153
+ normalized_slug = each_raw_slug.strip().lower() if isinstance(each_raw_slug, str) else ""
154
+ allowlisted_values = _normalized_allowlisted_values(each_raw_values)
155
+ if normalized_slug and allowlisted_values:
156
+ all_values_by_slug[normalized_slug] = allowlisted_values
157
+ return all_values_by_slug
158
+
159
+
109
160
  def nas_host() -> str:
110
161
  """Return the NAS host the ssh enforcer guards.
111
162
 
@@ -293,12 +293,6 @@ STAGED_LIST_FAILURE_REASON: str = (
293
293
  "(git diff --cached failed). Refuse commit until the index is readable."
294
294
  )
295
295
 
296
- REPOSITORY_ROOT_UNRESOLVED_REASON: str = (
297
- "BLOCKED [pii_prevention_blocker]: could not resolve the git repository root "
298
- "for a commit command (not a git work tree, git missing, or bad -C path). "
299
- "Refuse commit until the repository root is resolvable."
300
- )
301
-
302
296
  STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE: str = (
303
297
  "BLOCKED [pii_prevention_blocker]: staged file '{relative_path}' could not be "
304
298
  "scanned for PII ({reason}). Refuse commit, shrink the blob, or keep binary "
@@ -8,9 +8,11 @@ import sys
8
8
  from pathlib import Path
9
9
  from unittest.mock import patch
10
10
 
11
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
-
13
- from hooks_constants import local_identity # noqa: E402
11
+ try:
12
+ from hooks_constants import local_identity
13
+ except ImportError:
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
15
+ from hooks_constants import local_identity
14
16
 
15
17
  ALL_NAS_ENV_VARS = ("CLAUDE_NAS_HOST", "CLAUDE_NAS_SSH_USER", "CLAUDE_NAS_SSH_PORT")
16
18
 
@@ -134,6 +136,106 @@ class TestPiiExemptRepositorySlugs:
134
136
  assert slugs == frozenset({"fileowner/file-repo"})
135
137
 
136
138
 
139
+ class TestPiiAllowlistedValuesByRepository:
140
+ def test_reads_the_mapping_lowercasing_slugs_and_keeping_values_exact(
141
+ self, tmp_path: Path
142
+ ) -> None:
143
+ allowed_value = "owner.fixture" + "@" + "acme-corp" + ".example" + ".io"
144
+ identity_path = tmp_path / "local-identity.json"
145
+ identity_path.write_text(
146
+ json.dumps(
147
+ {"pii_allowlisted_values": {"Owner/Private-Repo": [allowed_value]}}
148
+ ),
149
+ encoding="utf-8",
150
+ )
151
+ with patch.dict(
152
+ os.environ,
153
+ {"CLAUDE_LOCAL_IDENTITY_PATH": str(identity_path)},
154
+ clear=False,
155
+ ):
156
+ values_by_slug = local_identity.pii_allowlisted_values_by_repository()
157
+ assert values_by_slug == {"owner/private-repo": frozenset({allowed_value})}
158
+
159
+ def test_returns_empty_mapping_when_the_file_is_missing(
160
+ self, tmp_path: Path
161
+ ) -> None:
162
+ with patch.dict(
163
+ os.environ,
164
+ {"CLAUDE_LOCAL_IDENTITY_PATH": str(tmp_path / "absent.json")},
165
+ clear=False,
166
+ ):
167
+ assert local_identity.pii_allowlisted_values_by_repository() == {}
168
+
169
+ def test_returns_empty_mapping_when_the_json_is_corrupt(
170
+ self, tmp_path: Path
171
+ ) -> None:
172
+ identity_path = tmp_path / "local-identity.json"
173
+ identity_path.write_text("{ not valid json", encoding="utf-8")
174
+ with patch.dict(
175
+ os.environ,
176
+ {"CLAUDE_LOCAL_IDENTITY_PATH": str(identity_path)},
177
+ clear=False,
178
+ ):
179
+ assert local_identity.pii_allowlisted_values_by_repository() == {}
180
+
181
+ def test_returns_empty_mapping_when_the_key_is_absent(
182
+ self, tmp_path: Path
183
+ ) -> None:
184
+ identity_path = tmp_path / "local-identity.json"
185
+ identity_path.write_text(
186
+ json.dumps({"nas": {"host": "10.0.0.5"}}), encoding="utf-8"
187
+ )
188
+ with patch.dict(
189
+ os.environ,
190
+ {"CLAUDE_LOCAL_IDENTITY_PATH": str(identity_path)},
191
+ clear=False,
192
+ ):
193
+ assert local_identity.pii_allowlisted_values_by_repository() == {}
194
+
195
+ def test_drops_blank_slugs_and_non_list_value_entries(
196
+ self, tmp_path: Path
197
+ ) -> None:
198
+ allowed_value = "keep.fixture" + "@" + "acme-corp" + ".example" + ".io"
199
+ identity_path = tmp_path / "local-identity.json"
200
+ identity_path.write_text(
201
+ json.dumps(
202
+ {
203
+ "pii_allowlisted_values": {
204
+ " ": [allowed_value],
205
+ "Owner/Repo": allowed_value,
206
+ "Owner/Kept": [allowed_value, 7],
207
+ }
208
+ }
209
+ ),
210
+ encoding="utf-8",
211
+ )
212
+ with patch.dict(
213
+ os.environ,
214
+ {"CLAUDE_LOCAL_IDENTITY_PATH": str(identity_path)},
215
+ clear=False,
216
+ ):
217
+ values_by_slug = local_identity.pii_allowlisted_values_by_repository()
218
+ assert values_by_slug == {"owner/kept": frozenset({allowed_value})}
219
+
220
+
221
+ class TestLocalIdentityPathOverride:
222
+ def test_prefers_the_env_path_over_the_home_directory_file(
223
+ self, tmp_path: Path
224
+ ) -> None:
225
+ override_path = tmp_path / "override" / "local-identity.json"
226
+ override_path.parent.mkdir()
227
+ override_path.write_text(
228
+ json.dumps({"nas": {"host": "10.9.9.9"}}), encoding="utf-8"
229
+ )
230
+ with patch.dict(
231
+ os.environ,
232
+ {"CLAUDE_LOCAL_IDENTITY_PATH": str(override_path)},
233
+ clear=False,
234
+ ), patch.object(Path, "home", return_value=tmp_path):
235
+ _clear_nas_env()
236
+ assert local_identity.nas_host() == "10.9.9.9"
237
+
238
+
137
239
  class TestDenyMessagesQuoteTheResolvedHost:
138
240
  def should_include_the_resolved_host_and_port_in_the_bare_binary_message(
139
241
  self,
@@ -15,19 +15,18 @@ no_implicit_optional = true
15
15
  strict_equality = true
16
16
  exclude = [
17
17
  "validators/test_files/",
18
- "test_.*\\.py$",
19
- ".*/test_.*\\.py$",
20
- # One-line mypy fixes re-surface whole-file CODE_RULES debt at pre-commit (#51).
21
- "abbreviation_checks\\.py$",
22
- "git_checks\\.py$",
23
- "exempt_paths\\.py$",
24
- "code_rules_typeddict_stub\\.py$",
25
- "validators/exempt_paths\\.py$",
26
- "validators/abbreviation_checks\\.py$",
27
- "validators/git_checks\\.py$",
28
- "blocking/code_rules_typeddict_stub\\.py$",
18
+ "test_",
19
+ "abbreviation_checks.py",
20
+ "git_checks.py",
21
+ "exempt_paths.py",
22
+ "code_rules_typeddict_stub.py",
29
23
  ]
30
24
 
25
+
26
+ [[tool.mypy.overrides]]
27
+ module = ["exempt_paths", "code_rules_path_utils", "gh_pr_author_enforcer"]
28
+ warn_unused_ignores = false
29
+
31
30
  [[tool.mypy.overrides]]
32
31
  module = "tests.*"
33
32
  disallow_untyped_defs = false
@@ -54,9 +53,7 @@ select = [
54
53
  "SIM",
55
54
  "PL",
56
55
  ]
57
- # Gate-restore (#51): principled + pure-style ignores. Non-negotiable defect
58
- # classes stay selected; residual hits live under per-file-ignores where a
59
- # one-line fix is blocked by whole-file CODE_RULES pre-commit debt.
56
+ # Principled + pure-style ignores (#51/#57). Non-negotiable defect classes stay selected.
60
57
  ignore = [
61
58
  "E501",
62
59
  "E402",
@@ -94,29 +91,9 @@ ignore = [
94
91
  ]
95
92
 
96
93
  [tool.ruff.lint.per-file-ignores]
97
- "advisory/refactor_guard.py" = ["F401", "PLW1510"]
98
- "blocking/block_main_commit.py" = ["PLW1510"]
99
- "blocking/code_rules_boolean_mustcheck.py" = ["B905"]
100
- "blocking/code_rules_mock_completeness.py" = ["B007"]
101
- "blocking/code_rules_optional_params.py" = ["B905"]
102
- "blocking/code_rules_shared.py" = ["B905"]
103
- "blocking/code_rules_test_assertions.py" = ["B905"]
104
- "blocking/code_rules_typeddict_stub.py" = ["B905"]
105
- "blocking/pii_prevention_blocker.py" = ["PLW1510", "B018"]
106
- "blocking/pr_description_command_parser.py" = ["B904"]
107
- "blocking/precommit_code_rules_gate.py" = ["PLW1510"]
108
- "diagnostic/hook_log_extractor.py" = ["SIM115"]
109
- "git-hooks/post_commit.py" = ["F401", "PLW1510"]
110
- "hooks_constants/banned_identifiers_constants.py" = ["B033"]
111
- "session/plugin_data_dir_cleanup.py" = ["F401"]
112
- "validation/mypy_validator.py" = ["PLW1510"]
113
- "validators/exempt_paths.py" = ["F401"]
114
- "validators/health_check.py" = ["PLW1510"]
115
- "validators/mypy_integration.py" = ["PLW1510"]
116
- "validators/python_style_checks.py" = ["B905"]
117
- "validators/ruff_integration.py" = ["PLW1510"]
94
+ # Residual production carve-outs re-homed on #57 (gate friction on multi-line touch).
118
95
  "validators/run_all_validators.py" = ["PLW1510"]
119
- "workflow/auto_formatter.py" = ["PLW1510"]
96
+ "validators/exempt_paths.py" = ["F401"]
120
97
  "test_*.py" = ["B011", "PLR2004", "F401", "F841", "PLW1510", "B905", "B033", "SIM115", "B007", "B018", "B904"]
121
98
  "*_test.py" = ["B011", "PLR2004", "F401", "F841", "PLW1510", "B905", "B033", "SIM115", "B007", "B018", "B904"]
122
99
  "conftest.py" = ["B011", "F401", "F841", "PLW1510"]
@@ -13,7 +13,6 @@ separate from the EEXIST fixes in v2.1.70-v2.1.72.
13
13
  """
14
14
 
15
15
  import os
16
- import sys
17
16
 
18
17
  PLUGINS_DATA_DIRECTORY = os.path.join(
19
18
  os.path.expanduser("~"), ".claude", "plugins", "data"
@@ -70,7 +70,7 @@ def discover_project_root(target_file: str) -> Path | None:
70
70
  try:
71
71
  completed_process = subprocess.run(
72
72
  ["git", "rev-parse", "--show-toplevel"],
73
- capture_output=True,
73
+ check=False, capture_output=True,
74
74
  text=True,
75
75
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
76
76
  cwd=str(Path(target_file).parent),
@@ -359,7 +359,7 @@ def run_mypy(target_file: str, project_root: str) -> tuple[int, str]:
359
359
 
360
360
  completed_process = subprocess.run(
361
361
  mypy_command,
362
- capture_output=True,
362
+ check=False, capture_output=True,
363
363
  text=True,
364
364
  env=os.environ.copy(),
365
365
  timeout=MYPY_TIMEOUT_SECONDS,
@@ -110,6 +110,7 @@ def check_optional_tool(tool_name: str) -> bool:
110
110
  try:
111
111
  result = subprocess.run(
112
112
  [tool_name, "--version"],
113
+ check=False,
113
114
  capture_output=True,
114
115
  text=True,
115
116
  )
@@ -18,6 +18,7 @@ def check_mypy_available() -> bool:
18
18
  try:
19
19
  result = subprocess.run(
20
20
  ["mypy", "--version"],
21
+ check=False,
21
22
  capture_output=True,
22
23
  text=True,
23
24
  )
@@ -85,6 +86,7 @@ def run_mypy_check(files: list[Path]) -> MypyResult:
85
86
  result = subprocess.run(
86
87
  ["mypy", *config_argument, "--ignore-missing-imports", "--no-error-summary"]
87
88
  + py_files,
89
+ check=False,
88
90
  capture_output=True,
89
91
  text=True,
90
92
  )
@@ -17,6 +17,7 @@ def check_ruff_available() -> bool:
17
17
  try:
18
18
  result = subprocess.run(
19
19
  ["ruff", "--version"],
20
+ check=False,
20
21
  capture_output=True,
21
22
  text=True,
22
23
  )
@@ -39,6 +40,7 @@ def run_ruff_check(files: list[Path]) -> RuffResult:
39
40
 
40
41
  result = subprocess.run(
41
42
  ["ruff", "check"] + py_files,
43
+ check=False,
42
44
  capture_output=True,
43
45
  text=True,
44
46
  )
@@ -61,6 +63,7 @@ def run_ruff_fix(files: list[Path]) -> RuffResult:
61
63
 
62
64
  result = subprocess.run(
63
65
  ["ruff", "check", "--fix"] + py_files,
66
+ check=False,
64
67
  capture_output=True,
65
68
  text=True,
66
69
  )