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