claude-dev-env 2.7.1 → 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.
- package/_shared/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +309 -12
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/scripts/CLAUDE.md +1 -1
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +28 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/spawn_grok_batch.py +38 -1
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +101 -0
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +9 -8
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/team-advisor/SKILL.md +4 -1
package/hooks/hooks.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
10
|
"command": "python3 -c \"import sys; sys.path.insert(0, r'${CLAUDE_PLUGIN_ROOT}/hooks'); from validators.run_all_validators import main; sys.exit(main())\" --pre-tool-use",
|
|
11
|
-
"timeout":
|
|
11
|
+
"timeout": 30
|
|
12
12
|
}
|
|
13
13
|
]
|
|
14
14
|
},
|
|
@@ -22,21 +22,6 @@
|
|
|
22
22
|
}
|
|
23
23
|
]
|
|
24
24
|
},
|
|
25
|
-
{
|
|
26
|
-
"matcher": "Edit",
|
|
27
|
-
"hooks": [
|
|
28
|
-
{
|
|
29
|
-
"type": "command",
|
|
30
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/advisory/refactor_guard.py",
|
|
31
|
-
"timeout": 15
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"type": "command",
|
|
35
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/advisory/migration_safety_advisor.py",
|
|
36
|
-
"timeout": 15
|
|
37
|
-
}
|
|
38
|
-
]
|
|
39
|
-
},
|
|
40
25
|
{
|
|
41
26
|
"matcher": "Bash|PowerShell",
|
|
42
27
|
"hooks": [
|
|
@@ -99,6 +84,11 @@
|
|
|
99
84
|
"type": "command",
|
|
100
85
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_verifier_spawn_preflight_gate.py",
|
|
101
86
|
"timeout": 30
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"type": "command",
|
|
90
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/fable_spawn_gate.py",
|
|
91
|
+
"timeout": 10
|
|
102
92
|
}
|
|
103
93
|
]
|
|
104
94
|
},
|
|
@@ -248,7 +238,7 @@
|
|
|
248
238
|
{
|
|
249
239
|
"type": "command",
|
|
250
240
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/observability/session_file_edit_tracker.py",
|
|
251
|
-
"timeout":
|
|
241
|
+
"timeout": 30
|
|
252
242
|
}
|
|
253
243
|
]
|
|
254
244
|
},
|
|
@@ -258,7 +248,7 @@
|
|
|
258
248
|
{
|
|
259
249
|
"type": "command",
|
|
260
250
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/investigation_tracker_reset.py",
|
|
261
|
-
"timeout":
|
|
251
|
+
"timeout": 30
|
|
262
252
|
}
|
|
263
253
|
]
|
|
264
254
|
},
|
|
@@ -268,7 +258,7 @@
|
|
|
268
258
|
{
|
|
269
259
|
"type": "command",
|
|
270
260
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/gh_pr_author_restore.py",
|
|
271
|
-
"timeout":
|
|
261
|
+
"timeout": 60
|
|
272
262
|
}
|
|
273
263
|
]
|
|
274
264
|
}
|
|
@@ -32,6 +32,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
32
32
|
| `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
|
|
33
33
|
| `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
|
|
34
34
|
| `eli11_reply_enforcer_constants.py` | Reply-shape thresholds (word cap, enforced word floor, bullet cap, lead-line window, per-line word cap, over-packed line cap), the `Long form:` escape prefix, the bullet / numbered-step / table-row / link-target / word patterns, the imperative instruction verbs, and the user-facing notice for the ELI11 reply-shape Stop hook |
|
|
35
|
+
| `fable_spawn_gate_constants.py` | The spawn tool-name set, the fable model alias and the model-id segment split pattern, the `FABLE-SPAWN-AUTHORIZED` authorization token, the advisor-protocol document path, payload field names, the deny message, and the deny `additionalContext` recovery text for the fable-tier spawn gate |
|
|
35
36
|
| `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
|
|
36
37
|
| `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
|
|
37
38
|
| `harness_scratchpad_constants.py` | Fixed path components (`claude` / `claude-` user-directory name and prefix, `scratchpad` leaf name), the PreToolUse session-id payload key, and the `CLAUDE_CODE_SESSION_ID` environment variable name that the code-rules and TDD gates use to recognize an existing harness session scratchpad directory |
|
|
@@ -53,6 +54,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
53
54
|
| `paired_test_coverage_constants.py` | Test-directory name, stem-test filename affixes, test-file globs, exempt public-function names, scan budget, coverage threshold, and guidance text for the public-function paired-test coverage check |
|
|
54
55
|
| `path_rewriter_constants.py` | Path rewriting patterns for the Everything-search path rewriter |
|
|
55
56
|
| `pii_prevention_constants.py` | Patterns, allowlists, path exemptions, and deny-message text for `pii_prevention_blocker` |
|
|
57
|
+
| `piped_pytest_blocker_constants.py` | Bash tool name, pytest program basenames and the python-interpreter basename pattern, the pipe and segment-reset operator token sets, and the deny message for the piped-pytest blocker |
|
|
56
58
|
| `plain_language_blocker_constants.py` | The list of heavy words and their everyday replacements |
|
|
57
59
|
| `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
|
|
58
60
|
| `pr_converge_bugteam_enforcer_state.py` | State-file helpers for the bugteam enforcer |
|
|
@@ -57,6 +57,7 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
|
|
|
57
57
|
BashHostedHookEntry("blocking/destructive_command_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
58
58
|
BashHostedHookEntry("blocking/gh_body_arg_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
59
59
|
BashHostedHookEntry("blocking/shell_substitution_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
60
|
+
BashHostedHookEntry("blocking/piped_pytest_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
60
61
|
BashHostedHookEntry(
|
|
61
62
|
"blocking/unscoped_search_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
62
63
|
),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Constants for the fable-tier subagent spawn gate.
|
|
2
|
+
|
|
3
|
+
The gate denies an ``Agent`` or ``Task`` spawn at the fable tier whose
|
|
4
|
+
``prompt`` lacks ``FABLE_SPAWN_AUTHORIZATION_MARKER``. ``DENY_REASON`` points
|
|
5
|
+
at the protocol document rather than quoting the token, so pasting a denial
|
|
6
|
+
into a retry prompt authorizes nothing.
|
|
7
|
+
``MODEL_SEGMENT_SPLIT_PATTERN`` finds that tier in a full model id too::
|
|
8
|
+
|
|
9
|
+
fable -> ['fable'] flag: fable tier
|
|
10
|
+
claude-fable-5 -> ['claude', 'fable', '5'] flag: fable tier
|
|
11
|
+
claude-sonnet-4 -> ['claude', 'sonnet', '4'] ok: another tier
|
|
12
|
+
|
|
13
|
+
Every literal the gate body reads lives here. ``AGENT_TOOL_NAME`` and
|
|
14
|
+
``TASK_TOOL_NAME`` come from the sibling spawn-gate constants, so the spawn
|
|
15
|
+
gates share one source for both tool names.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
from hooks_constants.code_verifier_spawn_preflight_gate_constants import TASK_TOOL_NAME
|
|
23
|
+
from hooks_constants.pr_converge_bugteam_enforcer_constants import AGENT_TOOL_NAME
|
|
24
|
+
|
|
25
|
+
ALL_SPAWN_TOOL_NAMES: frozenset[str] = frozenset({AGENT_TOOL_NAME, TASK_TOOL_NAME})
|
|
26
|
+
|
|
27
|
+
FABLE_MODEL_ALIAS: str = "fable"
|
|
28
|
+
MODEL_SEGMENT_SPLIT_PATTERN: re.Pattern[str] = re.compile(r"[^a-z0-9]+")
|
|
29
|
+
FABLE_SPAWN_AUTHORIZATION_MARKER: str = "FABLE-SPAWN-AUTHORIZED"
|
|
30
|
+
ADVISOR_PROTOCOL_DOCUMENT_PATH: str = "_shared/advisor/advisor-protocol.md"
|
|
31
|
+
|
|
32
|
+
TOOL_NAME_FIELD_NAME: str = "tool_name"
|
|
33
|
+
TOOL_INPUT_FIELD_NAME: str = "tool_input"
|
|
34
|
+
MODEL_FIELD_NAME: str = "model"
|
|
35
|
+
PROMPT_FIELD_NAME: str = "prompt"
|
|
36
|
+
|
|
37
|
+
CALLING_HOOK_NAME: str = "fable_spawn_gate.py"
|
|
38
|
+
HOOK_EVENT_NAME: str = "PreToolUse"
|
|
39
|
+
|
|
40
|
+
DENY_REASON: str = (
|
|
41
|
+
"BLOCKED [fable-spawn-gate]: this subagent spawn names the fable tier in "
|
|
42
|
+
"its model field and carries no authorization marker. To authorize a "
|
|
43
|
+
f"fable bind, put the token named in {ADVISOR_PROTOCOL_DOCUMENT_PATH} as "
|
|
44
|
+
"plain text in the spawn prompt (substring match). Otherwise set model "
|
|
45
|
+
"to opus, sonnet, or haiku. The gate checks only model tier and marker "
|
|
46
|
+
"presence — it does not verify which session placed the token."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
DENY_ADDITIONAL_CONTEXT: str = (
|
|
50
|
+
"[fable-spawn-gate] This Agent/Task spawn was denied: the model field "
|
|
51
|
+
"names the fable tier and the spawn prompt carries no authorization "
|
|
52
|
+
"marker. Preferred recovery: re-spawn at model opus, or an "
|
|
53
|
+
"opus-equivalent tier (sonnet/haiku when opus is unavailable) — those "
|
|
54
|
+
"tiers need no marker. Authorized fable bind only: put the token named "
|
|
55
|
+
f"in {ADVISOR_PROTOCOL_DOCUMENT_PATH} as plain text in the spawn prompt "
|
|
56
|
+
"(substring match), then retry. The gate checks only model tier and "
|
|
57
|
+
"marker presence. Do not paste this denial into a retry prompt — it "
|
|
58
|
+
"does not authorize the spawn."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
DENY_PREVIEW_TEMPLATE: str = "model={model_text} marker_present=False"
|
|
62
|
+
MAXIMUM_PREVIEW_MODEL_LENGTH: int = 40
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Constants for the piped-pytest PreToolUse Bash blocker.
|
|
2
|
+
|
|
3
|
+
Holds the tool names, the pytest program basenames and the python-interpreter
|
|
4
|
+
basename pattern, the module-run flag and module name, the pipe and
|
|
5
|
+
segment-reset operator token sets, the line-continuation and physical-line
|
|
6
|
+
patterns, the heredoc-opener pattern that marks a script body, the
|
|
7
|
+
comment-start pattern that ends a line early, the parenthesis-group counters
|
|
8
|
+
that join a multi-line subshell, the group-closing reserved words a pipe reads
|
|
9
|
+
a group's status through, the body-introducing reserved words that stand ahead
|
|
10
|
+
of the command whose status a compound reports, the single characters the hot
|
|
11
|
+
path tests before it parses, the option-token pattern that separates a wrapper's
|
|
12
|
+
flags from its first operand, the wrapper commands whose run passes through to
|
|
13
|
+
the program behind them, the separator that rejoins a wrapper's argument
|
|
14
|
+
tokens, the empty commenter set that leaves ``#`` to the comment-start pattern,
|
|
15
|
+
the quote characters stripped before a basename read, and the deny message.
|
|
16
|
+
Segment helpers come from ``shell_command_segments.py``.
|
|
17
|
+
|
|
18
|
+
Two wrapper shapes reach the program behind them differently. ``sudo`` and
|
|
19
|
+
``uvx`` take their own option flags and then the command, so the step-over drops
|
|
20
|
+
the flags. ``uv``, ``poetry``, ``pipenv``, ``pdm``, ``hatch``, ``rye``, and
|
|
21
|
+
``coverage`` take the literal ``run`` subcommand first, so the step-over reads
|
|
22
|
+
that word and passes only when it is there — ``uv sync`` and ``coverage report``
|
|
23
|
+
keep the wrapper as its own program. ``uv`` spells the same pass-through as
|
|
24
|
+
``uv tool run`` too, so ``TOOL_SUBCOMMAND_NAME`` names the word the step-over
|
|
25
|
+
reads before it looks for ``run``. A ``run`` subcommand takes its own flags too,
|
|
26
|
+
so the step-over drops those before it reads the program.
|
|
27
|
+
|
|
28
|
+
An option that takes a separate value swallows the token after it, so
|
|
29
|
+
``sudo -u someone pytest`` runs pytest while ``uv run --with pytest mypy .``
|
|
30
|
+
runs mypy. ``ALL_VALUE_TAKING_WRAPPER_OPTION_FLAGS`` names those flags so the
|
|
31
|
+
operand scan reads past the value rather than mistaking it for the program, and
|
|
32
|
+
``ALL_VALUE_TAKING_SHELL_OPTION_FLAGS`` does the same for the shells that run a
|
|
33
|
+
command string, so the ``-c`` behind ``bash -o pipefail`` stays visible. POSIX
|
|
34
|
+
lets short options cluster, and a cluster's value is the next token only when
|
|
35
|
+
the value-taking letter ends the cluster — ``sudo -nu ci`` takes ``ci`` while
|
|
36
|
+
``sudo -nuci`` carries the value glued on. ``SHORT_OPTION_CLUSTER_PATTERN``
|
|
37
|
+
marks the tokens that walk letter by letter for that reading.
|
|
38
|
+
``--`` ends an option list outright: every token after it is an operand however
|
|
39
|
+
it is spelled, so ``bash -- -c script`` runs a script named ``-c`` rather than a
|
|
40
|
+
command string.
|
|
41
|
+
|
|
42
|
+
A POSIX shell reads ``-c`` inside a cluster as well, so ``bash -euc 'pytest'``
|
|
43
|
+
runs the command string the same way ``bash -c 'pytest'`` does.
|
|
44
|
+
``ALL_CLUSTERED_STRING_EXEC_OPTION_LETTERS`` names the letters that carry that
|
|
45
|
+
reading, and ``ALL_SHORT_OPTION_CLUSTERING_SHELL_BASENAMES`` limits it to the
|
|
46
|
+
shells that cluster: PowerShell spells its options as words, where the ``c`` in
|
|
47
|
+
``-NonInteractive`` is a letter of a name rather than an option of its own. The
|
|
48
|
+
letter check reads the token in its own case, because ``-C`` is bash's noclobber
|
|
49
|
+
switch while ``-c`` takes the command string.
|
|
50
|
+
|
|
51
|
+
A shell ends a pipeable compound command with a reserved word as well as with
|
|
52
|
+
``)`` or ``}``: ``fi``, ``done``, and ``esac``. ``ALL_GROUP_CLOSE_TOKENS`` names
|
|
53
|
+
all five, so a pipe after one steps back to the command whose exit status the
|
|
54
|
+
compound reports. The body of such a compound opens with a reserved word of its
|
|
55
|
+
own — ``then``, ``do``, or ``else`` — which stands ahead of the command rather
|
|
56
|
+
than being part of it, so ``ALL_COMPOUND_BODY_INTRODUCER_TOKENS`` names the
|
|
57
|
+
words a status read drops off the front. Both sets match a whole token only, so
|
|
58
|
+
``ls done`` and ``echo fi`` name a path and a word rather than a keyword.
|
|
59
|
+
|
|
60
|
+
Such a compound reports the status of whichever branch ran, not of its last
|
|
61
|
+
branch alone, so ``if x; then pytest; else echo skip; fi`` exits with pytest's
|
|
62
|
+
code whenever ``x`` succeeds. ``ALL_BRANCH_CONTINUATION_TOKENS`` names ``else``
|
|
63
|
+
and ``elif``, the words that open the next branch and so mark the command
|
|
64
|
+
before them as the one its own branch ends on.
|
|
65
|
+
|
|
66
|
+
A redirection binds to the command it follows rather than being part of it, so
|
|
67
|
+
``fi 2>&1`` and ``done < list`` still close their compound. Reading the closer
|
|
68
|
+
past one means dropping the redirection operator together with the file
|
|
69
|
+
descriptor written ahead of it, which ``FILE_DESCRIPTOR_TOKEN_PATTERN`` marks —
|
|
70
|
+
``2>&1`` lexes as ``2``, ``>&``, ``1``.
|
|
71
|
+
|
|
72
|
+
An interpreter takes options of its own before its first operand, and a few of
|
|
73
|
+
them carry a separate value: ``ALL_VALUE_TAKING_INTERPRETER_OPTION_FLAGS`` names
|
|
74
|
+
those, so ``python -X dev -m pytest`` reads the ``-m`` behind the value while
|
|
75
|
+
``python myscript.py -m pytest`` reads the script as the operand that ends the
|
|
76
|
+
options. The set covers PyPy's ``--jit`` as well as CPython's own, because the
|
|
77
|
+
interpreter pattern takes ``pypy3`` and an unnamed value-taking flag would read
|
|
78
|
+
its value as the operand that ends the options.
|
|
79
|
+
|
|
80
|
+
``PIPE_CHARACTER``, ``COMMENT_START_CHARACTER``, and
|
|
81
|
+
``HEREDOC_OPENER_OPERATOR`` are the substrings the hot path tests before it does
|
|
82
|
+
the matching parse. Every pipe operator spelling — ``|`` and ``|&`` — carries
|
|
83
|
+
``PIPE_CHARACTER``, and no transform in the blocker inserts one, so a command
|
|
84
|
+
without it can hold no violation.
|
|
85
|
+
|
|
86
|
+
A heredoc delimiter is any shell word, so the opener pattern takes one written
|
|
87
|
+
bare, quoted, or backslash-escaped (``<<\\EOF`` quotes the delimiter the way
|
|
88
|
+
``<<'EOF'`` does). ``<<<`` is a here-string rather than a heredoc, so the
|
|
89
|
+
pattern refuses to read one as an opener and leave the lines below it unread.
|
|
90
|
+
The opener also reports its own ``-``, because ``<<`` closes on a line spelling
|
|
91
|
+
the delimiter exactly while ``<<-`` closes on one carrying leading tabs.
|
|
92
|
+
|
|
93
|
+
The string-executing shell basenames and command flags start from the shared
|
|
94
|
+
``unscoped_search_blocker_constants.py`` sets and add the Windows command shell
|
|
95
|
+
(``cmd /c "…"``) on top, locally. Widening the shared sets themselves would hand
|
|
96
|
+
``cmd`` unwrapping to ``unscoped_search_blocker`` as a side effect of a
|
|
97
|
+
piped-pytest fix, so the addition stays in this module and that blocker keeps
|
|
98
|
+
its current behavior.
|
|
99
|
+
|
|
100
|
+
The operator sets partition ``ALL_SHELL_CONTROL_OPERATOR_TOKENS``. That
|
|
101
|
+
derivation covers the operator inventory only; quote-awareness stays local to
|
|
102
|
+
the blocker's own lexer.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
from __future__ import annotations
|
|
106
|
+
|
|
107
|
+
import re
|
|
108
|
+
|
|
109
|
+
from hooks_constants.shell_command_segments import ALL_SHELL_CONTROL_OPERATOR_TOKENS
|
|
110
|
+
from hooks_constants.unscoped_search_blocker_constants import (
|
|
111
|
+
ALL_STRING_EXEC_COMMAND_FLAGS as _ALL_SHARED_STRING_EXEC_COMMAND_FLAGS,
|
|
112
|
+
)
|
|
113
|
+
from hooks_constants.unscoped_search_blocker_constants import (
|
|
114
|
+
ALL_STRING_EXECUTING_SHELL_BASENAMES as _ALL_SHARED_STRING_EXECUTING_SHELL_BASENAMES,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"BASH_TOOL_NAME",
|
|
119
|
+
"ALL_SUPPORTED_TOOL_NAMES",
|
|
120
|
+
"ALL_PYTEST_PROGRAM_BASENAMES",
|
|
121
|
+
"PYTHON_INTERPRETER_BASENAME_PATTERN",
|
|
122
|
+
"MODULE_RUN_FLAG",
|
|
123
|
+
"PYTEST_MODULE_NAME",
|
|
124
|
+
"ALL_PIPE_OPERATOR_TOKENS",
|
|
125
|
+
"ALL_SEGMENT_RESET_OPERATOR_TOKENS",
|
|
126
|
+
"ALL_OPERATOR_TOKENS_LONGEST_FIRST",
|
|
127
|
+
"PUNCTUATION_ONLY_TOKEN_PATTERN",
|
|
128
|
+
"ALL_REDIRECTION_SUFFIX_CHARACTERS",
|
|
129
|
+
"ALL_STRING_EXECUTING_SHELL_BASENAMES",
|
|
130
|
+
"ALL_STRING_EXEC_COMMAND_FLAGS",
|
|
131
|
+
"ALL_CLUSTERED_STRING_EXEC_OPTION_LETTERS",
|
|
132
|
+
"ALL_SHORT_OPTION_CLUSTERING_SHELL_BASENAMES",
|
|
133
|
+
"COMMAND_OPTION_TOKEN_PATTERN",
|
|
134
|
+
"SHORT_OPTION_CLUSTER_PATTERN",
|
|
135
|
+
"SHORT_OPTION_PREFIX",
|
|
136
|
+
"END_OF_OPTIONS_TOKEN",
|
|
137
|
+
"ALL_VALUE_TAKING_WRAPPER_OPTION_FLAGS",
|
|
138
|
+
"ALL_VALUE_TAKING_SHELL_OPTION_FLAGS",
|
|
139
|
+
"ALL_VALUE_TAKING_INTERPRETER_OPTION_FLAGS",
|
|
140
|
+
"ALL_FLAG_TAKING_WRAPPER_COMMANDS",
|
|
141
|
+
"ALL_RUN_SUBCOMMAND_WRAPPER_COMMANDS",
|
|
142
|
+
"RUN_SUBCOMMAND_NAME",
|
|
143
|
+
"TOOL_SUBCOMMAND_NAME",
|
|
144
|
+
"WRAPPED_COMMAND_TOKEN_JOIN",
|
|
145
|
+
"DISABLED_LEXER_COMMENTERS",
|
|
146
|
+
"LINE_CONTINUATION_PATTERN",
|
|
147
|
+
"LINE_CONTINUATION_JOIN",
|
|
148
|
+
"COMMAND_LINE_SPLIT_PATTERN",
|
|
149
|
+
"QUOTED_REGION_PATTERN",
|
|
150
|
+
"QUOTED_REGION_REPLACEMENT",
|
|
151
|
+
"COMMENT_START_SCAN_PATTERN",
|
|
152
|
+
"COMMENT_START_GROUP",
|
|
153
|
+
"PIPE_CHARACTER",
|
|
154
|
+
"COMMENT_START_CHARACTER",
|
|
155
|
+
"HEREDOC_OPENER_OPERATOR",
|
|
156
|
+
"GROUP_OPEN_CHARACTER",
|
|
157
|
+
"GROUP_CLOSE_CHARACTER",
|
|
158
|
+
"ALL_GROUP_CLOSE_TOKENS",
|
|
159
|
+
"ALL_COMPOUND_BODY_INTRODUCER_TOKENS",
|
|
160
|
+
"ALL_BRANCH_CONTINUATION_TOKENS",
|
|
161
|
+
"FILE_DESCRIPTOR_TOKEN_PATTERN",
|
|
162
|
+
"PAREN_GROUP_LINE_JOIN",
|
|
163
|
+
"CLOSED_GROUP_DEPTH",
|
|
164
|
+
"HEREDOC_OPENER_PATTERN",
|
|
165
|
+
"HEREDOC_TAB_STRIP_GROUP",
|
|
166
|
+
"HEREDOC_QUOTE_GROUP",
|
|
167
|
+
"HEREDOC_TERMINATOR_GROUP",
|
|
168
|
+
"HEREDOC_TAB_STRIP_MARKER",
|
|
169
|
+
"HEREDOC_STRIPPED_INDENT_CHARACTERS",
|
|
170
|
+
"NO_FOLLOWING_OPERATOR",
|
|
171
|
+
"ALL_QUOTE_CHARACTERS",
|
|
172
|
+
"HOOK_EVENT_NAME",
|
|
173
|
+
"DENY_DECISION",
|
|
174
|
+
"CALLING_HOOK_NAME",
|
|
175
|
+
"CORRECTIVE_MESSAGE",
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
BASH_TOOL_NAME = "Bash"
|
|
179
|
+
ALL_SUPPORTED_TOOL_NAMES: frozenset[str] = frozenset({BASH_TOOL_NAME})
|
|
180
|
+
|
|
181
|
+
ALL_PYTEST_PROGRAM_BASENAMES: frozenset[str] = frozenset(
|
|
182
|
+
{
|
|
183
|
+
"pytest",
|
|
184
|
+
"pytest.exe",
|
|
185
|
+
"pytest.bat",
|
|
186
|
+
"pytest.cmd",
|
|
187
|
+
"py.test",
|
|
188
|
+
"py.test.exe",
|
|
189
|
+
"py.test.bat",
|
|
190
|
+
"py.test.cmd",
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
PYTHON_INTERPRETER_BASENAME_PATTERN = re.compile(
|
|
194
|
+
r"^(?:(?:python|pypy)w?[0-9._]*t?|pyw?)(?:\.exe|\.bat|\.cmd)?$"
|
|
195
|
+
)
|
|
196
|
+
MODULE_RUN_FLAG = "-m"
|
|
197
|
+
PYTEST_MODULE_NAME = "pytest"
|
|
198
|
+
|
|
199
|
+
_ALL_PIPE_OPERATOR_SPELLINGS: frozenset[str] = frozenset({"|", "|&"})
|
|
200
|
+
_ALL_LOCAL_SEGMENT_RESET_EXTRAS: frozenset[str] = frozenset({";;", "(", "{"})
|
|
201
|
+
|
|
202
|
+
ALL_PIPE_OPERATOR_TOKENS: frozenset[str] = (
|
|
203
|
+
ALL_SHELL_CONTROL_OPERATOR_TOKENS & _ALL_PIPE_OPERATOR_SPELLINGS
|
|
204
|
+
)
|
|
205
|
+
ALL_SEGMENT_RESET_OPERATOR_TOKENS: frozenset[str] = (
|
|
206
|
+
ALL_SHELL_CONTROL_OPERATOR_TOKENS - ALL_PIPE_OPERATOR_TOKENS
|
|
207
|
+
) | _ALL_LOCAL_SEGMENT_RESET_EXTRAS
|
|
208
|
+
|
|
209
|
+
ALL_OPERATOR_TOKENS_LONGEST_FIRST: tuple[str, ...] = tuple(
|
|
210
|
+
sorted(
|
|
211
|
+
ALL_PIPE_OPERATOR_TOKENS | ALL_SEGMENT_RESET_OPERATOR_TOKENS,
|
|
212
|
+
key=len,
|
|
213
|
+
reverse=True,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
PUNCTUATION_ONLY_TOKEN_PATTERN = re.compile(r"[();<>|&]+")
|
|
217
|
+
ALL_REDIRECTION_SUFFIX_CHARACTERS: tuple[str, ...] = ("<", ">")
|
|
218
|
+
|
|
219
|
+
_ALL_WINDOWS_COMMAND_SHELL_BASENAMES: frozenset[str] = frozenset({"cmd", "cmd.exe"})
|
|
220
|
+
_ALL_WINDOWS_COMMAND_SHELL_FLAGS: frozenset[str] = frozenset({"/c", "/k"})
|
|
221
|
+
ALL_STRING_EXECUTING_SHELL_BASENAMES: frozenset[str] = (
|
|
222
|
+
_ALL_SHARED_STRING_EXECUTING_SHELL_BASENAMES | _ALL_WINDOWS_COMMAND_SHELL_BASENAMES
|
|
223
|
+
)
|
|
224
|
+
ALL_STRING_EXEC_COMMAND_FLAGS: frozenset[str] = (
|
|
225
|
+
_ALL_SHARED_STRING_EXEC_COMMAND_FLAGS | _ALL_WINDOWS_COMMAND_SHELL_FLAGS
|
|
226
|
+
)
|
|
227
|
+
ALL_CLUSTERED_STRING_EXEC_OPTION_LETTERS: frozenset[str] = frozenset({"c"})
|
|
228
|
+
ALL_SHORT_OPTION_CLUSTERING_SHELL_BASENAMES: frozenset[str] = frozenset(
|
|
229
|
+
{"bash", "bash.exe", "sh", "sh.exe"}
|
|
230
|
+
)
|
|
231
|
+
COMMAND_OPTION_TOKEN_PATTERN = re.compile(r"^(?:-.*|/[A-Za-z])$")
|
|
232
|
+
SHORT_OPTION_CLUSTER_PATTERN = re.compile(r"-[A-Za-z]{2,}")
|
|
233
|
+
SHORT_OPTION_PREFIX = "-"
|
|
234
|
+
END_OF_OPTIONS_TOKEN = "--"
|
|
235
|
+
ALL_VALUE_TAKING_WRAPPER_OPTION_FLAGS: frozenset[str] = frozenset(
|
|
236
|
+
{
|
|
237
|
+
"-C",
|
|
238
|
+
"-D",
|
|
239
|
+
"-R",
|
|
240
|
+
"-T",
|
|
241
|
+
"-U",
|
|
242
|
+
"-g",
|
|
243
|
+
"-h",
|
|
244
|
+
"-p",
|
|
245
|
+
"-r",
|
|
246
|
+
"-t",
|
|
247
|
+
"-u",
|
|
248
|
+
"--concurrency",
|
|
249
|
+
"--context",
|
|
250
|
+
"--data-file",
|
|
251
|
+
"--directory",
|
|
252
|
+
"--env-file",
|
|
253
|
+
"--extra",
|
|
254
|
+
"--from",
|
|
255
|
+
"--group",
|
|
256
|
+
"--include",
|
|
257
|
+
"--omit",
|
|
258
|
+
"--package",
|
|
259
|
+
"--project",
|
|
260
|
+
"--python",
|
|
261
|
+
"--rcfile",
|
|
262
|
+
"--source",
|
|
263
|
+
"--user",
|
|
264
|
+
"--with",
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
ALL_VALUE_TAKING_INTERPRETER_OPTION_FLAGS: frozenset[str] = frozenset(
|
|
268
|
+
{"-Q", "-W", "-X", "--check-hash-based-pycs", "--jit"}
|
|
269
|
+
)
|
|
270
|
+
ALL_VALUE_TAKING_SHELL_OPTION_FLAGS: frozenset[str] = frozenset(
|
|
271
|
+
{
|
|
272
|
+
"-o",
|
|
273
|
+
"--rcfile",
|
|
274
|
+
"--init-file",
|
|
275
|
+
"-configurationname",
|
|
276
|
+
"-custompipename",
|
|
277
|
+
"-executionpolicy",
|
|
278
|
+
"-inputformat",
|
|
279
|
+
"-outputformat",
|
|
280
|
+
"-settingsfile",
|
|
281
|
+
"-version",
|
|
282
|
+
"-windowstyle",
|
|
283
|
+
"-workingdirectory",
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
ALL_FLAG_TAKING_WRAPPER_COMMANDS: frozenset[str] = frozenset(
|
|
288
|
+
{"sudo", "sudo.exe", "uvx", "uvx.exe"}
|
|
289
|
+
)
|
|
290
|
+
ALL_RUN_SUBCOMMAND_WRAPPER_COMMANDS: frozenset[str] = frozenset(
|
|
291
|
+
{
|
|
292
|
+
"uv",
|
|
293
|
+
"uv.exe",
|
|
294
|
+
"poetry",
|
|
295
|
+
"poetry.exe",
|
|
296
|
+
"pipenv",
|
|
297
|
+
"pipenv.exe",
|
|
298
|
+
"pdm",
|
|
299
|
+
"pdm.exe",
|
|
300
|
+
"hatch",
|
|
301
|
+
"hatch.exe",
|
|
302
|
+
"rye",
|
|
303
|
+
"rye.exe",
|
|
304
|
+
"coverage",
|
|
305
|
+
"coverage.exe",
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
RUN_SUBCOMMAND_NAME = "run"
|
|
309
|
+
TOOL_SUBCOMMAND_NAME = "tool"
|
|
310
|
+
WRAPPED_COMMAND_TOKEN_JOIN = " "
|
|
311
|
+
DISABLED_LEXER_COMMENTERS = ""
|
|
312
|
+
|
|
313
|
+
LINE_CONTINUATION_PATTERN = re.compile(r"\\(?:\r\n|[\r\n])")
|
|
314
|
+
LINE_CONTINUATION_JOIN = ""
|
|
315
|
+
COMMAND_LINE_SPLIT_PATTERN = re.compile(r"[\n\r]+")
|
|
316
|
+
QUOTED_REGION_PATTERN = re.compile(r"'[^']*'|\"(?:\\.|[^\"\\])*\"|\\.")
|
|
317
|
+
QUOTED_REGION_REPLACEMENT = ""
|
|
318
|
+
PIPE_CHARACTER = "|"
|
|
319
|
+
COMMENT_START_CHARACTER = "#"
|
|
320
|
+
HEREDOC_OPENER_OPERATOR = "<<"
|
|
321
|
+
COMMENT_START_GROUP = "comment_start"
|
|
322
|
+
COMMENT_START_SCAN_PATTERN = re.compile(
|
|
323
|
+
rf"'[^']*'|\"(?:\\.|[^\"\\])*\"|\\.|(?P<{COMMENT_START_GROUP}>(?<![^\s])#)"
|
|
324
|
+
)
|
|
325
|
+
GROUP_OPEN_CHARACTER = "("
|
|
326
|
+
GROUP_CLOSE_CHARACTER = ")"
|
|
327
|
+
_BRACE_GROUP_CLOSE_TOKEN = "}"
|
|
328
|
+
_ALL_COMPOUND_CLOSE_KEYWORDS: frozenset[str] = frozenset({"fi", "done", "esac"})
|
|
329
|
+
ALL_GROUP_CLOSE_TOKENS: frozenset[str] = (
|
|
330
|
+
frozenset({GROUP_CLOSE_CHARACTER, _BRACE_GROUP_CLOSE_TOKEN}) | _ALL_COMPOUND_CLOSE_KEYWORDS
|
|
331
|
+
)
|
|
332
|
+
ALL_COMPOUND_BODY_INTRODUCER_TOKENS: frozenset[str] = frozenset({"then", "do", "else"})
|
|
333
|
+
ALL_BRANCH_CONTINUATION_TOKENS: frozenset[str] = frozenset({"else", "elif"})
|
|
334
|
+
FILE_DESCRIPTOR_TOKEN_PATTERN = re.compile(r"[0-9]+")
|
|
335
|
+
PAREN_GROUP_LINE_JOIN = " "
|
|
336
|
+
CLOSED_GROUP_DEPTH = 0
|
|
337
|
+
_HEREDOC_DELIMITER_CHARACTER_CLASS = r"[^\s'\"<>|&;()`$\\]"
|
|
338
|
+
HEREDOC_TAB_STRIP_GROUP = "heredoc_tab_strip"
|
|
339
|
+
HEREDOC_QUOTE_GROUP = "heredoc_quote"
|
|
340
|
+
HEREDOC_TERMINATOR_GROUP = "heredoc_terminator"
|
|
341
|
+
HEREDOC_OPENER_PATTERN = re.compile(
|
|
342
|
+
rf"(?<!<)<<(?!<)(?P<{HEREDOC_TAB_STRIP_GROUP}>-?)\s*(?P<{HEREDOC_QUOTE_GROUP}>['\"]?)\\?"
|
|
343
|
+
rf"(?P<{HEREDOC_TERMINATOR_GROUP}>{_HEREDOC_DELIMITER_CHARACTER_CLASS}+)"
|
|
344
|
+
rf"(?P={HEREDOC_QUOTE_GROUP})"
|
|
345
|
+
)
|
|
346
|
+
HEREDOC_TAB_STRIP_MARKER = "-"
|
|
347
|
+
HEREDOC_STRIPPED_INDENT_CHARACTERS = "\t"
|
|
348
|
+
NO_FOLLOWING_OPERATOR = ""
|
|
349
|
+
ALL_QUOTE_CHARACTERS = "\"'"
|
|
350
|
+
|
|
351
|
+
HOOK_EVENT_NAME = "PreToolUse"
|
|
352
|
+
DENY_DECISION = "deny"
|
|
353
|
+
CALLING_HOOK_NAME = "piped_pytest_blocker.py"
|
|
354
|
+
|
|
355
|
+
CORRECTIVE_MESSAGE = (
|
|
356
|
+
"Piped pytest run blocked. Run pytest alone; the pipe makes the tool report "
|
|
357
|
+
"the downstream command's exit code, so a failing suite reads as a pass. "
|
|
358
|
+
"When you need the output on disk, redirect it "
|
|
359
|
+
"(`python -m pytest tests > run.log 2>&1`) and read the file."
|
|
360
|
+
)
|
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
"""Configuration constants for the plain_language_blocker PreToolUse hook.
|
|
1
|
+
"""Configuration constants for the plain_language_blocker PreToolUse hook.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
ALL_CHAT_DETAIL_MARKERS
|
|
6
|
+
ok: "Which gate should run first?"
|
|
7
|
+
flag: "Which gate should run first?\\n- write\\n- commit"
|
|
8
|
+
INLINE_CODE_SPAN_PATTERN
|
|
9
|
+
ok: "Should we pass `--dry-run` here?" -> one word toward the caps
|
|
10
|
+
ALL_LINE_ENDING_REPLACEMENTS
|
|
11
|
+
flag: a blank line spelled "\\r\\n\\r\\n" reads as a paragraph break
|
|
12
|
+
MAXIMUM_OPTION_DESCRIPTION_WORD_COUNT
|
|
13
|
+
ok: "Runs on every write."
|
|
14
|
+
flag: a 30-word paragraph of tradeoffs
|
|
15
|
+
|
|
16
|
+
Holds the heavy words and their everyday replacements, the prose-region
|
|
17
|
+
strippers the word scan runs first, and the lean-question-block tunables: the
|
|
18
|
+
block-level chat-detail markers that belong in chat text, the line-ending
|
|
19
|
+
spellings normalized before those markers run, the inline-code span the length
|
|
20
|
+
counts collapse to one word, and the sentence and word caps a question and an
|
|
21
|
+
option description each answer to.
|
|
22
|
+
"""
|
|
2
23
|
|
|
3
24
|
from __future__ import annotations
|
|
4
25
|
|
|
@@ -298,3 +319,45 @@ USER_FACING_PLAIN_LANGUAGE_NOTICE: str = (
|
|
|
298
319
|
"Plain-language check: replace each flagged formal word with the simpler "
|
|
299
320
|
"word named in the reason."
|
|
300
321
|
)
|
|
322
|
+
|
|
323
|
+
ALL_CHAT_DETAIL_MARKERS: tuple[tuple[re.Pattern[str], str], ...] = (
|
|
324
|
+
(re.compile(r"^[ \t]*```", re.MULTILINE), "a fenced code block"),
|
|
325
|
+
(re.compile(r"^[ \t]*#{1,6}[ \t]+\S", re.MULTILINE), "a heading"),
|
|
326
|
+
(re.compile(r"^[ \t]*\|.*\|[ \t]*$", re.MULTILINE), "a table row"),
|
|
327
|
+
(
|
|
328
|
+
re.compile(r"^[ \t]*(?:[-*+][ \t]+|\d{1,2}[.)][ \t]+)\S", re.MULTILINE),
|
|
329
|
+
"a bullet or numbered list marker",
|
|
330
|
+
),
|
|
331
|
+
(re.compile(r"\n[ \t]*\n"), "more than one paragraph"),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
INLINE_CODE_SPAN_PATTERN: re.Pattern[str] = re.compile(r"`[^`\n]+`")
|
|
335
|
+
INLINE_CODE_PLACEHOLDER: str = "code"
|
|
336
|
+
|
|
337
|
+
ALL_LINE_ENDING_REPLACEMENTS: tuple[tuple[str, str], ...] = (
|
|
338
|
+
("\r\n", "\n"),
|
|
339
|
+
("\r", "\n"),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
SENTENCE_BOUNDARY_PATTERN: re.Pattern[str] = re.compile(r"[.!?]+(?=\s+[A-Z]|\s*$)")
|
|
343
|
+
COUNTABLE_WORD_PATTERN: re.Pattern[str] = re.compile(r"\S*[A-Za-z0-9]\S*")
|
|
344
|
+
|
|
345
|
+
MAXIMUM_QUESTION_SENTENCE_COUNT: int = 2
|
|
346
|
+
MAXIMUM_QUESTION_WORD_COUNT: int = 40
|
|
347
|
+
MAXIMUM_OPTION_DESCRIPTION_SENTENCE_COUNT: int = 1
|
|
348
|
+
MAXIMUM_OPTION_DESCRIPTION_WORD_COUNT: int = 15
|
|
349
|
+
|
|
350
|
+
QUESTION_SURFACE_NAME: str = "the question"
|
|
351
|
+
OPTION_DESCRIPTION_SURFACE_NAME: str = "an option description"
|
|
352
|
+
|
|
353
|
+
LEAN_QUESTION_BLOCK_PREFIX: str = "BLOCKED: [LEAN_QUESTION] Question block carries chat detail -- "
|
|
354
|
+
LEAN_QUESTION_VIOLATION_SEPARATOR: str = "; "
|
|
355
|
+
LEAN_QUESTION_BLOCK_GUIDANCE: str = (
|
|
356
|
+
"AskUserQuestion renders as one plain text block. Print the plan, the counts, "
|
|
357
|
+
"and the tradeoffs in chat text before the call, keep the question block to a "
|
|
358
|
+
"lean question and short options, and reach for an inline visualizer tool when "
|
|
359
|
+
"a choice needs formatting."
|
|
360
|
+
)
|
|
361
|
+
USER_FACING_LEAN_QUESTION_NOTICE: str = (
|
|
362
|
+
"Question-block check: move the detail into chat and ask a short question."
|
|
363
|
+
)
|
|
@@ -162,4 +162,14 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
|
|
|
162
162
|
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
163
163
|
native_module_name=PLAIN_LANGUAGE_BLOCKER_MODULE_NAME,
|
|
164
164
|
),
|
|
165
|
+
HostedHookEntry(
|
|
166
|
+
script_relative_path="advisory/refactor_guard.py",
|
|
167
|
+
applicable_tool_names=frozenset({EDIT_TOOL_NAME}),
|
|
168
|
+
is_blocking=False,
|
|
169
|
+
),
|
|
170
|
+
HostedHookEntry(
|
|
171
|
+
script_relative_path="advisory/migration_safety_advisor.py",
|
|
172
|
+
applicable_tool_names=frozenset({EDIT_TOOL_NAME}),
|
|
173
|
+
is_blocking=False,
|
|
174
|
+
),
|
|
165
175
|
)
|
|
@@ -22,7 +22,7 @@ __all__ = [
|
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
ALL_LAUNCHER_WRAPPER_COMMANDS: frozenset[str] = frozenset(
|
|
25
|
-
{"timeout", "nohup", "nice", "stdbuf", "setsid", "env"}
|
|
25
|
+
{"timeout", "nohup", "nice", "stdbuf", "setsid", "env", "time"}
|
|
26
26
|
)
|
|
27
27
|
ALL_SHELL_CONTROL_OPERATOR_TOKENS: frozenset[str] = frozenset(
|
|
28
28
|
{"&&", "||", ";", "|", "&", "|&"}
|
|
@@ -26,6 +26,7 @@ _EXPECTED_BASH_ORDER = (
|
|
|
26
26
|
"blocking/destructive_command_blocker.py",
|
|
27
27
|
"blocking/gh_body_arg_blocker.py",
|
|
28
28
|
"blocking/shell_substitution_blocker.py",
|
|
29
|
+
"blocking/piped_pytest_blocker.py",
|
|
29
30
|
"blocking/unscoped_search_blocker.py",
|
|
30
31
|
"blocking/nas_ssh_binary_enforcer.py",
|
|
31
32
|
"blocking/volatile_path_in_post_blocker.py",
|
|
@@ -87,6 +87,33 @@ def test_blocking_hook_crash_deny_reason_surfaces_the_constant() -> None:
|
|
|
87
87
|
assert BLOCKING_CRASH_DENY_REASON in decision.all_deny_reasons
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
def test_refactor_guard_is_hosted_as_an_edit_only_advisory_hook() -> None:
|
|
91
|
+
entry = _entry_for("advisory/refactor_guard.py")
|
|
92
|
+
assert entry is not None, (
|
|
93
|
+
"refactor_guard must be hosted by the dispatcher rather than spawning its own process"
|
|
94
|
+
)
|
|
95
|
+
assert entry.applicable_tool_names == frozenset({EDIT_TOOL_NAME})
|
|
96
|
+
assert entry.is_blocking is False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_migration_safety_advisor_is_hosted_as_an_edit_only_advisory_hook() -> None:
|
|
100
|
+
entry = _entry_for("advisory/migration_safety_advisor.py")
|
|
101
|
+
assert entry is not None, (
|
|
102
|
+
"migration_safety_advisor must be hosted by the dispatcher rather than "
|
|
103
|
+
"spawning its own process"
|
|
104
|
+
)
|
|
105
|
+
assert entry.applicable_tool_names == frozenset({EDIT_TOOL_NAME})
|
|
106
|
+
assert entry.is_blocking is False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_every_hosted_script_path_exists_under_the_hooks_root() -> None:
|
|
110
|
+
for each_entry in ALL_HOSTED_HOOK_ENTRIES:
|
|
111
|
+
hosted_script_path = _HOOKS_ROOT / each_entry.script_relative_path
|
|
112
|
+
assert hosted_script_path.is_file(), (
|
|
113
|
+
f"{each_entry.script_relative_path} is on the roster but missing from the hooks tree"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
90
117
|
def test_every_native_module_exposes_a_callable_evaluate() -> None:
|
|
91
118
|
nativized_entries = [
|
|
92
119
|
each_entry
|