claude-dev-env 2.4.0 → 2.5.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 (195) hide show
  1. package/CLAUDE.md +53 -49
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +2 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +0 -1
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/test_agent_frontmatter.py +67 -18
  31. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  32. package/bin/CLAUDE.md +68 -5
  33. package/bin/ever-shipped-skills.mjs +1 -0
  34. package/bin/install-constants.mjs +88 -0
  35. package/bin/install.mjs +1138 -114
  36. package/bin/install.prune.test.mjs +869 -19
  37. package/bin/install.test.mjs +906 -2
  38. package/commands/implement.md +1 -1
  39. package/commands/right-size.md +1 -1
  40. package/docs/CLAUDE.md +1 -0
  41. package/docs/host-pool-health-monitor.md +102 -0
  42. package/docs/references/CLAUDE.md +4 -2
  43. package/docs/references/advisor-tool.md +13 -0
  44. package/docs/references/code-review-enforcement.md +10 -0
  45. package/docs/references/team-advisor-skill.md +14 -0
  46. package/hooks/blocking/CLAUDE.md +1 -0
  47. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  48. package/hooks/blocking/code_review_push_gate.py +9 -4
  49. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  50. package/hooks/blocking/config/__init__.py +5 -5
  51. package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
  52. package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
  53. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  54. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  55. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  56. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  57. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  58. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  59. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  60. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  61. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  62. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  63. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  64. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  65. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  66. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  67. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  68. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  69. package/hooks/git-hooks/test_pre_push.py +25 -0
  70. package/hooks/hooks.json +10 -0
  71. package/hooks/hooks_constants/CLAUDE.md +2 -1
  72. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  73. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  74. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  75. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  76. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  77. package/hooks/pyproject.toml +1 -0
  78. package/hooks/validators/CLAUDE.md +1 -0
  79. package/hooks/validators/config/__init__.py +0 -0
  80. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  81. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  82. package/hooks/validators/conftest.py +4 -0
  83. package/hooks/validators/ruff_integration.py +49 -5
  84. package/hooks/validators/run_all_validators.py +206 -9
  85. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  86. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  87. package/hooks/validators/test_ruff_integration.py +92 -1
  88. package/hooks/validators/test_run_all_validators.py +115 -68
  89. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  90. package/package.json +10 -2
  91. package/rules/CLAUDE.md +1 -0
  92. package/rules/docstring-prose-matches-implementation.md +45 -44
  93. package/rules/state-what-is.md +25 -0
  94. package/rules/verified-commit-gate-skip.md +1 -1
  95. package/scripts/CLAUDE.md +1 -0
  96. package/scripts/Capture-PoolHealth.ps1 +410 -0
  97. package/scripts/_code_review_test_support.py +404 -0
  98. package/scripts/claude_chain_runner.py +141 -1
  99. package/scripts/conftest.py +16 -1
  100. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  101. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  102. package/scripts/resolve_worker_spawn.py +626 -626
  103. package/scripts/spawn_grok_batch.py +672 -672
  104. package/scripts/test_claude_chain_runner.py +131 -0
  105. package/scripts/test_invoke_code_review_chain.py +70 -0
  106. package/scripts/test_invoke_code_review_cli.py +192 -0
  107. package/scripts/test_invoke_code_review_contract.py +256 -0
  108. package/scripts/test_invoke_code_review_git.py +123 -0
  109. package/scripts/test_invoke_code_review_mode.py +99 -0
  110. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  111. package/skills/CLAUDE.md +2 -0
  112. package/skills/auditing-claude-config/SKILL.md +114 -114
  113. package/skills/autoconverge/SKILL.md +427 -427
  114. package/skills/autoconverge/reference/convergence.md +24 -3
  115. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  116. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  117. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  118. package/skills/autoconverge/workflow/converge.mjs +167 -0
  119. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  120. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  121. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  122. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  123. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  124. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  125. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  126. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  127. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  128. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  129. package/skills/closeout/SKILL.md +33 -50
  130. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  131. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  132. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  133. package/skills/condensing-instructions/SKILL.md +81 -0
  134. package/skills/copilot-review/SKILL.md +119 -119
  135. package/skills/e-code-review/SKILL.md +52 -0
  136. package/skills/e-code-review/reference/fix.md +54 -0
  137. package/skills/e-code-review/reference/loop.md +43 -0
  138. package/skills/e-code-review/reference/low.md +57 -0
  139. package/skills/e-code-review/reference/medium.md +153 -0
  140. package/skills/e-code-review/reference/xhigh.md +182 -0
  141. package/skills/e-simplify/SKILL.md +97 -0
  142. package/skills/issue-tracker/SKILL.md +92 -0
  143. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  144. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  145. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  146. package/skills/orchestrator/SKILL.md +162 -21
  147. package/skills/orchestrator/scripts/status_gate.py +625 -0
  148. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  149. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  150. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  151. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  152. package/skills/orchestrator-refresh/SKILL.md +110 -35
  153. package/skills/plan-to-pr/SKILL.md +155 -0
  154. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  155. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  156. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  157. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  158. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  159. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  160. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  161. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  162. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  163. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  164. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  165. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  166. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  167. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  168. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  169. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  170. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  171. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  172. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  173. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  174. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  175. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  176. package/skills/pr-converge/SKILL.md +472 -469
  177. package/skills/pr-converge/reference/examples.md +3 -3
  178. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  179. package/skills/pr-converge/reference/ground-rules.md +7 -4
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  181. package/skills/pr-converge/reference/per-tick.md +5 -5
  182. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  183. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  184. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  185. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  186. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  187. package/skills/pr-fix-protocol/SKILL.md +1 -1
  188. package/skills/privacy-hygiene/SKILL.md +68 -68
  189. package/skills/prototype/workflows/promotion.md +1 -1
  190. package/skills/release-notes-html/SKILL.md +164 -0
  191. package/skills/task-build/CLAUDE.md +8 -7
  192. package/skills/task-build/SKILL.md +16 -8
  193. package/skills/task-build/reference/tool-routing.md +19 -0
  194. package/scripts/test_invoke_code_review.py +0 -966
  195. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -4,8 +4,6 @@ import ast
