claude-dev-env 2.7.0 → 2.8.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 (88) hide show
  1. package/_shared/CLAUDE.md +1 -0
  2. package/_shared/advisor/advisor-protocol.md +19 -9
  3. package/_shared/pr-loop/audit-contract.md +4 -4
  4. package/_shared/pr-loop/precatch-rubric.md +2 -2
  5. package/_shared/pr-loop/worker-spawn.md +3 -1
  6. package/_shared/process-tree/CLAUDE.md +41 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  8. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  9. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  10. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  11. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  12. package/agents/code-quality-agent.md +6 -5
  13. package/agents/deep-research.md +7 -24
  14. package/agents/docs-agent.md +1 -27
  15. package/agents/issue-tracker.md +1 -7
  16. package/agents/skill-writer-agent.md +1 -2
  17. package/agents/test_agent_frontmatter.py +309 -12
  18. package/hooks/blocking/CLAUDE.md +2 -0
  19. package/hooks/blocking/fable_spawn_gate.py +187 -0
  20. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  21. package/hooks/blocking/plain_language_blocker.py +287 -15
  22. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  23. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  24. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  26. package/hooks/git-hooks/CLAUDE.md +2 -2
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  28. package/hooks/git-hooks/pre_push.py +343 -54
  29. package/hooks/git-hooks/test_pre_push.py +852 -6
  30. package/hooks/hooks.json +9 -19
  31. package/hooks/hooks_constants/CLAUDE.md +2 -0
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  33. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  34. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  35. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  36. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  37. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  40. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  41. package/package.json +1 -1
  42. package/rules/CLAUDE.md +1 -0
  43. package/rules/ask-user-question-required.md +26 -0
  44. package/rules/claims-as-quotes.md +65 -0
  45. package/scripts/CLAUDE.md +4 -4
  46. package/scripts/_code_review_test_support.py +6 -0
  47. package/scripts/check.ps1 +18 -5
  48. package/scripts/claude_chain_runner.py +203 -31
  49. package/scripts/codec_forwarding_test_support.py +2 -0
  50. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  51. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  52. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  53. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +83 -13
  54. package/scripts/grok_headless_runner.py +148 -18
  55. package/scripts/resolve_worker_spawn.py +56 -10
  56. package/scripts/spawn_grok_batch.py +81 -23
  57. package/scripts/test_claude_chain_runner.py +358 -0
  58. package/scripts/test_grok_headless_runner.py +547 -10
  59. package/scripts/test_invoke_code_review.py +298 -0
  60. package/scripts/test_resolve_worker_spawn.py +185 -15
  61. package/scripts/test_spawn_grok_batch.py +326 -22
  62. package/scripts/tests/CLAUDE.md +1 -0
  63. package/scripts/tests/test_grok_worker_constants.py +59 -0
  64. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
  66. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
  67. package/skills/autoconverge/workflow/converge.mjs +113 -31
  68. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  69. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  70. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  71. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  72. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  73. package/skills/e-code-review/SKILL.md +9 -8
  74. package/skills/e-code-review/reference/fix.md +29 -7
  75. package/skills/e-code-review/reference/loop.md +230 -14
  76. package/skills/e-code-review/reference/low.md +33 -15
  77. package/skills/e-code-review/reference/medium.md +55 -21
  78. package/skills/e-code-review/reference/xhigh.md +30 -12
  79. package/skills/fresh-branch/CLAUDE.md +5 -5
  80. package/skills/fresh-branch/SKILL.md +14 -6
  81. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  82. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  83. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  84. package/skills/grok-spawn/SKILL.md +10 -3
  85. package/skills/grok-spawn/reference/flag-profiles.md +3 -1
  86. package/skills/orchestrator/SKILL.md +4 -1
  87. package/skills/orchestrator-refresh/SKILL.md +5 -1
  88. package/skills/team-advisor/SKILL.md +4 -1
