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.
Files changed (143) hide show
  1. package/README.md +71 -22
  2. package/VERSION +1 -1
  3. package/docs/ANV-263-hook-logging-investigation.md +116 -0
  4. package/docs/command-reference.md +398 -17
  5. package/docs/session-workflow.md +62 -9
  6. package/docs/system-architecture.md +584 -0
  7. package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
  8. package/global/api/openapi.yaml +357 -0
  9. package/global/api/ralph_api.py +528 -0
  10. package/global/commands/anvil-settings.md +47 -19
  11. package/global/commands/audit.md +163 -0
  12. package/global/commands/checklist.md +180 -0
  13. package/global/commands/coderabbit-fix.md +282 -0
  14. package/global/commands/efficiency.md +356 -0
  15. package/global/commands/evidence.md +117 -33
  16. package/global/commands/hud.md +24 -0
  17. package/global/commands/insights.md +101 -3
  18. package/global/commands/orient.md +22 -21
  19. package/global/commands/patterns.md +115 -0
  20. package/global/commands/ralph.md +47 -1
  21. package/global/commands/token-budget.md +214 -0
  22. package/global/commands/weekly-review.md +21 -1
  23. package/global/config/notifications.yaml.template +50 -0
  24. package/global/hooks/ralph_stop.sh +33 -1
  25. package/global/hooks/statusline.sh +67 -2
  26. package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
  27. package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
  28. package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
  29. package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
  30. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  31. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  32. package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
  33. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  34. package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
  35. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  36. package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
  37. package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
  38. package/global/lib/coderabbit_metrics.py +647 -0
  39. package/global/lib/command_tracker.py +147 -0
  40. package/global/lib/context_optimizer.py +323 -0
  41. package/global/lib/linear_provider.py +210 -16
  42. package/global/lib/log_rotation.py +287 -0
  43. package/global/lib/optimization_applier.py +582 -0
  44. package/global/lib/ralph_events.py +398 -0
  45. package/global/lib/ralph_notifier.py +366 -0
  46. package/global/lib/ralph_state.py +264 -24
  47. package/global/lib/ralph_webhooks.py +470 -0
  48. package/global/lib/state_manager.py +121 -0
  49. package/global/lib/token_analyzer.py +1383 -0
  50. package/global/lib/token_metrics.py +919 -0
  51. package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
  52. package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
  53. package/global/tests/__pycache__/test_doc_coverage.cpython-314-pytest-9.0.2.pyc +0 -0
  54. package/global/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  55. package/global/tests/__pycache__/test_issue_models.cpython-314-pytest-9.0.2.pyc +0 -0
  56. package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
  57. package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  58. package/global/tests/__pycache__/test_local_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  59. package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
  60. package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
  61. package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
  62. package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
  63. package/global/tests/test_command_tracker.py +172 -0
  64. package/global/tests/test_context_optimizer.py +321 -0
  65. package/global/tests/test_linear_filtering.py +319 -0
  66. package/global/tests/test_linear_provider.py +40 -1
  67. package/global/tests/test_optimization_applier.py +508 -0
  68. package/global/tests/test_token_analyzer.py +735 -0
  69. package/global/tests/test_token_analyzer_phase6.py +537 -0
  70. package/global/tests/test_token_metrics.py +829 -0
  71. package/global/tools/README.md +153 -0
  72. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  73. package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
  74. package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
  75. package/global/tools/anvil-hud.py +86 -1
  76. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
  77. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
  78. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
  79. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
  80. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
  81. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
  82. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
  83. package/global/tools/anvil-memory/src/commands/context.ts +322 -0
  84. package/global/tools/anvil-memory/src/db.ts +108 -0
  85. package/global/tools/anvil-memory/src/index.ts +2 -8
  86. package/global/tools/orient_linear.py +159 -0
  87. package/global/tools/ralph-watch +423 -0
  88. package/package.json +2 -1
  89. package/project/.anvil-project.yaml.template +93 -0
  90. package/project/CLAUDE.md.template +343 -0
  91. package/project/agents/README.md +119 -0
  92. package/project/agents/cross-layer-debugger.md +217 -0
  93. package/project/agents/security-code-reviewer.md +162 -0
  94. package/project/constitution.md.template +235 -0
  95. package/project/coordination.md +103 -0
  96. package/project/docs/background-tasks.md +258 -0
  97. package/project/docs/skills-frontmatter.md +243 -0
  98. package/project/examples/README.md +106 -0
  99. package/project/examples/api-route-template.ts +171 -0
  100. package/project/examples/component-template.tsx +110 -0
  101. package/project/examples/hook-template.ts +152 -0
  102. package/project/examples/service-template.ts +207 -0
  103. package/project/examples/test-template.test.tsx +249 -0
  104. package/project/hooks/README.md +491 -0
  105. package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
  106. package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
  107. package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
  108. package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  109. package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  110. package/project/hooks/notification.py +183 -0
  111. package/project/hooks/permission_request.py +438 -0
  112. package/project/hooks/post_tool_use.py +397 -0
  113. package/project/hooks/pre_compact.py +126 -0
  114. package/project/hooks/pre_tool_use.py +454 -0
  115. package/project/hooks/session_start.py +656 -0
  116. package/project/hooks/stop.py +356 -0
  117. package/project/hooks/subagent_start.py +223 -0
  118. package/project/hooks/subagent_stop.py +215 -0
  119. package/project/hooks/user_prompt_submit.py +110 -0
  120. package/project/hooks/utils/llm/anth.py +114 -0
  121. package/project/hooks/utils/llm/oai.py +114 -0
  122. package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
  123. package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
  124. package/project/hooks/utils/tts/openai_tts.py +92 -0
  125. package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
  126. package/project/linear.yaml.template +23 -0
  127. package/project/product.md.template +238 -0
  128. package/project/retros/README.md +126 -0
  129. package/project/rules/README.md +90 -0
  130. package/project/rules/debugging.md +139 -0
  131. package/project/rules/security-review.md +115 -0
  132. package/project/settings.yaml.template +185 -0
  133. package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
  134. package/project/templates/api-python/CLAUDE.md +547 -0
  135. package/project/templates/generic/CLAUDE.md +260 -0
  136. package/project/templates/saas/CLAUDE.md +478 -0
  137. package/project/tests/README.md +140 -0
  138. package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
  139. package/project/tests/fixtures/sample-transcript.jsonl +21 -0
  140. package/project/tests/test-hooks.sh +259 -0
  141. package/project/tests/test-lib.sh +248 -0
  142. package/project/tests/test-statusline.sh +165 -0
  143. 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 |
@@ -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()