4
4
  import sys
5
5
  from pathlib import Path
6
6
 
7
- import pytest
8
-
9
7
  from .python_antipattern_checks import (
10
8
  check_mutable_default_args,
11
9
  check_bare_except,
@@ -198,16 +196,23 @@ def emit_status(message):
198
196
  '''
199
197
 
200
198
 
201
- def _antipattern_result_for(file_path: str, content: str) -> ValidatorResult:
202
- """Return the Python Anti-patterns result for content proposed at file_path."""
199
+ def _named_result(
200
+ file_path: str, content: str, validator_name: str
201
+ ) -> ValidatorResult:
202
+ """Return the named validator result for content proposed at file_path."""
203
203
  all_results = validate_proposed_file(file_path, content)
204
204
  return next(
205
205
  each_result
206
206
  for each_result in all_results
207
- if each_result.name == ANTIPATTERN_VALIDATOR_NAME
207
+ if each_result.name == validator_name
208
208
  )
209
209
 
210
210
 
211
+ def _antipattern_result_for(file_path: str, content: str) -> ValidatorResult:
212
+ """Return the Python Anti-patterns result for content proposed at file_path."""
213
+ return _named_result(file_path, content, ANTIPATTERN_VALIDATOR_NAME)
214
+
215
+
211
216
  class TestPrintCliExemptionThroughPipeline:
212
217
  """Pin the CLI-marker print exemption through the validate_proposed_file pipeline.
213
218
 
@@ -222,3 +227,103 @@ class TestPrintCliExemptionThroughPipeline:
222
227
  antipattern_result = _antipattern_result_for(CLI_BASENAME, PIPELINE_PRINT_SOURCE)
223
228
  assert antipattern_result.passed
224
229
  assert PRINT_FINDING_FRAGMENT not in antipattern_result.output
230
+
231
+
232
+ SCRIPTS_DIRECTORY_PATH = "packages/foo/scripts/run_job.py"
233
+ NON_EXEMPT_PRODUCTION_PATH = "packages/foo/services/worker.py"
234
+ TEST_FILE_PROPOSED_PATH = "packages/foo/tests/test_helpers.py"
235
+ USELESS_TEST_VALIDATOR_NAME = "Useless Tests"
236
+ USELESS_TEST_SOURCE = '''
237
+ def test_exists():
238
+ assert callable(str)
239
+ '''
240
+ TEST_HELPER_DIRECTORY_PATH = "packages/foo/tests/helper_functions.py"
241
+ NON_TEST_DIRECTORY_HELPER_PATH = "packages/foo/helpers/helper_functions.py"
242
+ CONFIG_DIRECTORY_MAGIC_PATH = "packages/foo/config/threshold_values.py"
243
+ CONFIG_CONSTANTS_DIRECTORY_MAGIC_PATH = (
244
+ "packages/foo/pr_converge_skill_constants/threshold_values.py"
245
+ )
246
+ NON_EXEMPT_MAGIC_PATH = "packages/foo/services/threshold_values.py"
247
+ MAGIC_VALUE_VALIDATOR_NAME = "Magic Values"
248
+ MAGIC_NUMBER_SOURCE = '''
249
+ def scale_amount(quantity):
250
+ return quantity * 199 * 42
251
+ '''
252
+
253
+
254
+ class TestDirectoryExemptionThroughPipeline:
255
+ """Directory-based exemptions must survive validate_proposed_file staging.
256
+
257
+ Basename-only temp staging drops /scripts/, so print() was falsely denied.
258
+ The real-path unit check allows the same content; the pipeline must match.
259
+ """
260
+
261
+ def test_scripts_directory_print_passes_through_pipeline(self) -> None:
262
+ tree = ast.parse(PIPELINE_PRINT_SOURCE)
263
+ basename_only = Path(SCRIPTS_DIRECTORY_PATH).name
264
+
265
+ assert len(check_print_in_production(tree, basename_only)) == 1
266
+ assert check_print_in_production(tree, SCRIPTS_DIRECTORY_PATH) == []
267
+
268
+ antipattern_result = _antipattern_result_for(
269
+ SCRIPTS_DIRECTORY_PATH, PIPELINE_PRINT_SOURCE
270
+ )
271
+ assert antipattern_result.passed
272
+ assert PRINT_FINDING_FRAGMENT not in antipattern_result.output
273
+
274
+ def test_non_exempt_production_print_still_fails_through_pipeline(self) -> None:
275
+ antipattern_result = _antipattern_result_for(
276
+ NON_EXEMPT_PRODUCTION_PATH, PIPELINE_PRINT_SOURCE
277
+ )
278
+ assert not antipattern_result.passed
279
+ assert PRINT_FINDING_FRAGMENT in antipattern_result.output
280
+
281
+ def test_test_filename_filtering_still_recognizes_test_path(self) -> None:
282
+ all_results = validate_proposed_file(TEST_FILE_PROPOSED_PATH, USELESS_TEST_SOURCE)
283
+ useless_result = next(
284
+ each_result
285
+ for each_result in all_results
286
+ if each_result.name == USELESS_TEST_VALIDATOR_NAME
287
+ )
288
+ assert not useless_result.passed
289
+ assert "callable" in useless_result.output.lower()
290
+
291
+ antipattern_result = _antipattern_result_for(
292
+ TEST_FILE_PROPOSED_PATH, TEST_FILE_WITH_PRINT
293
+ )
294
+ assert antipattern_result.passed
295
+ assert PRINT_FINDING_FRAGMENT not in antipattern_result.output
296
+
297
+ def test_tests_directory_recognized_without_test_basename(self) -> None:
298
+ exempt_result = _antipattern_result_for(
299
+ TEST_HELPER_DIRECTORY_PATH, PIPELINE_PRINT_SOURCE
300
+ )
301
+ assert exempt_result.passed
302
+ assert PRINT_FINDING_FRAGMENT not in exempt_result.output
303
+
304
+ flagged_result = _antipattern_result_for(
305
+ NON_TEST_DIRECTORY_HELPER_PATH, PIPELINE_PRINT_SOURCE
306
+ )
307
+ assert not flagged_result.passed
308
+ assert PRINT_FINDING_FRAGMENT in flagged_result.output
309
+
310
+ def test_config_directory_magic_value_exempt_through_pipeline(self) -> None:
311
+ exempt_result = _named_result(
312
+ CONFIG_DIRECTORY_MAGIC_PATH, MAGIC_NUMBER_SOURCE, MAGIC_VALUE_VALIDATOR_NAME
313
+ )
314
+ assert exempt_result.passed
315
+
316
+ denied_result = _named_result(
317
+ NON_EXEMPT_MAGIC_PATH, MAGIC_NUMBER_SOURCE, MAGIC_VALUE_VALIDATOR_NAME
318
+ )
319
+ assert not denied_result.passed
320
+
321
+ def test_config_exemption_survives_with_skill_constants_directory_ancestor(
322
+ self,
323
+ ) -> None:
324
+ exempt_result = _named_result(
325
+ CONFIG_CONSTANTS_DIRECTORY_MAGIC_PATH,
326
+ MAGIC_NUMBER_SOURCE,
327
+ MAGIC_VALUE_VALIDATOR_NAME,
328
+ )
329
+ assert exempt_result.passed
@@ -1,5 +1,7 @@
1
1
  """Tests for ruff integration module."""
2
2
 
3
+ import subprocess
4
+ import sys
3
5
  from pathlib import Path
4
6
  from unittest.mock import patch
5
7
 
@@ -7,6 +9,7 @@ from .ruff_integration import (
7
9
  RuffResult,
8
10
  _config_relative_stdin_filename,
9
11
  _parse_fixed_count,
12
+ _staged_ruff_result,
10
13
  check_ruff_available,
11
14
  run_ruff_check,
12
15
  )
@@ -94,6 +97,92 @@ def test_run_ruff_check_applies_config_resolved_from_config_source_path(
94
97
  assert "B011" in with_source.output
95
98
 
96
99
 
100
+ def test_run_ruff_check_grades_a_staged_copy_holding_non_ascii_text(
101
+ tmp_path: Path,
102
+ ) -> None:
103
+ """A staged copy carrying non-ASCII text reaches ruff intact.
104
+
105
+ The staged shape pipes the copy's text to ruff on stdin, and ruff reads
106
+ stdin as UTF-8. Encoding that pipe with the host locale codec instead
107
+ hands ruff a byte it rejects, so every Python file holding a non-ASCII
108
+ character fails the gate on a host whose locale codec is not UTF-8.
109
+ """
110
+ ruff_repo = tmp_path / "ruff_repo"
111
+ ruff_repo.mkdir()
112
+ (ruff_repo / "pyproject.toml").write_text("[tool.ruff]\n", encoding="utf-8")
113
+ original_target = ruff_repo / "dashes.py"
114
+ staged_copy = tmp_path / "detached" / "dashes.py"
115
+ staged_copy.parent.mkdir(parents=True)
116
+ staged_copy.write_text('DASH_NOTE = "an em dash — here"\n', encoding="utf-8")
117
+
118
+ result = run_ruff_check([staged_copy], config_source_path=original_target)
119
+
120
+ assert result.passed is True
121
+
122
+
123
+ def test_staged_ruff_result_pins_utf8_encoding_with_tolerant_decode(
124
+ tmp_path: Path,
125
+ ) -> None:
126
+ """The staged ruff call pins UTF-8 and decodes ruff output tolerantly.
127
+
128
+ ::
129
+
130
+ subprocess.run(..., encoding="utf-8", errors="replace", ...)
131
+ ok: locale-independent stdin; a non-UTF-8 byte in ruff output never raises
132
+ flag: encoding omitted -> locale codec; errors omitted -> strict decode
133
+ raises out of subprocess.run on POSIX (fail-open in the gate)
134
+
135
+ The kwargs carry the contract host-independently. The locale fallback the
136
+ integration test above depends on cannot be forced portably, so this guard
137
+ asserts the call itself rather than the emergent behavior.
138
+ """
139
+ pyproject = tmp_path / "pyproject.toml"
140
+ pyproject.write_text("[tool.ruff]\n", encoding="utf-8")
141
+ captured_keyword_arguments: dict[str, object] = {}
142
+
143
+ def record_call(*positional: object, **keyword: object) -> subprocess.CompletedProcess[str]:
144
+ captured_keyword_arguments.update(keyword)
145
+ return subprocess.CompletedProcess([], 0, stdout="", stderr="")
146
+
147
+ with patch("subprocess.run", side_effect=record_call):
148
+ _staged_ruff_result(pyproject, "probe.py", "x = 1\n")
149
+
150
+ assert captured_keyword_arguments["encoding"] == "utf-8"
151
+ assert captured_keyword_arguments["errors"] == "replace"
152
+
153
+
154
+ def test_staged_ruff_result_tolerates_non_utf8_bytes_in_ruff_output(
155
+ tmp_path: Path,
156
+ ) -> None:
157
+ """A non-UTF-8 byte in ruff's output is decoded tolerantly, not dropped.
158
+
159
+ ::
160
+
161
+ ruff output carries byte 0x92 (a cp1252 curly quote, invalid UTF-8)
162
+ ok: errors="replace" -> output keeps the surrounding text
163
+ flag: errors="strict" -> POSIX raises out of run; Windows drops the
164
+ stream to None and the surrounding text is lost
165
+
166
+ Swaps the ruff argv for a python one-liner emitting the bad byte, so the
167
+ real subprocess decode path runs against real bytes.
168
+ """
169
+ pyproject = tmp_path / "pyproject.toml"
170
+ pyproject.write_text("[tool.ruff]\n", encoding="utf-8")
171
+ emit_bad_byte_command = [
172
+ sys.executable,
173
+ "-c",
174
+ "import sys; sys.stdout.buffer.write(bytes([0x62, 0x92, 0x63]))",
175
+ ]
176
+ staged_module = sys.modules[_staged_ruff_result.__module__]
177
+ with patch.object(
178
+ staged_module, "_staged_ruff_command", return_value=emit_bad_byte_command
179
+ ):
180
+ result = _staged_ruff_result(pyproject, "probe.py", "x = 1\n")
181
+
182
+ assert "b" in result.output
183
+ assert "c" in result.output
184
+
185
+
97
186
  def test_run_ruff_check_emits_location_prefixed_lines(tmp_path: Path) -> None:
98
187
  """Each reported violation carries a ``path:line:col:`` prefix on its own line.
99
188
 
@@ -105,7 +194,8 @@ def test_run_ruff_check_emits_location_prefixed_lines(tmp_path: Path) -> None:
105
194
  violating_file = tmp_path / "unused_import_module.py"
106
195
  violating_file.write_text("import os\n", encoding="utf-8")
107
196
 
108
- result = run_ruff_check([violating_file])
197
+ with patch.dict("os.environ", {"FORCE_COLOR": "1"}):
198
+ result = run_ruff_check([violating_file])
109
199
 
110
200
  assert result.passed is False
111
201
  location_prefix = f"{violating_file}:1:8:"
@@ -113,3 +203,4 @@ def test_run_ruff_check_emits_location_prefixed_lines(tmp_path: Path) -> None:
113
203
  each_line.startswith(location_prefix) and "F401" in each_line
114
204
  for each_line in result.output.splitlines()
115
205
  ), result.output
206
+ assert "\x1b[" not in result.output
@@ -8,9 +8,11 @@ from unittest.mock import MagicMock, patch
8
8
 
9
9
  import pytest
10
10
 
11
+ from .exempt_paths import is_test_file
11
12
  from .run_all_validators import (
12
13
  ValidatorResult,
13
14
  _hooks_subprocess_working_directory_and_environment,
15
+ _temporary_path_preserving_directory_signal,
14
16
  add_timing,
15
17
  build_json_output,
16
18
  create_timing_metrics,
@@ -28,6 +30,22 @@ _ASSERT_FALSE_SOURCE = (
28
30
  "def probe_condition(observed_total: int) -> None:\n"
29
31
  " assert False, observed_total\n"
30
32
  )
33
+ PRINT_PRODUCTION_CONTENT = 'def run() -> None:\n print("x")\n'
34
+ ALL_PYTHON_TEST_EXEMPT_PATHS = [
35
+ "pkg/test_helpers/worker.py",
36
+ "pkg/conftest.py",
37
+ "pkg/foo_test.py",
38
+ ]
39
+ ALL_NON_TEST_PRODUCTION_PATHS = [
40
+ "pkg/worker.py",
41
+ "pkg/latest/worker.py",
42
+ "pkg/protest/worker.py",
43
+ ]
44
+ ALL_IS_TEST_FILE_PARITY_PATHS = (
45
+ ALL_PYTHON_TEST_EXEMPT_PATHS
46
+ + ALL_NON_TEST_PRODUCTION_PATHS
47
+ + ["pkg/data.spec.helpers/worker.py"]
48
+ )
31
49
 
32
50
 
33
51
  def test_validate_proposed_file_threads_ruff_config_from_original_path() -> None:
@@ -48,6 +66,52 @@ def test_validate_proposed_file_threads_ruff_config_from_original_path() -> None
48
66
  assert "B011" in ruff_output
49
67
 
50
68
 
69
+ def _validators_deny_print(file_path: str) -> bool:
70
+ all_results = validate_proposed_file(file_path, PRINT_PRODUCTION_CONTENT)
71
+ return any(not each_result.passed for each_result in all_results)
72
+
73
+
74
+ @pytest.mark.parametrize("file_path", ALL_PYTHON_TEST_EXEMPT_PATHS)
75
+ def test_staging_keeps_substring_test_exemption(file_path: str) -> None:
76
+ assert is_test_file(file_path) is True
77
+ assert _validators_deny_print(file_path) is False
78
+
79
+
80
+ @pytest.mark.parametrize("file_path", ALL_NON_TEST_PRODUCTION_PATHS)
81
+ def test_staging_leaves_non_test_paths_denied(file_path: str) -> None:
82
+ assert is_test_file(file_path) is False
83
+ assert _validators_deny_print(file_path) is True
84
+
85
+
86
+ @pytest.mark.parametrize("file_path", ALL_IS_TEST_FILE_PARITY_PATHS)
87
+ def test_staged_path_matches_is_test_file_verdict(
88
+ file_path: str, tmp_path: Path
89
+ ) -> None:
90
+ staged_path = _temporary_path_preserving_directory_signal(tmp_path, file_path)
91
+ staged_relative_path = staged_path.relative_to(tmp_path)
92
+ assert is_test_file(str(staged_relative_path)) == is_test_file(file_path)
93
+
94
+
95
+ def _passing_mock_result() -> MagicMock:
96
+ """Return a mock validator result that reports a passing check."""
97
+ mock_result = MagicMock()
98
+ mock_result.passed = True
99
+ mock_result.name = "Test"
100
+ mock_result.checks = "test"
101
+ mock_result.output = ""
102
+ return mock_result
103
+
104
+
105
+ def _run_main_with_argv(command_line_arguments: list[str]) -> int | None:
106
+ """Run ``main`` under the given argv and always restore the original argv."""
107
+ original_argv = sys.argv
108
+ try:
109
+ sys.argv = command_line_arguments
110
+ return main()
111
+ finally:
112
+ sys.argv = original_argv
113
+
114
+
51
115
  class TestFixFlag:
52
116
  """Test --fix flag functionality."""
53
117
 
@@ -56,25 +120,11 @@ class TestFixFlag:
56
120
  with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
57
121
  patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
58
122
  patch("validators.run_all_validators.run_git_checks") as mock_git:
59
-
60
123
  mock_get_files.return_value = []
124
+ mock_file.return_value = _passing_mock_result()
125
+ mock_git.return_value = _passing_mock_result()
61
126
 
62
- mock_result = MagicMock()
63
- mock_result.passed = True
64
- mock_result.name = "Test"
65
- mock_result.checks = "test"
66
- mock_result.output = ""
67
-
68
- mock_file.return_value = mock_result
69
- mock_git.return_value = mock_result
70
-
71
- original_argv = sys.argv
72
- try:
73
- sys.argv = ["run_all_validators.py", "--fix"]
74
- result = main()
75
- assert result == 0
76
- finally:
77
- sys.argv = original_argv
127
+ assert _run_main_with_argv(["run_all_validators.py", "--fix"]) == 0
78
128
 
79
129
  def test_fix_flag_calls_fix_python_style(self) -> None:
80
130
  """Verify --fix flag triggers fix_python_style when files exist."""
@@ -86,29 +136,12 @@ class TestFixFlag:
86
136
  patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
87
137
  patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
88
138
  patch("validators.run_all_validators.run_git_checks") as mock_git:
89
-
90
139
  mock_get_files.return_value = [Path("test.py")]
91
140
  mock_fix.return_value = ["test.py"]
141
+ for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file, mock_git):
142
+ each_mock.return_value = _passing_mock_result()
92
143
 
93
- mock_result = MagicMock()
94
- mock_result.passed = True
95
- mock_result.name = "Test"
96
- mock_result.checks = "test"
97
- mock_result.output = ""
98
-
99
- mock_style.return_value = mock_result
100
- mock_test.return_value = mock_result
101
- mock_react.return_value = mock_result
102
- mock_comment.return_value = mock_result
103
- mock_file.return_value = mock_result
104
- mock_git.return_value = mock_result
105
-
106
- original_argv = sys.argv
107
- try:
108
- sys.argv = ["run_all_validators.py", "--fix"]
109
- main()
110
- finally:
111
- sys.argv = original_argv
144
+ _run_main_with_argv(["run_all_validators.py", "--fix"])
112
145
 
113
146
  mock_fix.assert_called_once()
114
147
 
@@ -122,59 +155,71 @@ class TestFixFlag:
122
155
  patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
123
156
  patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
124
157
  patch("validators.run_all_validators.run_git_checks") as mock_git:
125
-
126
158
  mock_get_files.return_value = [Path("test.py")]
159
+ for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file, mock_git):
160
+ each_mock.return_value = _passing_mock_result()
127
161
 
128
- mock_result = MagicMock()
129
- mock_result.passed = True
130
- mock_result.name = "Test"
131
- mock_result.checks = "test"
132
- mock_result.output = ""
133
-
134
- mock_style.return_value = mock_result
135
- mock_test.return_value = mock_result
136
- mock_react.return_value = mock_result
137
- mock_comment.return_value = mock_result
138
- mock_file.return_value = mock_result
139
- mock_git.return_value = mock_result
140
-
141
- original_argv = sys.argv
142
- try:
143
- sys.argv = ["run_all_validators.py"]
144
- main()
145
- finally:
146
- sys.argv = original_argv
162
+ _run_main_with_argv(["run_all_validators.py"])
147
163
 
148
164
  mock_fix.assert_not_called()
149
165
 
150
166
 
167
+ class TestStagedPathContainment:
168
+ """Staged validation paths must never escape the temporary root."""
169
+
170
+ def test_parent_traversal_path_stays_inside_temp_root(self, tmp_path: Path) -> None:
171
+ temporary_root = tmp_path / "staging"
172
+ temporary_root.mkdir()
173
+ traversal_path = "scripts/../../../../escaped_evil.py"
174
+
175
+ staged_file = _temporary_path_preserving_directory_signal(
176
+ temporary_root, traversal_path
177
+ )
178
+ staged_file.write_text("payload", encoding="utf-8")
179
+
180
+ assert staged_file.resolve().is_relative_to(temporary_root.resolve())
181
+ assert staged_file.read_text(encoding="utf-8") == "payload"
182
+
183
+ def test_absolute_component_path_stays_inside_temp_root(self, tmp_path: Path) -> None:
184
+ temporary_root = tmp_path / "staging"
185
+ temporary_root.mkdir()
186
+ absolute_style_path = "/etc/cron.d/evil.py"
187
+
188
+ staged_file = _temporary_path_preserving_directory_signal(
189
+ temporary_root, absolute_style_path
190
+ )
191
+ staged_file.write_text("payload", encoding="utf-8")
192
+
193
+ assert staged_file.resolve().is_relative_to(temporary_root.resolve())
194
+
195
+
151
196
  class TestGracefulDegradation:
152
197
  def test_missing_validator_returns_skipped_result(self) -> None:
153
198
  def failing_validator() -> ValidatorResult:
154
199
  raise FileNotFoundError("validator.py not found")
155
200
 
156
- result = run_with_fallback(
201
+ skipped_result = run_with_fallback(
157
202
  failing_validator,
158
203
  "Missing Validator",
159
204
  "99",
160
205
  )
161
206
 
162
- assert result.skipped is True
163
- assert "skipped" in result.output.lower()
164
- assert result.passed is False
207
+ assert skipped_result.skipped is True
208
+ assert "skipped" in skipped_result.output.lower()
209
+ assert skipped_result.passed is False
165
210
 
166
211
  def test_validator_exception_returns_skipped_result(self) -> None:
167
212
  def crashing_validator() -> ValidatorResult:
168
213
  raise RuntimeError("Unexpected crash")
169
214
 
170
- result = run_with_fallback(
215
+ skipped_result = run_with_fallback(
171
216
  crashing_validator,
172
217
  "Crashing Validator",
173
218
  "99",
174
219
  )
175
220
 
176
- assert result.skipped is True
177
- assert "skipped" in result.output.lower()
221
+ assert skipped_result.skipped is True
222
+ assert "skipped" in skipped_result.output.lower()
178
223
 
179
224
  def test_successful_validator_returns_normal_result(self) -> None:
180
225
  def working_validator() -> ValidatorResult:
@@ -185,14 +230,14 @@ class TestGracefulDegradation:
185
230
  output="All good",
186
231
  )
187
232
 
188
- result = run_with_fallback(
233
+ normal_result = run_with_fallback(
189
234
  working_validator,
190
235
  "Working",
191
236
  "1",
192
237
  )
193
238
 
194
- assert result.skipped is False
195
- assert result.passed is True
239
+ assert normal_result.skipped is False
240
+ assert normal_result.passed is True
196
241
 
197
242
 
198
243
  class TestStderrSurfacing:
@@ -269,7 +314,9 @@ class TestTimingMetrics:
269
314
 
270
315
 
271
316
  class TestVersionHeader:
272
- def test_print_header_includes_version(self, capsys) -> None:
317
+ def test_print_header_includes_version(
318
+ self, capsys: pytest.CaptureFixture[str]
319
+ ) -> None:
273
320
  print_header()
274
321
  captured = capsys.readouterr()
275
322