@@ -0,0 +1,587 @@
1
+ """Tests for the piped-pytest Bash blocker.
2
+
3
+ The deny cases each carry a pytest spelling whose output feeds a pipe; the allow
4
+ cases each carry a pytest or pipe shape that keeps the pytest exit code. The
5
+ dispatcher case drives the payload through the hosted-hook chain, so it holds
6
+ only while the roster carries the segment.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import shlex
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ _BLOCKING_DIR = Path(__file__).resolve().parent
20
+ _HOOKS_DIR = str(_BLOCKING_DIR.parent)
21
+ if _HOOKS_DIR not in sys.path:
22
+ sys.path.insert(0, _HOOKS_DIR)
23
+ _BLOCKING_DIR_TEXT = str(_BLOCKING_DIR)
24
+ if _BLOCKING_DIR_TEXT not in sys.path:
25
+ sys.path.insert(0, _BLOCKING_DIR_TEXT)
26
+
27
+ from hooks_constants.piped_pytest_blocker_constants import ( # noqa: E402
28
+ CORRECTIVE_MESSAGE,
29
+ )
30
+ from hooks_constants.shell_command_segments import split_into_segments # noqa: E402
31
+ from piped_pytest_blocker import ( # noqa: E402
32
+ find_piped_pytest_violation,
33
+ segment_runs_pytest,
34
+ )
35
+
36
+ SCRIPT_PATH = _BLOCKING_DIR / "piped_pytest_blocker.py"
37
+ DISPATCHER_PATH = _BLOCKING_DIR / "bash_pre_tool_use_dispatcher.py"
38
+ QUOTED_PIPE_COMMAND = 'pytest -k "a|b"'
39
+ SHARED_SPLITTER_SEGMENTS_FOR_QUOTED_PIPE = [["pytest", "-k", "a"], ["b"]]
40
+
41
+ ALL_PIPED_PYTEST_COMMANDS = [
42
+ "pytest tests | tee run.log",
43
+ "python -m pytest tests | head -50",
44
+ r"C:\Python313\python.exe -m pytest tests | tee run.log",
45
+ "python -m pytest tests 2>&1 | tee run.log",
46
+ "cd packages/claude-dev-env && pytest | cat",
47
+ "(python -m pytest tests) | tee run.log",
48
+ "(python -m pytest tests)|tee run.log",
49
+ "(python -m pytest tests)|&tee run.log",
50
+ "(python -m pytest tests)|(tee run.log)",
51
+ "(python -m pytest tests)|&(tee run.log)",
52
+ "((python -m pytest tests))|(tee run.log)",
53
+ 'cmd /c "python -m pytest tests | tee run.log"',
54
+ 'cmd.exe /C "pytest tests | tee run.log"',
55
+ "cmd /c python -m pytest tests | tee run.log",
56
+ "cmd.exe /c python -m pytest | cat",
57
+ "bash -c python -m pytest tests | tee run.log",
58
+ "pwsh -Command python -m pytest tests | tee run.log",
59
+ "pytest tests#tag | tee run.log",
60
+ "python -m pytest tests --junitxml=r#1.xml | tee run.log",
61
+ "bash -c 'pytest | tee run.log'",
62
+ "bash -c 'pytest tests | tee x'",
63
+ "python -mpytest tests | tee run.log",
64
+ "time pytest tests | tee run.log",
65
+ ]
66
+
67
+ ALL_BRACE_GROUP_PYTEST_COMMANDS = [
68
+ "{ pytest tests; } | tee run.log",
69
+ "{ python -m pytest tests; }|tee run.log",
70
+ "{ echo start; pytest tests; } | tee run.log",
71
+ ]
72
+
73
+ ALL_KEYWORD_COMPOUND_PYTEST_COMMANDS = [
74
+ "if true; then pytest tests; fi | tee run.log",
75
+ "while true; do pytest tests; done | tee run.log",
76
+ "for x in a; do pytest tests; done | tee run.log",
77
+ "if false; then echo skip; else pytest tests; fi | tee run.log",
78
+ ]
79
+
80
+ ALL_EARLIER_BRANCH_PYTEST_COMMANDS = [
81
+ "if true; then pytest tests; else echo skip; fi | tee run.log",
82
+ "if true; then pytest tests; elif false; then echo b; fi | tee run.log",
83
+ "if true; then echo start; pytest tests; else echo skip; fi | tee run.log",
84
+ "if a; then echo x; elif b; then pytest tests; else echo y; fi | tee run.log",
85
+ ]
86
+
87
+ ALL_REDIRECTED_CLOSER_PYTEST_COMMANDS = [
88
+ "if true; then pytest tests; fi 2>&1 | tee run.log",
89
+ "if true; then pytest tests; fi > out.log | tee run.log",
90
+ "while read x; do pytest $x; done < list | tee run.log",
91
+ "{ pytest tests; } 2>&1 | tee run.log",
92
+ "{ pytest tests; } >> out.log | tee run.log",
93
+ ]
94
+
95
+ SCRIPT_OPERAND_CARRYING_A_MODULE_FLAG = "python myscript.py -m pytest | tee run.log"
96
+ ALL_INTERPRETER_MODULE_RUN_PYTEST_COMMANDS = [
97
+ "python -m pytest tests | tee run.log",
98
+ "python -mpytest tests | tee run.log",
99
+ "python -m pytest tests --junitxml=r#1.xml | tee run.log",
100
+ "python -X dev -m pytest tests | tee run.log",
101
+ "python -W ignore -m pytest tests | tee run.log",
102
+ "python -Xdev -m pytest tests | tee run.log",
103
+ "python -u -m pytest tests | tee run.log",
104
+ "python -B -m pytest tests | tee run.log",
105
+ "python -OO -m pytest tests | tee run.log",
106
+ "python --check-hash-based-pycs always -m pytest tests | tee run.log",
107
+ "pypy3 --jit off -m pytest tests | tee run.log",
108
+ "sudo python -m pytest tests | tee run.log",
109
+ "uv run python -m pytest tests | tee run.log",
110
+ "uvx python -m pytest tests | tee run.log",
111
+ "coverage run -m pytest tests | tee run.log",
112
+ "poetry run python -m pytest tests | tee run.log",
113
+ ]
114
+
115
+ WRAPPER_CARRYING_THE_ONLY_PIPE = "bash -c 'python -m pytest tests | tee wrapped.log'"
116
+
117
+ ALL_WINDOWS_SHIM_PYTEST_COMMANDS = [
118
+ "pytest.bat tests | tee run.log",
119
+ "pytest.cmd tests | tee run.log",
120
+ "py.test.bat tests | tee run.log",
121
+ "py.test.cmd tests | tee run.log",
122
+ "python.bat -m pytest tests | tee run.log",
123
+ "python.cmd -m pytest tests | tee run.log",
124
+ ]
125
+
126
+ ALL_PASS_THROUGH_WRAPPER_PYTEST_COMMANDS = [
127
+ "sudo pytest tests | tee run.log",
128
+ "uv run pytest tests | tee run.log",
129
+ "poetry run pytest tests | tee run.log",
130
+ "pipenv run pytest tests | tee run.log",
131
+ "uv run python -m pytest tests | tee run.log",
132
+ ]
133
+
134
+ ALL_FLAGGED_WRAPPER_PYTEST_COMMANDS = [
135
+ "uv run --frozen pytest tests | tee run.log",
136
+ "uv run --no-sync pytest tests | tee run.log",
137
+ "poetry run --no-plugins pytest tests | tee run.log",
138
+ "uv run --python 3.13 pytest tests | tee run.log",
139
+ "uv run -- pytest tests | tee run.log",
140
+ "sudo -n pytest tests | tee run.log",
141
+ "sudo -u ci pytest tests | tee run.log",
142
+ "sudo -- pytest tests | tee run.log",
143
+ ]
144
+
145
+ ALL_CLUSTERED_SHORT_OPTION_PYTEST_COMMANDS = [
146
+ "sudo -nu ci pytest tests | tee run.log",
147
+ "sudo -nuci pytest tests | tee run.log",
148
+ ]
149
+
150
+ ALL_SHELL_OPTION_VALUE_PYTEST_COMMANDS = [
151
+ "bash -o pipefail -c 'pytest tests | tee run.log'",
152
+ "bash -euo pipefail -c 'pytest tests | tee run.log'",
153
+ "pwsh -ExecutionPolicy Bypass -Command 'pytest tests | tee run.log'",
154
+ ]
155
+
156
+ ALL_CLUSTERED_STRING_EXEC_PYTEST_COMMANDS = [
157
+ "bash -euc 'pytest tests | tee run.log'",
158
+ "bash -ec 'pytest tests | tee run.log'",
159
+ "bash -xc 'pytest tests | tee run.log'",
160
+ "bash -ceu 'pytest tests | tee run.log'",
161
+ "sh -ec 'pytest tests | tee run.log'",
162
+ "bash -euoc pipefail 'pytest tests | tee run.log'",
163
+ ]
164
+
165
+ ALL_COVERAGE_RUN_PYTEST_COMMANDS = [
166
+ "coverage run -m pytest tests | tee run.log",
167
+ "coverage run pytest tests | tee run.log",
168
+ ]
169
+
170
+ ALL_TOOL_RUNNER_WRAPPER_PYTEST_COMMANDS = [
171
+ "uvx pytest tests | tee run.log",
172
+ "uvx --from pytest-xdist pytest tests | tee run.log",
173
+ "uv tool run pytest tests | tee run.log",
174
+ "uv tool run --from pytest-xdist pytest tests | tee run.log",
175
+ "pdm run pytest tests | tee run.log",
176
+ "hatch run pytest tests | tee run.log",
177
+ "rye run pytest tests | tee run.log",
178
+ ]
179
+
180
+ ALL_ALTERNATE_INTERPRETER_PYTEST_COMMANDS = [
181
+ "python3.13t -m pytest tests | tee run.log",
182
+ "pypy3 -m pytest tests | tee run.log",
183
+ "pythonw -m pytest tests | tee run.log",
184
+ "pyw -m pytest tests | tee run.log",
185
+ "pythonw3.13.exe -m pytest tests | tee run.log",
186
+ ]
187
+
188
+ MULTI_LINE_SUBSHELL_PYTEST_COMMAND = "(\npython -m pytest tests\n) | tee run.log"
189
+ SUBSHELL_WRITING_A_PIPED_PYTEST_HEREDOC = (
190
+ "(\ncat > run.sh <<'EOF'\npytest tests | tee out.log\nEOF\n) | tee wrote.log"
191
+ )
192
+ SUBSHELL_CARRYING_A_COMMENT_LINE = "(\n# the fast run\npython -m pytest tests\n) | tee run.log"
193
+ SUBSHELL_CARRYING_A_TRAILING_COMMENT = "(\npython -m pytest tests # fast\n) | tee run.log"
194
+ COMMENT_CARRYING_A_STRAY_OPEN_PAREN = "# rerun the failing case (see the note\npytest | tee run.log"
195
+ COMMENT_NAMING_A_HEREDOC_OPENER = "# write it with <<EOF next time\npytest tests | tee run.log"
196
+ TRAILING_COMMENT_NAMING_A_HEREDOC_OPENER = (
197
+ "echo hi # cat <<EOF\npython -m pytest tests | tee run.log"
198
+ )
199
+
200
+ ALL_NESTED_MODULE_PYTEST_COMMANDS = [
201
+ "python -m coverage run -m pytest tests | tee run.log",
202
+ "python -m debugpy -m pytest tests | tee out.log",
203
+ ]
204
+
205
+ ALL_WRAPPED_PYTEST_COMMANDS_PIPED_FROM_OUTSIDE = [
206
+ "bash -c 'pytest tests' | tee run.log",
207
+ "pwsh -Command 'pytest tests' | tee run.log",
208
+ ]
209
+
210
+ ALL_HEREDOC_OPENER_SPELLINGS = ["<<EOF", "<<'EOF'", '<<"EOF"', "<<-EOF"]
211
+ HEREDOC_SCRIPT_TEMPLATE = "cat > run.sh {opener}\npytest tests | tee out.log\nEOF"
212
+
213
+ ALL_WIDE_HEREDOC_DELIMITERS = ["END-OF-TEST", "EOF-2.1", "run.sh-1", "EOF2"]
214
+ ALL_WIDE_HEREDOC_SCRIPT_TEMPLATES = [
215
+ "cat > run.sh <<{delimiter}\npytest tests | tee out.log\n{delimiter}",
216
+ "cat > run.sh <<'{delimiter}'\npytest tests | tee out.log\n{delimiter}",
217
+ 'cat > run.sh <<"{delimiter}"\npytest tests | tee out.log\n{delimiter}',
218
+ "cat > run.sh <<-{delimiter}\npytest tests | tee out.log\n{delimiter}",
219
+ "cat > run.sh <<\\{delimiter}\npytest tests | tee out.log\n{delimiter}",
220
+ ]
221
+ HERE_STRING_ABOVE_A_PIPED_PYTEST_RUN = "cat file <<<word\npytest tests | tee run.log"
222
+ COMMAND_AFTER_A_HEREDOC = "cat > run.sh <<'EOF'\necho hi\nEOF\npytest tests | tee out.log"
223
+
224
+ HEREDOC_BODY_LINE_SPACED_LIKE_ITS_TERMINATOR = (
225
+ "cat > run.sh <<EOF\n EOF \npytest tests | tee out.log\nEOF"
226
+ )
227
+ LIVE_RUN_BELOW_A_SPACED_TERMINATOR_LOOKALIKE = (
228
+ f"{HEREDOC_BODY_LINE_SPACED_LIKE_ITS_TERMINATOR}\npytest tests | tee live.log"
229
+ )
230
+ LIVE_RUN_BELOW_A_TAB_INDENTED_TERMINATOR = (
231
+ "cat > run.sh <<-EOF\npytest tests | tee out.log\n\tEOF\npytest tests | tee live.log"
232
+ )
233
+
234
+ ALL_LINE_CONTINUATION_TERMINATORS = ["\r\n", "\r", "\n"]
235
+
236
+ ALL_EXIT_CODE_PRESERVING_COMMANDS = [
237
+ "pytest tests",
238
+ "python -m pytest tests > run.log 2>&1",
239
+ "git status --short | head -20",
240
+ "cat ids.txt | pytest --stdin",
241
+ "pytest tests && echo done | tee run.log",
242
+ "python -m mypy . | tee types.log",
243
+ "pytest tests -q\ngit status | head",
244
+ """bash -c 'pytest -k "a|b"'""",
245
+ "pytest tests >| run.log",
246
+ "(pytest tests)||echo failed",
247
+ "pytest tests # | tee run.log",
248
+ "cmd /c python -m mypy . | tee types.log",
249
+ "cmd /c python -m pytest tests > run.log 2>&1",
250
+ "bash -c 'git status' | tee status.log",
251
+ "bash scripts/ci.sh -c 'pytest tests' | tee run.log",
252
+ "sh scripts/ci.sh -lc 'pytest tests' | tee run.log",
253
+ "cp file{a,b}.txt dst | tee log",
254
+ "{ git status; } | tee run.log",
255
+ "sudo apt update | tee log",
256
+ "uv sync | tee build.log",
257
+ "poetry run mypy . | tee types.log",
258
+ "uv run --with pytest mypy . | tee types.log",
259
+ "uv run --directory sub mypy . | tee types.log",
260
+ "sudo -u pytest apt update | tee log",
261
+ "bash -- -c 'pytest tests' | tee run.log",
262
+ "bash scripts/ci.sh -c 'pytest tests'",
263
+ "pwsh -File run.ps1 -c 'pytest tests' | tee run.log",
264
+ "sudo -nu ci apt update | tee log",
265
+ "sudo -nuci apt update | tee log",
266
+ "coverage run -m mypy . | tee types.log",
267
+ "coverage run --source pytest -m mypy . | tee types.log",
268
+ "pyright -m pytest . | tee out.log",
269
+ "bash -Cu scripts/ci.sh | tee run.log",
270
+ "bash -eux scripts/ci.sh | tee run.log",
271
+ "pwsh -NonInteractive -File run.ps1 | tee run.log",
272
+ "uvx mypy . | tee types.log",
273
+ "uv tool install pytest | tee install.log",
274
+ "pdm run mypy . | tee types.log",
275
+ "ls done | tee log",
276
+ "tar -cf out.tar done | tee log",
277
+ "echo fi | tee log",
278
+ "echo esac | tee log",
279
+ "ls do | tee log",
280
+ "ls else | tee log",
281
+ "if pytest tests; then echo ok; fi | tee run.log",
282
+ "if cd repo && pytest tests; then echo ok; fi | tee run.log",
283
+ "until pytest tests; do echo retry; done | tee run.log",
284
+ "while pytest tests; do echo again; done | tee run.log",
285
+ "if a; then echo x; elif pytest tests; then echo b; fi | tee run.log",
286
+ "if true; then pytest tests; echo after; else echo skip; fi | tee run.log",
287
+ "if a; then pytest tests; fi; if b; then echo c; fi | tee run.log",
288
+ "{ pytest tests; }; if b; then echo c; else echo d; fi | tee run.log",
289
+ "pytest tests; { echo a; } | tee run.log",
290
+ "python -c 'import os' -m pytest tests | tee run.log",
291
+ "python -Wu ignore -m pytest tests | tee run.log",
292
+ ]
293
+
294
+
295
+ def _run_process(script_path: Path, payload: dict) -> subprocess.CompletedProcess[str]:
296
+ return subprocess.run(
297
+ [sys.executable, str(script_path)],
298
+ input=json.dumps(payload),
299
+ text=True,
300
+ capture_output=True,
301
+ check=False,
302
+ )
303
+
304
+
305
+ def _bash_payload(command: str) -> dict:
306
+ return {"tool_name": "Bash", "tool_input": {"command": command}}
307
+
308
+
309
+ def _deny_reason_from_stdout(stdout_text: str) -> str:
310
+ parsed_payload = json.loads(stdout_text)
311
+ hook_specific = parsed_payload["hookSpecificOutput"]
312
+ assert hook_specific["permissionDecision"] == "deny"
313
+ return hook_specific["permissionDecisionReason"]
314
+
315
+
316
+ @pytest.mark.parametrize("each_command", ALL_PIPED_PYTEST_COMMANDS)
317
+ def test_denies_a_pytest_run_feeding_a_pipe(each_command: str) -> None:
318
+ """Each covered pytest spelling returns the deny message when it feeds a pipe."""
319
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
320
+
321
+
322
+ @pytest.mark.parametrize("each_command", ALL_EXIT_CODE_PRESERVING_COMMANDS)
323
+ def test_allows_a_command_that_keeps_the_pytest_exit_code(each_command: str) -> None:
324
+ """Bare pytest, redirection-only pytest, and unrelated pipes stay allowed."""
325
+ assert find_piped_pytest_violation(each_command) is None
326
+
327
+
328
+ @pytest.mark.parametrize("each_command", ALL_NESTED_MODULE_PYTEST_COMMANDS)
329
+ def test_denies_pytest_named_by_a_later_module_flag(each_command: str) -> None:
330
+ """A runner module ahead of ``-m pytest`` still leaves pytest as the run."""
331
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
332
+
333
+
334
+ @pytest.mark.parametrize("each_command", ALL_WRAPPED_PYTEST_COMMANDS_PIPED_FROM_OUTSIDE)
335
+ def test_denies_a_wrapped_pytest_run_piped_from_outside(each_command: str) -> None:
336
+ """A shell wrapper exits with pytest's code, so a pipe after it hides a failure."""
337
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
338
+
339
+
340
+ @pytest.mark.parametrize("each_opener", ALL_HEREDOC_OPENER_SPELLINGS)
341
+ def test_allows_a_heredoc_that_writes_a_piped_pytest_line(each_opener: str) -> None:
342
+ """A heredoc body is script text this call writes, not a command it runs."""
343
+ script_writing_command = HEREDOC_SCRIPT_TEMPLATE.format(opener=each_opener)
344
+ assert find_piped_pytest_violation(script_writing_command) is None
345
+
346
+
347
+ @pytest.mark.parametrize("each_template", ALL_WIDE_HEREDOC_SCRIPT_TEMPLATES)
348
+ @pytest.mark.parametrize("each_delimiter", ALL_WIDE_HEREDOC_DELIMITERS)
349
+ def test_allows_a_heredoc_whose_delimiter_carries_a_hyphen_digit_or_dot(
350
+ each_delimiter: str, each_template: str
351
+ ) -> None:
352
+ """A shell takes any word as a delimiter, so the body below one stays script text."""
353
+ script_writing_command = each_template.format(delimiter=each_delimiter)
354
+ assert find_piped_pytest_violation(script_writing_command) is None
355
+
356
+
357
+ @pytest.mark.parametrize("each_command", ALL_BRACE_GROUP_PYTEST_COMMANDS)
358
+ def test_denies_a_brace_group_whose_last_command_is_pytest(each_command: str) -> None:
359
+ """A brace group exits with its last command's code, so the pipe after it hides it."""
360
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
361
+
362
+
363
+ @pytest.mark.parametrize("each_command", ALL_KEYWORD_COMPOUND_PYTEST_COMMANDS)
364
+ def test_denies_a_keyword_compound_whose_last_command_is_pytest(each_command: str) -> None:
365
+ """``fi`` and ``done`` end a compound, so the pipe after one reads pytest's code."""
366
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
367
+
368
+
369
+ @pytest.mark.parametrize("each_command", ALL_EARLIER_BRANCH_PYTEST_COMMANDS)
370
+ def test_denies_a_compound_whose_earlier_branch_runs_pytest(each_command: str) -> None:
371
+ """A branch other than the last one still runs, so its exit code reaches the pipe."""
372
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
373
+
374
+
375
+ @pytest.mark.parametrize("each_command", ALL_REDIRECTED_CLOSER_PYTEST_COMMANDS)
376
+ def test_denies_a_compound_whose_closer_carries_a_redirection(each_command: str) -> None:
377
+ """A redirection binds to the compound, so the closer before it still closes it."""
378
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
379
+
380
+
381
+ def test_allows_a_module_flag_that_belongs_to_a_script_operand() -> None:
382
+ """A script path ends the interpreter's options, so its own ``-m pytest`` runs nothing."""
383
+ assert find_piped_pytest_violation(SCRIPT_OPERAND_CARRYING_A_MODULE_FLAG) is None
384
+
385
+
386
+ @pytest.mark.parametrize("each_command", ALL_INTERPRETER_MODULE_RUN_PYTEST_COMMANDS)
387
+ def test_denies_an_interpreter_module_run_of_pytest(each_command: str) -> None:
388
+ """An interpreter's own ``-m pytest`` still denies, glued or behind a valued option."""
389
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
390
+
391
+
392
+ def test_denies_a_pipe_carried_only_inside_a_wrapper_command_string() -> None:
393
+ """The pipe prefilter reads the whole command, so a wrapper's inner pipe still counts."""
394
+ assert find_piped_pytest_violation(WRAPPER_CARRYING_THE_ONLY_PIPE) == CORRECTIVE_MESSAGE
395
+
396
+
397
+ @pytest.mark.parametrize("each_command", ALL_FLAGGED_WRAPPER_PYTEST_COMMANDS)
398
+ def test_denies_a_pytest_run_behind_a_wrapper_carrying_its_own_flags(each_command: str) -> None:
399
+ """A wrapper's own flags sit before the program, so the run behind them still counts."""
400
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
401
+
402
+
403
+ @pytest.mark.parametrize("each_command", ALL_CLUSTERED_SHORT_OPTION_PYTEST_COMMANDS)
404
+ def test_denies_a_pytest_run_behind_a_clustered_short_option_wrapper(each_command: str) -> None:
405
+ """A cluster ending in a value-taking flag takes the token after it, not the program."""
406
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
407
+
408
+
409
+ @pytest.mark.parametrize("each_command", ALL_SHELL_OPTION_VALUE_PYTEST_COMMANDS)
410
+ def test_denies_a_wrapped_pytest_run_behind_a_shell_option_carrying_a_value(
411
+ each_command: str,
412
+ ) -> None:
413
+ """A shell option's value is not the operand, so the string-exec flag behind it counts."""
414
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
415
+
416
+
417
+ @pytest.mark.parametrize("each_command", ALL_CLUSTERED_STRING_EXEC_PYTEST_COMMANDS)
418
+ def test_denies_a_wrapped_pytest_run_behind_a_clustered_string_exec_flag(
419
+ each_command: str,
420
+ ) -> None:
421
+ """A POSIX shell reads the ``-c`` inside ``-euc``, so the string behind it is a command."""
422
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
423
+
424
+
425
+ @pytest.mark.parametrize("each_command", ALL_COVERAGE_RUN_PYTEST_COMMANDS)
426
+ def test_denies_a_pytest_run_behind_a_coverage_run_wrapper(each_command: str) -> None:
427
+ """``coverage run`` exits with the program's code, so the pytest behind it counts."""
428
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
429
+
430
+
431
+ @pytest.mark.parametrize("each_command", ALL_TOOL_RUNNER_WRAPPER_PYTEST_COMMANDS)
432
+ def test_denies_a_pytest_run_behind_a_tool_runner_wrapper(each_command: str) -> None:
433
+ """Each tool runner exits with the program's code, so the pytest behind it counts."""
434
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
435
+
436
+
437
+ @pytest.mark.parametrize("each_command", ALL_ALTERNATE_INTERPRETER_PYTEST_COMMANDS)
438
+ def test_denies_a_pytest_module_run_under_an_alternate_interpreter(each_command: str) -> None:
439
+ """A free-threaded build and PyPy run the module the same way CPython does."""
440
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
441
+
442
+
443
+ def test_allows_a_heredoc_whose_body_line_is_its_terminator_with_surrounding_spaces() -> None:
444
+ """Bash closes a ``<<`` heredoc on the exact word only, so a spaced lookalike is body text."""
445
+ assert find_piped_pytest_violation(HEREDOC_BODY_LINE_SPACED_LIKE_ITS_TERMINATOR) is None
446
+
447
+
448
+ def test_denies_a_live_run_below_a_heredoc_a_spaced_lookalike_did_not_close() -> None:
449
+ """Scanning resumes at the exact terminator, so the run under it is live and denies."""
450
+ assert (
451
+ find_piped_pytest_violation(LIVE_RUN_BELOW_A_SPACED_TERMINATOR_LOOKALIKE)
452
+ == CORRECTIVE_MESSAGE
453
+ )
454
+
455
+
456
+ def test_denies_a_live_run_below_a_tab_indented_terminator_that_closes_its_heredoc() -> None:
457
+ """``<<-`` strips leading tabs, so a tab-indented terminator still closes the body."""
458
+ assert (
459
+ find_piped_pytest_violation(LIVE_RUN_BELOW_A_TAB_INDENTED_TERMINATOR) == CORRECTIVE_MESSAGE
460
+ )
461
+
462
+
463
+ def test_denies_a_piped_pytest_run_placed_below_a_here_string() -> None:
464
+ """``<<<`` feeds one line rather than opening a body, so the lines under it run."""
465
+ assert find_piped_pytest_violation(HERE_STRING_ABOVE_A_PIPED_PYTEST_RUN) == CORRECTIVE_MESSAGE
466
+
467
+
468
+ @pytest.mark.parametrize("each_command", ALL_WINDOWS_SHIM_PYTEST_COMMANDS)
469
+ def test_denies_a_windows_shim_spelling_of_a_pytest_run(each_command: str) -> None:
470
+ """A ``.bat`` or ``.cmd`` shim runs the same suite as the bare spelling."""
471
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
472
+
473
+
474
+ @pytest.mark.parametrize("each_command", ALL_PASS_THROUGH_WRAPPER_PYTEST_COMMANDS)
475
+ def test_denies_a_pytest_run_behind_a_pass_through_wrapper(each_command: str) -> None:
476
+ """A wrapper exits with the program's code, so the pytest behind it still counts."""
477
+ assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
478
+
479
+
480
+ def test_denies_a_piped_pytest_run_placed_after_a_heredoc_body() -> None:
481
+ """Scanning resumes at the terminator, so a live pipe below it still denies."""
482
+ assert find_piped_pytest_violation(COMMAND_AFTER_A_HEREDOC) == CORRECTIVE_MESSAGE
483
+
484
+
485
+ def test_denies_a_subshell_opened_and_closed_on_different_lines() -> None:
486
+ """A parenthesis group spanning lines is one command, so the pipe after it denies."""
487
+ assert find_piped_pytest_violation(MULTI_LINE_SUBSHELL_PYTEST_COMMAND) == CORRECTIVE_MESSAGE
488
+
489
+
490
+ def test_allows_a_subshell_whose_heredoc_body_carries_a_piped_pytest_line() -> None:
491
+ """Body lines inside a grouped heredoc stay script text, so the group writes only."""
492
+ assert find_piped_pytest_violation(SUBSHELL_WRITING_A_PIPED_PYTEST_HEREDOC) is None
493
+
494
+
495
+ def test_denies_a_subshell_whose_own_line_is_a_comment() -> None:
496
+ """A comment line inside a group ends at its own newline, so the pipe after it denies."""
497
+ assert find_piped_pytest_violation(SUBSHELL_CARRYING_A_COMMENT_LINE) == CORRECTIVE_MESSAGE
498
+
499
+
500
+ def test_denies_a_subshell_whose_pytest_line_ends_in_a_comment() -> None:
501
+ """A trailing comment hides only the rest of its own line, not the lines below it."""
502
+ assert find_piped_pytest_violation(SUBSHELL_CARRYING_A_TRAILING_COMMENT) == CORRECTIVE_MESSAGE
503
+
504
+
505
+ def test_denies_a_piped_pytest_line_below_a_comment_holding_an_open_paren() -> None:
506
+ """A parenthesis inside a comment opens no group, so the next line still denies."""
507
+ assert find_piped_pytest_violation(COMMENT_CARRYING_A_STRAY_OPEN_PAREN) == CORRECTIVE_MESSAGE
508
+
509
+
510
+ def test_denies_a_piped_pytest_line_below_a_comment_naming_a_heredoc_opener() -> None:
511
+ """A ``<<WORD`` inside a comment opens no heredoc, so the line below stays live."""
512
+ assert find_piped_pytest_violation(COMMENT_NAMING_A_HEREDOC_OPENER) == CORRECTIVE_MESSAGE
513
+
514
+
515
+ def test_denies_a_piped_pytest_line_below_a_trailing_comment_naming_a_heredoc_opener() -> None:
516
+ """A ``<<WORD`` in a line's trailing comment opens no heredoc either."""
517
+ assert (
518
+ find_piped_pytest_violation(TRAILING_COMMENT_NAMING_A_HEREDOC_OPENER) == CORRECTIVE_MESSAGE
519
+ )
520
+
521
+
522
+ @pytest.mark.parametrize("each_terminator", ALL_LINE_CONTINUATION_TERMINATORS)
523
+ def test_denies_a_continued_pytest_line_for_every_terminator(each_terminator: str) -> None:
524
+ """A backslash continuation reads as one logical line for each line ending."""
525
+ continued_command = f"python -m pytest tests \\{each_terminator}| tee run.log"
526
+ assert find_piped_pytest_violation(continued_command) == CORRECTIVE_MESSAGE
527
+
528
+
529
+ def test_quoted_pipe_reads_as_unpiped_here_and_as_a_cut_by_the_shared_splitter() -> None:
530
+ """One input, both readings: this hook allows it, the shared splitter cuts it.
531
+
532
+ The shared splitter's segments for the input are pinned, so a quote-aware
533
+ ``shell_command_segments`` turns this test red — the signal to move this
534
+ module onto the shared helper.
535
+ """
536
+ assert find_piped_pytest_violation(QUOTED_PIPE_COMMAND) is None
537
+ assert (
538
+ split_into_segments(shlex.split(QUOTED_PIPE_COMMAND))
539
+ == SHARED_SPLITTER_SEGMENTS_FOR_QUOTED_PIPE
540
+ )
541
+
542
+
543
+ def test_segment_leading_with_pytest_is_recognized() -> None:
544
+ """A segment whose program is pytest reads as a pytest run."""
545
+ assert segment_runs_pytest(["pytest", "tests", "-q"]) is True
546
+
547
+
548
+ def test_segment_running_another_python_module_is_not_pytest() -> None:
549
+ """An interpreter running a different module reads as no pytest run."""
550
+ assert segment_runs_pytest(["python", "-m", "mypy", "."]) is False
551
+
552
+
553
+ def test_hook_emits_a_deny_payload_for_a_piped_pytest_run() -> None:
554
+ """The standalone hook writes the deny message for a piped pytest command."""
555
+ completed = _run_process(SCRIPT_PATH, _bash_payload("pytest tests | tee run.log"))
556
+ assert _deny_reason_from_stdout(completed.stdout) == CORRECTIVE_MESSAGE
557
+
558
+
559
+ def test_deny_message_names_running_pytest_alone_and_the_exit_code() -> None:
560
+ """The deny message states the fix and why the pipe hides a failure."""
561
+ completed = _run_process(SCRIPT_PATH, _bash_payload("pytest | tee run.log"))
562
+ deny_reason = _deny_reason_from_stdout(completed.stdout)
563
+ assert "run pytest alone" in deny_reason.lower()
564
+ assert "exit code" in deny_reason.lower()
565
+
566
+
567
+ def test_hook_stays_silent_on_a_bare_pytest_run() -> None:
568
+ """A pytest command with no pipe produces no decision."""
569
+ completed = _run_process(SCRIPT_PATH, _bash_payload("pytest tests"))
570
+ assert completed.stdout == ""
571
+
572
+
573
+ def test_ignores_an_unsupported_tool_name() -> None:
574
+ """A non-Bash tool payload produces no decision."""
575
+ payload = {"tool_name": "Edit", "tool_input": {"command": "pytest | tee run.log"}}
576
+ assert _run_process(SCRIPT_PATH, payload).stdout == ""
577
+
578
+
579
+ def test_ignores_a_payload_without_a_command() -> None:
580
+ """A Bash payload carrying no command produces no decision."""
581
+ assert _run_process(SCRIPT_PATH, {"tool_name": "Bash", "tool_input": {}}).stdout == ""
582
+
583
+
584
+ def test_dispatcher_denies_a_piped_pytest_run() -> None:
585
+ """The hosted-hook chain surfaces the deny, so the roster carries the segment."""
586
+ completed = _run_process(DISPATCHER_PATH, _bash_payload("pytest tests | tee run.log"))
587
+ assert "exit code" in _deny_reason_from_stdout(completed.stdout).lower()