claude-dev-env 4.1.0 → 4.3.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/.agents/skills/_shared/pr-loop/scripts/durable_post_lint.py +242 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py +106 -0
- package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint.py +332 -0
- package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py +25 -0
- package/.agents/skills/everything-search/SKILL.md +18 -3
- package/.agents/skills/everything-search/scripts/everything_search.py +204 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/__init__.py +1 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/__init__.py +1 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/constants.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +172 -0
- package/.agents/skills/issue-tracker/SKILL.md +6 -0
- package/.agents/skills/pull-request/SKILL.md +218 -0
- package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +61 -0
- package/.agents/skills/pull-request/scripts/pull_request.py +276 -0
- package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +49 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +398 -0
- package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
- package/.agents/skills/test-runner/SKILL.md +69 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
- package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/install.mjs +9 -1
- package/bin/install.prune.test.mjs +149 -0
- package/bin/install.test.mjs +24 -7
- package/hooks/blocking/pii_prevention_blocker.py +32 -51
- package/hooks/blocking/test_bash_post_call_dispatcher.py +5 -28
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
- package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
- package/hooks/gh_post_body_texts.py +195 -0
- package/hooks/hooks.json +0 -15
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +5 -16
- package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +0 -7
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +7 -20
- package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
- package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
- package/hooks/observability/test_failure_recorder.py +1 -1
- package/hooks/pyproject.toml +1 -1
- package/hooks/test_gh_post_body_texts.py +79 -0
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +31 -20
- package/rules/filesystem-search.md +5 -5
- package/rules/gh-cli-conventions.md +23 -13
- package/rules/git-workflow.md +21 -8
- package/scripts/policy_lint/adapter_configuration.py +53 -12
- package/scripts/repository_checks/config/constants.py +0 -5
- package/scripts/tests/test_adapter_configuration.py +54 -0
- package/hooks/_gh_pr_author_swap_utils.py +0 -1225
- package/hooks/blocking/conventional_pr_title_gate.py +0 -443
- package/hooks/blocking/es_exe_path_rewriter.py +0 -159
- package/hooks/blocking/gh_body_arg_blocker.py +0 -176
- package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
- package/hooks/blocking/gh_pr_author_restore.py +0 -105
- package/hooks/blocking/pr_description_writer_gate.py +0 -256
- package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
- package/hooks/blocking/test_es_exe_path_rewriter.py +0 -447
- package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
- package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
- package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
- package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
- package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
- package/hooks/blocking/test_preflight_check.py +0 -343
- package/hooks/blocking/test_test_preflight_check.py +0 -80
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
- package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
- package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
- package/hooks/hooks_constants/path_rewriter_constants.py +0 -13
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
- package/hooks/hooks_constants/test_path_rewriter_constants.py +0 -57
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
- package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
- package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
- package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
- package/hooks/test__gh_pr_author_swap_utils.py +0 -333
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PostToolUse hook: restore the prior gh CLI account after `gh pr create` runs.
|
|
3
|
-
|
|
4
|
-
Companion to ``gh_pr_author_enforcer.py``. When the PreToolUse enforcer
|
|
5
|
-
silently swaps the active gh account to ``GITHUB_DEFAULT_ACCOUNT`` and
|
|
6
|
-
records the original account in a per-session state file, this hook
|
|
7
|
-
reads that state file after the matching Bash invocation finishes and
|
|
8
|
-
runs ``gh auth switch --user <original>`` to put the prior account back
|
|
9
|
-
in place.
|
|
10
|
-
|
|
11
|
-
The state file is deleted only when the restore switch succeeds. If
|
|
12
|
-
``gh auth switch`` fails the state file is left in place so the
|
|
13
|
-
SessionStart cleanup hook (``gh_pr_author_session_cleanup.py``) can
|
|
14
|
-
retry on the next session start instead of stranding the user on the
|
|
15
|
-
canonical author account.
|
|
16
|
-
|
|
17
|
-
Behavior:
|
|
18
|
-
- No-op when tool_name is not Bash.
|
|
19
|
-
- No-op when the command did not invoke ``gh pr create`` (uses the same
|
|
20
|
-
regex as the enforcer so the pair stays in sync).
|
|
21
|
-
- No-op when no per-session state file exists — means the enforcer
|
|
22
|
-
never swapped on this command.
|
|
23
|
-
- Otherwise reads the state file, runs ``gh auth switch --user <original>``,
|
|
24
|
-
and deletes the state file only when the switch succeeded. Failures
|
|
25
|
-
are logged to stderr; this hook never blocks the workflow.
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
from __future__ import annotations
|
|
29
|
-
|
|
30
|
-
import json
|
|
31
|
-
import sys
|
|
32
|
-
from pathlib import Path
|
|
33
|
-
|
|
34
|
-
hooks_parent_directory = str(Path(__file__).resolve().parent.parent)
|
|
35
|
-
if hooks_parent_directory not in sys.path:
|
|
36
|
-
sys.path.insert(0, hooks_parent_directory)
|
|
37
|
-
|
|
38
|
-
from _gh_pr_author_swap_utils import ( # noqa: E402
|
|
39
|
-
_build_restore_failure_message,
|
|
40
|
-
_command_invokes_gh_pr_create_in_stripped,
|
|
41
|
-
_delete_state_file,
|
|
42
|
-
_preprocess_command_for_matching,
|
|
43
|
-
_read_original_account,
|
|
44
|
-
_state_file_is_attacker_planted,
|
|
45
|
-
_state_file_path,
|
|
46
|
-
_switch_gh_account,
|
|
47
|
-
_write_line,
|
|
48
|
-
)
|
|
49
|
-
from hooks_constants.gh_pr_author_swap_constants import BASH_TOOL_NAME # noqa: E402
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def main() -> None:
|
|
53
|
-
"""Read PostToolUse hook input on stdin and restore the prior gh account.
|
|
54
|
-
|
|
55
|
-
Exits 0 in every path. Errors are logged to stderr only — this hook
|
|
56
|
-
must never block subsequent commands.
|
|
57
|
-
"""
|
|
58
|
-
try:
|
|
59
|
-
hook_input = json.load(sys.stdin)
|
|
60
|
-
except json.JSONDecodeError:
|
|
61
|
-
sys.exit(0)
|
|
62
|
-
|
|
63
|
-
if hook_input.get("tool_name") != BASH_TOOL_NAME:
|
|
64
|
-
sys.exit(0)
|
|
65
|
-
|
|
66
|
-
command = hook_input.get("tool_input", {}).get("command", "")
|
|
67
|
-
if not command:
|
|
68
|
-
sys.exit(0)
|
|
69
|
-
preprocessed_command = _preprocess_command_for_matching(command)
|
|
70
|
-
if not _command_invokes_gh_pr_create_in_stripped(preprocessed_command):
|
|
71
|
-
sys.exit(0)
|
|
72
|
-
|
|
73
|
-
session_id = str(hook_input.get("session_id") or "")
|
|
74
|
-
state_file = _state_file_path(session_id)
|
|
75
|
-
if _state_file_is_attacker_planted(state_file):
|
|
76
|
-
_write_line(
|
|
77
|
-
f"[gh-pr-author-restore] state file at {state_file} has unexpected mode/owner; "
|
|
78
|
-
f"skipping restore and preserving file for inspection",
|
|
79
|
-
sys.stderr,
|
|
80
|
-
)
|
|
81
|
-
sys.exit(0)
|
|
82
|
-
original_account = _read_original_account(state_file)
|
|
83
|
-
if original_account is None:
|
|
84
|
-
if state_file.exists():
|
|
85
|
-
_delete_state_file(state_file)
|
|
86
|
-
sys.exit(0)
|
|
87
|
-
|
|
88
|
-
has_restored_account = _switch_gh_account(original_account)
|
|
89
|
-
if has_restored_account:
|
|
90
|
-
_delete_state_file(state_file)
|
|
91
|
-
else:
|
|
92
|
-
_write_line(
|
|
93
|
-
_build_restore_failure_message(
|
|
94
|
-
"gh-pr-author-restore",
|
|
95
|
-
original_account,
|
|
96
|
-
state_file,
|
|
97
|
-
"state file",
|
|
98
|
-
),
|
|
99
|
-
sys.stderr,
|
|
100
|
-
)
|
|
101
|
-
sys.exit(0)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if __name__ == "__main__":
|
|
105
|
-
main()
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: block gh pr create when no pr-description-writer spawn is recorded.
|
|
3
|
-
|
|
4
|
-
Picture a reviewer opening a fresh pull request and meeting a body that repeats
|
|
5
|
-
the branch name. The ``pr-description-writer`` agent exists to write that body
|
|
6
|
-
from the diff, and the spawn tracker records each time this session runs it.
|
|
7
|
-
|
|
8
|
-
This gate reads that record when the session runs ``gh pr create``::
|
|
9
|
-
|
|
10
|
-
gh pr create --body-file body.md flag: no spawn recorded
|
|
11
|
-
gh pr create --body-file body.md ok: a spawn recorded earlier
|
|
12
|
-
gh pr create ... # pr-description-skip ok: the author opted out
|
|
13
|
-
|
|
14
|
-
Detection strategy: tokenize the command, then check each position that opens a
|
|
15
|
-
command segment for a ``gh`` executable followed by ``pr`` and ``create``. The
|
|
16
|
-
bypass marker counts only as the command's final tokens, so the same text
|
|
17
|
-
inside a quoted title leaves the gate running.
|
|
18
|
-
|
|
19
|
-
The gate fails OPEN on anything it cannot read: a command that will not
|
|
20
|
-
tokenize, a payload that is not a shell tool, or a missing command. Only a
|
|
21
|
-
recognized ``gh pr create`` with no recorded spawn and no bypass marker denies.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import json
|
|
27
|
-
import shlex
|
|
28
|
-
import sys
|
|
29
|
-
from pathlib import Path
|
|
30
|
-
|
|
31
|
-
_blocking_dir = str(Path(__file__).resolve().parent)
|
|
32
|
-
if _blocking_dir not in sys.path:
|
|
33
|
-
sys.path.insert(0, _blocking_dir)
|
|
34
|
-
|
|
35
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
36
|
-
if _hooks_dir not in sys.path:
|
|
37
|
-
sys.path.insert(0, _hooks_dir)
|
|
38
|
-
|
|
39
|
-
from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E402
|
|
40
|
-
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
41
|
-
)
|
|
42
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
43
|
-
from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
|
|
44
|
-
read_hook_input_dictionary_from_stdin,
|
|
45
|
-
)
|
|
46
|
-
from hooks_constants.pr_description_writer_gate_constants import ( # noqa: E402
|
|
47
|
-
ALL_GH_EXECUTABLE_BASENAMES,
|
|
48
|
-
COMMAND_FIELD_NAME,
|
|
49
|
-
CORRECTIVE_MESSAGE,
|
|
50
|
-
CREATE_SUBCOMMAND_TOKEN,
|
|
51
|
-
GH_PR_CREATE_MINIMUM_TOKEN_COUNT,
|
|
52
|
-
PR_SUBCOMMAND_TOKEN,
|
|
53
|
-
SPAWN_BYPASS_MARKER,
|
|
54
|
-
TOOL_INPUT_FIELD_NAME,
|
|
55
|
-
TOOL_NAME_FIELD_NAME,
|
|
56
|
-
spawn_marker_path,
|
|
57
|
-
)
|
|
58
|
-
from hooks_constants.session_edit_stage_gate_constants import ( # noqa: E402
|
|
59
|
-
ALL_COMMAND_SEPARATOR_TOKENS,
|
|
60
|
-
)
|
|
61
|
-
from sensitive_file_protector import build_deny_response # noqa: E402
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _all_command_tokens(shell_command: str) -> list[str] | None:
|
|
65
|
-
"""Return the command's tokens, or None when it will not tokenize.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
shell_command: The raw shell command string.
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
The token list, or None when shlex rejects the command.
|
|
72
|
-
"""
|
|
73
|
-
try:
|
|
74
|
-
return shlex.split(shell_command, posix=True)
|
|
75
|
-
except ValueError:
|
|
76
|
-
return None
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _carries_trailing_bypass_marker(all_tokens: list[str]) -> bool:
|
|
80
|
-
"""Report whether the bypass marker sits at the very end of the command.
|
|
81
|
-
|
|
82
|
-
The marker opts out only as a trailing shell comment the author adds on
|
|
83
|
-
purpose, so its parts are the command's final tokens. The same text inside
|
|
84
|
-
a quoted title tokenizes into the middle of the list and does not match.
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
all_tokens: The tokens of the whole command.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
True when the marker's tokens close the command.
|
|
91
|
-
"""
|
|
92
|
-
all_marker_tokens = SPAWN_BYPASS_MARKER.split()
|
|
93
|
-
marker_length = len(all_marker_tokens)
|
|
94
|
-
if marker_length > len(all_tokens):
|
|
95
|
-
return False
|
|
96
|
-
return all_tokens[-marker_length:] == all_marker_tokens
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def _opens_pull_request_creation(all_leading_tokens: list[str]) -> bool:
|
|
100
|
-
"""Report whether these leading tokens open a gh pr create invocation.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
all_leading_tokens: The tokens that open one command segment.
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
True when they name a gh executable, then ``pr``, then ``create``.
|
|
107
|
-
"""
|
|
108
|
-
if len(all_leading_tokens) < GH_PR_CREATE_MINIMUM_TOKEN_COUNT:
|
|
109
|
-
return False
|
|
110
|
-
executable_basename = Path(all_leading_tokens[0]).name
|
|
111
|
-
if executable_basename not in ALL_GH_EXECUTABLE_BASENAMES:
|
|
112
|
-
return False
|
|
113
|
-
return all_leading_tokens[1:] == [PR_SUBCOMMAND_TOKEN, CREATE_SUBCOMMAND_TOKEN]
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _all_command_segments(all_tokens: list[str]) -> list[list[str]]:
|
|
117
|
-
"""Return the command's token segments in their original order.
|
|
118
|
-
|
|
119
|
-
A shell separator ends the current segment and starts the next one.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
all_tokens: The tokens of the whole command.
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
One token list for each command segment.
|
|
126
|
-
"""
|
|
127
|
-
all_segments: list[list[str]] = [[]]
|
|
128
|
-
for each_token in all_tokens:
|
|
129
|
-
if each_token in ALL_COMMAND_SEPARATOR_TOKENS:
|
|
130
|
-
all_segments.append([])
|
|
131
|
-
continue
|
|
132
|
-
all_segments[-1].append(each_token)
|
|
133
|
-
return all_segments
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _all_pull_request_creation_segment_indexes(all_segments: list[list[str]]) -> list[int]:
|
|
137
|
-
"""Return indexes for segments that open a gh pr create command.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
all_segments: The shell command's token segments.
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
The indexes of segments that open a gh pr create command.
|
|
144
|
-
"""
|
|
145
|
-
return [
|
|
146
|
-
each_index
|
|
147
|
-
for each_index, each_segment in enumerate(all_segments)
|
|
148
|
-
if _opens_pull_request_creation(each_segment[:GH_PR_CREATE_MINIMUM_TOKEN_COUNT])
|
|
149
|
-
]
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _only_final_create_carries_bypass(
|
|
153
|
-
all_tokens: list[str],
|
|
154
|
-
all_segments: list[list[str]],
|
|
155
|
-
all_creation_segment_indexes: list[int],
|
|
156
|
-
) -> bool:
|
|
157
|
-
"""Report whether one final gh pr create carries the final bypass comment.
|
|
158
|
-
|
|
159
|
-
The marker belongs to the command segment that creates the pull request.
|
|
160
|
-
A marker after another command, or after a second create, cannot opt an
|
|
161
|
-
earlier create out.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
all_tokens: The tokens of the whole command.
|
|
165
|
-
all_segments: The command's token segments, already split by the caller.
|
|
166
|
-
all_creation_segment_indexes: Indexes of segments that create pull requests.
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
True when one final create segment carries the final bypass marker.
|
|
170
|
-
"""
|
|
171
|
-
if not _carries_trailing_bypass_marker(all_tokens):
|
|
172
|
-
return False
|
|
173
|
-
final_segment_index = len(all_segments) - 1
|
|
174
|
-
return all_creation_segment_indexes == [final_segment_index]
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _spawn_is_recorded(session_id: str) -> bool:
|
|
178
|
-
"""Report whether this session recorded a pr-description-writer spawn.
|
|
179
|
-
|
|
180
|
-
A payload naming no session reads as no record, so the gate denies rather
|
|
181
|
-
than falling back to a shared marker a different session could have left.
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
session_id: Raw ``session_id`` from the hook payload.
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
True when this session's marker file is present.
|
|
188
|
-
"""
|
|
189
|
-
marker_file = spawn_marker_path(session_id)
|
|
190
|
-
return marker_file is not None and marker_file.exists()
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def _shell_command_or_none(all_hook_payload: dict[str, object]) -> str | None:
|
|
194
|
-
"""Return the shell command this payload carries, or None.
|
|
195
|
-
|
|
196
|
-
Args:
|
|
197
|
-
all_hook_payload: The parsed PreToolUse payload.
|
|
198
|
-
|
|
199
|
-
Returns:
|
|
200
|
-
The command string, or None when the payload runs no shell command.
|
|
201
|
-
"""
|
|
202
|
-
if all_hook_payload.get(TOOL_NAME_FIELD_NAME, "") not in ALL_BASH_AND_POWERSHELL_TOOL_NAMES:
|
|
203
|
-
return None
|
|
204
|
-
tool_input = all_hook_payload.get(TOOL_INPUT_FIELD_NAME, {})
|
|
205
|
-
if not isinstance(tool_input, dict):
|
|
206
|
-
return None
|
|
207
|
-
shell_command = tool_input.get(COMMAND_FIELD_NAME, "")
|
|
208
|
-
if not isinstance(shell_command, str) or not shell_command:
|
|
209
|
-
return None
|
|
210
|
-
return shell_command
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def _emit_denial() -> None:
|
|
214
|
-
"""Write the PreToolUse deny payload to stdout and log the block."""
|
|
215
|
-
log_hook_block(
|
|
216
|
-
calling_hook_name="pr_description_writer_gate.py",
|
|
217
|
-
hook_event="PreToolUse",
|
|
218
|
-
block_reason=CORRECTIVE_MESSAGE,
|
|
219
|
-
)
|
|
220
|
-
sys.stdout.write(json.dumps(build_deny_response(CORRECTIVE_MESSAGE)) + "\n")
|
|
221
|
-
sys.stdout.flush()
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def main() -> None:
|
|
225
|
-
"""Deny a gh pr create that no recorded agent spawn or bypass marker covers.
|
|
226
|
-
|
|
227
|
-
Reads the PreToolUse payload from stdin and exits zero on every branch. A
|
|
228
|
-
tool that runs no shell, a missing command, a command that will not
|
|
229
|
-
tokenize, a command opening no pull request, a trailing bypass marker, and
|
|
230
|
-
a recorded spawn each pass through untouched.
|
|
231
|
-
"""
|
|
232
|
-
hook_payload = read_hook_input_dictionary_from_stdin()
|
|
233
|
-
if hook_payload is None:
|
|
234
|
-
sys.exit(0)
|
|
235
|
-
shell_command = _shell_command_or_none(hook_payload)
|
|
236
|
-
if shell_command is None:
|
|
237
|
-
sys.exit(0)
|
|
238
|
-
all_tokens = _all_command_tokens(shell_command)
|
|
239
|
-
if all_tokens is None:
|
|
240
|
-
sys.exit(0)
|
|
241
|
-
all_segments = _all_command_segments(all_tokens)
|
|
242
|
-
all_creation_segment_indexes = _all_pull_request_creation_segment_indexes(all_segments)
|
|
243
|
-
if not all_creation_segment_indexes:
|
|
244
|
-
sys.exit(0)
|
|
245
|
-
if _only_final_create_carries_bypass(
|
|
246
|
-
all_tokens, all_segments, all_creation_segment_indexes
|
|
247
|
-
):
|
|
248
|
-
sys.exit(0)
|
|
249
|
-
if _spawn_is_recorded(str(hook_payload.get("session_id") or "")):
|
|
250
|
-
sys.exit(0)
|
|
251
|
-
_emit_denial()
|
|
252
|
-
sys.exit(0)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if __name__ == "__main__":
|
|
256
|
-
main()
|