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.
- package/CLAUDE.md +7 -1
- 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/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/clean-coder.md +9 -19
- 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 +335 -12
- package/docs/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +2 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/team-advisor-skill.md +14 -8
- 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/code_rules_path_utils_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/output-styles/CLAUDE.md +17 -0
- package/output-styles/caveman-agent.md +37 -0
- package/package.json +2 -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/rules/code-standards.md +33 -7
- package/rules/eli11-replies.md +1 -1
- package/scripts/CLAUDE.md +3 -3
- 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_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -0
- package/scripts/spawn_grok_batch.py +591 -10
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +396 -0
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/CLAUDE.md +4 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- 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 +15 -9
- 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/runner-selection.md +40 -0
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
- package/skills/e-code-review/scripts/grok_code_review.py +221 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
- 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 -0
- package/skills/orchestrator/SKILL.md +9 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -41
- package/system-prompts/software-engineer.xml +6 -3
- package/skills/test_markdown_link_integrity.py +0 -107
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
# Advisor Tool
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
## What it is
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
11
|
+
When `advisor()` is absent, use `/team-advisor` (see `team-advisor-skill.md`).
|
|
12
12
|
|
|
13
|
-
|
|
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
|
|
1
|
+
# Team-Advisor Skill
|
|
2
2
|
|
|
3
|
-
`/team-advisor`
|
|
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
|
-
|
|
6
|
-
- Codex session -> consult Sol xhigh
|
|
5
|
+
## Refs
|
|
7
6
|
|
|
8
|
-
|
|
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
|
-
|
|
14
|
+
## When to use
|
|
11
15
|
|
|
12
|
-
|
|
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
|
-
|
|
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.
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -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()
|