claude-dev-env 2.7.1 → 2.9.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.
Files changed (127) hide show
  1. package/CLAUDE.md +7 -1
  2. package/_shared/CLAUDE.md +1 -0
  3. package/_shared/advisor/advisor-protocol.md +19 -9
  4. package/_shared/pr-loop/audit-contract.md +4 -4
  5. package/_shared/pr-loop/precatch-rubric.md +2 -2
  6. package/_shared/process-tree/CLAUDE.md +41 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  8. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  9. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  10. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  11. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  12. package/agents/clean-coder.md +9 -19
  13. package/agents/code-quality-agent.md +6 -5
  14. package/agents/deep-research.md +7 -24
  15. package/agents/docs-agent.md +1 -27
  16. package/agents/issue-tracker.md +1 -7
  17. package/agents/skill-writer-agent.md +1 -2
  18. package/agents/test_agent_frontmatter.py +335 -12
  19. package/docs/CODE_RULES.md +4 -2
  20. package/docs/references/CLAUDE.md +2 -2
  21. package/docs/references/advisor-tool.md +44 -6
  22. package/docs/references/team-advisor-skill.md +14 -8
  23. package/hooks/blocking/CLAUDE.md +2 -0
  24. package/hooks/blocking/fable_spawn_gate.py +187 -0
  25. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  26. package/hooks/blocking/plain_language_blocker.py +287 -15
  27. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  28. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  29. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  30. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  31. package/hooks/git-hooks/CLAUDE.md +2 -2
  32. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  33. package/hooks/git-hooks/pre_push.py +343 -54
  34. package/hooks/git-hooks/test_pre_push.py +852 -6
  35. package/hooks/hooks.json +9 -19
  36. package/hooks/hooks_constants/CLAUDE.md +2 -0
  37. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  39. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  40. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  41. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  43. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  46. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  47. package/output-styles/CLAUDE.md +17 -0
  48. package/output-styles/caveman-agent.md +37 -0
  49. package/package.json +2 -1
  50. package/rules/CLAUDE.md +1 -0
  51. package/rules/ask-user-question-required.md +26 -0
  52. package/rules/claims-as-quotes.md +65 -0
  53. package/rules/code-standards.md +33 -7
  54. package/rules/eli11-replies.md +1 -1
  55. package/scripts/CLAUDE.md +3 -3
  56. package/scripts/_code_review_test_support.py +6 -0
  57. package/scripts/check.ps1 +18 -5
  58. package/scripts/claude_chain_runner.py +203 -31
  59. package/scripts/codec_forwarding_test_support.py +2 -0
  60. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  61. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  62. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  63. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  64. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
  65. package/scripts/grok_headless_runner.py +16 -83
  66. package/scripts/grok_patch_artifacts.py +123 -0
  67. package/scripts/grok_run_ledger.py +318 -0
  68. package/scripts/spawn_grok_batch.py +591 -10
  69. package/scripts/test_claude_chain_runner.py +358 -0
  70. package/scripts/test_grok_headless_runner.py +18 -63
  71. package/scripts/test_grok_patch_artifacts.py +82 -0
  72. package/scripts/test_grok_run_ledger.py +116 -0
  73. package/scripts/test_invoke_code_review.py +298 -0
  74. package/scripts/test_resolve_worker_spawn.py +6 -0
  75. package/scripts/test_spawn_grok_batch.py +396 -0
  76. package/scripts/tests/CLAUDE.md +1 -0
  77. package/scripts/tests/test_grok_worker_constants.py +59 -0
  78. package/skills/CLAUDE.md +4 -2
  79. package/skills/_shared/CLAUDE.md +37 -4
  80. package/skills/_shared/advisor/CLAUDE.md +9 -0
  81. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  82. package/skills/_shared/advisor/scripts/README.md +9 -0
  83. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  84. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  85. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  86. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  87. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  88. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  89. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  90. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  91. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  92. package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
  93. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  94. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  95. package/skills/_shared/pr-loop/state-schema.md +5 -0
  96. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  97. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  98. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  99. package/skills/autoconverge/workflow/converge.mjs +66 -33
  100. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  101. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  102. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  103. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  104. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  105. package/skills/e-code-review/SKILL.md +15 -9
  106. package/skills/e-code-review/reference/fix.md +29 -7
  107. package/skills/e-code-review/reference/loop.md +230 -14
  108. package/skills/e-code-review/reference/low.md +33 -15
  109. package/skills/e-code-review/reference/medium.md +55 -21
  110. package/skills/e-code-review/reference/runner-selection.md +40 -0
  111. package/skills/e-code-review/reference/xhigh.md +30 -12
  112. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  113. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  114. package/skills/e-code-review/scripts/grok_code_review.py +221 -0
  115. package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
  116. package/skills/fresh-branch/CLAUDE.md +5 -5
  117. package/skills/fresh-branch/SKILL.md +14 -6
  118. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  119. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  120. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  121. package/skills/grok-spawn/SKILL.md +10 -0
  122. package/skills/orchestrator/SKILL.md +9 -1
  123. package/skills/orchestrator-refresh/SKILL.md +5 -1
  124. package/skills/task-build/reference/tool-routing.md +3 -0
  125. package/skills/team-advisor/SKILL.md +23 -41
  126. package/system-prompts/software-engineer.xml +6 -3
  127. package/skills/test_markdown_link_integrity.py +0 -107
