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