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,356 @@
|
|
|
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 sys
|
|
13
|
+
import random
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from dotenv import load_dotenv
|
|
19
|
+
load_dotenv()
|
|
20
|
+
except ImportError:
|
|
21
|
+
pass # dotenv is optional
|
|
22
|
+
|
|
23
|
+
# Add global lib to path for agent_registry
|
|
24
|
+
_global_lib = Path(__file__).parent.parent.parent / "global" / "lib"
|
|
25
|
+
if _global_lib.exists():
|
|
26
|
+
sys.path.insert(0, str(_global_lib))
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from agent_registry import deregister_agent
|
|
30
|
+
AGENT_REGISTRY_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
AGENT_REGISTRY_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_my_codename() -> str:
|
|
36
|
+
"""Get this agent's codename (A1, A2, etc.) from registry.
|
|
37
|
+
|
|
38
|
+
Reads agent ID from local anvil-state.json, then looks up
|
|
39
|
+
the codename from the global agent registry.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Codename like "A1" or empty string if not found.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
state_file = Path(".claude/anvil-state.json")
|
|
46
|
+
if not state_file.exists():
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
state = json.loads(state_file.read_text())
|
|
50
|
+
agent_id = state.get("session", {}).get("agentId", "")
|
|
51
|
+
if not agent_id:
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
registry_file = Path.home() / ".anvil" / "agents.json"
|
|
55
|
+
if registry_file.exists():
|
|
56
|
+
registry = json.loads(registry_file.read_text())
|
|
57
|
+
agent = registry.get("agents", {}).get(agent_id, {})
|
|
58
|
+
return agent.get("codename") or ""
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cleanup_agent_registration():
|
|
65
|
+
"""Deregister agent from global registry on session stop (ANV-222).
|
|
66
|
+
|
|
67
|
+
This ensures the agent count stays accurate by immediately removing
|
|
68
|
+
the agent when the session ends, rather than waiting for stale cleanup.
|
|
69
|
+
"""
|
|
70
|
+
if not AGENT_REGISTRY_AVAILABLE:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# Read agent ID from anvil-state.json
|
|
75
|
+
state_file = Path(".claude/anvil-state.json")
|
|
76
|
+
if not state_file.exists():
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
state = json.loads(state_file.read_text())
|
|
80
|
+
agent_id = state.get("session", {}).get("agentId")
|
|
81
|
+
if agent_id:
|
|
82
|
+
deregister_agent(agent_id)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass # Fail silently
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_completion_messages():
|
|
88
|
+
"""Return list of friendly completion messages."""
|
|
89
|
+
return [
|
|
90
|
+
"Work complete!",
|
|
91
|
+
"All done!",
|
|
92
|
+
"Task finished!",
|
|
93
|
+
"Job complete!",
|
|
94
|
+
"Ready for next task!"
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def read_anvil_config():
|
|
99
|
+
"""
|
|
100
|
+
Read Anvil framework configuration from .claude/anvil.config.json.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
dict: Config with autoRetro and autoHealthcheck settings, or defaults if missing.
|
|
104
|
+
"""
|
|
105
|
+
defaults = {
|
|
106
|
+
"version": "1.0",
|
|
107
|
+
"autoRetro": False,
|
|
108
|
+
"autoHealthcheck": False
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
config_path = Path(os.getcwd()) / ".claude" / "anvil.config.json"
|
|
112
|
+
|
|
113
|
+
if not config_path.exists():
|
|
114
|
+
return defaults
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
with open(config_path, 'r') as f:
|
|
118
|
+
config = json.load(f)
|
|
119
|
+
# Merge with defaults to ensure all keys exist
|
|
120
|
+
return {**defaults, **config}
|
|
121
|
+
except (json.JSONDecodeError, IOError):
|
|
122
|
+
return defaults
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_auto_trigger_commands(config):
|
|
126
|
+
"""
|
|
127
|
+
Determine which commands should be suggested based on config.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
config: Dict with autoRetro and autoHealthcheck settings
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
list: Commands to suggest running
|
|
134
|
+
"""
|
|
135
|
+
commands = []
|
|
136
|
+
|
|
137
|
+
if config.get("autoHealthcheck", False):
|
|
138
|
+
commands.append("/healthcheck")
|
|
139
|
+
|
|
140
|
+
if config.get("autoRetro", False):
|
|
141
|
+
commands.append("/retro")
|
|
142
|
+
|
|
143
|
+
return commands
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_tts_script_path():
|
|
147
|
+
"""
|
|
148
|
+
Determine which TTS script to use based on availability and API keys.
|
|
149
|
+
Priority order: MLX Audio (local) > ElevenLabs > OpenAI > pyttsx3
|
|
150
|
+
"""
|
|
151
|
+
# Get current script directory and construct utils/tts path
|
|
152
|
+
script_dir = Path(__file__).parent
|
|
153
|
+
tts_dir = script_dir / "utils" / "tts"
|
|
154
|
+
|
|
155
|
+
# Check for MLX Audio (highest priority - fast, free, local)
|
|
156
|
+
mlx_script = tts_dir / "mlx_audio_tts.py"
|
|
157
|
+
if mlx_script.exists():
|
|
158
|
+
return str(mlx_script)
|
|
159
|
+
|
|
160
|
+
# Check for ElevenLabs API key
|
|
161
|
+
if os.getenv('ELEVENLABS_API_KEY'):
|
|
162
|
+
elevenlabs_script = tts_dir / "elevenlabs_tts.py"
|
|
163
|
+
if elevenlabs_script.exists():
|
|
164
|
+
return str(elevenlabs_script)
|
|
165
|
+
|
|
166
|
+
# Check for OpenAI API key
|
|
167
|
+
if os.getenv('OPENAI_API_KEY'):
|
|
168
|
+
openai_script = tts_dir / "openai_tts.py"
|
|
169
|
+
if openai_script.exists():
|
|
170
|
+
return str(openai_script)
|
|
171
|
+
|
|
172
|
+
# Fall back to pyttsx3 (no API key required)
|
|
173
|
+
pyttsx3_script = tts_dir / "pyttsx3_tts.py"
|
|
174
|
+
if pyttsx3_script.exists():
|
|
175
|
+
return str(pyttsx3_script)
|
|
176
|
+
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_llm_completion_message():
|
|
181
|
+
"""
|
|
182
|
+
Generate completion message using available LLM services.
|
|
183
|
+
Priority order: OpenAI > Anthropic > fallback to random message
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
str: Generated or fallback completion message
|
|
187
|
+
"""
|
|
188
|
+
# Get current script directory and construct utils/llm path
|
|
189
|
+
script_dir = Path(__file__).parent
|
|
190
|
+
llm_dir = script_dir / "utils" / "llm"
|
|
191
|
+
|
|
192
|
+
# Try OpenAI first (highest priority)
|
|
193
|
+
if os.getenv('OPENAI_API_KEY'):
|
|
194
|
+
oai_script = llm_dir / "oai.py"
|
|
195
|
+
if oai_script.exists():
|
|
196
|
+
try:
|
|
197
|
+
result = subprocess.run([
|
|
198
|
+
"uv", "run", str(oai_script), "--completion"
|
|
199
|
+
],
|
|
200
|
+
capture_output=True,
|
|
201
|
+
text=True,
|
|
202
|
+
timeout=10
|
|
203
|
+
)
|
|
204
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
205
|
+
return result.stdout.strip()
|
|
206
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
# Try Anthropic second
|
|
210
|
+
if os.getenv('ANTHROPIC_API_KEY'):
|
|
211
|
+
anth_script = llm_dir / "anth.py"
|
|
212
|
+
if anth_script.exists():
|
|
213
|
+
try:
|
|
214
|
+
result = subprocess.run([
|
|
215
|
+
"uv", "run", str(anth_script), "--completion"
|
|
216
|
+
],
|
|
217
|
+
capture_output=True,
|
|
218
|
+
text=True,
|
|
219
|
+
timeout=10
|
|
220
|
+
)
|
|
221
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
222
|
+
return result.stdout.strip()
|
|
223
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
# Fallback to random predefined message
|
|
227
|
+
messages = get_completion_messages()
|
|
228
|
+
return random.choice(messages)
|
|
229
|
+
|
|
230
|
+
def announce_completion():
|
|
231
|
+
"""Announce completion using the best available TTS service."""
|
|
232
|
+
try:
|
|
233
|
+
tts_script = get_tts_script_path()
|
|
234
|
+
if not tts_script:
|
|
235
|
+
return # No TTS scripts available
|
|
236
|
+
|
|
237
|
+
# Get agent codename for identification (ANV-135)
|
|
238
|
+
codename = get_my_codename()
|
|
239
|
+
agent_prefix = f"Agent {codename[1:]}" if codename.startswith("A") else ""
|
|
240
|
+
|
|
241
|
+
# Get completion message (LLM-generated or fallback)
|
|
242
|
+
base_message = get_llm_completion_message()
|
|
243
|
+
|
|
244
|
+
# Prepend agent identifier if available
|
|
245
|
+
if agent_prefix:
|
|
246
|
+
completion_message = f"{agent_prefix}: {base_message}"
|
|
247
|
+
else:
|
|
248
|
+
completion_message = base_message
|
|
249
|
+
|
|
250
|
+
# Call the TTS script with the completion message
|
|
251
|
+
subprocess.run([
|
|
252
|
+
"uv", "run", tts_script, completion_message
|
|
253
|
+
],
|
|
254
|
+
capture_output=True, # Suppress output
|
|
255
|
+
timeout=10 # 10-second timeout
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
|
|
259
|
+
# Fail silently if TTS encounters issues
|
|
260
|
+
pass
|
|
261
|
+
except Exception:
|
|
262
|
+
# Fail silently for any other errors
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def main():
|
|
267
|
+
try:
|
|
268
|
+
# Parse command line arguments
|
|
269
|
+
parser = argparse.ArgumentParser()
|
|
270
|
+
parser.add_argument('--chat', action='store_true', help='Copy transcript to chat.json')
|
|
271
|
+
# Note: --cleanup-agent flag removed in ANV-222; deregistration now always happens
|
|
272
|
+
args = parser.parse_args()
|
|
273
|
+
|
|
274
|
+
# Read JSON input from stdin
|
|
275
|
+
input_data = json.load(sys.stdin)
|
|
276
|
+
|
|
277
|
+
# Extract required fields
|
|
278
|
+
session_id = input_data.get("session_id", "")
|
|
279
|
+
input_data.get("stop_hook_active", False)
|
|
280
|
+
|
|
281
|
+
# Ensure log directory exists
|
|
282
|
+
log_dir = os.path.join(os.getcwd(), "logs")
|
|
283
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
284
|
+
log_path = os.path.join(log_dir, "stop.json")
|
|
285
|
+
|
|
286
|
+
# Read existing log data or initialize empty list
|
|
287
|
+
if os.path.exists(log_path):
|
|
288
|
+
with open(log_path, 'r') as f:
|
|
289
|
+
try:
|
|
290
|
+
log_data = json.load(f)
|
|
291
|
+
except (json.JSONDecodeError, ValueError):
|
|
292
|
+
log_data = []
|
|
293
|
+
else:
|
|
294
|
+
log_data = []
|
|
295
|
+
|
|
296
|
+
# Append new data
|
|
297
|
+
log_data.append(input_data)
|
|
298
|
+
|
|
299
|
+
# Write back to file with formatting
|
|
300
|
+
with open(log_path, 'w') as f:
|
|
301
|
+
json.dump(log_data, f, indent=2)
|
|
302
|
+
|
|
303
|
+
# ANV-222: Always deregister agent from global registry on stop
|
|
304
|
+
# This ensures accurate agent counts without waiting for stale cleanup
|
|
305
|
+
cleanup_agent_registration()
|
|
306
|
+
|
|
307
|
+
# Handle --chat switch
|
|
308
|
+
if args.chat and 'transcript_path' in input_data:
|
|
309
|
+
transcript_path = input_data['transcript_path']
|
|
310
|
+
if os.path.exists(transcript_path):
|
|
311
|
+
# Read .jsonl file and convert to JSON array
|
|
312
|
+
chat_data = []
|
|
313
|
+
try:
|
|
314
|
+
with open(transcript_path, 'r') as f:
|
|
315
|
+
for line in f:
|
|
316
|
+
line = line.strip()
|
|
317
|
+
if line:
|
|
318
|
+
try:
|
|
319
|
+
chat_data.append(json.loads(line))
|
|
320
|
+
except json.JSONDecodeError:
|
|
321
|
+
pass # Skip invalid lines
|
|
322
|
+
|
|
323
|
+
# Write to logs/chat.json
|
|
324
|
+
chat_file = os.path.join(log_dir, 'chat.json')
|
|
325
|
+
with open(chat_file, 'w') as f:
|
|
326
|
+
json.dump(chat_data, f, indent=2)
|
|
327
|
+
except Exception:
|
|
328
|
+
pass # Fail silently
|
|
329
|
+
|
|
330
|
+
# Check for auto-trigger commands from anvil.config.json
|
|
331
|
+
config = read_anvil_config()
|
|
332
|
+
auto_commands = get_auto_trigger_commands(config)
|
|
333
|
+
|
|
334
|
+
if auto_commands:
|
|
335
|
+
# Output reminder for Claude to see
|
|
336
|
+
print("\n[Anvil Auto-Trigger]")
|
|
337
|
+
print("The following commands are configured to run at session end:")
|
|
338
|
+
for cmd in auto_commands:
|
|
339
|
+
print(f" → {cmd}")
|
|
340
|
+
print("\nConsider running these commands before ending the session.")
|
|
341
|
+
|
|
342
|
+
# Announce completion via TTS
|
|
343
|
+
announce_completion()
|
|
344
|
+
|
|
345
|
+
sys.exit(0)
|
|
346
|
+
|
|
347
|
+
except json.JSONDecodeError:
|
|
348
|
+
# Handle JSON decode errors gracefully
|
|
349
|
+
sys.exit(0)
|
|
350
|
+
except Exception:
|
|
351
|
+
# Handle any other errors gracefully
|
|
352
|
+
sys.exit(0)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if __name__ == "__main__":
|
|
356
|
+
main()
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "python-dotenv",
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
"""
|
|
9
|
+
SubagentStart Hook
|
|
10
|
+
|
|
11
|
+
Fires when a subagent begins execution. Enables:
|
|
12
|
+
- Logging subagent invocations for observability
|
|
13
|
+
- Injecting context specific to the subagent type
|
|
14
|
+
- Tracking active agents for coordination
|
|
15
|
+
|
|
16
|
+
Input Schema:
|
|
17
|
+
{
|
|
18
|
+
"session_id": "string",
|
|
19
|
+
"agent_id": "string",
|
|
20
|
+
"agent_type": "string",
|
|
21
|
+
"tool_use_id": "string"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Output Schema (for context injection):
|
|
25
|
+
{
|
|
26
|
+
"hookSpecificOutput": {
|
|
27
|
+
"hookEventName": "SubagentStart",
|
|
28
|
+
"additionalContext": "string"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
uv run .claude/hooks/subagent_start.py --log --inject-context
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import json
|
|
38
|
+
import sys
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from datetime import datetime
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from dotenv import load_dotenv
|
|
44
|
+
load_dotenv()
|
|
45
|
+
except ImportError:
|
|
46
|
+
pass # dotenv is optional
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Context mapping: agent_type -> rules file
|
|
50
|
+
# Add your custom agent types and their corresponding rules here
|
|
51
|
+
CONTEXT_MAP = {
|
|
52
|
+
"security-code-reviewer": ".claude/rules/security-review.md",
|
|
53
|
+
"cross-layer-debugger": ".claude/rules/debugging.md",
|
|
54
|
+
# Add more mappings as needed:
|
|
55
|
+
# "api-reviewer": ".claude/rules/api-design.md",
|
|
56
|
+
# "test-writer": ".claude/rules/testing.md",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def log_subagent_start(input_data):
|
|
61
|
+
"""
|
|
62
|
+
Log subagent start event to logs/subagent_start.json.
|
|
63
|
+
|
|
64
|
+
Creates a timestamped record of each subagent invocation for:
|
|
65
|
+
- Debugging and observability
|
|
66
|
+
- Tracking agent coordination patterns
|
|
67
|
+
- Auditing agent usage
|
|
68
|
+
"""
|
|
69
|
+
log_dir = Path("logs")
|
|
70
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
log_file = log_dir / 'subagent_start.json'
|
|
72
|
+
|
|
73
|
+
# Read existing log data or initialize empty list
|
|
74
|
+
if log_file.exists():
|
|
75
|
+
try:
|
|
76
|
+
with open(log_file, 'r') as f:
|
|
77
|
+
log_data = json.load(f)
|
|
78
|
+
except (json.JSONDecodeError, ValueError):
|
|
79
|
+
log_data = []
|
|
80
|
+
else:
|
|
81
|
+
log_data = []
|
|
82
|
+
|
|
83
|
+
# Append new entry with timestamp
|
|
84
|
+
log_data.append({
|
|
85
|
+
**input_data,
|
|
86
|
+
"timestamp": datetime.now().isoformat()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
# Write back to file
|
|
90
|
+
with open(log_file, 'w') as f:
|
|
91
|
+
json.dump(log_data, f, indent=2)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def inject_context(agent_type):
|
|
95
|
+
"""
|
|
96
|
+
Inject context based on subagent type.
|
|
97
|
+
|
|
98
|
+
Reads rules files from .claude/rules/ directory and returns
|
|
99
|
+
their content to be injected into the subagent's context.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
agent_type: The type/name of the subagent being invoked
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
String content of the rules file, or None if not found
|
|
106
|
+
"""
|
|
107
|
+
context_file = CONTEXT_MAP.get(agent_type)
|
|
108
|
+
|
|
109
|
+
if context_file:
|
|
110
|
+
context_path = Path(context_file)
|
|
111
|
+
if context_path.exists():
|
|
112
|
+
try:
|
|
113
|
+
with open(context_path, 'r') as f:
|
|
114
|
+
return f.read()
|
|
115
|
+
except (IOError, OSError):
|
|
116
|
+
pass # Fail silently if file can't be read
|
|
117
|
+
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def register_agent(input_data):
|
|
122
|
+
"""
|
|
123
|
+
Register agent in global registry for multi-agent coordination.
|
|
124
|
+
|
|
125
|
+
This is optional - used by Anvil HUD for tracking active agents.
|
|
126
|
+
Registry location: ~/.anvil/agents.json
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
registry_dir = Path.home() / ".anvil"
|
|
130
|
+
registry_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
registry_file = registry_dir / "agents.json"
|
|
132
|
+
|
|
133
|
+
# Read existing registry
|
|
134
|
+
if registry_file.exists():
|
|
135
|
+
try:
|
|
136
|
+
with open(registry_file, 'r') as f:
|
|
137
|
+
registry = json.load(f)
|
|
138
|
+
except (json.JSONDecodeError, ValueError):
|
|
139
|
+
registry = {"agents": []}
|
|
140
|
+
else:
|
|
141
|
+
registry = {"agents": []}
|
|
142
|
+
|
|
143
|
+
# Add new agent entry
|
|
144
|
+
agent_entry = {
|
|
145
|
+
"agent_id": input_data.get("agent_id", ""),
|
|
146
|
+
"agent_type": input_data.get("agent_type", ""),
|
|
147
|
+
"session_id": input_data.get("session_id", ""),
|
|
148
|
+
"tool_use_id": input_data.get("tool_use_id", ""),
|
|
149
|
+
"started_at": datetime.now().isoformat(),
|
|
150
|
+
"status": "running"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
registry["agents"].append(agent_entry)
|
|
154
|
+
registry["last_updated"] = datetime.now().isoformat()
|
|
155
|
+
|
|
156
|
+
# Write back
|
|
157
|
+
with open(registry_file, 'w') as f:
|
|
158
|
+
json.dump(registry, f, indent=2)
|
|
159
|
+
|
|
160
|
+
except Exception:
|
|
161
|
+
pass # Fail silently - registry is optional
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main():
|
|
165
|
+
parser = argparse.ArgumentParser(
|
|
166
|
+
description='SubagentStart hook - log and inject context for subagents'
|
|
167
|
+
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
'--log',
|
|
170
|
+
action='store_true',
|
|
171
|
+
help='Log subagent start to logs/subagent_start.json'
|
|
172
|
+
)
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
'--inject-context',
|
|
175
|
+
action='store_true',
|
|
176
|
+
help='Inject context from .claude/rules/ based on agent type'
|
|
177
|
+
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
'--register',
|
|
180
|
+
action='store_true',
|
|
181
|
+
help='Register agent in global registry (~/.anvil/agents.json)'
|
|
182
|
+
)
|
|
183
|
+
args = parser.parse_args()
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# Read JSON input from stdin
|
|
187
|
+
input_data = json.load(sys.stdin)
|
|
188
|
+
|
|
189
|
+
# Log subagent start if requested
|
|
190
|
+
if args.log:
|
|
191
|
+
log_subagent_start(input_data)
|
|
192
|
+
|
|
193
|
+
# Register in global registry if requested
|
|
194
|
+
if args.register:
|
|
195
|
+
register_agent(input_data)
|
|
196
|
+
|
|
197
|
+
# Inject context if requested
|
|
198
|
+
if args.inject_context:
|
|
199
|
+
agent_type = input_data.get('agent_type', '')
|
|
200
|
+
context = inject_context(agent_type)
|
|
201
|
+
|
|
202
|
+
if context:
|
|
203
|
+
# Output hook-specific response for context injection
|
|
204
|
+
output = {
|
|
205
|
+
"hookSpecificOutput": {
|
|
206
|
+
"hookEventName": "SubagentStart",
|
|
207
|
+
"additionalContext": context
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
print(json.dumps(output))
|
|
211
|
+
|
|
212
|
+
sys.exit(0)
|
|
213
|
+
|
|
214
|
+
except json.JSONDecodeError:
|
|
215
|
+
# Handle JSON decode errors gracefully
|
|
216
|
+
sys.exit(0)
|
|
217
|
+
except Exception:
|
|
218
|
+
# Handle any other errors gracefully
|
|
219
|
+
sys.exit(0)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == '__main__':
|
|
223
|
+
main()
|