@@ -1,13 +1,51 @@
1
1
  # Advisor Tool
2
2
 
3
- `advisor()` is a review tool backed by a stronger reviewer model. It takes no parameters — calling it forwards the entire conversation history automatically, so the reviewer sees the task, every tool call made, and every result seen so far.
3
+ Canonical consult timing and weight for any stronger-reviewer path: the native `advisor()` tool, `/team-advisor`, and the shared warm advisor.
4
4
 
5
- ## When to call it
5
+ Source bones: [Anthropic Advisor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool) (Suggested system prompt for coding tasks). API shape, model pairs, cost, and caching live there. This file carries only the call rules a session needs every time.
6
6
 
7
- Call `advisor()` before substantive work: before writing, before committing to an interpretation, before building on an assumption. Orientation work — finding files, fetching a source, seeing what exists — comes first; call `advisor()` once that orientation is done and before the substantive step begins.
7
+ ## What it is
8
8
 
9
- Treat the advisor as more experienced than the calling agent. Consult it whenever a path forward is uncertain.
9
+ `advisor()` is a no-parameter review call. The platform forwards the full conversation (task, tool calls, results) to a stronger model. The executor continues with that guidance.
10
10
 
11
- ## Availability
11
+ When `advisor()` is absent, use `/team-advisor` (see `team-advisor-skill.md`).
12
12
 
13
- `advisor()` is present only in environments where it has been configured as a tool. Check the available tools before relying on it; when it is absent, fall back to `/team-advisor` (see `team-advisor-skill.md` in this directory).
13
+ ## When to call
14
+
15
+ Call **before substantive work** — before writing, before locking an interpretation, before building on an assumption.
16
+
17
+ If the task needs orientation first (find files, fetch a source, see what exists), do that, then call. Orientation is not substantive work. Writing, editing, and declaring an answer are.
18
+
19
+ Also call:
20
+
21
+ - **When you believe the task is complete.** Before this call, make the deliverable durable: write the file, save the result, commit the change. The call takes time; if the session ends during it, a durable result survives and an unwritten one does not.
22
+ - **When stuck** — errors recur, approach does not converge, results do not fit.
23
+ - **When considering a change of approach.**
24
+
25
+ On tasks longer than a few steps, call at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you do not need repeated calls — most value is on the first call, before the approach hardens.
26
+
27
+ Call for design, architecture, and risk questions where you will not touch a file. If the response would be analysis or a recommendation with no other tool calls, call first. That judgment is where a second opinion is highest value. Simple factual lookups and arithmetic do not need a call.
28
+
29
+ ## Hard rule
30
+
31
+ Your first write, edit, or state-changing shell call on a task must be preceded by an advisor call in the same or an earlier turn. Read-only orientation (`ls`, `cat`, `grep`, `find`, and harness equivalents) is not state-changing. This is a checkpoint, not a difficulty judgment. It applies to one-line edits too.
32
+
33
+ ## How to treat advice
34
+
35
+ Give the advice serious weight. If a step fails empirically, or primary-source evidence contradicts a claim (the file says X, the paper states Y), adapt. A passing self-test is not evidence the advice is wrong — it is evidence the test does not check what the advice is checking.
36
+
37
+ If your data points one way and the advisor points another: do not silently switch. Surface the conflict in one more call — "I found X, you suggest Y, which constraint breaks the tie?" A reconcile call is cheaper than the wrong branch.
38
+
39
+ ## Brevity cue
40
+
41
+ When the consult path supports a free-text brief, append:
42
+
43
+ `(Advisor: please keep your guidance under 80 words — I need a focused starting point, not a comprehensive plan.)`
44
+
45
+ ## Related
46
+
47
+ | Doc | Holds |
48
+ |---|---|
49
+ | [Anthropic Advisor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool) | API shape, model pairs, cost, caching, full best practices |
50
+ | `team-advisor-skill.md` | Standing warm advisor when `advisor()` is missing |
51
+ | `~/.claude/_shared/advisor/advisor-protocol.md` | Host bind, floor walk, lifecycle, executor paste blocks |
@@ -1,14 +1,20 @@
1
- # Team-Advisor Skill Invocation
1
+ # Team-Advisor Skill
2
2
 
