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
|
@@ -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()
|