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,656 @@
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 subprocess
14
+ import platform
15
+ import hashlib
16
+ import random
17
+ import re
18
+ from pathlib import Path
19
+ from datetime import datetime, timezone
20
+
21
+ try:
22
+ from dotenv import load_dotenv
23
+ load_dotenv()
24
+ except ImportError:
25
+ pass # dotenv is optional
26
+
27
+ # Add global lib to path for agent_registry
28
+ _global_lib = Path(__file__).parent.parent.parent / "global" / "lib"
29
+ if _global_lib.exists():
30
+ sys.path.insert(0, str(_global_lib))
31
+
32
+ try:
33
+ from agent_registry import register_agent as hud_register_agent
34
+ AGENT_REGISTRY_AVAILABLE = True
35
+ except ImportError:
36
+ AGENT_REGISTRY_AVAILABLE = False
37
+
38
+ # Doc coverage service for gap detection (ANV-218)
39
+ try:
40
+ from doc_coverage_service import DocCoverageService
41
+ DOC_COVERAGE_AVAILABLE = True
42
+ except ImportError:
43
+ DOC_COVERAGE_AVAILABLE = False
44
+
45
+
46
+ def get_my_codename() -> str:
47
+ """Get this agent's codename (A1, A2, etc.) from registry.
48
+
49
+ Reads agent ID from local anvil-state.json, then looks up
50
+ the codename from the global agent registry.
51
+
52
+ Returns:
53
+ Codename like "A1" or empty string if not found.
54
+ """
55
+ try:
56
+ state_file = Path(".claude/anvil-state.json")
57
+ if not state_file.exists():
58
+ return ""
59
+
60
+ state = json.loads(state_file.read_text())
61
+ agent_id = state.get("session", {}).get("agentId", "")
62
+ if not agent_id:
63
+ return ""
64
+
65
+ registry_file = Path.home() / ".anvil" / "agents.json"
66
+ if registry_file.exists():
67
+ registry = json.loads(registry_file.read_text())
68
+ agent = registry.get("agents", {}).get(agent_id, {})
69
+ return agent.get("codename") or ""
70
+ except Exception:
71
+ pass
72
+ return ""
73
+
74
+
75
+ # Agent ID word lists for human-readable identifiers
76
+ ADJECTIVES = [
77
+ "swift", "calm", "bold", "keen", "wise",
78
+ "warm", "cool", "fair", "bright", "quick",
79
+ "steady", "agile", "clever", "noble", "eager"
80
+ ]
81
+
82
+ NOUNS = [
83
+ "falcon", "raven", "wolf", "bear", "hawk",
84
+ "fox", "owl", "lion", "tiger", "eagle",
85
+ "otter", "heron", "lynx", "stag", "crane"
86
+ ]
87
+
88
+
89
+ def generate_agent_id():
90
+ """Generate a unique, human-readable agent ID.
91
+
92
+ Format: {adjective}-{noun}-{4char-hash}
93
+ Example: swift-falcon-a3f2
94
+ """
95
+ adjective = random.choice(ADJECTIVES)
96
+ noun = random.choice(NOUNS)
97
+
98
+ # Generate 4-char hash from timestamp + random for uniqueness
99
+ unique_str = f"{datetime.now().isoformat()}-{random.randint(0, 999999)}"
100
+ hash_suffix = hashlib.md5(unique_str.encode()).hexdigest()[:4]
101
+
102
+ return f"{adjective}-{noun}-{hash_suffix}"
103
+
104
+
105
+ def update_coordination_file(agent_id, working_on="(starting up)"):
106
+ """Register agent in coordination.md Active Sessions table.
107
+
108
+ Args:
109
+ agent_id: The unique agent identifier
110
+ working_on: What the agent is working on (default: starting up)
111
+
112
+ Returns:
113
+ bool: True if successful, False otherwise
114
+ """
115
+ coord_file = Path(".claude/coordination.md")
116
+
117
+ if not coord_file.exists():
118
+ return False
119
+
120
+ try:
121
+ content = coord_file.read_text()
122
+
123
+ # Find the Active Sessions table
124
+ # Pattern: table header followed by separator, then rows until empty line or ---
125
+ table_pattern = r'(\| Session ID \| Started \| Working On \| Status \|\n\|[-|]+\|)\n'
126
+ match = re.search(table_pattern, content)
127
+
128
+ if not match:
129
+ return False
130
+
131
+ # Create new row
132
+ started_time = datetime.now().strftime("%H:%M")
133
+ new_row = f"| {agent_id} | {started_time} | {working_on} | active |\n"
134
+
135
+ # Insert after table header
136
+ insert_pos = match.end()
137
+ updated_content = content[:insert_pos] + new_row + content[insert_pos:]
138
+
139
+ # Also add to Session Log
140
+ log_entry = f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] [{agent_id}]: started - session begin\n"
141
+
142
+ # Find "### Today" section and add entry after it
143
+ today_pattern = r'(### Today\n+)'
144
+ today_match = re.search(today_pattern, updated_content)
145
+ if today_match:
146
+ log_insert_pos = today_match.end()
147
+ updated_content = updated_content[:log_insert_pos] + log_entry + updated_content[log_insert_pos:]
148
+
149
+ coord_file.write_text(updated_content)
150
+ return True
151
+
152
+ except Exception:
153
+ return False
154
+
155
+
156
+ def update_anvil_state(agent_id):
157
+ """Store agent ID in anvil-state.json.
158
+
159
+ Args:
160
+ agent_id: The unique agent identifier
161
+
162
+ Returns:
163
+ bool: True if successful, False otherwise
164
+ """
165
+ state_file = Path(".claude/anvil-state.json")
166
+
167
+ try:
168
+ # Read existing state or create new
169
+ if state_file.exists():
170
+ state = json.loads(state_file.read_text())
171
+ else:
172
+ state = {
173
+ "version": "1.0",
174
+ "session": {},
175
+ "cache": {"git": {}},
176
+ "meta": {"createdAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")}
177
+ }
178
+
179
+ # Add agent info to session
180
+ state["session"]["agentId"] = agent_id
181
+ state["session"]["agentStartedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
182
+ state["meta"]["updatedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
183
+
184
+ # Write back
185
+ state_file.parent.mkdir(parents=True, exist_ok=True)
186
+ state_file.write_text(json.dumps(state, indent=2) + "\n")
187
+ return True
188
+
189
+ except Exception:
190
+ return False
191
+
192
+
193
+ def register_agent():
194
+ """Register this session as an active agent.
195
+
196
+ Returns:
197
+ str: The generated agent ID, or None if registration failed
198
+ """
199
+ agent_id = generate_agent_id()
200
+
201
+ # Get current branch to include in "working on"
202
+ branch, _ = get_git_status()
203
+ working_on = f"branch: {branch}" if branch else "(starting up)"
204
+
205
+ # Update coordination file
206
+ coord_success = update_coordination_file(agent_id, working_on)
207
+
208
+ # Update anvil state
209
+ state_success = update_anvil_state(agent_id)
210
+
211
+ if coord_success or state_success:
212
+ return agent_id
213
+
214
+ return None
215
+
216
+
217
+ def get_agent_id_from_state():
218
+ """Read agent ID from anvil-state.json.
219
+
220
+ Returns:
221
+ str: The agent ID, or None if not found
222
+ """
223
+ state_file = Path(".claude/anvil-state.json")
224
+
225
+ try:
226
+ if state_file.exists():
227
+ state = json.loads(state_file.read_text())
228
+ return state.get("session", {}).get("agentId")
229
+ except Exception:
230
+ pass
231
+
232
+ return None
233
+
234
+
235
+ def remove_agent_from_coordination(agent_id):
236
+ """Remove agent from coordination.md Active Sessions table.
237
+
238
+ Args:
239
+ agent_id: The agent ID to remove
240
+
241
+ Returns:
242
+ bool: True if successful, False otherwise
243
+ """
244
+ coord_file = Path(".claude/coordination.md")
245
+
246
+ if not coord_file.exists():
247
+ return False
248
+
249
+ try:
250
+ content = coord_file.read_text()
251
+
252
+ # Find and remove the agent's row from Active Sessions
253
+ # Pattern: | agent_id | ... | ... | ... |\n
254
+ row_pattern = rf'\| {re.escape(agent_id)} \|[^\n]+\|\n'
255
+ updated_content = re.sub(row_pattern, '', content)
256
+
257
+ if updated_content != content:
258
+ coord_file.write_text(updated_content)
259
+ return True
260
+
261
+ return False
262
+
263
+ except Exception:
264
+ return False
265
+
266
+
267
+ def add_completion_log(agent_id, summary="session end"):
268
+ """Add completion entry to coordination.md Session Log.
269
+
270
+ Args:
271
+ agent_id: The agent ID
272
+ summary: Brief summary of what was done (default: "session end")
273
+
274
+ Returns:
275
+ bool: True if successful, False otherwise
276
+ """
277
+ coord_file = Path(".claude/coordination.md")
278
+
279
+ if not coord_file.exists():
280
+ return False
281
+
282
+ try:
283
+ content = coord_file.read_text()
284
+
285
+ # Add completion log entry after "### Today"
286
+ log_entry = f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] [{agent_id}]: completed - {summary}\n"
287
+
288
+ today_pattern = r'(### Today\n+)'
289
+ match = re.search(today_pattern, content)
290
+
291
+ if match:
292
+ insert_pos = match.end()
293
+ updated_content = content[:insert_pos] + log_entry + content[insert_pos:]
294
+ coord_file.write_text(updated_content)
295
+ return True
296
+
297
+ return False
298
+
299
+ except Exception:
300
+ return False
301
+
302
+
303
+ def clear_agent_from_state():
304
+ """Clear agent info from anvil-state.json.
305
+
306
+ Returns:
307
+ bool: True if successful, False otherwise
308
+ """
309
+ state_file = Path(".claude/anvil-state.json")
310
+
311
+ try:
312
+ if state_file.exists():
313
+ state = json.loads(state_file.read_text())
314
+
315
+ # Remove agent-specific fields
316
+ session = state.get("session", {})
317
+ session.pop("agentId", None)
318
+ session.pop("agentStartedAt", None)
319
+
320
+ state["meta"]["updatedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
321
+
322
+ state_file.write_text(json.dumps(state, indent=2) + "\n")
323
+ return True
324
+
325
+ except Exception:
326
+ pass
327
+
328
+ return False
329
+
330
+
331
+ def deregister_agent(summary="session end"):
332
+ """Deregister this session's agent.
333
+
334
+ Args:
335
+ summary: Brief summary of what was accomplished (default: "session end")
336
+
337
+ Returns:
338
+ str: The agent ID that was deregistered, or None if no agent was registered
339
+ """
340
+ agent_id = get_agent_id_from_state()
341
+
342
+ if not agent_id:
343
+ return None
344
+
345
+ # Add completion log first (before removing from active)
346
+ add_completion_log(agent_id, summary)
347
+
348
+ # Remove from Active Sessions table
349
+ remove_agent_from_coordination(agent_id)
350
+
351
+ # Clear from anvil state
352
+ clear_agent_from_state()
353
+
354
+ return agent_id
355
+
356
+
357
+ def log_session_start(input_data):
358
+ """Log session start event to logs directory."""
359
+ # Ensure logs directory exists
360
+ log_dir = Path("logs")
361
+ log_dir.mkdir(parents=True, exist_ok=True)
362
+ log_file = log_dir / 'session_start.json'
363
+
364
+ # Read existing log data or initialize empty list
365
+ if log_file.exists():
366
+ with open(log_file, 'r') as f:
367
+ try:
368
+ log_data = json.load(f)
369
+ except (json.JSONDecodeError, ValueError):
370
+ log_data = []
371
+ else:
372
+ log_data = []
373
+
374
+ # Append the entire input data
375
+ log_data.append(input_data)
376
+
377
+ # Write back to file with formatting
378
+ with open(log_file, 'w') as f:
379
+ json.dump(log_data, f, indent=2)
380
+
381
+
382
+ def get_git_status():
383
+ """Get current git status information."""
384
+ try:
385
+ # Get current branch
386
+ branch_result = subprocess.run(
387
+ ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
388
+ capture_output=True,
389
+ text=True,
390
+ timeout=5
391
+ )
392
+ current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "unknown"
393
+
394
+ # Get uncommitted changes count
395
+ status_result = subprocess.run(
396
+ ['git', 'status', '--porcelain'],
397
+ capture_output=True,
398
+ text=True,
399
+ timeout=5
400
+ )
401
+ if status_result.returncode == 0:
402
+ changes = status_result.stdout.strip().split('\n') if status_result.stdout.strip() else []
403
+ uncommitted_count = len(changes)
404
+ else:
405
+ uncommitted_count = 0
406
+
407
+ return current_branch, uncommitted_count
408
+ except Exception:
409
+ return None, None
410
+
411
+
412
+ def get_recent_issues():
413
+ """Get recent GitHub issues if gh CLI is available."""
414
+ try:
415
+ # Check if gh is available
416
+ gh_check = subprocess.run(['which', 'gh'], capture_output=True)
417
+ if gh_check.returncode != 0:
418
+ return None
419
+
420
+ # Get recent open issues
421
+ result = subprocess.run(
422
+ ['gh', 'issue', 'list', '--limit', '5', '--state', 'open'],
423
+ capture_output=True,
424
+ text=True,
425
+ timeout=10
426
+ )
427
+ if result.returncode == 0 and result.stdout.strip():
428
+ return result.stdout.strip()
429
+ except Exception:
430
+ pass
431
+ return None
432
+
433
+
434
+ def load_development_context(source):
435
+ """Load relevant development context based on session source."""
436
+ context_parts = []
437
+
438
+ # Add timestamp
439
+ context_parts.append(f"Session started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
440
+ context_parts.append(f"Session source: {source}")
441
+
442
+ # Add git information
443
+ branch, changes = get_git_status()
444
+ if branch:
445
+ context_parts.append(f"Git branch: {branch}")
446
+ if changes > 0:
447
+ context_parts.append(f"Uncommitted changes: {changes} files")
448
+
449
+ # Add doc coverage warnings if service is available (ANV-218)
450
+ if DOC_COVERAGE_AVAILABLE:
451
+ try:
452
+ service = DocCoverageService()
453
+ warnings = service.get_session_warnings(sensitivity="balanced")
454
+ if warnings:
455
+ context_parts.append(warnings)
456
+ except Exception:
457
+ pass # Don't fail session start on doc coverage errors
458
+
459
+ # Load project-specific context files if they exist
460
+ context_files = [
461
+ ".claude/CONTEXT.md",
462
+ ".claude/TODO.md",
463
+ "TODO.md",
464
+ ".github/ISSUE_TEMPLATE.md"
465
+ ]
466
+
467
+ for file_path in context_files:
468
+ if Path(file_path).exists():
469
+ try:
470
+ with open(file_path, 'r') as f:
471
+ content = f.read().strip()
472
+ if content:
473
+ context_parts.append(f"\n--- Content from {file_path} ---")
474
+ context_parts.append(content[:1000]) # Limit to first 1000 chars
475
+ except Exception:
476
+ pass
477
+
478
+ # Add recent issues if available
479
+ issues = get_recent_issues()
480
+ if issues:
481
+ context_parts.append("\n--- Recent GitHub Issues ---")
482
+ context_parts.append(issues)
483
+
484
+ return "\n".join(context_parts)
485
+
486
+
487
+ def main():
488
+ try:
489
+ # Parse command line arguments
490
+ parser = argparse.ArgumentParser()
491
+ parser.add_argument('--load-context', action='store_true',
492
+ help='Load development context at session start')
493
+ parser.add_argument('--register-agent', action='store_true',
494
+ help='Register agent in coordination.md and HUD registry')
495
+ parser.add_argument('--deregister-agent', action='store_true',
496
+ help='Deregister agent from coordination.md (call on session end)')
497
+ parser.add_argument('--summary', type=str, default='session end',
498
+ help='Summary for deregistration log (used with --deregister-agent)')
499
+ parser.add_argument('--announce', action='store_true',
500
+ help='Announce session start via TTS')
501
+ args = parser.parse_args()
502
+
503
+ # Handle deregistration (doesn't require stdin input)
504
+ if args.deregister_agent:
505
+ agent_id = deregister_agent(args.summary)
506
+ if agent_id:
507
+ print(json.dumps({"success": True, "agentId": agent_id, "action": "deregistered"}))
508
+ else:
509
+ print(json.dumps({"success": False, "error": "No agent registered"}))
510
+ sys.exit(0)
511
+
512
+ # Read JSON input from stdin (for session start operations)
513
+ input_data = json.loads(sys.stdin.read())
514
+
515
+ # Extract fields
516
+ session_id = input_data.get('session_id', 'unknown')
517
+ source = input_data.get('source', 'unknown') # "startup", "resume", or "clear"
518
+
519
+ # Log the session start event
520
+ log_session_start(input_data)
521
+
522
+ # Register agent in global HUD registry for multi-agent visibility
523
+ if args.register_agent and AGENT_REGISTRY_AVAILABLE:
524
+ try:
525
+ # Generate human-readable agent ID (swift-falcon-a3f2 style)
526
+ hud_agent_id = os.getenv("ANVIL_AGENT_ID", generate_agent_id())
527
+
528
+ # Extract model name from input data
529
+ model_info = input_data.get("model", {})
530
+ model_name = model_info.get("display_name", "Claude")
531
+ # Shorten model name
532
+ if "Opus" in model_name:
533
+ model_name = "Opus"
534
+ elif "Sonnet" in model_name:
535
+ model_name = "Sonnet"
536
+ elif "Haiku" in model_name:
537
+ model_name = "Haiku"
538
+
539
+ # Get current project path
540
+ project_path = os.getcwd()
541
+
542
+ # Register in HUD registry (~/.anvil/agents.json)
543
+ hud_register_agent(
544
+ agent_id=hud_agent_id,
545
+ project=project_path,
546
+ session_id=session_id,
547
+ model=model_name
548
+ )
549
+
550
+ # Store HUD agent ID in anvil-state.json for sync script
551
+ state_file = Path(".claude/anvil-state.json")
552
+ try:
553
+ if state_file.exists():
554
+ state = json.loads(state_file.read_text())
555
+ else:
556
+ state = {"version": "1.0", "session": {}, "cache": {"git": {}}, "meta": {}}
557
+ state["session"]["agentId"] = hud_agent_id
558
+ state["meta"]["updatedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
559
+ state_file.write_text(json.dumps(state, indent=2) + "\n")
560
+ except Exception:
561
+ pass
562
+ except Exception:
563
+ pass # HUD registration is optional, don't fail session start
564
+
565
+ # Announce session start if requested (do this first, before potential exit)
566
+ if args.announce:
567
+ try:
568
+ script_dir = Path(__file__).parent
569
+ tts_dir = script_dir / "utils" / "tts"
570
+
571
+ # TTS priority: MLX Audio > ElevenLabs > OpenAI > pyttsx3 > macOS say
572
+ tts_script = None
573
+
574
+ # Check for Apple Silicon and MLX Audio
575
+ if platform.system() == "Darwin" and platform.machine() == "arm64":
576
+ mlx_script = tts_dir / "mlx_audio_tts.py"
577
+ if mlx_script.exists():
578
+ tts_script = mlx_script
579
+
580
+ # ElevenLabs if API key set
581
+ if not tts_script and os.getenv('ELEVENLABS_API_KEY'):
582
+ el_script = tts_dir / "elevenlabs_tts.py"
583
+ if el_script.exists():
584
+ tts_script = el_script
585
+
586
+ # OpenAI TTS if API key set
587
+ if not tts_script and os.getenv('OPENAI_API_KEY'):
588
+ oai_script = tts_dir / "openai_tts.py"
589
+ if oai_script.exists():
590
+ tts_script = oai_script
591
+
592
+ # pyttsx3 fallback
593
+ if not tts_script:
594
+ py_script = tts_dir / "pyttsx3_tts.py"
595
+ if py_script.exists():
596
+ tts_script = py_script
597
+
598
+ # Get agent codename for identification (ANV-135)
599
+ codename = get_my_codename()
600
+ agent_prefix = f"Agent {codename[1:]}" if codename.startswith("A") else "Claude Code"
601
+
602
+ messages = {
603
+ "startup": f"{agent_prefix} session started.",
604
+ "resume": f"{agent_prefix} resuming session.",
605
+ "clear": f"{agent_prefix} starting fresh."
606
+ }
607
+ message = messages.get(source, f"{agent_prefix} ready.")
608
+
609
+ if tts_script:
610
+ result = subprocess.run(
611
+ ["uv", "run", str(tts_script), message],
612
+ capture_output=True,
613
+ timeout=15
614
+ )
615
+ # Fallback to say if script failed
616
+ if result.returncode != 0 and platform.system() == "Darwin":
617
+ subprocess.run(['say', message], capture_output=True, timeout=10)
618
+ elif platform.system() == "Darwin":
619
+ subprocess.run(['say', message], capture_output=True, timeout=10)
620
+ except Exception:
621
+ pass
622
+
623
+ # Register agent if requested (for multi-terminal coordination)
624
+ agent_id = None
625
+ if args.register_agent:
626
+ agent_id = register_agent()
627
+
628
+ # Load development context if requested
629
+ if args.load_context:
630
+ context = load_development_context(source)
631
+ if context:
632
+ # Using JSON output to add context
633
+ output = {
634
+ "hookSpecificOutput": {
635
+ "hookEventName": "SessionStart",
636
+ "additionalContext": context
637
+ }
638
+ }
639
+ # Include agent ID in context if registered
640
+ if agent_id:
641
+ output["hookSpecificOutput"]["additionalContext"] += f"\nAgent ID: {agent_id}"
642
+ print(json.dumps(output))
643
+
644
+ # Success
645
+ sys.exit(0)
646
+
647
+ except json.JSONDecodeError:
648
+ # Handle JSON decode errors gracefully
649
+ sys.exit(0)
650
+ except Exception:
651
+ # Handle any other errors gracefully
652
+ sys.exit(0)
653
+
654
+
655
+ if __name__ == '__main__':
656
+ main()