3
- `/team-advisor` spawns a standing review agent for the session at the strongest reachable model tier, run at the highest reasoning effort. Use it to get a second opinion from a distinct model line than the one driving the session. For example:
3
+ `/team-advisor` binds one standing warm advisor for this session at the strongest reachable tier. Use it when `advisor()` is absent, or when you want a standing four-signal reviewer across many decision points.
4
4
 
5
- - Claude session -> consult Fable xhigh
6
- - Codex session -> consult Sol xhigh
5
+ ## Refs
7
6
 
8
- ## When to use it
7
+ | Doc | Holds |
8
+ |---|---|
9
+ | `skills/team-advisor/SKILL.md` | Sole-consumer bind wiring and constraints |
10
+ | `advisor-tool.md` | Consult timing, hard rule, how to treat advice |
11
+ | `~/.claude/_shared/advisor/advisor-protocol.md` | Host bind, floor, lifecycle |
12
+ | `agents/session-advisor.md` | ENDORSE / CORRECTION / PLAN / STOP |
9
13
 
10
- Consult the spawned advisor before big decisions, before declaring work complete, before any commit, when a failure repeats, or when reconsidering a chosen approach.
14
+ ## When to use
11
15
 
12
- ## Relationship to the advisor tool
16
+ Follow the call rules in `advisor-tool.md` (orientation first, then consult before substantive work; durable deliverable before the completion consult; stuck or reapproach; long tasks twice).
13
17
 
14
- `/team-advisor` works standalone; it needs no `advisor()` tool present. When both are available (see `advisor-tool.md` in this directory), use `advisor()` for a fast, history-forwarding check before substantive work, and `/team-advisor` for a standing reviewer consulted at decision points.
18
+ ## Relation to `advisor()`
19
+
20
+ `/team-advisor` works with no `advisor()` tool. When both exist: `advisor()` for a fast history-forwarded check; `/team-advisor` for a standing named reviewer consulted at the same cadence.
@@ -85,6 +85,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
85
85
  | `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than 120 reader-visible words, more than 6 bullet lines, more than 2 lines carrying over 20 words each, or instruction lines telling the user to act with no numbered step among the lead lines. Code fences, inline code, blockquotes, table rows, and link targets come off before the reply is judged, replies under 60 words always pass, and a reply opening with `Long form:` opts out entirely |
86
86
  | `env_var_table_code_drift_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A markdown env-var summary table row attributing an environment variable to a code file whose source never references that variable name |
87
87
  | `es_exe_path_rewriter.py` | PreToolUse | Rewrites paths referencing `.exe` under the Everything search path |
88
+ | `fable_spawn_gate.py` | PreToolUse (Agent/Task) | An `Agent` or `Task` spawn whose prompt carries no `FABLE-SPAWN-AUTHORIZED` token and whose model field reads `fable` in any letter case — the bare alias, or a delimiter segment of a full model id, so `claude-fable-5` is denied too |
88
89
  | `gh_body_arg_blocker.py` | PreToolUse (Bash) | `gh` commands passing `--body`/`-b` directly (requires `--body-file` instead) |
89
90
  | `gh_pr_author_enforcer.py` | PreToolUse | Enforces PR author identity rules |
90
91
  | `gh_pr_author_restore.py` | PostToolUse | Restores PR author after a tool call |
@@ -98,6 +99,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
98
99
  | `pii_payload_scan.py` | library | Write/Edit and durable post-body PII evaluation reused by `pii_prevention_blocker.py` |
99
100
  | `pii_prevention_blocker.py` | PreToolUse (Write/Edit/MultiEdit/Bash/PowerShell/MCP GitHub) | Entry hook — content that carries high-confidence personal data or secrets (real emails, home-dir paths, private IPs, credential material) on write, durable GitHub posts, or staged commit paths; resolves the staged-commit repository from the command it gates (via `pii_prevention_blocker_parts`), not the session working directory |
