claude-dev-env 1.73.0 → 1.74.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 +2 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/block_main_commit.py +14 -0
- package/hooks/blocking/bot_mention_comment_blocker.py +7 -0
- package/hooks/blocking/claude_md_orphan_file_blocker.py +14 -42
- package/hooks/blocking/code_rules_docstrings.py +223 -0
- package/hooks/blocking/code_rules_enforcer.py +16 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +10 -4
- package/hooks/blocking/convergence_gate_blocker.py +17 -3
- package/hooks/blocking/destructive_command_blocker.py +7 -0
- package/hooks/blocking/docstring_rule_gate_count_blocker.py +321 -0
- package/hooks/blocking/gh_body_arg_blocker.py +8 -0
- package/hooks/blocking/gh_pr_author_enforcer.py +7 -0
- package/hooks/blocking/hedging_language_blocker.py +16 -10
- package/hooks/blocking/hook_prose_detector_consistency.py +7 -0
- package/hooks/blocking/intent_only_ending_blocker.py +17 -11
- package/hooks/blocking/md_to_html_blocker.py +10 -2
- package/hooks/blocking/open_questions_in_plans_blocker.py +10 -2
- package/hooks/blocking/package_inventory_stale_blocker.py +398 -0
- package/hooks/blocking/plain_language_blocker.py +6 -0
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +6 -0
- package/hooks/blocking/pr_description_enforcer.py +6 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/precommit_code_rules_gate.py +10 -1
- package/hooks/blocking/pytest_testpaths_orphan_blocker.py +8 -0
- package/hooks/blocking/question_to_user_enforcer.py +18 -12
- package/hooks/blocking/send_user_file_open_locally_blocker.py +70 -0
- package/hooks/blocking/sensitive_file_protector.py +15 -1
- package/hooks/blocking/session_handoff_blocker.py +14 -8
- package/hooks/blocking/state_description_blocker.py +6 -0
- package/hooks/blocking/subprocess_budget_completeness.py +9 -3
- package/hooks/blocking/tdd_enforcer.py +6 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +207 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +188 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +45 -0
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +203 -0
- package/hooks/blocking/test_hook_block_logger_coverage.py +53 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +329 -0
- package/hooks/blocking/test_plain_language_blocker.py +36 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +114 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +42 -0
- package/hooks/blocking/test_state_description_blocker.py +41 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +49 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +4 -19
- package/hooks/blocking/verdict_directory_write_blocker.py +10 -1
- package/hooks/blocking/verified_commit_gate.py +11 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +16 -1
- package/hooks/blocking/windows_rmtree_blocker.py +7 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +10 -5
- package/hooks/blocking/write_existing_file_blocker.py +16 -1
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +4 -0
- package/hooks/hooks_constants/blocking_check_limits.py +13 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/docstring_rule_gate_count_blocker_constants.py +90 -0
- package/hooks/hooks_constants/hook_block_logger.py +59 -0
- package/hooks/hooks_constants/multi_edit_reconstruction.py +56 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +111 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +1 -2
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +9 -1
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +18 -0
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +44 -0
- package/hooks/hooks_constants/test_hook_block_logger.py +159 -0
- package/hooks/lifecycle/config_change_guard.py +12 -0
- package/hooks/lifecycle/test_config_change_guard.py +23 -0
- package/hooks/validation/hook_format_validator.py +13 -0
- package/hooks/validation/mypy_validator.py +30 -1
- package/hooks/validation/test_hook_format_validator.py +64 -0
- package/hooks/validation/test_mypy_validator.py +22 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +2 -1
- package/rules/package-inventory-stale-entry.md +24 -0
- package/skills/autoconverge/SKILL.md +18 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +106 -0
- package/skills/autoconverge/workflow/converge.mjs +2 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse hook: blocks a stale gate-validator count in the docstring-prose rule.
|
|
3
|
+
|
|
4
|
+
The rule ``docstring-prose-matches-implementation.md`` enumerates the
|
|
5
|
+
``check_docstring_*`` gate validators that cover deterministic slices of docstring
|
|
6
|
+
prose, both as a spelled-out count ("Four more gate validators", "five gated
|
|
7
|
+
slices") and as a backticked list of the validator names. When a new gate
|
|
8
|
+
validator is registered but the count word is left unchanged, the rule's stated
|
|
9
|
+
count drifts from the validators it actually names — the same companion-doc drift
|
|
10
|
+
the rule itself governs. This hook fires on a Write, Edit, or MultiEdit targeting
|
|
11
|
+
that rule file and blocks the write when the spelled-out "<count> more gate
|
|
12
|
+
validators" count disagrees with the number of distinct free-form validators the
|
|
13
|
+
prose names, or when the "<count> gated slices" total is not that count plus one
|
|
14
|
+
(the count plus the ``Args:`` gate). An edit that leaves the count words and the
|
|
15
|
+
named-validator list in step is allowed.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TextIO
|
|
23
|
+
|
|
24
|
+
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
25
|
+
if _hooks_dir not in sys.path:
|
|
26
|
+
sys.path.insert(0, _hooks_dir)
|
|
27
|
+
|
|
28
|
+
from hooks_constants.docstring_rule_gate_count_blocker_constants import ( # noqa: E402
|
|
29
|
+
ALL_NUMBER_WORDS_BY_VALUE,
|
|
30
|
+
ARGS_GATE_VALIDATOR_NAME,
|
|
31
|
+
CODE_FENCE_PATTERN,
|
|
32
|
+
FREE_FORM_GATE_COUNT_PATTERN,
|
|
33
|
+
GATE_COUNT_ADDITIONAL_CONTEXT,
|
|
34
|
+
GATE_COUNT_MESSAGE_TEMPLATE,
|
|
35
|
+
GATE_COUNT_SYSTEM_MESSAGE,
|
|
36
|
+
GATE_VALIDATOR_NAME_PATTERN,
|
|
37
|
+
MAX_GATE_COUNT_ISSUES,
|
|
38
|
+
TARGET_RULE_BASENAME,
|
|
39
|
+
TOTAL_GATED_SLICE_COUNT_PATTERN,
|
|
40
|
+
)
|
|
41
|
+
from hooks_constants.multi_edit_reconstruction import ( # noqa: E402
|
|
42
|
+
apply_edits,
|
|
43
|
+
edits_for_tool,
|
|
44
|
+
)
|
|
45
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
46
|
+
from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
|
|
47
|
+
read_hook_input_dictionary_from_stdin,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_target_rule_file(file_path: str) -> bool:
|
|
52
|
+
"""Return whether *file_path* names the docstring-prose rule this hook guards.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
file_path: The destination path of the write or edit.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True when the path's basename is the target rule basename.
|
|
59
|
+
"""
|
|
60
|
+
return os.path.basename(file_path) == TARGET_RULE_BASENAME
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _lines_outside_code_fences(content: str) -> list[str]:
|
|
64
|
+
"""Return the rule lines that sit outside any fenced code block.
|
|
65
|
+
|
|
66
|
+
A line inside a ``` or ~~~ fence pair is example or sample text, not a live
|
|
67
|
+
enumeration, so it is dropped. This mirrors the fence handling in the sibling
|
|
68
|
+
inventory and orphan-file blockers.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
content: The full rule-file text being written.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The lines that lie outside every code fence, in document order.
|
|
75
|
+
"""
|
|
76
|
+
live_lines: list[str] = []
|
|
77
|
+
is_inside_code_fence = False
|
|
78
|
+
for each_line in content.splitlines():
|
|
79
|
+
if CODE_FENCE_PATTERN.match(each_line) is not None:
|
|
80
|
+
is_inside_code_fence = not is_inside_code_fence
|
|
81
|
+
continue
|
|
82
|
+
if is_inside_code_fence:
|
|
83
|
+
continue
|
|
84
|
+
live_lines.append(each_line)
|
|
85
|
+
return live_lines
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _named_free_form_validators(enumeration_window: str) -> list[str]:
|
|
89
|
+
"""Return the distinct free-form gate validators an enumeration window names.
|
|
90
|
+
|
|
91
|
+
The window is the prose between the "<count> more gate validators" phrase and
|
|
92
|
+
the "<count> gated slices" total — the span where the rule enumerates its
|
|
93
|
+
free-form gates. Every backticked ``check_*`` token in that window is
|
|
94
|
+
collected, the ``Args:`` gate (``check_docstring_args_match_signature``) is
|
|
95
|
+
removed since it is counted separately as part of the total rather than the
|
|
96
|
+
"more gate validators" tally, and duplicates are dropped while first-seen order
|
|
97
|
+
is preserved. Scoping to the window keeps a validator named only in the
|
|
98
|
+
worked-example or enforcement sections from inflating the count.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
enumeration_window: The prose between the two count clauses.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Each distinct free-form gate validator name, in first-seen order.
|
|
105
|
+
"""
|
|
106
|
+
all_named_validators: list[str] = []
|
|
107
|
+
already_seen: set[str] = set()
|
|
108
|
+
for each_match in GATE_VALIDATOR_NAME_PATTERN.finditer(enumeration_window):
|
|
109
|
+
validator_name = each_match.group(1)
|
|
110
|
+
if validator_name == ARGS_GATE_VALIDATOR_NAME:
|
|
111
|
+
continue
|
|
112
|
+
if validator_name in already_seen:
|
|
113
|
+
continue
|
|
114
|
+
already_seen.add(validator_name)
|
|
115
|
+
all_named_validators.append(validator_name)
|
|
116
|
+
return all_named_validators
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _count_word_value(count_word: str) -> int | None:
|
|
120
|
+
"""Return the integer a spelled-out count word names, or None when unknown.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
count_word: The captured count word, such as ``four``.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The integer the word maps to, or None when the word is not a known
|
|
127
|
+
spelled-out number.
|
|
128
|
+
"""
|
|
129
|
+
return ALL_NUMBER_WORDS_BY_VALUE.get(count_word.strip().lower())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def find_gate_count_drift(content: str) -> list[str]:
|
|
133
|
+
"""Return one issue per gate-count word that drifts from the named validators.
|
|
134
|
+
|
|
135
|
+
Requires both count clauses to be present and in document order, since the
|
|
136
|
+
validators are enumerated in the window between them: the "<count> more gate
|
|
137
|
+
validators" phrase first, then the "<count> gated slices" total. When either
|
|
138
|
+
clause is absent, or the total clause does not sit after the free-form clause,
|
|
139
|
+
the content is not a judgeable enumeration, so no issue results. The free-form
|
|
140
|
+
validators are the
|
|
141
|
+
distinct backticked ``check_*`` tokens in that window, excluding the ``Args:``
|
|
142
|
+
gate. When the "<count> more gate validators" spelled-out count disagrees with
|
|
143
|
+
the number of validators named in the window, it is an issue; when the
|
|
144
|
+
"<count> gated slices" total is not that count plus one (plus the ``Args:``
|
|
145
|
+
gate), it is an issue. A count word that is not a known spelled-out number
|
|
146
|
+
yields no issue.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
content: The full rule-file text being written.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Each drift issue message, capped at the issue budget.
|
|
153
|
+
"""
|
|
154
|
+
prose_text = "\n".join(_lines_outside_code_fences(content))
|
|
155
|
+
free_form_match = FREE_FORM_GATE_COUNT_PATTERN.search(prose_text)
|
|
156
|
+
total_match = TOTAL_GATED_SLICE_COUNT_PATTERN.search(prose_text)
|
|
157
|
+
if free_form_match is None or total_match is None:
|
|
158
|
+
return []
|
|
159
|
+
if total_match.start() <= free_form_match.end():
|
|
160
|
+
return []
|
|
161
|
+
enumeration_window = prose_text[free_form_match.end() : total_match.start()]
|
|
162
|
+
all_named_validators = _named_free_form_validators(enumeration_window)
|
|
163
|
+
named_count = len(all_named_validators)
|
|
164
|
+
issues: list[str] = []
|
|
165
|
+
stated_free_form = _count_word_value(free_form_match.group(1))
|
|
166
|
+
if stated_free_form is not None and stated_free_form != named_count:
|
|
167
|
+
issues.append(
|
|
168
|
+
_format_issue(
|
|
169
|
+
free_form_match.group(0), stated_free_form, all_named_validators, named_count
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
stated_total = _count_word_value(total_match.group(1))
|
|
173
|
+
if stated_total is not None and stated_total != named_count + 1:
|
|
174
|
+
issues.append(
|
|
175
|
+
_format_issue(total_match.group(0), stated_total, all_named_validators, named_count)
|
|
176
|
+
)
|
|
177
|
+
return issues[:MAX_GATE_COUNT_ISSUES]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _format_issue(
|
|
181
|
+
stated_phrase: str,
|
|
182
|
+
stated_count: int,
|
|
183
|
+
all_named_validators: list[str],
|
|
184
|
+
named_count: int,
|
|
185
|
+
) -> str:
|
|
186
|
+
"""Build one drift-issue message for a stale count clause.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
stated_phrase: The matched count clause text, such as ``four gated slices``.
|
|
190
|
+
stated_count: The integer the clause's count word names.
|
|
191
|
+
all_named_validators: The distinct free-form validators the prose names.
|
|
192
|
+
named_count: The number of distinct free-form validators named.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The formatted block-reason message for this drift.
|
|
196
|
+
"""
|
|
197
|
+
formatted_validators = ", ".join(f"`{each_name}`" for each_name in all_named_validators)
|
|
198
|
+
return GATE_COUNT_MESSAGE_TEMPLATE.format(
|
|
199
|
+
rule_basename=TARGET_RULE_BASENAME,
|
|
200
|
+
stated_phrase=stated_phrase,
|
|
201
|
+
stated_count=stated_count,
|
|
202
|
+
named_count=named_count,
|
|
203
|
+
named_validators=formatted_validators,
|
|
204
|
+
total_count=named_count + 1,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _read_existing_file_content(file_path: str) -> str | None:
|
|
209
|
+
"""Return the current on-disk content of *file_path*, or None when unreadable.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
file_path: The path of the file the edit targets.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
The file's text, or None when the file is missing or cannot be decoded.
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
return Path(file_path).read_text(encoding="utf-8")
|
|
219
|
+
except (OSError, UnicodeDecodeError):
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _post_edit_content(tool_name: str, tool_input: dict, file_path: str) -> str | None:
|
|
224
|
+
"""Return the content the write or edit would leave on disk, or None.
|
|
225
|
+
|
|
226
|
+
For Write the content is the full new payload. For Edit and MultiEdit the
|
|
227
|
+
existing file is read and the replacements applied, so a count clause on a line
|
|
228
|
+
the edit does not touch still participates in the check. When the existing file
|
|
229
|
+
cannot be read, None results so the hook stays silent rather than judging a
|
|
230
|
+
partial fragment.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
tool_name: The intercepted tool — ``Write``, ``Edit``, or ``MultiEdit``.
|
|
234
|
+
tool_input: The tool's input payload.
|
|
235
|
+
file_path: The destination path of the write or edit.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
The reconstructed post-edit content, or None when it cannot be built.
|
|
239
|
+
"""
|
|
240
|
+
if tool_name == "Write":
|
|
241
|
+
content = tool_input.get("content", "")
|
|
242
|
+
return content if isinstance(content, str) and content else None
|
|
243
|
+
existing_content = _read_existing_file_content(file_path)
|
|
244
|
+
if existing_content is None:
|
|
245
|
+
return None
|
|
246
|
+
return apply_edits(existing_content, edits_for_tool(tool_name, tool_input))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _build_block_payload(all_issues: list[str]) -> dict:
|
|
250
|
+
"""Build the PreToolUse deny payload carrying each gate-count drift issue.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
all_issues: The drift-issue messages the check produced.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
The hook-result dictionary the harness reads to deny the write.
|
|
257
|
+
"""
|
|
258
|
+
reason = " | ".join(all_issues)
|
|
259
|
+
return {
|
|
260
|
+
"hookSpecificOutput": {
|
|
261
|
+
"hookEventName": "PreToolUse",
|
|
262
|
+
"permissionDecision": "deny",
|
|
263
|
+
"permissionDecisionReason": reason,
|
|
264
|
+
"additionalContext": GATE_COUNT_ADDITIONAL_CONTEXT,
|
|
265
|
+
},
|
|
266
|
+
"systemMessage": GATE_COUNT_SYSTEM_MESSAGE,
|
|
267
|
+
"suppressOutput": True,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _emit_hook_result(all_hook_data: dict, output_stream: TextIO) -> None:
|
|
272
|
+
"""Write the hook result JSON to the given output stream.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
all_hook_data: The hook-result dictionary to serialize.
|
|
276
|
+
output_stream: The stream the harness reads the decision from.
|
|
277
|
+
"""
|
|
278
|
+
output_stream.write(json.dumps(all_hook_data) + "\n")
|
|
279
|
+
output_stream.flush()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def main() -> None:
|
|
283
|
+
"""Read the PreToolUse payload from stdin and block a stale gate-count edit."""
|
|
284
|
+
input_data = read_hook_input_dictionary_from_stdin()
|
|
285
|
+
if input_data is None:
|
|
286
|
+
sys.exit(0)
|
|
287
|
+
|
|
288
|
+
tool_name = input_data.get("tool_name", "")
|
|
289
|
+
if not isinstance(tool_name, str) or tool_name not in ("Write", "Edit", "MultiEdit"):
|
|
290
|
+
sys.exit(0)
|
|
291
|
+
|
|
292
|
+
tool_input = input_data.get("tool_input", {})
|
|
293
|
+
if not isinstance(tool_input, dict):
|
|
294
|
+
sys.exit(0)
|
|
295
|
+
|
|
296
|
+
file_path = tool_input.get("file_path", "")
|
|
297
|
+
if not isinstance(file_path, str) or not is_target_rule_file(file_path):
|
|
298
|
+
sys.exit(0)
|
|
299
|
+
|
|
300
|
+
post_edit_content = _post_edit_content(tool_name, tool_input, file_path)
|
|
301
|
+
if post_edit_content is None:
|
|
302
|
+
sys.exit(0)
|
|
303
|
+
|
|
304
|
+
gate_count_issues = find_gate_count_drift(post_edit_content)
|
|
305
|
+
if not gate_count_issues:
|
|
306
|
+
sys.exit(0)
|
|
307
|
+
|
|
308
|
+
block_payload = _build_block_payload(gate_count_issues)
|
|
309
|
+
log_hook_block(
|
|
310
|
+
calling_hook_name="docstring_rule_gate_count_blocker.py",
|
|
311
|
+
hook_event="PreToolUse",
|
|
312
|
+
block_reason=block_payload["hookSpecificOutput"]["permissionDecisionReason"],
|
|
313
|
+
tool_name=tool_name,
|
|
314
|
+
offending_input_preview=file_path,
|
|
315
|
+
)
|
|
316
|
+
_emit_hook_result(block_payload, sys.stdout)
|
|
317
|
+
sys.exit(0)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
main()
|
|
@@ -35,6 +35,7 @@ from blocking._gh_body_arg_utils import ( # noqa: E402
|
|
|
35
35
|
get_logical_first_line,
|
|
36
36
|
iter_significant_tokens,
|
|
37
37
|
)
|
|
38
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
38
39
|
|
|
39
40
|
_GH_BODY_SUBCOMMANDS = re.compile(
|
|
40
41
|
r"\bgh\s+(?:"
|
|
@@ -159,6 +160,13 @@ def main() -> None:
|
|
|
159
160
|
"permissionDecisionReason": _CORRECTIVE_MESSAGE,
|
|
160
161
|
}
|
|
161
162
|
}
|
|
163
|
+
log_hook_block(
|
|
164
|
+
calling_hook_name="gh_body_arg_blocker.py",
|
|
165
|
+
hook_event="PreToolUse",
|
|
166
|
+
block_reason=_CORRECTIVE_MESSAGE,
|
|
167
|
+
tool_name=tool_name,
|
|
168
|
+
offending_input_preview=command,
|
|
169
|
+
)
|
|
162
170
|
print(json.dumps(deny_payload))
|
|
163
171
|
sys.stdout.flush()
|
|
164
172
|
sys.exit(0)
|
|
@@ -59,6 +59,7 @@ from hooks_constants.gh_pr_author_swap_constants import (
|
|
|
59
59
|
STATE_FILE_PRIMARY_ACCOUNT_KEY,
|
|
60
60
|
WEB_FLAG_PATTERN,
|
|
61
61
|
)
|
|
62
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
def _active_gh_account() -> str | None:
|
|
@@ -410,6 +411,12 @@ def _emit_deny_payload(reason_text: str) -> None:
|
|
|
410
411
|
"permissionDecisionReason": reason_text,
|
|
411
412
|
}
|
|
412
413
|
}
|
|
414
|
+
log_hook_block(
|
|
415
|
+
calling_hook_name="gh_pr_author_enforcer.py",
|
|
416
|
+
hook_event="PreToolUse",
|
|
417
|
+
block_reason=reason_text,
|
|
418
|
+
tool_name=BASH_TOOL_NAME,
|
|
419
|
+
)
|
|
413
420
|
_write_line(json.dumps(deny_payload), sys.stdout)
|
|
414
421
|
|
|
415
422
|
|
|
@@ -16,6 +16,7 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
|
16
16
|
if _hooks_dir not in sys.path:
|
|
17
17
|
sys.path.insert(0, _hooks_dir)
|
|
18
18
|
|
|
19
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
19
20
|
from hooks_constants.messages import USER_FACING_NOTICE # noqa: E402
|
|
20
21
|
|
|
21
22
|
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
@@ -120,21 +121,26 @@ def main() -> None:
|
|
|
120
121
|
"(no research-mode skill installed; verify with sources or prompt the user via AskUserQuestion with potential options + context)"
|
|
121
122
|
)
|
|
122
123
|
|
|
124
|
+
block_reason = (
|
|
125
|
+
f"ANTI-HALLUCINATION GUARDRAIL: Your response contains hedging language: "
|
|
126
|
+
f"{formatted_term_list}. "
|
|
127
|
+
f"These words signal unverified claims. You MUST rewrite your response "
|
|
128
|
+
f"{skill_reference}\n\n"
|
|
129
|
+
f"Do NOT simply remove the hedging word and keep the unverified claim. "
|
|
130
|
+
f"Do more research to VERIFY it with a source, or prompt the user via AskUserQuestion with some potential options + context if you are unable to find anything online.\n\n"
|
|
131
|
+
f"You MUST re-output the complete, revised response with the corrections applied."
|
|
132
|
+
)
|
|
123
133
|
block_response = {
|
|
124
134
|
"decision": "block",
|
|
125
|
-
"reason":
|
|
126
|
-
f"ANTI-HALLUCINATION GUARDRAIL: Your response contains hedging language: "
|
|
127
|
-
f"{formatted_term_list}. "
|
|
128
|
-
f"These words signal unverified claims. You MUST rewrite your response "
|
|
129
|
-
f"{skill_reference}\n\n"
|
|
130
|
-
f"Do NOT simply remove the hedging word and keep the unverified claim. "
|
|
131
|
-
f"Do more research to VERIFY it with a source, or prompt the user via AskUserQuestion with some potential options + context if you are unable to find anything online.\n\n"
|
|
132
|
-
f"You MUST re-output the complete, revised response with the corrections applied."
|
|
133
|
-
),
|
|
135
|
+
"reason": block_reason,
|
|
134
136
|
"systemMessage": USER_FACING_NOTICE,
|
|
135
137
|
"suppressOutput": True,
|
|
136
138
|
}
|
|
137
|
-
|
|
139
|
+
log_hook_block(
|
|
140
|
+
calling_hook_name="hedging_language_blocker.py",
|
|
141
|
+
hook_event="Stop",
|
|
142
|
+
block_reason=block_reason,
|
|
143
|
+
)
|
|
138
144
|
print(json.dumps(block_response))
|
|
139
145
|
sys.exit(0)
|
|
140
146
|
|
|
@@ -39,6 +39,7 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
|
39
39
|
if _hooks_dir not in sys.path:
|
|
40
40
|
sys.path.insert(0, _hooks_dir)
|
|
41
41
|
|
|
42
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
42
43
|
from hooks_constants.hook_prose_detector_consistency_constants import ( # noqa: E402
|
|
43
44
|
CONSTANTS_MODULE_SUFFIX,
|
|
44
45
|
CORRECTIVE_MESSAGE,
|
|
@@ -141,6 +142,12 @@ def main() -> None:
|
|
|
141
142
|
"permissionDecisionReason": CORRECTIVE_MESSAGE,
|
|
142
143
|
}
|
|
143
144
|
}
|
|
145
|
+
log_hook_block(
|
|
146
|
+
calling_hook_name="hook_prose_detector_consistency.py",
|
|
147
|
+
hook_event="PreToolUse",
|
|
148
|
+
block_reason=CORRECTIVE_MESSAGE,
|
|
149
|
+
tool_name=tool_name,
|
|
150
|
+
)
|
|
144
151
|
print(json.dumps(deny_payload))
|
|
145
152
|
sys.stdout.flush()
|
|
146
153
|
sys.exit(0)
|
|
@@ -18,6 +18,7 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
|
18
18
|
if _hooks_dir not in sys.path:
|
|
19
19
|
sys.path.insert(0, _hooks_dir)
|
|
20
20
|
|
|
21
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
21
22
|
from hooks_constants.messages import USER_FACING_INTENT_ENDING_NOTICE # noqa: E402
|
|
22
23
|
|
|
23
24
|
|
|
@@ -131,22 +132,27 @@ def main() -> None:
|
|
|
131
132
|
if not find_intent_only_ending(assistant_message):
|
|
132
133
|
sys.exit(0)
|
|
133
134
|
|
|
135
|
+
block_reason = (
|
|
136
|
+
"LONG-HORIZON-AUTONOMY GUARDRAIL: Your turn ends on a promise about work "
|
|
137
|
+
"that is not yet done, rather than doing it. Do the work NOW with tool calls "
|
|
138
|
+
"instead of describing what you are about to do.\n\n"
|
|
139
|
+
"If the work is genuinely blocked on input only the user can give, route the "
|
|
140
|
+
"ask through an AskUserQuestion tool call and end the turn cleanly. Otherwise, "
|
|
141
|
+
"carry out the stated action this turn.\n\n"
|
|
142
|
+
"You MUST re-output the complete response with the work actually performed, "
|
|
143
|
+
"per the long-horizon-autonomy rule."
|
|
144
|
+
)
|
|
134
145
|
block_response = {
|
|
135
146
|
"decision": "block",
|
|
136
|
-
"reason":
|
|
137
|
-
"LONG-HORIZON-AUTONOMY GUARDRAIL: Your turn ends on a promise about work "
|
|
138
|
-
"that is not yet done, rather than doing it. Do the work NOW with tool calls "
|
|
139
|
-
"instead of describing what you are about to do.\n\n"
|
|
140
|
-
"If the work is genuinely blocked on input only the user can give, route the "
|
|
141
|
-
"ask through an AskUserQuestion tool call and end the turn cleanly. Otherwise, "
|
|
142
|
-
"carry out the stated action this turn.\n\n"
|
|
143
|
-
"You MUST re-output the complete response with the work actually performed, "
|
|
144
|
-
"per the long-horizon-autonomy rule."
|
|
145
|
-
),
|
|
147
|
+
"reason": block_reason,
|
|
146
148
|
"systemMessage": USER_FACING_INTENT_ENDING_NOTICE,
|
|
147
149
|
"suppressOutput": True,
|
|
148
150
|
}
|
|
149
|
-
|
|
151
|
+
log_hook_block(
|
|
152
|
+
calling_hook_name="intent_only_ending_blocker.py",
|
|
153
|
+
hook_event="Stop",
|
|
154
|
+
block_reason=block_reason,
|
|
155
|
+
)
|
|
150
156
|
print(json.dumps(block_response))
|
|
151
157
|
sys.exit(0)
|
|
152
158
|
|
|
@@ -31,6 +31,7 @@ from hooks_constants.md_to_html_blocker_constants import ( # noqa: E402
|
|
|
31
31
|
PACKAGES_TOP_LEVEL_SEGMENT,
|
|
32
32
|
PLUGIN_ROOT_MARKER_DIRECTORY_NAME,
|
|
33
33
|
)
|
|
34
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
34
35
|
from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
|
|
35
36
|
read_hook_input_dictionary_from_stdin,
|
|
36
37
|
)
|
|
@@ -123,17 +124,24 @@ def main() -> None:
|
|
|
123
124
|
if is_exempt_path(file_path):
|
|
124
125
|
sys.exit(0)
|
|
125
126
|
|
|
127
|
+
deny_reason = _block_reason(file_path)
|
|
126
128
|
block_payload = {
|
|
127
129
|
"hookSpecificOutput": {
|
|
128
130
|
"hookEventName": "PreToolUse",
|
|
129
131
|
"permissionDecision": "deny",
|
|
130
|
-
"permissionDecisionReason":
|
|
132
|
+
"permissionDecisionReason": deny_reason,
|
|
131
133
|
"additionalContext": _block_context(),
|
|
132
134
|
},
|
|
133
135
|
"systemMessage": _block_system_message(),
|
|
134
136
|
"suppressOutput": True,
|
|
135
137
|
}
|
|
136
|
-
|
|
138
|
+
log_hook_block(
|
|
139
|
+
calling_hook_name="md_to_html_blocker.py",
|
|
140
|
+
hook_event="PreToolUse",
|
|
141
|
+
block_reason=deny_reason,
|
|
142
|
+
tool_name=tool_name,
|
|
143
|
+
offending_input_preview=file_path,
|
|
144
|
+
)
|
|
137
145
|
_emit_hook_result(block_payload, sys.stdout)
|
|
138
146
|
sys.exit(0)
|
|
139
147
|
|
|
@@ -31,6 +31,7 @@ from hooks_constants.open_questions_in_plans_blocker_constants import ( # noqa:
|
|
|
31
31
|
PLANS_PATH_SEGMENT,
|
|
32
32
|
UNREADABLE_FILE_SYNTHETIC_CONTENT,
|
|
33
33
|
)
|
|
34
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
34
35
|
from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
|
|
35
36
|
read_hook_input_dictionary_from_stdin,
|
|
36
37
|
)
|
|
@@ -236,17 +237,24 @@ def main() -> None:
|
|
|
236
237
|
if not _content_has_open_questions(candidate_content):
|
|
237
238
|
sys.exit(0)
|
|
238
239
|
|
|
240
|
+
deny_reason = _block_reason(file_path)
|
|
239
241
|
block_payload = {
|
|
240
242
|
"hookSpecificOutput": {
|
|
241
243
|
"hookEventName": "PreToolUse",
|
|
242
244
|
"permissionDecision": "deny",
|
|
243
|
-
"permissionDecisionReason":
|
|
245
|
+
"permissionDecisionReason": deny_reason,
|
|
244
246
|
"additionalContext": _block_context(),
|
|
245
247
|
},
|
|
246
248
|
"systemMessage": _block_system_message(),
|
|
247
249
|
"suppressOutput": True,
|
|
248
250
|
}
|
|
249
|
-
|
|
251
|
+
log_hook_block(
|
|
252
|
+
calling_hook_name="open_questions_in_plans_blocker.py",
|
|
253
|
+
hook_event="PreToolUse",
|
|
254
|
+
block_reason=deny_reason,
|
|
255
|
+
tool_name=tool_name,
|
|
256
|
+
offending_input_preview=file_path,
|
|
257
|
+
)
|
|
250
258
|
_emit_hook_result(block_payload, sys.stdout)
|
|
251
259
|
sys.exit(0)
|
|
252
260
|
|