claude-dev-env 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/pr-loop/worker-spawn.md +3 -1
- 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 +4 -4
- 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 +83 -13
- package/scripts/grok_headless_runner.py +148 -18
- package/scripts/resolve_worker_spawn.py +56 -10
- package/scripts/spawn_grok_batch.py +81 -23
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +547 -10
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +185 -15
- package/scripts/test_spawn_grok_batch.py +326 -22
- 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 +133 -8
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
- package/skills/autoconverge/workflow/converge.mjs +113 -31
- 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 +10 -3
- package/skills/grok-spawn/reference/flag-profiles.md +3 -1
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/team-advisor/SKILL.md +4 -1
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`start` over `initiate`, `enough` over `sufficient`.
|
|
6
|
-
AskUserQuestion (its question and option
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
"""PreToolUse hook guarding AskUserQuestion prose and .md writes.
|
|
3
|
+
|
|
4
|
+
Two checks run. The word check reaches for the everyday word over the formal
|
|
5
|
+
one: `use` over `utilize`, `start` over `initiate`, `enough` over `sufficient`.
|
|
6
|
+
It reads both guarded surfaces -- AskUserQuestion (its question and option
|
|
7
|
+
prose) and Write/Edit/MultiEdit targeting a .md file -- with code fences,
|
|
8
|
+
inline code, blockquotes, URLs, and file paths stripped first, so exact
|
|
9
|
+
identifiers and paths are never flagged.
|
|
10
|
+
|
|
11
|
+
The lean-block check reads the AskUserQuestion surface alone. AskUserQuestion
|
|
12
|
+
renders as one plain text block, so the question text and each option
|
|
13
|
+
description stay short and unformatted: a fenced block, a heading, a table row,
|
|
14
|
+
a bullet or numbered list marker, a second paragraph, or prose past the
|
|
15
|
+
sentence and word caps all belong in chat text before the call. Structure is
|
|
16
|
+
read at block level on the raw text, with line endings folded to one spelling
|
|
17
|
+
first; an inline code span naming a path, a flag, or a command weighs one word
|
|
18
|
+
against the length caps.
|
|
19
|
+
|
|
20
|
+
See the plain-language and ask-user-question-required rules for the full
|
|
21
|
+
guidance this hook enforces.
|
|
11
22
|
"""
|
|
12
23
|
|
|
13
24
|
import json
|
|
@@ -22,20 +33,36 @@ if _hooks_dir not in sys.path:
|
|
|
22
33
|
from blocking.code_rules_shared import is_ephemeral_path # noqa: E402
|
|
23
34
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
24
35
|
from hooks_constants.plain_language_blocker_constants import ( # noqa: E402
|
|
36
|
+
ALL_CHAT_DETAIL_MARKERS,
|
|
37
|
+
ALL_LINE_ENDING_REPLACEMENTS,
|
|
25
38
|
ALL_SOFTWARE_TERMS,
|
|
26
39
|
ALL_TERM_PATTERNS,
|
|
27
40
|
ALL_WRITE_EDIT_TOOL_NAMES,
|
|
28
41
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
29
42
|
BLOCKQUOTE_LINE_PATTERN,
|
|
43
|
+
COUNTABLE_WORD_PATTERN,
|
|
30
44
|
DOT_CLAUDE_DIRECTORY_NAME,
|
|
31
45
|
FENCED_CODE_BLOCK_PATTERN,
|
|
32
46
|
FILE_PATH_PATTERN,
|
|
33
47
|
INLINE_CODE_PATTERN,
|
|
48
|
+
INLINE_CODE_PLACEHOLDER,
|
|
49
|
+
INLINE_CODE_SPAN_PATTERN,
|
|
50
|
+
LEAN_QUESTION_BLOCK_GUIDANCE,
|
|
51
|
+
LEAN_QUESTION_BLOCK_PREFIX,
|
|
52
|
+
LEAN_QUESTION_VIOLATION_SEPARATOR,
|
|
34
53
|
MARKDOWN_EXTENSION,
|
|
54
|
+
MAXIMUM_OPTION_DESCRIPTION_SENTENCE_COUNT,
|
|
55
|
+
MAXIMUM_OPTION_DESCRIPTION_WORD_COUNT,
|
|
56
|
+
MAXIMUM_QUESTION_SENTENCE_COUNT,
|
|
57
|
+
MAXIMUM_QUESTION_WORD_COUNT,
|
|
58
|
+
OPTION_DESCRIPTION_SURFACE_NAME,
|
|
35
59
|
PROJECT_ALLOWLIST_FILENAME,
|
|
36
60
|
PROJECT_ROOT_WALK_LIMIT,
|
|
61
|
+
QUESTION_SURFACE_NAME,
|
|
37
62
|
REPOSITORY_MARKER_NAME,
|
|
63
|
+
SENTENCE_BOUNDARY_PATTERN,
|
|
38
64
|
URL_PATTERN,
|
|
65
|
+
USER_FACING_LEAN_QUESTION_NOTICE,
|
|
39
66
|
USER_FACING_PLAIN_LANGUAGE_NOTICE,
|
|
40
67
|
)
|
|
41
68
|
|
|
@@ -219,6 +246,229 @@ def build_block_reason(all_matches: list[tuple[str, str]]) -> str:
|
|
|
219
246
|
)
|
|
220
247
|
|
|
221
248
|
|
|
249
|
+
def _normalize_line_endings(prose_text: str) -> str:
|
|
250
|
+
"""Return the prose with every line ending spelled as a bare line feed.
|
|
251
|
+
|
|
252
|
+
::
|
|
253
|
+
|
|
254
|
+
in: "Which gate?\\r\\n\\r\\nThe write gate reads it."
|
|
255
|
+
out: "Which gate?\\n\\nThe write gate reads it."
|
|
256
|
+
|
|
257
|
+
The structure markers read line boundaries, so a payload carrying carriage
|
|
258
|
+
returns is normalized once here rather than at each pattern.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
prose_text: One question text or one option description.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
The prose with carriage returns folded into line feeds.
|
|
265
|
+
"""
|
|
266
|
+
normalized_text = prose_text
|
|
267
|
+
for each_line_ending, each_replacement in ALL_LINE_ENDING_REPLACEMENTS:
|
|
268
|
+
normalized_text = normalized_text.replace(each_line_ending, each_replacement)
|
|
269
|
+
return normalized_text
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _mask_inline_code(prose_text: str) -> str:
|
|
273
|
+
"""Return the prose with each inline code span collapsed to a single word.
|
|
274
|
+
|
|
275
|
+
::
|
|
276
|
+
|
|
277
|
+
in: "Does `git diff --cached` list it?"
|
|
278
|
+
out: "Does code list it?"
|
|
279
|
+
|
|
280
|
+
A span names a path, a flag, or a command the reader needs verbatim, so it
|
|
281
|
+
weighs one word against the length caps. Only the length counts read the
|
|
282
|
+
masked prose: the structure markers read the raw text, so a one-line closed
|
|
283
|
+
fence still reads as a fence rather than as a span.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
prose_text: One question text or one option description.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
The prose with every single-line inline code span replaced.
|
|
290
|
+
"""
|
|
291
|
+
return INLINE_CODE_SPAN_PATTERN.sub(INLINE_CODE_PLACEHOLDER, prose_text)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _count_prose_sentences(prose_text: str) -> int:
|
|
295
|
+
"""Return how many sentences one piece of question-block prose carries.
|
|
296
|
+
|
|
297
|
+
::
|
|
298
|
+
|
|
299
|
+
in: "Which gate runs first? Both read the same lines."
|
|
300
|
+
out: 2
|
|
301
|
+
|
|
302
|
+
A sentence closes on `.`, `!`, or `?` followed by a capitalized word or the
|
|
303
|
+
end of the text, so an abbreviation mid-sentence closes nothing.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
prose_text: One question text or one option description.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
The count of sentence closings the prose carries.
|
|
310
|
+
"""
|
|
311
|
+
return len(SENTENCE_BOUNDARY_PATTERN.findall(prose_text))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _count_prose_words(prose_text: str) -> int:
|
|
315
|
+
"""Return how many reader-visible words one piece of prose carries.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
prose_text: One question text or one option description.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
The count of whitespace-separated tokens carrying a letter or digit.
|
|
322
|
+
"""
|
|
323
|
+
return len(COUNTABLE_WORD_PATTERN.findall(prose_text))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _find_chat_detail_markers(prose_text: str) -> list[str]:
|
|
327
|
+
"""Return the name of each chat-detail marker the prose carries.
|
|
328
|
+
|
|
329
|
+
::
|
|
330
|
+
|
|
331
|
+
ok: "Which gate should run first?" -> []
|
|
332
|
+
flag: "Which gate?\\n- write\\n- commit" -> ["a bullet or numbered
|
|
333
|
+
list marker"]
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
prose_text: One question text or one option description.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
One marker name per marker found, in the order the markers are tried.
|
|
340
|
+
"""
|
|
341
|
+
return [
|
|
342
|
+
each_marker_name
|
|
343
|
+
for each_pattern, each_marker_name in ALL_CHAT_DETAIL_MARKERS
|
|
344
|
+
if each_pattern.search(prose_text)
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _find_lean_block_violations(
|
|
349
|
+
prose_text: str,
|
|
350
|
+
surface_name: str,
|
|
351
|
+
maximum_sentence_count: int,
|
|
352
|
+
maximum_word_count: int,
|
|
353
|
+
) -> list[str]:
|
|
354
|
+
"""Return every lean-block rule one piece of question-block prose breaks.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
prose_text: One question text or one option description.
|
|
358
|
+
surface_name: How the violation text names this piece of prose.
|
|
359
|
+
maximum_sentence_count: The sentence cap this piece answers to.
|
|
360
|
+
maximum_word_count: The word cap this piece answers to.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
One violation text per broken rule, empty when the prose is lean.
|
|
364
|
+
|
|
365
|
+
The structure markers read the line-ending-normalized prose; the sentence
|
|
366
|
+
and word counts read that prose with its inline code spans masked.
|
|
367
|
+
"""
|
|
368
|
+
normalized_text = _normalize_line_endings(prose_text)
|
|
369
|
+
masked_text = _mask_inline_code(normalized_text)
|
|
370
|
+
all_violations = [
|
|
371
|
+
f"{surface_name} carries {each_marker_name}"
|
|
372
|
+
for each_marker_name in _find_chat_detail_markers(normalized_text)
|
|
373
|
+
]
|
|
374
|
+
sentence_count = _count_prose_sentences(masked_text)
|
|
375
|
+
if sentence_count > maximum_sentence_count:
|
|
376
|
+
all_violations.append(
|
|
377
|
+
f"{surface_name} runs {sentence_count} sentences, over the "
|
|
378
|
+
f"{maximum_sentence_count}-sentence cap"
|
|
379
|
+
)
|
|
380
|
+
word_count = _count_prose_words(masked_text)
|
|
381
|
+
if word_count > maximum_word_count:
|
|
382
|
+
all_violations.append(
|
|
383
|
+
f"{surface_name} runs {word_count} words, over the "
|
|
384
|
+
f"{maximum_word_count}-word cap"
|
|
385
|
+
)
|
|
386
|
+
return all_violations
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _violations_for_one_question(question_by_key: dict) -> list[str]:
|
|
390
|
+
"""Return the lean-block violations one question entry carries.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
question_by_key: One entry of the AskUserQuestion questions list.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
One violation text per broken rule across the question text and every
|
|
397
|
+
option description the entry carries.
|
|
398
|
+
"""
|
|
399
|
+
all_violations: list[str] = []
|
|
400
|
+
question_text = question_by_key.get("question", "")
|
|
401
|
+
if isinstance(question_text, str):
|
|
402
|
+
all_violations.extend(
|
|
403
|
+
_find_lean_block_violations(
|
|
404
|
+
question_text,
|
|
405
|
+
QUESTION_SURFACE_NAME,
|
|
406
|
+
MAXIMUM_QUESTION_SENTENCE_COUNT,
|
|
407
|
+
MAXIMUM_QUESTION_WORD_COUNT,
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
all_options = question_by_key.get("options", [])
|
|
411
|
+
if not isinstance(all_options, list):
|
|
412
|
+
return all_violations
|
|
413
|
+
for each_option in all_options:
|
|
414
|
+
if not isinstance(each_option, dict):
|
|
415
|
+
continue
|
|
416
|
+
option_description = each_option.get("description", "")
|
|
417
|
+
if not isinstance(option_description, str):
|
|
418
|
+
continue
|
|
419
|
+
all_violations.extend(
|
|
420
|
+
_find_lean_block_violations(
|
|
421
|
+
option_description,
|
|
422
|
+
OPTION_DESCRIPTION_SURFACE_NAME,
|
|
423
|
+
MAXIMUM_OPTION_DESCRIPTION_SENTENCE_COUNT,
|
|
424
|
+
MAXIMUM_OPTION_DESCRIPTION_WORD_COUNT,
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
return all_violations
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def find_question_block_violations(tool_input: dict) -> list[str]:
|
|
431
|
+
"""Return every lean-block violation an AskUserQuestion payload carries.
|
|
432
|
+
|
|
433
|
+
::
|
|
434
|
+
|
|
435
|
+
ok: "Which gate should run first?" + "Runs on every write." -> []
|
|
436
|
+
flag: a question with a three-bullet plan under it
|
|
437
|
+
-> ["the question carries a bullet or numbered list marker"]
|
|
438
|
+
|
|
439
|
+
Each violation text appears once, so two options breaking the same cap read
|
|
440
|
+
as one line.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
tool_input: The AskUserQuestion tool input carrying the questions list.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
One violation text per broken rule, in first-seen order, empty when the
|
|
447
|
+
whole question block is lean.
|
|
448
|
+
"""
|
|
449
|
+
all_questions = tool_input.get("questions", [])
|
|
450
|
+
if not isinstance(all_questions, list):
|
|
451
|
+
return []
|
|
452
|
+
all_violations: list[str] = []
|
|
453
|
+
for each_question in all_questions:
|
|
454
|
+
if isinstance(each_question, dict):
|
|
455
|
+
all_violations.extend(_violations_for_one_question(each_question))
|
|
456
|
+
return list(dict.fromkeys(all_violations))
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def build_lean_block_reason(all_violations: list[str]) -> str:
|
|
460
|
+
"""Return a deny reason naming each chat detail and where the detail belongs.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
all_violations: The violation texts the question block earned.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
The full deny reason the model rewrites its question block against.
|
|
467
|
+
"""
|
|
468
|
+
violation_list = LEAN_QUESTION_VIOLATION_SEPARATOR.join(all_violations)
|
|
469
|
+
return f"{LEAN_QUESTION_BLOCK_PREFIX}{violation_list}. {LEAN_QUESTION_BLOCK_GUIDANCE}"
|
|
470
|
+
|
|
471
|
+
|
|
222
472
|
def _collect_ask_user_question_prose(tool_input: dict) -> str:
|
|
223
473
|
all_questions = tool_input.get("questions", [])
|
|
224
474
|
if not isinstance(all_questions, list):
|
|
@@ -273,6 +523,21 @@ def _collect_prose_for_tool(tool_name: str, tool_input: dict) -> str:
|
|
|
273
523
|
return ""
|
|
274
524
|
|
|
275
525
|
|
|
526
|
+
def _notice_for_deny_reason(deny_reason: str) -> str:
|
|
527
|
+
"""Return the short notice matching the check that produced a deny reason.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
deny_reason: The permissionDecisionReason text for the denial.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
The lean-block notice for a question-block denial, the word-swap notice
|
|
534
|
+
otherwise.
|
|
535
|
+
"""
|
|
536
|
+
if deny_reason.startswith(LEAN_QUESTION_BLOCK_PREFIX):
|
|
537
|
+
return USER_FACING_LEAN_QUESTION_NOTICE
|
|
538
|
+
return USER_FACING_PLAIN_LANGUAGE_NOTICE
|
|
539
|
+
|
|
540
|
+
|
|
276
541
|
def build_deny_payload(deny_reason: str) -> dict[str, object]:
|
|
277
542
|
"""Build the full deny payload the hook writes for a deny-reason string.
|
|
278
543
|
|
|
@@ -297,7 +562,7 @@ def build_deny_payload(deny_reason: str) -> dict[str, object]:
|
|
|
297
562
|
"permissionDecision": "deny",
|
|
298
563
|
"permissionDecisionReason": deny_reason,
|
|
299
564
|
},
|
|
300
|
-
"systemMessage":
|
|
565
|
+
"systemMessage": _notice_for_deny_reason(deny_reason),
|
|
301
566
|
"suppressOutput": True,
|
|
302
567
|
}
|
|
303
568
|
|
|
@@ -308,17 +573,19 @@ def _emit_deny(deny_reason: str, output_stream: TextIO) -> None:
|
|
|
308
573
|
|
|
309
574
|
|
|
310
575
|
def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
311
|
-
"""Decide whether a payload
|
|
576
|
+
"""Decide whether a payload carries a question block or heavy words to block.
|
|
312
577
|
|
|
313
|
-
|
|
314
|
-
|
|
578
|
+
An AskUserQuestion payload meets the lean-block check first, so a question
|
|
579
|
+
text or an option description carrying chat detail returns a LEAN_QUESTION
|
|
580
|
+
deny reason. Every guarded payload then meets the word scan, which returns a
|
|
581
|
+
PLAIN_LANGUAGE deny reason for the first heavy word it finds.
|
|
315
582
|
|
|
316
583
|
Args:
|
|
317
584
|
payload_by_key: The PreToolUse payload with tool_name and tool_input.
|
|
318
585
|
|
|
319
586
|
Returns:
|
|
320
|
-
The permissionDecisionReason text when the
|
|
321
|
-
|
|
587
|
+
The permissionDecisionReason text when the payload is denied, or None
|
|
588
|
+
when it is allowed.
|
|
322
589
|
"""
|
|
323
590
|
raw_tool_name = payload_by_key.get("tool_name", "")
|
|
324
591
|
raw_tool_input = payload_by_key.get("tool_input", {})
|
|
@@ -332,6 +599,11 @@ def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
|
332
599
|
):
|
|
333
600
|
return None
|
|
334
601
|
|
|
602
|
+
if raw_tool_name == ASK_USER_QUESTION_TOOL_NAME:
|
|
603
|
+
all_block_violations = find_question_block_violations(raw_tool_input)
|
|
604
|
+
if all_block_violations:
|
|
605
|
+
return build_lean_block_reason(all_block_violations)
|
|
606
|
+
|
|
335
607
|
prose_text = _collect_prose_for_tool(raw_tool_name, raw_tool_input)
|
|
336
608
|
if not prose_text:
|
|
337
609
|
return None
|