100
101
  | `pii_scanner.py` | library | Pure text scanners shared by `pii_prevention_blocker.py` |
102
+ | `piped_pytest_blocker.py` | PreToolUse (Bash) | A pytest run whose output feeds a pipe, where the pipeline reports the exit code of the command on the right |
101
103
  | `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose |
102
104
  | `pr_converge_bugteam_enforcer.py` | PreToolUse | Enforces that bugteam runs in parallel with bugbot in pr-converge loops |
103
105
  | `pr_description_enforcer.py` | PreToolUse (Bash) | `gh pr create`/`edit`/`comment` bodies that fail the Anthropic claude-code style audit, proof-shaped `gh pr comment` bodies missing proof-of-work parts, and `gh pr ready` while the PR carries no passing proof comment |
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse gate: deny a fable-tier subagent spawn that carries no marker.
3
+
4
+ One exact token in the spawn prompt authorizes a spawn at the fable tier::
5
+
6
+ model: fable, no FABLE-SPAWN-AUTHORIZED flag: deny
7
+ model: claude-fable-5, no FABLE-SPAWN-AUTHORIZED flag: deny
8
+ model: fable, FABLE-SPAWN-AUTHORIZED ok: allow
9
+ model: claude-sonnet-4, prompt either way ok: allow
10
+ no model field, prompt either way ok: allow
11
+
12
+ The gate reads an ``Agent`` or ``Task`` call and finds the tier in the model
13
+ field whether that field carries the bare alias or a full model id, in any
14
+ letter case. Its deny reason points at the advisor-protocol document rather
15
+ than quoting the token, so a denial pasted into a retry authorizes nothing.
16
+ A deny also sets ``additionalContext`` so the spawner sees the recovery path
17
+ (re-spawn at opus / an opus-equivalent tier, or authorize a fable bind).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Mapping, TextIO
26
+
27
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
28
+ if _hooks_dir not in sys.path:
29
+ sys.path.insert(0, _hooks_dir)
30
+
31
+ from hooks_constants.fable_spawn_gate_constants import ( # noqa: E402
32
+ ALL_SPAWN_TOOL_NAMES,
33
+ CALLING_HOOK_NAME,
34
+ DENY_ADDITIONAL_CONTEXT,
35
+ DENY_PREVIEW_TEMPLATE,
36
+ DENY_REASON,
37
+ FABLE_MODEL_ALIAS,
38
+ FABLE_SPAWN_AUTHORIZATION_MARKER,
39
+ HOOK_EVENT_NAME,
40
+ MAXIMUM_PREVIEW_MODEL_LENGTH,
41
+ MODEL_FIELD_NAME,
42
+ MODEL_SEGMENT_SPLIT_PATTERN,
43
+ PROMPT_FIELD_NAME,
44
+ TOOL_INPUT_FIELD_NAME,
45
+ TOOL_NAME_FIELD_NAME,
46
+ )
47
+ from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
48
+ from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
49
+ read_hook_input_dictionary_from_stdin,
50
+ )
51
+
52
+
53
+ def _model_names_the_fable_tier(model_identifier: str) -> bool:
54
+ """Report whether a model string names the fable tier.
55
+
56
+ The field carries either the bare alias or a full model id, so the
57
+ lowercased string is cut on its delimiters and the tier is looked for as
58
+ a whole segment::
59
+
60
+ fable -> ['fable'] flag: fable tier
61
+ claude-fable-5 -> ['claude', 'fable', '5'] flag: fable tier
62
+ claude-sonnet-4 -> ['claude', 'sonnet', '4'] ok: another tier
63
+ affable -> ['affable'] ok: another tier
64
+
65
+ Args:
66
+ model_identifier: The spawn's ``model`` string.
67
+
68
+ Returns:
69
+ True when a model segment reads ``fable`` in any letter case; False
70
+ for another tier.
71
+ """
72
+ all_segments = MODEL_SEGMENT_SPLIT_PATTERN.split(model_identifier.strip().lower())
73
+ return FABLE_MODEL_ALIAS in all_segments
74
+
75
+
76
+ def _prompt_carries_authorization_marker(all_tool_input: Mapping[str, object]) -> bool:
77
+ """Report whether the spawn prompt holds the exact authorization token.
78
+
79
+ Args:
80
+ all_tool_input: The spawn's ``tool_input`` mapping.
81
+
82
+ Returns:
83
+ True when the prompt is a string holding the marker token; False for
84
+ an unmarked prompt and for an absent or non-string field.
85
+ """
86
+ spawn_prompt = all_tool_input.get(PROMPT_FIELD_NAME)
87
+ if not isinstance(spawn_prompt, str):
88
+ return False
89
+ return FABLE_SPAWN_AUTHORIZATION_MARKER in spawn_prompt
90
+
91
+
92
+ def _denied_spawn_model(all_payload_by_field: Mapping[str, object]) -> str | None:
93
+ """Return the model string of an unauthorized fable spawn, or None.
94
+
95
+ The model string travels back as the decision, so the deny path holds a
96
+ proven fable string and never re-derives one.
97
+
98
+ Args:
99
+ all_payload_by_field: The parsed PreToolUse payload, keyed by
100
+ top-level field name.
101
+
102
+ Returns:
103
+ The ``model`` string for a spawn tool call at the fable tier whose
104
+ prompt lacks the authorization marker; None for every allowed call.
105
+ """
106
+ if all_payload_by_field.get(TOOL_NAME_FIELD_NAME, "") not in ALL_SPAWN_TOOL_NAMES:
107
+ return None
108
+ tool_input = all_payload_by_field.get(TOOL_INPUT_FIELD_NAME, {})
109
+ if not isinstance(tool_input, dict):
110
+ return None
111
+ model_identifier = tool_input.get(MODEL_FIELD_NAME)
112
+ if not isinstance(model_identifier, str):
113
+ return None
114
+ if not _model_names_the_fable_tier(model_identifier):
115
+ return None
116
+ if _prompt_carries_authorization_marker(tool_input):
117
+ return None
118
+ return model_identifier
119
+
120
+
121
+ def _build_denial_preview(model_identifier: str) -> str:
122
+ """Build the bounded preview the hook-blocks log records for a denial.
123
+
124
+ The preview names the model field that tripped the gate, so its length
125
+ holds steady whatever the spawn prompt carries::
126
+
127
+ fable -> model=fable marker_present=False
128
+ claude-fable-5 -> model=claude-fable-5 marker_present=False
129
+
130
+ Args:
131
+ model_identifier: The fable model string the denial was decided on.
132
+
133
+ Returns:
134
+ One preview line naming the model text and the marker state.
135
+ """
136
+ return DENY_PREVIEW_TEMPLATE.format(
137
+ model_text=model_identifier[:MAXIMUM_PREVIEW_MODEL_LENGTH]
138
+ )
139
+
140
+
141
+ def _emit_denial(
142
+ decision_stream: TextIO,
143
+ all_payload_by_field: Mapping[str, object],
144
+ model_identifier: str,
145
+ ) -> None:
146
+ """Log the denied spawn and write the PreToolUse deny payload.
147
+
148
+ Args:
149
+ decision_stream: Writable text stream — production code passes
150
+ ``sys.stdout``; tests pass a ``StringIO`` to capture the JSON.
151
+ all_payload_by_field: The parsed PreToolUse payload, whose tool name
152
+ names the denied spawn in the hook-blocks log.
153
+ model_identifier: The fable model string the denial was decided on.
154
+ """
155
+ denial = {
156
+ "hookSpecificOutput": {
157
+ "hookEventName": HOOK_EVENT_NAME,
158
+ "permissionDecision": "deny",
159
+ "permissionDecisionReason": DENY_REASON,
160
+ "additionalContext": DENY_ADDITIONAL_CONTEXT,
161
+ }
162
+ }
163
+ log_hook_block(
164
+ calling_hook_name=CALLING_HOOK_NAME,
165
+ hook_event=HOOK_EVENT_NAME,
166
+ block_reason=DENY_REASON,
167
+ tool_name=str(all_payload_by_field.get(TOOL_NAME_FIELD_NAME, "")),
168
+ offending_input_preview=_build_denial_preview(model_identifier),
169
+ )
170
+ decision_stream.write(json.dumps(denial) + "\n")
171
+ decision_stream.flush()
172
+
173
+
174
+ def main() -> None:
175
+ """Read the PreToolUse payload and deny an unmarked fable spawn."""
176
+ hook_payload = read_hook_input_dictionary_from_stdin()
177
+ if hook_payload is None:
178
+ sys.exit(0)
179
+ denied_model = _denied_spawn_model(hook_payload)
180
+ if denied_model is None:
181
+ sys.exit(0)
182
+ _emit_denial(sys.stdout, hook_payload, denied_model)
183
+ sys.exit(0)
184
+
185
+
186
+ if __name__ == "__main__":
187
+ main()