anvil-dev-framework 0.1.7 → 0.1.9
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/README.md +71 -22
- package/VERSION +1 -1
- package/docs/ANV-263-hook-logging-investigation.md +116 -0
- package/docs/command-reference.md +398 -17
- package/docs/session-workflow.md +62 -9
- package/docs/system-architecture.md +584 -0
- package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
- package/global/api/openapi.yaml +357 -0
- package/global/api/ralph_api.py +528 -0
- package/global/commands/anvil-settings.md +47 -19
- package/global/commands/audit.md +163 -0
- package/global/commands/checklist.md +180 -0
- package/global/commands/coderabbit-fix.md +282 -0
- package/global/commands/efficiency.md +356 -0
- package/global/commands/evidence.md +117 -33
- package/global/commands/hud.md +24 -0
- package/global/commands/insights.md +101 -3
- package/global/commands/orient.md +22 -21
- package/global/commands/patterns.md +115 -0
- package/global/commands/ralph.md +47 -1
- package/global/commands/token-budget.md +214 -0
- package/global/commands/weekly-review.md +21 -1
- package/global/config/notifications.yaml.template +50 -0
- package/global/hooks/ralph_stop.sh +33 -1
- package/global/hooks/statusline.sh +67 -2
- package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
- package/global/lib/coderabbit_metrics.py +647 -0
- package/global/lib/command_tracker.py +147 -0
- package/global/lib/context_optimizer.py +323 -0
- package/global/lib/linear_provider.py +210 -16
- package/global/lib/log_rotation.py +287 -0
- package/global/lib/optimization_applier.py +582 -0
- package/global/lib/ralph_events.py +398 -0
- package/global/lib/ralph_notifier.py +366 -0
- package/global/lib/ralph_state.py +264 -24
- package/global/lib/ralph_webhooks.py +470 -0
- package/global/lib/state_manager.py +121 -0
- package/global/lib/token_analyzer.py +1383 -0
- package/global/lib/token_metrics.py +919 -0
- package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_issue_models.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_local_provider.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/test_command_tracker.py +172 -0
- package/global/tests/test_context_optimizer.py +321 -0
- package/global/tests/test_linear_filtering.py +319 -0
- package/global/tests/test_linear_provider.py +40 -1
- package/global/tests/test_optimization_applier.py +508 -0
- package/global/tests/test_token_analyzer.py +735 -0
- package/global/tests/test_token_analyzer_phase6.py +537 -0
- package/global/tests/test_token_metrics.py +829 -0
- package/global/tools/README.md +153 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +86 -1
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
- package/global/tools/anvil-memory/src/commands/context.ts +322 -0
- package/global/tools/anvil-memory/src/db.ts +108 -0
- package/global/tools/anvil-memory/src/index.ts +2 -8
- package/global/tools/orient_linear.py +159 -0
- package/global/tools/ralph-watch +423 -0
- package/package.json +2 -1
- package/project/.anvil-project.yaml.template +93 -0
- package/project/CLAUDE.md.template +343 -0
- package/project/agents/README.md +119 -0
- package/project/agents/cross-layer-debugger.md +217 -0
- package/project/agents/security-code-reviewer.md +162 -0
- package/project/constitution.md.template +235 -0
- package/project/coordination.md +103 -0
- package/project/docs/background-tasks.md +258 -0
- package/project/docs/skills-frontmatter.md +243 -0
- package/project/examples/README.md +106 -0
- package/project/examples/api-route-template.ts +171 -0
- package/project/examples/component-template.tsx +110 -0
- package/project/examples/hook-template.ts +152 -0
- package/project/examples/service-template.ts +207 -0
- package/project/examples/test-template.test.tsx +249 -0
- package/project/hooks/README.md +491 -0
- package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/project/hooks/notification.py +183 -0
- package/project/hooks/permission_request.py +438 -0
- package/project/hooks/post_tool_use.py +397 -0
- package/project/hooks/pre_compact.py +126 -0
- package/project/hooks/pre_tool_use.py +454 -0
- package/project/hooks/session_start.py +656 -0
- package/project/hooks/stop.py +356 -0
- package/project/hooks/subagent_start.py +223 -0
- package/project/hooks/subagent_stop.py +215 -0
- package/project/hooks/user_prompt_submit.py +110 -0
- package/project/hooks/utils/llm/anth.py +114 -0
- package/project/hooks/utils/llm/oai.py +114 -0
- package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
- package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
- package/project/hooks/utils/tts/openai_tts.py +92 -0
- package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
- package/project/linear.yaml.template +23 -0
- package/project/product.md.template +238 -0
- package/project/retros/README.md +126 -0
- package/project/rules/README.md +90 -0
- package/project/rules/debugging.md +139 -0
- package/project/rules/security-review.md +115 -0
- package/project/settings.yaml.template +185 -0
- package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
- package/project/templates/api-python/CLAUDE.md +547 -0
- package/project/templates/generic/CLAUDE.md +260 -0
- package/project/templates/saas/CLAUDE.md +478 -0
- package/project/tests/README.md +140 -0
- package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
- package/project/tests/fixtures/sample-transcript.jsonl +21 -0
- package/project/tests/test-hooks.sh +259 -0
- package/project/tests/test-lib.sh +248 -0
- package/project/tests/test-statusline.sh +165 -0
- package/project/tests/test_transcript_parser.py +323 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# Claude Code Hooks
|
|
2
|
+
|
|
3
|
+
Hooks integrate with Claude Code's lifecycle events to automate common tasks.
|
|
4
|
+
|
|
5
|
+
## Available Hooks
|
|
6
|
+
|
|
7
|
+
| Hook | Trigger | Purpose |
|
|
8
|
+
|------|---------|---------|
|
|
9
|
+
| `session_start.py` | Session begins | Load context, log session |
|
|
10
|
+
| `stop.py` | Claude stops | Log completion, announce via TTS |
|
|
11
|
+
| `pre_compact.py` | Before compaction | Backup transcript |
|
|
12
|
+
| `pre_tool_use.py` | Before tool execution | Safety checks, logging |
|
|
13
|
+
| `post_tool_use.py` | After tool execution | Log tool usage, announcements |
|
|
14
|
+
| `subagent_start.py` | Subagent begins | Inject context, log start |
|
|
15
|
+
| `subagent_stop.py` | Subagent finishes | Log and announce |
|
|
16
|
+
| `permission_request.py` | Permission requested | Auto-approve/deny logic |
|
|
17
|
+
| `user_prompt_submit.py` | User submits prompt | Log prompts |
|
|
18
|
+
| `notification.py` | Notification event | Custom notifications |
|
|
19
|
+
|
|
20
|
+
## Hook Types Reference
|
|
21
|
+
|
|
22
|
+
### Lifecycle Hooks
|
|
23
|
+
- **SessionStart** — Fires when a new Claude Code session begins
|
|
24
|
+
- **Stop** — Fires when Claude Code stops (user or automatic)
|
|
25
|
+
- **PreCompact** — Fires before conversation compaction
|
|
26
|
+
|
|
27
|
+
### Tool Hooks
|
|
28
|
+
- **PreToolUse** — Fires before any tool is executed
|
|
29
|
+
- **PostToolUse** — Fires after any tool completes
|
|
30
|
+
- **PermissionRequest** — Fires when Claude requests tool permission (NEW in 2.0.45)
|
|
31
|
+
|
|
32
|
+
### Subagent Hooks
|
|
33
|
+
- **SubagentStart** — Fires when a subagent begins execution (NEW in 2.0.43)
|
|
34
|
+
- **SubagentStop** — Fires when a subagent completes
|
|
35
|
+
|
|
36
|
+
### Other Hooks
|
|
37
|
+
- **UserPromptSubmit** — Fires when user submits a prompt
|
|
38
|
+
- **Notification** — Fires on notification events
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
1. Copy hooks to your project's `.claude/hooks/` directory
|
|
43
|
+
2. Make executable: `chmod +x .claude/hooks/*.py`
|
|
44
|
+
3. Configure in `.claude/settings.local.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"hooks": {
|
|
49
|
+
"SessionStart": [
|
|
50
|
+
{
|
|
51
|
+
"matcher": "",
|
|
52
|
+
"hooks": [
|
|
53
|
+
{
|
|
54
|
+
"type": "command",
|
|
55
|
+
"command": "uv run .claude/hooks/session_start.py --load-context"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"Stop": [
|
|
61
|
+
{
|
|
62
|
+
"matcher": "",
|
|
63
|
+
"hooks": [
|
|
64
|
+
{
|
|
65
|
+
"type": "command",
|
|
66
|
+
"command": "uv run .claude/hooks/stop.py"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"PreToolUse": [
|
|
72
|
+
{
|
|
73
|
+
"matcher": "",
|
|
74
|
+
"hooks": [
|
|
75
|
+
{
|
|
76
|
+
"type": "command",
|
|
77
|
+
"command": "uv run .claude/hooks/pre_tool_use.py"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"PostToolUse": [
|
|
83
|
+
{
|
|
84
|
+
"matcher": "",
|
|
85
|
+
"hooks": [
|
|
86
|
+
{
|
|
87
|
+
"type": "command",
|
|
88
|
+
"command": "uv run .claude/hooks/post_tool_use.py"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"SubagentStart": [
|
|
94
|
+
{
|
|
95
|
+
"matcher": "",
|
|
96
|
+
"hooks": [
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": "uv run .claude/hooks/subagent_start.py --log --inject-context"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"SubagentStop": [
|
|
105
|
+
{
|
|
106
|
+
"matcher": "",
|
|
107
|
+
"hooks": [
|
|
108
|
+
{
|
|
109
|
+
"type": "command",
|
|
110
|
+
"command": "uv run .claude/hooks/subagent_stop.py"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"PermissionRequest": [
|
|
116
|
+
{
|
|
117
|
+
"matcher": "",
|
|
118
|
+
"hooks": [
|
|
119
|
+
{
|
|
120
|
+
"type": "command",
|
|
121
|
+
"command": "uv run .claude/hooks/permission_request.py --log"
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Requirements
|
|
131
|
+
|
|
132
|
+
Hooks use `uv` for dependency management (inline script dependencies):
|
|
133
|
+
```bash
|
|
134
|
+
# Install uv if not present
|
|
135
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Optional: TTS Announcements
|
|
139
|
+
|
|
140
|
+
For voice announcements when tasks complete:
|
|
141
|
+
|
|
142
|
+
1. Copy `utils/tts/` directory to `.claude/hooks/utils/tts/`
|
|
143
|
+
2. Set API key (choose one):
|
|
144
|
+
- `ELEVENLABS_API_KEY` — Best quality
|
|
145
|
+
- `OPENAI_API_KEY` — Good quality
|
|
146
|
+
- None — Falls back to pyttsx3 (local, no API)
|
|
147
|
+
|
|
148
|
+
3. Add `--announce` flag to hooks:
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"hooks": {
|
|
152
|
+
"Stop": [
|
|
153
|
+
{
|
|
154
|
+
"matcher": "",
|
|
155
|
+
"hooks": [
|
|
156
|
+
{
|
|
157
|
+
"type": "command",
|
|
158
|
+
"command": "uv run .claude/hooks/stop.py --announce"
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Optional: HUD (Multi-Agent Dashboard)
|
|
168
|
+
|
|
169
|
+
For visibility into all active Claude Code sessions across projects:
|
|
170
|
+
|
|
171
|
+
1. Enable agent registration in your hooks:
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"hooks": {
|
|
175
|
+
"session_start": [".claude/hooks/session_start.py --load-context --register-agent"],
|
|
176
|
+
"post_tool_use": [".claude/hooks/post_tool_use.py --update-agent"],
|
|
177
|
+
"stop": [".claude/hooks/stop.py --cleanup-agent"]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
2. Run the dashboard in a separate terminal:
|
|
183
|
+
```bash
|
|
184
|
+
uv run global/tools/anvil-hud.py
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The HUD shows:
|
|
188
|
+
- All active agents with context usage bars
|
|
189
|
+
- Per-agent costs and workflow phases
|
|
190
|
+
- Aggregate totals across all sessions
|
|
191
|
+
|
|
192
|
+
Configure via `.claude/anvil.config.json`:
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"hud": {
|
|
196
|
+
"enabled": true,
|
|
197
|
+
"showAgentCount": true
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Logs
|
|
203
|
+
|
|
204
|
+
Hooks write logs to `logs/` directory:
|
|
205
|
+
- `logs/session_start.json` — Session start events
|
|
206
|
+
- `logs/stop.json` — Stop events
|
|
207
|
+
- `logs/pre_compact.json` — Compaction events
|
|
208
|
+
- `logs/pre_tool_use.json` — Pre-tool events
|
|
209
|
+
- `logs/post_tool_use.json` — Post-tool events
|
|
210
|
+
- `logs/subagent_start.json` — Subagent start events
|
|
211
|
+
- `logs/subagent_stop.json` — Subagent stop events
|
|
212
|
+
- `logs/permission_requests.json` — Permission request decisions
|
|
213
|
+
- `logs/transcript_backups/` — Transcript backups
|
|
214
|
+
|
|
215
|
+
## Customization
|
|
216
|
+
|
|
217
|
+
### Adding Context at Session Start
|
|
218
|
+
|
|
219
|
+
Edit `session_start.py` to load additional context files:
|
|
220
|
+
```python
|
|
221
|
+
context_files = [
|
|
222
|
+
".claude/CONTEXT.md",
|
|
223
|
+
".claude/TODO.md",
|
|
224
|
+
"docs/ARCHITECTURE.md", # Add your own
|
|
225
|
+
]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Custom Stop Actions
|
|
229
|
+
|
|
230
|
+
Edit `stop.py` to add actions when Claude finishes:
|
|
231
|
+
```python
|
|
232
|
+
# Example: Send notification
|
|
233
|
+
def on_stop():
|
|
234
|
+
# Your custom logic here
|
|
235
|
+
pass
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Injecting Context for Subagents
|
|
239
|
+
|
|
240
|
+
Edit `subagent_start.py` to inject context based on agent type:
|
|
241
|
+
```python
|
|
242
|
+
context_map = {
|
|
243
|
+
"security-code-reviewer": ".claude/rules/security-review.md",
|
|
244
|
+
"cross-layer-debugger": ".claude/rules/debugging.md",
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Permission Request Modes
|
|
249
|
+
|
|
250
|
+
The `permission_request.py` hook supports multiple modes:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# Standard mode - log requests, block dangerous patterns
|
|
254
|
+
uv run .claude/hooks/permission_request.py --log
|
|
255
|
+
|
|
256
|
+
# CI mode - auto-approve safe commands, deny unknown
|
|
257
|
+
uv run .claude/hooks/permission_request.py --ci-mode --log
|
|
258
|
+
|
|
259
|
+
# Strict mode - require approval for all write operations
|
|
260
|
+
uv run .claude/hooks/permission_request.py --strict --log
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Dangerous patterns always blocked:**
|
|
264
|
+
- Recursive deletion of system paths (`rm -rf /`, `rm -rf ~`)
|
|
265
|
+
- Disk formatting commands (`mkfs`, `fdisk`, `dd of=/dev/`)
|
|
266
|
+
- Fork bombs and system overload patterns
|
|
267
|
+
- Curl/wget piped to bash/sh
|
|
268
|
+
- Credential exfiltration patterns
|
|
269
|
+
|
|
270
|
+
**CI-safe patterns (auto-approved in --ci-mode):**
|
|
271
|
+
- Package management (`npm install`, `pip install`, `uv sync`)
|
|
272
|
+
- Build/test commands (`npm run build`, `pytest`, `vitest`)
|
|
273
|
+
- Git read operations (`git status`, `git log`, `git diff`)
|
|
274
|
+
- File listing/reading (`ls`, `cat`, `grep`, `find`)
|
|
275
|
+
|
|
276
|
+
**Strict mode requirements:**
|
|
277
|
+
- All Write/Edit operations require approval
|
|
278
|
+
- All rm/mv/cp commands require approval
|
|
279
|
+
- Git write operations require approval
|
|
280
|
+
- Network operations require approval
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Hook Input/Output Schemas
|
|
285
|
+
|
|
286
|
+
Hooks receive JSON on stdin with event-specific data.
|
|
287
|
+
|
|
288
|
+
### SessionStart
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"session_id": "...",
|
|
292
|
+
"source": "startup|resume|clear"
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Stop
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"session_id": "...",
|
|
300
|
+
"stop_hook_active": true,
|
|
301
|
+
"transcript_path": "/path/to/transcript.jsonl"
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### PreCompact
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"session_id": "...",
|
|
309
|
+
"transcript_path": "/path/to/transcript.jsonl",
|
|
310
|
+
"trigger": "manual|auto",
|
|
311
|
+
"custom_instructions": "..."
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### PreToolUse
|
|
316
|
+
```json
|
|
317
|
+
{
|
|
318
|
+
"session_id": "...",
|
|
319
|
+
"tool_name": "Bash",
|
|
320
|
+
"tool_input": {
|
|
321
|
+
"command": "npm test"
|
|
322
|
+
},
|
|
323
|
+
"tool_use_id": "toolu_abc123..."
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Fields:**
|
|
328
|
+
- `tool_use_id` — Unique identifier for this tool invocation (NEW in 2.0.43)
|
|
329
|
+
|
|
330
|
+
### PostToolUse
|
|
331
|
+
```json
|
|
332
|
+
{
|
|
333
|
+
"session_id": "...",
|
|
334
|
+
"tool_name": "Bash",
|
|
335
|
+
"tool_input": {
|
|
336
|
+
"command": "npm test"
|
|
337
|
+
},
|
|
338
|
+
"tool_output": "...",
|
|
339
|
+
"tool_use_id": "toolu_abc123..."
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Fields:**
|
|
344
|
+
- `tool_use_id` — Unique identifier for this tool invocation (NEW in 2.0.43)
|
|
345
|
+
|
|
346
|
+
### SubagentStart (NEW in 2.0.43)
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"session_id": "...",
|
|
350
|
+
"agent_id": "subagent-abc123",
|
|
351
|
+
"agent_type": "security-code-reviewer",
|
|
352
|
+
"tool_use_id": "toolu_xyz789..."
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Fields:**
|
|
357
|
+
- `agent_id` — Unique identifier for the subagent instance
|
|
358
|
+
- `agent_type` — Type/name of the subagent being invoked
|
|
359
|
+
- `tool_use_id` — ID of the Task tool invocation that spawned this subagent
|
|
360
|
+
|
|
361
|
+
**Output:** Can return `hookSpecificOutput.additionalContext` to inject context:
|
|
362
|
+
```json
|
|
363
|
+
{
|
|
364
|
+
"hookSpecificOutput": {
|
|
365
|
+
"hookEventName": "SubagentStart",
|
|
366
|
+
"additionalContext": "# Security Review Rules\n..."
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### SubagentStop
|
|
372
|
+
```json
|
|
373
|
+
{
|
|
374
|
+
"session_id": "...",
|
|
375
|
+
"agent_id": "subagent-abc123",
|
|
376
|
+
"agent_transcript_path": "/path/to/agent/transcript.jsonl",
|
|
377
|
+
"stop_hook_active": true,
|
|
378
|
+
"transcript_path": "/path/to/transcript.jsonl"
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Fields:**
|
|
383
|
+
- `agent_id` — Unique identifier for the subagent (NEW in 2.0.42)
|
|
384
|
+
- `agent_transcript_path` — Path to subagent's transcript file (NEW in 2.0.42)
|
|
385
|
+
|
|
386
|
+
### PermissionRequest (NEW in 2.0.45)
|
|
387
|
+
```json
|
|
388
|
+
{
|
|
389
|
+
"session_id": "...",
|
|
390
|
+
"tool_name": "Bash",
|
|
391
|
+
"tool_input": {
|
|
392
|
+
"command": "npm install"
|
|
393
|
+
},
|
|
394
|
+
"permission_type": "tool_execution"
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**Output:** Must return a decision:
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"decision": "approve|deny|ask",
|
|
402
|
+
"reason": "Optional explanation"
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Decision values:**
|
|
407
|
+
- `approve` — Automatically approve the permission request
|
|
408
|
+
- `deny` — Automatically deny the permission request
|
|
409
|
+
- `ask` — Fall back to asking the user (default behavior)
|
|
410
|
+
|
|
411
|
+
### UserPromptSubmit
|
|
412
|
+
```json
|
|
413
|
+
{
|
|
414
|
+
"session_id": "...",
|
|
415
|
+
"prompt": "User's input text"
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Notification
|
|
420
|
+
```json
|
|
421
|
+
{
|
|
422
|
+
"session_id": "...",
|
|
423
|
+
"notification_type": "...",
|
|
424
|
+
"message": "..."
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Hook Matchers
|
|
431
|
+
|
|
432
|
+
Hooks support matchers to filter when they run:
|
|
433
|
+
|
|
434
|
+
```json
|
|
435
|
+
{
|
|
436
|
+
"hooks": {
|
|
437
|
+
"PreToolUse": [
|
|
438
|
+
{
|
|
439
|
+
"matcher": "Bash",
|
|
440
|
+
"hooks": [
|
|
441
|
+
{
|
|
442
|
+
"type": "command",
|
|
443
|
+
"command": "uv run .claude/hooks/bash_safety.py"
|
|
444
|
+
}
|
|
445
|
+
]
|
|
446
|
+
}
|
|
447
|
+
]
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Matcher patterns:**
|
|
453
|
+
- Empty string `""` — Match all events
|
|
454
|
+
- `"Bash"` — Match only Bash tool
|
|
455
|
+
- `"Write|Edit"` — Match Write or Edit tools
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Testing Hooks
|
|
460
|
+
|
|
461
|
+
Test harnesses are available in `project/tests/` (or `.claude/tests/` after `anvil init`):
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
# Run all hook tests
|
|
465
|
+
./test-hooks.sh
|
|
466
|
+
|
|
467
|
+
# Run all statusline tests
|
|
468
|
+
./test-statusline.sh
|
|
469
|
+
|
|
470
|
+
# Run specific hook tests
|
|
471
|
+
./test-hooks.sh pre_tool_use
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
These harnesses use **heredocs** to define JSON fixtures, eliminating shell escaping issues entirely.
|
|
475
|
+
|
|
476
|
+
See `project/tests/README.md` for full documentation on:
|
|
477
|
+
- Writing custom tests
|
|
478
|
+
- Assertion functions (`assert_contains`, `assert_exit_code`, etc.)
|
|
479
|
+
- Test environment setup
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Version History
|
|
484
|
+
|
|
485
|
+
| Version | Additions |
|
|
486
|
+
|---------|-----------|
|
|
487
|
+
| 2.0.45 | PermissionRequest hook |
|
|
488
|
+
| 2.0.43 | SubagentStart hook, `tool_use_id` field, `skills` frontmatter |
|
|
489
|
+
| 2.0.42 | `agent_id` and `agent_transcript_path` in SubagentStop |
|
|
490
|
+
| 2.0.41 | Prompt-based stop hooks, custom timeouts |
|
|
491
|
+
| 2.0.37 | Notification hook with matcher values |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "python-dotenv",
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import sys
|
|
14
|
+
import subprocess
|
|
15
|
+
import random
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from dotenv import load_dotenv
|
|
20
|
+
load_dotenv()
|
|
21
|
+
except ImportError:
|
|
22
|
+
pass # dotenv is optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_apple_silicon():
|
|
26
|
+
"""Check if running on Apple Silicon Mac."""
|
|
27
|
+
return platform.system() == "Darwin" and platform.machine() == "arm64"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_my_codename() -> str:
|
|
31
|
+
"""Get this agent's codename (A1, A2, etc.) from registry.
|
|
32
|
+
|
|
33
|
+
Reads agent ID from local anvil-state.json, then looks up
|
|
34
|
+
the codename from the global agent registry.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Codename like "A1" or empty string if not found.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
state_file = Path(".claude/anvil-state.json")
|
|
41
|
+
if not state_file.exists():
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
state = json.loads(state_file.read_text())
|
|
45
|
+
agent_id = state.get("session", {}).get("agentId", "")
|
|
46
|
+
if not agent_id:
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
registry_file = Path.home() / ".anvil" / "agents.json"
|
|
50
|
+
if registry_file.exists():
|
|
51
|
+
registry = json.loads(registry_file.read_text())
|
|
52
|
+
agent = registry.get("agents", {}).get(agent_id, {})
|
|
53
|
+
return agent.get("codename") or ""
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
return ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_tts_script_path():
|
|
60
|
+
"""
|
|
61
|
+
Determine which TTS script to use.
|
|
62
|
+
Priority order: MLX Audio (Apple Silicon) > ElevenLabs > OpenAI > pyttsx3 > macOS say
|
|
63
|
+
|
|
64
|
+
MLX Audio with Kokoro provides ~300ms latency, free, local TTS on Apple Silicon.
|
|
65
|
+
Falls back to API-based TTS or system TTS if unavailable.
|
|
66
|
+
"""
|
|
67
|
+
script_dir = Path(__file__).parent
|
|
68
|
+
tts_dir = script_dir / "utils" / "tts"
|
|
69
|
+
|
|
70
|
+
# MLX Audio is the primary choice for Apple Silicon (free, fast, local)
|
|
71
|
+
if is_apple_silicon():
|
|
72
|
+
mlx_script = tts_dir / "mlx_audio_tts.py"
|
|
73
|
+
if mlx_script.exists():
|
|
74
|
+
return str(mlx_script)
|
|
75
|
+
|
|
76
|
+
# ElevenLabs if API key is set (highest quality but costs money)
|
|
77
|
+
if os.getenv('ELEVENLABS_API_KEY'):
|
|
78
|
+
elevenlabs_script = tts_dir / "elevenlabs_tts.py"
|
|
79
|
+
if elevenlabs_script.exists():
|
|
80
|
+
return str(elevenlabs_script)
|
|
81
|
+
|
|
82
|
+
# OpenAI TTS if API key is set
|
|
83
|
+
if os.getenv('OPENAI_API_KEY'):
|
|
84
|
+
openai_script = tts_dir / "openai_tts.py"
|
|
85
|
+
if openai_script.exists():
|
|
86
|
+
return str(openai_script)
|
|
87
|
+
|
|
88
|
+
# pyttsx3 as offline fallback
|
|
89
|
+
pyttsx3_script = tts_dir / "pyttsx3_tts.py"
|
|
90
|
+
if pyttsx3_script.exists():
|
|
91
|
+
return str(pyttsx3_script)
|
|
92
|
+
|
|
93
|
+
# Final fallback: return None and use macOS say command
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def announce_notification():
|
|
98
|
+
"""Announce that the agent needs user input."""
|
|
99
|
+
try:
|
|
100
|
+
# Get agent codename for identification (ANV-135)
|
|
101
|
+
codename = get_my_codename()
|
|
102
|
+
agent_prefix = f"Agent {codename[1:]}" if codename.startswith("A") else "Your agent"
|
|
103
|
+
|
|
104
|
+
# Get engineer name if available
|
|
105
|
+
engineer_name = os.getenv('ENGINEER_NAME', '').strip()
|
|
106
|
+
|
|
107
|
+
# Create notification message with 30% chance to include name
|
|
108
|
+
if engineer_name and random.random() < 0.3:
|
|
109
|
+
notification_message = f"{engineer_name}, {agent_prefix} needs your input"
|
|
110
|
+
else:
|
|
111
|
+
notification_message = f"{agent_prefix} needs your input"
|
|
112
|
+
|
|
113
|
+
tts_script = get_tts_script_path()
|
|
114
|
+
|
|
115
|
+
if tts_script:
|
|
116
|
+
# Use the selected TTS script
|
|
117
|
+
subprocess.run([
|
|
118
|
+
"uv", "run", tts_script, notification_message
|
|
119
|
+
],
|
|
120
|
+
capture_output=True,
|
|
121
|
+
timeout=10
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
# Ultimate fallback: macOS say command (~10ms, robotic but reliable)
|
|
125
|
+
subprocess.run(['say', notification_message], capture_output=True, timeout=10)
|
|
126
|
+
|
|
127
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
|
|
128
|
+
# Fail silently if TTS encounters issues
|
|
129
|
+
pass
|
|
130
|
+
except Exception:
|
|
131
|
+
# Fail silently for any other errors
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main():
|
|
136
|
+
try:
|
|
137
|
+
# Parse command line arguments
|
|
138
|
+
parser = argparse.ArgumentParser()
|
|
139
|
+
parser.add_argument('--notify', action='store_true', help='Enable TTS notifications')
|
|
140
|
+
args = parser.parse_args()
|
|
141
|
+
|
|
142
|
+
# Read JSON input from stdin
|
|
143
|
+
input_data = json.loads(sys.stdin.read())
|
|
144
|
+
|
|
145
|
+
# Ensure log directory exists
|
|
146
|
+
import os
|
|
147
|
+
log_dir = os.path.join(os.getcwd(), 'logs')
|
|
148
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
149
|
+
log_file = os.path.join(log_dir, 'notification.json')
|
|
150
|
+
|
|
151
|
+
# Read existing log data or initialize empty list
|
|
152
|
+
if os.path.exists(log_file):
|
|
153
|
+
with open(log_file, 'r') as f:
|
|
154
|
+
try:
|
|
155
|
+
log_data = json.load(f)
|
|
156
|
+
except (json.JSONDecodeError, ValueError):
|
|
157
|
+
log_data = []
|
|
158
|
+
else:
|
|
159
|
+
log_data = []
|
|
160
|
+
|
|
161
|
+
# Append new data
|
|
162
|
+
log_data.append(input_data)
|
|
163
|
+
|
|
164
|
+
# Write back to file with formatting
|
|
165
|
+
with open(log_file, 'w') as f:
|
|
166
|
+
json.dump(log_data, f, indent=2)
|
|
167
|
+
|
|
168
|
+
# Announce notification via TTS only if --notify flag is set
|
|
169
|
+
# Skip TTS for the generic "Claude is waiting for your input" message
|
|
170
|
+
if args.notify and input_data.get('message') != 'Claude is waiting for your input':
|
|
171
|
+
announce_notification()
|
|
172
|
+
|
|
173
|
+
sys.exit(0)
|
|
174
|
+
|
|
175
|
+
except json.JSONDecodeError:
|
|
176
|
+
# Handle JSON decode errors gracefully
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
except Exception:
|
|
179
|
+
# Handle any other errors gracefully
|
|
180
|
+
sys.exit(0)
|
|
181
|
+
|
|
182
|
+
if __name__ == '__main__':
|
|
183
|
+
main()
|