claude-dev-env 1.89.0 → 1.92.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/CLAUDE.md +0 -13
- package/_shared/pr-loop/scripts/code_rules_gate.py +106 -14
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +26 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +160 -0
- package/agents/clean-coder.md +2 -2
- package/agents/code-verifier.md +0 -1
- package/agents/test_agent_frontmatter.py +78 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
- package/bin/install.mjs +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +6 -2
- package/hooks/blocking/code_rules_comments.py +2 -2
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
- package/hooks/blocking/config/verified_commit_constants.py +2 -0
- package/hooks/blocking/conftest.py +115 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
- package/hooks/blocking/pr_description_enforcer.py +46 -22
- package/hooks/blocking/pr_description_pr_number.py +5 -3
- package/hooks/blocking/pr_description_proof_of_work.py +367 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
- package/hooks/blocking/test_verification_verdict_store.py +11 -0
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
- package/hooks/blocking/test_verified_commit_gate.py +11 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +210 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
- package/hooks/blocking/verification_verdict_store.py +73 -5
- package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
- package/hooks/blocking/verified_commit_gate.py +6 -0
- package/hooks/blocking/verifier_verdict_minter.py +6 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +351 -0
- package/hooks/hooks.json +32 -2
- package/hooks/hooks_constants/CLAUDE.md +4 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +48 -0
- package/hooks/lifecycle/CLAUDE.md +3 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +146 -0
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +178 -0
- package/hooks/validators/run_all_validators.py +216 -4
- package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +3 -0
- package/rules/durable-post-artifacts.md +35 -0
- package/rules/nas-ssh-invocation.md +21 -0
- package/rules/proof-of-work-pr-comments.md +26 -0
- package/scripts/CLAUDE.md +2 -0
- package/scripts/claude-chain.example.json +8 -0
- package/scripts/claude_chain_runner.py +400 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
- package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +43 -0
- package/scripts/gh_artifact_upload.py +256 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_claude_chain_runner.py +472 -0
- package/scripts/tests/test_gh_artifact_upload.py +205 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/team-advisor/SKILL.md +188 -0
- package/skills/team-advisor-refresh/SKILL.md +25 -0
- package/skills/usage-pause/SKILL.md +108 -0
- package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
- package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
- package/system-prompts/software-engineer.xml +3 -2
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse hook: block durable GitHub posts that reference volatile paths.
|
|
3
|
+
|
|
4
|
+
Root cause: a background job posted a PR comment citing an artifact under a job
|
|
5
|
+
scratch directory. Job tmp dirs, worktrees, and system temp locations are
|
|
6
|
+
ephemeral — they are cleaned soon after the run — while a posted comment, PR
|
|
7
|
+
body, issue, or review lives forever. A durable post that points at ephemeral
|
|
8
|
+
scratch breaks the moment that scratch is removed.
|
|
9
|
+
|
|
10
|
+
Detection strategy: gather the post body text from the tool call, then scan it
|
|
11
|
+
for volatile-path markers. Two tool families carry post bodies: shell ``gh``
|
|
12
|
+
post subcommands (``pr create/comment/edit/review``, ``issue
|
|
13
|
+
create/comment/edit``) and the GitHub MCP post tools (any
|
|
14
|
+
``mcp__plugin_github_github__*`` tool that carries a ``body`` or ``comment``
|
|
15
|
+
parameter). For a ``gh`` command the body comes from ``--body``/``-b`` inline
|
|
16
|
+
strings and from the file named by ``--body-file``/``-F`` (its contents are read
|
|
17
|
+
and scanned, since that content is what gets embedded in the post). The ``gh``
|
|
18
|
+
command is tokenized with ``shlex.split`` and the ``gh`` word must be the first
|
|
19
|
+
command token, so a ``gh pr comment`` that only appears as quoted data inside
|
|
20
|
+
another argument never classifies.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import shlex
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
29
|
+
if _hooks_dir not in sys.path:
|
|
30
|
+
sys.path.insert(0, _hooks_dir)
|
|
31
|
+
|
|
32
|
+
from blocking._gh_body_arg_utils import ( # noqa: E402
|
|
33
|
+
all_body_flags,
|
|
34
|
+
body_file_flag,
|
|
35
|
+
body_file_short_flag,
|
|
36
|
+
count_extra_tokens_to_skip_for_split_quoted_value,
|
|
37
|
+
get_logical_first_line,
|
|
38
|
+
is_unresolvable_shell_value,
|
|
39
|
+
match_body_file_equals_prefix,
|
|
40
|
+
match_body_flag_equals_prefix,
|
|
41
|
+
strip_surrounding_quotes,
|
|
42
|
+
)
|
|
43
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
44
|
+
from hooks_constants.volatile_path_in_post_blocker_constants import ( # noqa: E402
|
|
45
|
+
ALL_GH_POST_SUBCOMMANDS,
|
|
46
|
+
ALL_MCP_BODY_PARAM_NAMES,
|
|
47
|
+
ALL_VOLATILE_PATH_MARKERS,
|
|
48
|
+
BASH_TOOL_NAME,
|
|
49
|
+
BODY_FILE_ENCODING,
|
|
50
|
+
CORRECTIVE_MESSAGE,
|
|
51
|
+
GH_COMMAND_NAME,
|
|
52
|
+
MCP_GITHUB_TOOL_PREFIX,
|
|
53
|
+
MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT,
|
|
54
|
+
TOKEN_JOIN_SEPARATOR,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def scan_text_for_volatile_marker(text: str) -> str | None:
|
|
59
|
+
"""Return the first volatile-path marker found in text, or None.
|
|
60
|
+
|
|
61
|
+
Backslashes normalize to forward slashes and the text lowercases before the
|
|
62
|
+
scan, so a marker matches regardless of slash direction or letter case::
|
|
63
|
+
|
|
64
|
+
ok: "See the log table pasted below." -> None
|
|
65
|
+
flag: r"C:\\Users\\me\\.claude-editor\\jobs\\x" -> ".claude-editor/jobs/"
|
|
66
|
+
|
|
67
|
+
Env-token markers such as ``%TEMP%`` and ``$CLAUDE_JOB_DIR`` match on the
|
|
68
|
+
same lowercased text.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
text: The post body text to scan.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The matched marker string, or None when the text names no volatile path.
|
|
75
|
+
"""
|
|
76
|
+
normalized_text = text.replace("\\", "/").lower()
|
|
77
|
+
for each_marker in ALL_VOLATILE_PATH_MARKERS:
|
|
78
|
+
if each_marker in normalized_text:
|
|
79
|
+
return each_marker
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_environment_assignment(token: str) -> bool:
|
|
84
|
+
"""Return whether a token is a ``NAME=value`` shell environment assignment."""
|
|
85
|
+
equals_index = token.find("=")
|
|
86
|
+
if equals_index <= 0:
|
|
87
|
+
return False
|
|
88
|
+
name_part = token[:equals_index]
|
|
89
|
+
if not (name_part[0].isalpha() or name_part[0] == "_"):
|
|
90
|
+
return False
|
|
91
|
+
return all(
|
|
92
|
+
each_character.isalnum() or each_character == "_"
|
|
93
|
+
for each_character in name_part
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _tokens_name_gh_post_command(all_command_tokens: list[str]) -> bool:
|
|
98
|
+
"""Return whether the tokens start with an affected ``gh`` post subcommand.
|
|
99
|
+
|
|
100
|
+
Leading ``NAME=value`` env assignments are skipped, then the next token must
|
|
101
|
+
be the literal ``gh`` command word followed by a post noun and verb. Anchoring
|
|
102
|
+
to the first command word keeps a ``gh pr comment`` mentioned only as quoted
|
|
103
|
+
argument data from classifying.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
all_command_tokens: The shlex-split tokens of the logical command line.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True when the command invokes an affected ``gh`` post subcommand.
|
|
110
|
+
"""
|
|
111
|
+
command_index = 0
|
|
112
|
+
while command_index < len(all_command_tokens) and _is_environment_assignment(
|
|
113
|
+
all_command_tokens[command_index]
|
|
114
|
+
):
|
|
115
|
+
command_index += 1
|
|
116
|
+
if command_index >= len(all_command_tokens):
|
|
117
|
+
return False
|
|
118
|
+
if all_command_tokens[command_index] != GH_COMMAND_NAME:
|
|
119
|
+
return False
|
|
120
|
+
remaining_tokens = all_command_tokens[command_index + 1 :]
|
|
121
|
+
if len(remaining_tokens) < MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT:
|
|
122
|
+
return False
|
|
123
|
+
post_noun = remaining_tokens[0]
|
|
124
|
+
post_verb = remaining_tokens[1]
|
|
125
|
+
return post_verb in ALL_GH_POST_SUBCOMMANDS.get(post_noun, frozenset())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _reassemble_split_quoted_value(
|
|
129
|
+
value_token: str, all_following_tokens: list[str]
|
|
130
|
+
) -> tuple[str, int]:
|
|
131
|
+
"""Rejoin a quoted value that ``shlex.split(posix=False)`` split on spaces.
|
|
132
|
+
|
|
133
|
+
``posix=False`` keeps backslashes intact (so Windows paths survive) but
|
|
134
|
+
splits a quoted value such as ``"dump in %TEMP%"`` into three tokens. This
|
|
135
|
+
rejoins the value token with the follow-on tokens up to the closing quote,
|
|
136
|
+
then strips the surrounding quotes so the full body text can be scanned.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
value_token: The first token of the value, following the flag.
|
|
140
|
+
all_following_tokens: The tokens after ``value_token`` on the line.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
A ``(full_value, extra_tokens_consumed)`` pair; ``extra_tokens_consumed``
|
|
144
|
+
counts the follow-on tokens joined into the value.
|
|
145
|
+
"""
|
|
146
|
+
extra_tokens = count_extra_tokens_to_skip_for_split_quoted_value(
|
|
147
|
+
all_following_tokens, value_token
|
|
148
|
+
)
|
|
149
|
+
if not extra_tokens:
|
|
150
|
+
return strip_surrounding_quotes(value_token), 0
|
|
151
|
+
joined_value = value_token + TOKEN_JOIN_SEPARATOR + TOKEN_JOIN_SEPARATOR.join(
|
|
152
|
+
all_following_tokens[:extra_tokens]
|
|
153
|
+
)
|
|
154
|
+
return strip_surrounding_quotes(joined_value), extra_tokens
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _extract_flag_value_at(
|
|
158
|
+
all_command_tokens: list[str], token_index: int
|
|
159
|
+
) -> tuple[bool, str, int] | None:
|
|
160
|
+
"""Return a body/body-file value starting at token_index, or None.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
all_command_tokens: The shlex-split tokens of the logical command line.
|
|
164
|
+
token_index: The index of the token to inspect.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
An ``(is_body_file, value, next_index)`` triple when the token opens a
|
|
168
|
+
body or body-file flag, else None. ``next_index`` is where scanning
|
|
169
|
+
resumes after the value's tokens.
|
|
170
|
+
"""
|
|
171
|
+
current_token = all_command_tokens[token_index]
|
|
172
|
+
following_tokens = all_command_tokens[token_index + 1:]
|
|
173
|
+
body_equals_prefix = match_body_flag_equals_prefix(current_token)
|
|
174
|
+
if body_equals_prefix is not None:
|
|
175
|
+
value, extra = _reassemble_split_quoted_value(
|
|
176
|
+
current_token[len(body_equals_prefix):], following_tokens
|
|
177
|
+
)
|
|
178
|
+
return False, value, token_index + 1 + extra
|
|
179
|
+
body_file_equals_prefix = match_body_file_equals_prefix(current_token)
|
|
180
|
+
if body_file_equals_prefix is not None:
|
|
181
|
+
value, extra = _reassemble_split_quoted_value(
|
|
182
|
+
current_token[len(body_file_equals_prefix):], following_tokens
|
|
183
|
+
)
|
|
184
|
+
return True, value, token_index + 1 + extra
|
|
185
|
+
if current_token in all_body_flags and following_tokens:
|
|
186
|
+
value, extra = _reassemble_split_quoted_value(following_tokens[0], following_tokens[1:])
|
|
187
|
+
return False, value, token_index + 2 + extra
|
|
188
|
+
if current_token in (body_file_flag, body_file_short_flag) and following_tokens:
|
|
189
|
+
value, extra = _reassemble_split_quoted_value(following_tokens[0], following_tokens[1:])
|
|
190
|
+
return True, value, token_index + 2 + extra
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _collect_body_flag_values(
|
|
195
|
+
all_command_tokens: list[str],
|
|
196
|
+
) -> tuple[list[str], list[str]]:
|
|
197
|
+
"""Return inline body strings and body-file paths from the command tokens.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
all_command_tokens: The shlex-split tokens of the logical command line.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
A ``(all_inline_bodies, all_body_file_paths)`` pair, each value stripped
|
|
204
|
+
of its surrounding quotes and rejoined across split-quote tokens.
|
|
205
|
+
"""
|
|
206
|
+
all_inline_bodies: list[str] = []
|
|
207
|
+
all_body_file_paths: list[str] = []
|
|
208
|
+
token_index = 0
|
|
209
|
+
while token_index < len(all_command_tokens):
|
|
210
|
+
extraction = _extract_flag_value_at(all_command_tokens, token_index)
|
|
211
|
+
if extraction is None:
|
|
212
|
+
token_index += 1
|
|
213
|
+
continue
|
|
214
|
+
is_body_file, value, next_index = extraction
|
|
215
|
+
if is_body_file:
|
|
216
|
+
all_body_file_paths.append(value)
|
|
217
|
+
else:
|
|
218
|
+
all_inline_bodies.append(value)
|
|
219
|
+
token_index = next_index
|
|
220
|
+
return all_inline_bodies, all_body_file_paths
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _read_body_file(body_file_path: str) -> str | None:
|
|
224
|
+
"""Return the contents of a body-file path, or None when it cannot be read.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
body_file_path: The path given to ``--body-file``/``-F``.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
The file text, or None for an unresolvable shell value (such as ``-`` for
|
|
231
|
+
stdin or a ``$VAR`` path) or an unreadable file.
|
|
232
|
+
"""
|
|
233
|
+
if is_unresolvable_shell_value(body_file_path):
|
|
234
|
+
return None
|
|
235
|
+
try:
|
|
236
|
+
return Path(body_file_path).read_text(encoding=BODY_FILE_ENCODING)
|
|
237
|
+
except OSError:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def extract_gh_post_body_texts(command: str) -> list[str]:
|
|
242
|
+
"""Return every post body text an affected ``gh`` command would send.
|
|
243
|
+
|
|
244
|
+
Non-post ``gh`` commands and unparseable command lines yield an empty list.
|
|
245
|
+
Body-file contents are read from disk so the embedded text is scanned rather
|
|
246
|
+
than the file path.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
command: The raw Bash tool command string.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Inline ``--body`` strings plus the contents of each readable body-file.
|
|
253
|
+
"""
|
|
254
|
+
logical_line = get_logical_first_line(command)
|
|
255
|
+
if not logical_line:
|
|
256
|
+
return []
|
|
257
|
+
try:
|
|
258
|
+
all_command_tokens = shlex.split(logical_line, posix=False)
|
|
259
|
+
except ValueError:
|
|
260
|
+
return []
|
|
261
|
+
if not _tokens_name_gh_post_command(all_command_tokens):
|
|
262
|
+
return []
|
|
263
|
+
all_inline_bodies, all_body_file_paths = _collect_body_flag_values(
|
|
264
|
+
all_command_tokens
|
|
265
|
+
)
|
|
266
|
+
all_body_texts = list(all_inline_bodies)
|
|
267
|
+
for each_path in all_body_file_paths:
|
|
268
|
+
file_text = _read_body_file(each_path)
|
|
269
|
+
if file_text is not None:
|
|
270
|
+
all_body_texts.append(file_text)
|
|
271
|
+
return all_body_texts
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def extract_mcp_body_texts(all_tool_input: dict[str, object]) -> list[str]:
|
|
275
|
+
"""Return the body and comment strings from a GitHub MCP post tool input.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
all_tool_input: The MCP tool's input mapping.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
The non-empty string values of the ``body`` and ``comment`` parameters.
|
|
282
|
+
"""
|
|
283
|
+
all_body_texts: list[str] = []
|
|
284
|
+
for each_param_name in ALL_MCP_BODY_PARAM_NAMES:
|
|
285
|
+
param_value = all_tool_input.get(each_param_name)
|
|
286
|
+
if isinstance(param_value, str) and param_value:
|
|
287
|
+
all_body_texts.append(param_value)
|
|
288
|
+
return all_body_texts
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _collect_body_texts(tool_name: str, all_tool_input: dict[str, object]) -> list[str]:
|
|
292
|
+
if tool_name == BASH_TOOL_NAME:
|
|
293
|
+
command = all_tool_input.get("command", "")
|
|
294
|
+
if not isinstance(command, str) or not command:
|
|
295
|
+
return []
|
|
296
|
+
return extract_gh_post_body_texts(command)
|
|
297
|
+
if tool_name.startswith(MCP_GITHUB_TOOL_PREFIX):
|
|
298
|
+
return extract_mcp_body_texts(all_tool_input)
|
|
299
|
+
return []
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _first_volatile_marker(all_body_texts: list[str]) -> str | None:
|
|
303
|
+
for each_text in all_body_texts:
|
|
304
|
+
matched_marker = scan_text_for_volatile_marker(each_text)
|
|
305
|
+
if matched_marker is not None:
|
|
306
|
+
return matched_marker
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _emit_block(tool_name: str, matched_marker: str) -> None:
|
|
311
|
+
deny_payload = {
|
|
312
|
+
"hookSpecificOutput": {
|
|
313
|
+
"hookEventName": "PreToolUse",
|
|
314
|
+
"permissionDecision": "deny",
|
|
315
|
+
"permissionDecisionReason": CORRECTIVE_MESSAGE,
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
log_hook_block(
|
|
319
|
+
calling_hook_name="volatile_path_in_post_blocker.py",
|
|
320
|
+
hook_event="PreToolUse",
|
|
321
|
+
block_reason=CORRECTIVE_MESSAGE,
|
|
322
|
+
tool_name=tool_name,
|
|
323
|
+
offending_input_preview=matched_marker,
|
|
324
|
+
)
|
|
325
|
+
print(json.dumps(deny_payload))
|
|
326
|
+
sys.stdout.flush()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def main() -> None:
|
|
330
|
+
"""Read the PreToolUse payload and block a post that names a volatile path."""
|
|
331
|
+
try:
|
|
332
|
+
hook_input = json.load(sys.stdin)
|
|
333
|
+
except json.JSONDecodeError:
|
|
334
|
+
sys.exit(0)
|
|
335
|
+
|
|
336
|
+
tool_name = hook_input.get("tool_name", "")
|
|
337
|
+
tool_input = hook_input.get("tool_input", {})
|
|
338
|
+
if not isinstance(tool_name, str) or not isinstance(tool_input, dict):
|
|
339
|
+
sys.exit(0)
|
|
340
|
+
|
|
341
|
+
all_body_texts = _collect_body_texts(tool_name, tool_input)
|
|
342
|
+
matched_marker = _first_volatile_marker(all_body_texts)
|
|
343
|
+
if matched_marker is None:
|
|
344
|
+
sys.exit(0)
|
|
345
|
+
|
|
346
|
+
_emit_block(tool_name, matched_marker)
|
|
347
|
+
sys.exit(0)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if __name__ == "__main__":
|
|
351
|
+
main()
|
package/hooks/hooks.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
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())\"",
|
|
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
11
|
"timeout": 15
|
|
12
12
|
}
|
|
13
13
|
]
|
|
@@ -55,6 +55,16 @@
|
|
|
55
55
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/gh_body_arg_blocker.py",
|
|
56
56
|
"timeout": 10
|
|
57
57
|
},
|
|
58
|
+
{
|
|
59
|
+
"type": "command",
|
|
60
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/nas_ssh_binary_enforcer.py",
|
|
61
|
+
"timeout": 10
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/volatile_path_in_post_blocker.py",
|
|
66
|
+
"timeout": 10
|
|
67
|
+
},
|
|
58
68
|
{
|
|
59
69
|
"type": "command",
|
|
60
70
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/conventional_pr_title_gate.py",
|
|
@@ -83,7 +93,7 @@
|
|
|
83
93
|
{
|
|
84
94
|
"type": "command",
|
|
85
95
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pr_description_enforcer.py",
|
|
86
|
-
"timeout":
|
|
96
|
+
"timeout": 30
|
|
87
97
|
},
|
|
88
98
|
{
|
|
89
99
|
"type": "command",
|
|
@@ -142,6 +152,16 @@
|
|
|
142
152
|
}
|
|
143
153
|
]
|
|
144
154
|
},
|
|
155
|
+
{
|
|
156
|
+
"matcher": "mcp__plugin_github_github__.*",
|
|
157
|
+
"hooks": [
|
|
158
|
+
{
|
|
159
|
+
"type": "command",
|
|
160
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/volatile_path_in_post_blocker.py",
|
|
161
|
+
"timeout": 10
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
145
165
|
{
|
|
146
166
|
"matcher": "Agent",
|
|
147
167
|
"hooks": [
|
|
@@ -186,6 +206,16 @@
|
|
|
186
206
|
"timeout": 10
|
|
187
207
|
}
|
|
188
208
|
]
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"matcher": "EnterWorktree",
|
|
212
|
+
"hooks": [
|
|
213
|
+
{
|
|
214
|
+
"type": "command",
|
|
215
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/enter_worktree_origin_prefetch.py",
|
|
216
|
+
"timeout": 25
|
|
217
|
+
}
|
|
218
|
+
]
|
|
189
219
|
}
|
|
190
220
|
],
|
|
191
221
|
"SessionStart": [
|
|
@@ -13,6 +13,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
13
13
|
| `bot_mention_comment_blocker_constants.py` | Patterns for detecting bot @-mentions in PR comments |
|
|
14
14
|
| `claude_md_orphan_file_blocker_constants.py` | Table patterns, file extensions, scan budget, and block-message text for the CLAUDE.md orphan-file blocker |
|
|
15
15
|
| `code_rules_enforcer_constants.py` | File-extension sets, test-path patterns, advisory line thresholds, boolean-name prefixes |
|
|
16
|
+
| `enter_worktree_prefetch_constants.py` | Tool name, origin-remote ref names, and git-command timeouts for the EnterWorktree origin-prefetch hook |
|
|
16
17
|
| `env_var_table_code_drift_constants.py` | Table patterns, env-var-name and code-file recognizers, scan budget, and block-message text for the env-var-table code-drift blocker |
|
|
17
18
|
| `code_rules_path_utils_constants.py` | Path-matching helpers used by the code-rules check modules |
|
|
18
19
|
| `code_verifier_spawn_preflight_gate_constants.py` | Subagent type, merge-tree command flags, timeouts, and deny-message text for the code-verifier spawn pre-flight gate |
|
|
@@ -37,6 +38,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
37
38
|
| `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
|
|
38
39
|
| `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
|
|
39
40
|
| `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
|
|
41
|
+
| `nas_ssh_binary_enforcer_constants.py` | Bash tool name, the NAS address pattern, ssh-family basenames, OpenSSH binary path suffixes, launcher-wrapper set, shell control-operator tokens and split pattern, leading-assignment and duration patterns, the batch-mode pattern, and the two deny messages for the NAS ssh binary enforcer |
|
|
40
42
|
| `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
|
|
41
43
|
| `orphan_css_class_constants.py` | Scan radius and selector patterns for the orphan-CSS-class check |
|
|
42
44
|
| `package_inventory_stale_blocker_constants.py` | Inventory document names, production code extensions, backtick token pattern, smallest inventory size, exempt names, scan budget, and block-message text for the package-inventory stale-entry blocker |
|
|
@@ -46,6 +48,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
46
48
|
| `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
|
|
47
49
|
| `pr_converge_bugteam_enforcer_state.py` | State-file helpers for the bugteam enforcer |
|
|
48
50
|
| `pr_description_enforcer_constants.py` | PR-description shape rules and command patterns |
|
|
51
|
+
| `pr_description_proof_of_work_constants.py` | Proof-part detection patterns, gh command tokens, and corrective messages for the proof-of-work comment audit |
|
|
49
52
|
| `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
|
|
50
53
|
| `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
|
|
51
54
|
| `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
|
|
@@ -65,6 +68,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
65
68
|
| `task_list_loop_starter_constants.py` | The one-line task-list instruction and the full session-start directive text for the task-list loop starter hook |
|
|
66
69
|
| `text_stripping.py` | `strip_code_and_quotes()` — shared helper that removes fenced code blocks, inline code, and blockquotes from prose, imported by the Stop-hook prose blockers |
|
|
67
70
|
| `unused_module_import_constants.py` | Patterns for detecting unused module-level imports |
|
|
71
|
+
| `volatile_path_in_post_blocker_constants.py` | Volatile path markers, affected `gh` post subcommands, MCP body param names, and the corrective message for the volatile-path post blocker |
|
|
68
72
|
| `windows_rmtree_blocker_constants.py` | The unsafe `shutil.rmtree` pattern and the safe replacement pattern |
|
|
69
73
|
| `workflow_substitution_slot_blocker_constants.py` | Per-iteration token patterns for the workflow-slot blocker |
|
|
70
74
|
|
|
@@ -26,6 +26,9 @@ MERGE_TREE_CONFLICT_EXIT_CODE: int = 1
|
|
|
26
26
|
MERGE_TREE_CLEAN_EXIT_CODE: int = 0
|
|
27
27
|
MERGE_TREE_TIMEOUT_SECONDS: int = 30
|
|
28
28
|
|
|
29
|
+
ALL_MERGE_HEAD_PROBE_FLAGS: tuple[str, ...] = ("rev-parse", "--verify", "--quiet", "MERGE_HEAD")
|
|
30
|
+
ALL_UNMERGED_PATHS_DIFF_FLAGS: tuple[str, ...] = ("diff", "--name-only", "--diff-filter=U")
|
|
31
|
+
|
|
29
32
|
ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS: tuple[str, ...] = (
|
|
30
33
|
"-c",
|
|
31
34
|
"core.quotePath=false",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Configuration constants for the ``enter_worktree_origin_prefetch`` hook.
|
|
2
|
+
|
|
3
|
+
The hook keeps a freshly created worktree from starting on a stale cached
|
|
4
|
+
``refs/remotes/origin/<default-branch>`` value: it fetches that ref right
|
|
5
|
+
before ``EnterWorktree`` resolves its base.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
ENTER_WORKTREE_TOOL_NAME: str = "EnterWorktree"
|
|
11
|
+
ENTER_WORKTREE_PATH_INPUT_KEY: str = "path"
|
|
12
|
+
|
|
13
|
+
ORIGIN_REMOTE_NAME: str = "origin"
|
|
14
|
+
ORIGIN_HEAD_SYMBOLIC_REF: str = "refs/remotes/origin/HEAD"
|
|
15
|
+
ORIGIN_REMOTE_REF_PREFIX: str = "refs/remotes/origin/"
|
|
16
|
+
|
|
17
|
+
GIT_SYMBOLIC_REF_TIMEOUT_SECONDS: int = 5
|
|
18
|
+
GIT_FETCH_TIMEOUT_SECONDS: int = 15
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Constants for the NAS ssh binary enforcer hook.
|
|
2
|
+
|
|
3
|
+
Holds the Bash tool name, the NAS host-token pattern, the ssh-family command
|
|
4
|
+
basenames, the Windows OpenSSH binary path suffixes, the launcher-wrapper set, the
|
|
5
|
+
shell control-operator tokens and their split pattern, the leading-assignment and
|
|
6
|
+
launcher-duration patterns, the batch-mode pattern, and the two deny messages.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
BASH_TOOL_NAME = "Bash"
|
|
12
|
+
|
|
13
|
+
NAS_HOST_TOKEN_PATTERN = re.compile(r"(?:^|@)192\.168\.1\.100(?::|$)")
|
|
14
|
+
"""Match the NAS address only as a connection host.
|
|
15
|
+
|
|
16
|
+
Anchored to token start or an ``@`` and followed by ``:`` or token end, so the
|
|
17
|
+
address is matched as a bare host (``192.168.1.100``), a ``user@host``
|
|
18
|
+
(``jon@192.168.1.100``), or a ``host:path`` target (``192.168.1.100:/vol1/``,
|
|
19
|
+
``jon@192.168.1.100:/vol1/``). The address inside a remote-command argument
|
|
20
|
+
(``"ping 192.168.1.100"``) or a remote path component on another host
|
|
21
|
+
(``jon@other:/backup/192.168.1.100/``) does not count as a NAS connection.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
ALL_SSH_FAMILY_COMMAND_BASENAMES = frozenset(
|
|
25
|
+
{"ssh", "scp", "sftp", "ssh.exe", "scp.exe", "sftp.exe"}
|
|
26
|
+
)
|
|
27
|
+
ALL_OPENSSH_BINARY_PATH_SUFFIXES = (
|
|
28
|
+
"/openssh/ssh.exe",
|
|
29
|
+
"/openssh/scp.exe",
|
|
30
|
+
"/openssh/sftp.exe",
|
|
31
|
+
)
|
|
32
|
+
ALL_LAUNCHER_WRAPPER_COMMANDS = frozenset({"timeout", "nohup", "nice", "stdbuf", "setsid", "env"})
|
|
33
|
+
ALL_SHELL_CONTROL_OPERATOR_TOKENS = frozenset({"&&", "||", ";", "|", "&", "|&"})
|
|
34
|
+
CONTROL_OPERATOR_SPLIT_PATTERN = re.compile(r"(&&|\|\||;|\|&|\||(?<!>)&(?!>))")
|
|
35
|
+
LEADING_ASSIGNMENT_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
36
|
+
LAUNCHER_DURATION_PATTERN = re.compile(r"^\d+[a-z]*$", re.IGNORECASE)
|
|
37
|
+
BATCH_MODE_PATTERN = re.compile(r"batchmode\s*=\s*yes", re.IGNORECASE)
|
|
38
|
+
|
|
39
|
+
BARE_SSH_BINARY_MESSAGE = (
|
|
40
|
+
"BLOCKED [nas-ssh-binary]: Git Bash's MSYS ssh reads ~/.ssh/id_ed25519 as "
|
|
41
|
+
"world-readable through its ACL mapping, rejects the key as bad permissions, "
|
|
42
|
+
"and falls back to an interactive password prompt that hangs unattended "
|
|
43
|
+
"sessions against the NAS at 192.168.1.100.\n\n"
|
|
44
|
+
"Use the Windows OpenSSH binary, which authenticates the key without prompting:\n"
|
|
45
|
+
' "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 '
|
|
46
|
+
'-p 9222 jon@192.168.1.100 "<cmd>"\n\n'
|
|
47
|
+
"See ~/.claude/rules/nas-ssh-invocation.md for full guidance."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
MISSING_BATCH_MODE_MESSAGE = (
|
|
51
|
+
"BLOCKED [nas-ssh-binary]: this NAS ssh command uses the Windows OpenSSH binary "
|
|
52
|
+
"but omits -o BatchMode=yes, so an authentication regression falls back to an "
|
|
53
|
+
"interactive password prompt that hangs unattended sessions against the NAS at "
|
|
54
|
+
"192.168.1.100.\n\n"
|
|
55
|
+
"Add -o BatchMode=yes so a key failure exits loudly rather than prompting:\n"
|
|
56
|
+
' "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 '
|
|
57
|
+
'-p 9222 jon@192.168.1.100 "<cmd>"\n\n'
|
|
58
|
+
"See ~/.claude/rules/nas-ssh-invocation.md for full guidance."
|
|
59
|
+
)
|
|
@@ -41,6 +41,7 @@ ALL_HEAVY_TESTING_HEADERS: frozenset[str] = frozenset(
|
|
|
41
41
|
{TEST_PLAN_HEADER, TESTING_HEADER, TESTS_HEADER, VERIFICATION_HEADER, VALIDATION_HEADER}
|
|
42
42
|
)
|
|
43
43
|
GH_PR_COMMAND_MIN_TOKEN_COUNT: int = 3
|
|
44
|
+
ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS: frozenset[str] = frozenset({"edit", "comment", "ready"})
|
|
44
45
|
BODY_FILE_STDIN_SENTINEL: str = "-"
|
|
45
46
|
ATOMIC_WRITE_TEMP_SUFFIX: str = ".tmp"
|
|
46
47
|
SELF_CLOSING_REFERENCE_MESSAGE_PREFIX: str = "PR body references its own PR number #"
|
|
@@ -134,6 +135,7 @@ __all__ = [
|
|
|
134
135
|
"FLESCH_SYLLABLES_PER_WORD_COEFFICIENT",
|
|
135
136
|
"FLESCH_WORDS_PER_SENTENCE_COEFFICIENT",
|
|
136
137
|
"GH_PR_COMMAND_MIN_TOKEN_COUNT",
|
|
138
|
+
"ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
|
|
137
139
|
"HEADING_LINE_PATTERN",
|
|
138
140
|
"HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION",
|
|
139
141
|
"HEAVY_SHAPE",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Configuration constants for the proof-of-work PR-comment audit.
|
|
2
|
+
|
|
3
|
+
Holds the proof-part detection patterns, the gap and linkage word lists,
|
|
4
|
+
the gh command tokens the audit runs to read a PR's comments and diff, and
|
|
5
|
+
the corrective messages returned when a proof part is missing or when
|
|
6
|
+
``gh pr ready`` runs with no passing proof comment on the PR.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
ALL_PROOF_HEADING_KEYWORDS: tuple[str, ...] = ("proof", "verification")
|
|
14
|
+
|
|
15
|
+
PR_READY_INVOCATION_PATTERN: re.Pattern[str] = re.compile(r"(?:^|[;&|])\s*gh\s+pr\s+ready\b")
|
|
16
|
+
PR_READY_UNDO_FLAG: str = "--undo"
|
|
17
|
+
|
|
18
|
+
GH_EXECUTABLE: str = "gh"
|
|
19
|
+
GH_API_SUBCOMMAND: str = "api"
|
|
20
|
+
GH_PAGINATE_FLAG: str = "--paginate"
|
|
21
|
+
GH_SLURP_FLAG: str = "--slurp"
|
|
22
|
+
PR_COMMENTS_API_PATH_TEMPLATE: str = "repos/{{owner}}/{{repo}}/issues/{pr_number}/comments"
|
|
23
|
+
ALL_PR_DIFF_SUBCOMMANDS: tuple[str, ...] = ("pr", "diff")
|
|
24
|
+
PR_DIFF_NAME_ONLY_FLAG: str = "--name-only"
|
|
25
|
+
ALL_PR_VIEW_NUMBER_ARGUMENTS: tuple[str, ...] = ("pr", "view", "--json", "number")
|
|
26
|
+
PR_NUMBER_JSON_FIELD: str = "number"
|
|
27
|
+
COMMENT_BODY_JSON_FIELD: str = "body"
|
|
28
|
+
GH_COMMAND_TIMEOUT_SECONDS: int = 6
|
|
29
|
+
MAX_DIFF_SCAN_CHARS: int = 200000
|
|
30
|
+
|
|
31
|
+
ALL_VISUAL_FILE_SUFFIXES: frozenset[str] = frozenset(
|
|
32
|
+
{".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico", ".html", ".css"}
|
|
33
|
+
)
|
|
34
|
+
HEX_COLOR_ADDED_LINE_PATTERN: re.Pattern[str] = re.compile(r"^\+.*#[0-9A-Fa-f]{6}\b", re.MULTILINE)
|
|
35
|
+
IMAGE_EMBED_PATTERN: re.Pattern[str] = re.compile(r"!\[[^\]]*\]\([^)\s]+\)")
|
|
36
|
+
ISSUE_REFERENCE_PATTERN: re.Pattern[str] = re.compile(r"#\d+")
|
|
37
|
+
PLAN_LINKAGE_KEYWORD_PATTERN: re.Pattern[str] = re.compile(
|
|
38
|
+
r"\b(?:issue|phase|plan|parent|advanc\w*|milestone|part of)\b", re.IGNORECASE
|
|
39
|
+
)
|
|
40
|
+
DIGIT_PATTERN: re.Pattern[str] = re.compile(r"\d")
|
|
41
|
+
ALL_HONEST_GAP_PHRASES: tuple[str, ...] = (
|
|
42
|
+
"gap",
|
|
43
|
+
"limitation",
|
|
44
|
+
"cannot",
|
|
45
|
+
"does not show",
|
|
46
|
+
"not shown",
|
|
47
|
+
"unverified",
|
|
48
|
+
"not covered",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
PROOF_PART_COMMAND_MESSAGE: str = (
|
|
52
|
+
"proof comment missing a fenced command block showing the exact command(s) run on real data"
|
|
53
|
+
)
|
|
54
|
+
PROOF_PART_MEASURED_MESSAGE: str = (
|
|
55
|
+
"proof comment missing a measured-value element -- add a table row or bullet fact carrying "
|
|
56
|
+
"the numbers read from the produced artifact"
|
|
57
|
+
)
|
|
58
|
+
PROOF_PART_PLAN_LINKAGE_MESSAGE: str = (
|
|
59
|
+
"proof comment missing a plan-linkage sentence -- add one line naming the parent issue or "
|
|
60
|
+
"phase this PR advances, with its #number"
|
|
61
|
+
)
|
|
62
|
+
PROOF_PART_VISUAL_MESSAGE: str = (
|
|
63
|
+
"proof comment missing a visual element on a visual change -- embed an image (color "
|
|
64
|
+
"swatches, before/after screenshots) for values a human cannot read at a glance"
|
|
65
|
+
)
|
|
66
|
+
PROOF_PART_HONEST_GAPS_MESSAGE: str = (
|
|
67
|
+
"proof comment missing an honest-gaps statement -- say plainly what this offline proof "
|
|
68
|
+
"cannot show and what covers that gap"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
PR_READY_GATE_MESSAGE_TEMPLATE: str = (
|
|
72
|
+
"BLOCKED: [PROOF_OF_WORK] PR #{pr_number} carries no passing proof-of-work comment. "
|
|
73
|
+
"Post one comment with a proof or verification heading carrying: "
|
|
74
|
+
"(1) the exact command(s) run on real data in a fenced code block, "
|
|
75
|
+
"(2) measured outcomes from the produced artifact as a table row or bullet facts, "
|
|
76
|
+
"(3) one sentence naming the parent issue or phase this PR advances, with its #number, "
|
|
77
|
+
"(4) an image embed when the diff touches images, HTML, CSS, or hex color values, and "
|
|
78
|
+
"(5) an honest statement of what the offline proof cannot show. "
|
|
79
|
+
"Then re-run gh pr ready."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"ALL_HONEST_GAP_PHRASES",
|
|
84
|
+
"ALL_PROOF_HEADING_KEYWORDS",
|
|
85
|
+
"ALL_PR_DIFF_SUBCOMMANDS",
|
|
86
|
+
"ALL_PR_VIEW_NUMBER_ARGUMENTS",
|
|
87
|
+
"ALL_VISUAL_FILE_SUFFIXES",
|
|
88
|
+
"COMMENT_BODY_JSON_FIELD",
|
|
89
|
+
"DIGIT_PATTERN",
|
|
90
|
+
"GH_API_SUBCOMMAND",
|
|
91
|
+
"GH_COMMAND_TIMEOUT_SECONDS",
|
|
92
|
+
"GH_EXECUTABLE",
|
|
93
|
+
"GH_PAGINATE_FLAG",
|
|
94
|
+
"GH_SLURP_FLAG",
|
|
95
|
+
"HEX_COLOR_ADDED_LINE_PATTERN",
|
|
96
|
+
"IMAGE_EMBED_PATTERN",
|
|
97
|
+
"ISSUE_REFERENCE_PATTERN",
|
|
98
|
+
"MAX_DIFF_SCAN_CHARS",
|
|
99
|
+
"PLAN_LINKAGE_KEYWORD_PATTERN",
|
|
100
|
+
"PROOF_PART_COMMAND_MESSAGE",
|
|
101
|
+
"PROOF_PART_HONEST_GAPS_MESSAGE",
|
|
102
|
+
"PROOF_PART_MEASURED_MESSAGE",
|
|
103
|
+
"PROOF_PART_PLAN_LINKAGE_MESSAGE",
|
|
104
|
+
"PROOF_PART_VISUAL_MESSAGE",
|
|
105
|
+
"PR_COMMENTS_API_PATH_TEMPLATE",
|
|
106
|
+
"PR_DIFF_NAME_ONLY_FLAG",
|
|
107
|
+
"PR_NUMBER_JSON_FIELD",
|
|
108
|
+
"PR_READY_GATE_MESSAGE_TEMPLATE",
|
|
109
|
+
"PR_READY_INVOCATION_PATTERN",
|
|
110
|
+
"PR_READY_UNDO_FLAG",
|
|
111
|
+
]
|