anvil-dev-framework 0.1.8 → 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 +48 -18
- package/VERSION +1 -1
- package/docs/command-reference.md +97 -16
- package/docs/system-architecture.md +15 -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 +44 -18
- package/global/commands/coderabbit-fix.md +282 -0
- package/global/commands/evidence.md +23 -6
- package/global/commands/hud.md +24 -0
- package/global/commands/orient.md +22 -21
- 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__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/optimization_applier.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/log_rotation.py +287 -0
- package/global/lib/ralph_events.py +398 -0
- package/global/lib/ralph_notifier.py +366 -0
- package/global/lib/ralph_webhooks.py +470 -0
- package/global/lib/state_manager.py +121 -0
- package/global/lib/token_analyzer.py +28 -2
- package/global/lib/token_metrics.py +49 -3
- 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_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_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_token_metrics.py +38 -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
|
@@ -356,12 +356,36 @@ class TokenMetrics:
|
|
|
356
356
|
# Recording Methods
|
|
357
357
|
# =========================================================================
|
|
358
358
|
|
|
359
|
+
def _update_session_total(self, tokens: int) -> None:
|
|
360
|
+
"""
|
|
361
|
+
Add tokens to the session total in the database.
|
|
362
|
+
|
|
363
|
+
This ensures the sessions table reflects actual consumption,
|
|
364
|
+
not just the in-memory cumulative total.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
tokens: Number of tokens to add to the session total
|
|
368
|
+
"""
|
|
369
|
+
if not self._session_id:
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
with self._get_connection() as conn:
|
|
373
|
+
cursor = conn.cursor()
|
|
374
|
+
cursor.execute("""
|
|
375
|
+
UPDATE sessions
|
|
376
|
+
SET total_tokens = total_tokens + ?,
|
|
377
|
+
peak_context_tokens = MAX(peak_context_tokens, ?)
|
|
378
|
+
WHERE session_id = ?
|
|
379
|
+
""", (tokens, self._peak_tokens, self._session_id))
|
|
380
|
+
conn.commit()
|
|
381
|
+
|
|
359
382
|
def record_component_load(
|
|
360
383
|
self,
|
|
361
384
|
component_type: str,
|
|
362
385
|
component_name: str,
|
|
363
386
|
tokens: int,
|
|
364
|
-
source: Optional[str] = None
|
|
387
|
+
source: Optional[str] = None,
|
|
388
|
+
_skip_total_update: bool = False
|
|
365
389
|
) -> None:
|
|
366
390
|
"""
|
|
367
391
|
Record a component being loaded into context.
|
|
@@ -371,12 +395,14 @@ class TokenMetrics:
|
|
|
371
395
|
component_name: Name of the component
|
|
372
396
|
tokens: Estimated token count
|
|
373
397
|
source: Source file path or API endpoint
|
|
398
|
+
_skip_total_update: Internal flag to skip total updates (for cross-referencing)
|
|
374
399
|
"""
|
|
375
400
|
if not self._session_id:
|
|
376
401
|
return
|
|
377
402
|
|
|
378
|
-
|
|
379
|
-
|
|
403
|
+
if not _skip_total_update:
|
|
404
|
+
self._cumulative_tokens += tokens
|
|
405
|
+
self._peak_tokens = max(self._peak_tokens, self._cumulative_tokens)
|
|
380
406
|
|
|
381
407
|
with self._get_connection() as conn:
|
|
382
408
|
cursor = conn.cursor()
|
|
@@ -387,6 +413,10 @@ class TokenMetrics:
|
|
|
387
413
|
""", (self._session_id, component_type, component_name, tokens, source))
|
|
388
414
|
conn.commit()
|
|
389
415
|
|
|
416
|
+
# Update session total in database
|
|
417
|
+
if not _skip_total_update:
|
|
418
|
+
self._update_session_total(tokens)
|
|
419
|
+
|
|
390
420
|
def record_tool_call(
|
|
391
421
|
self,
|
|
392
422
|
tool_name: str,
|
|
@@ -417,6 +447,22 @@ class TokenMetrics:
|
|
|
417
447
|
""", (self._session_id, tool_name, input_tokens, output_tokens))
|
|
418
448
|
conn.commit()
|
|
419
449
|
|
|
450
|
+
# Update session total in database
|
|
451
|
+
self._update_session_total(total)
|
|
452
|
+
|
|
453
|
+
# Auto-mark tool as used if it produced output (ANV-294)
|
|
454
|
+
# First record as component load, then mark as used (CR feedback)
|
|
455
|
+
# Skip total update since tool_call already counted these tokens
|
|
456
|
+
if output_tokens > 0:
|
|
457
|
+
self.record_component_load(
|
|
458
|
+
component_type="tool",
|
|
459
|
+
component_name=f"tool:{tool_name}",
|
|
460
|
+
tokens=total,
|
|
461
|
+
source="tool_call",
|
|
462
|
+
_skip_total_update=True
|
|
463
|
+
)
|
|
464
|
+
self.mark_component_used(f"tool:{tool_name}")
|
|
465
|
+
|
|
420
466
|
def mark_component_used(self, component_name: str) -> None:
|
|
421
467
|
"""
|
|
422
468
|
Mark a loaded component as actually used.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Unit tests for command_tracker.py (ANV-293)
|
|
4
|
+
|
|
5
|
+
Run with: python3 -m pytest global/tests/test_command_tracker.py -v
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Add parent directory to path for imports
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
|
15
|
+
|
|
16
|
+
from token_metrics import TokenMetrics
|
|
17
|
+
from command_tracker import (
|
|
18
|
+
track_command_load,
|
|
19
|
+
get_command_token_estimate,
|
|
20
|
+
_find_command_content,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestCommandTracker:
|
|
25
|
+
"""Tests for command tracking functionality."""
|
|
26
|
+
|
|
27
|
+
def setup_method(self):
|
|
28
|
+
"""Create a fresh metrics instance with temp database."""
|
|
29
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
30
|
+
self.db_path = Path(self.temp_dir) / "test_metrics.db"
|
|
31
|
+
|
|
32
|
+
# Create a temp command file
|
|
33
|
+
self.cmd_dir = Path(self.temp_dir) / ".claude" / "commands"
|
|
34
|
+
self.cmd_dir.mkdir(parents=True)
|
|
35
|
+
self.test_command = self.cmd_dir / "test-cmd.md"
|
|
36
|
+
self.test_command.write_text("# Test Command\n\nThis is a test command for tracking.")
|
|
37
|
+
|
|
38
|
+
# Change to temp dir so command finding works
|
|
39
|
+
self.original_cwd = os.getcwd()
|
|
40
|
+
os.chdir(self.temp_dir)
|
|
41
|
+
|
|
42
|
+
# Initialize metrics with temp database
|
|
43
|
+
self.metrics = TokenMetrics(db_path=self.db_path)
|
|
44
|
+
self.metrics.start_session()
|
|
45
|
+
|
|
46
|
+
# Patch get_metrics to return our test instance
|
|
47
|
+
import command_tracker
|
|
48
|
+
self._original_get_metrics = command_tracker.get_metrics
|
|
49
|
+
command_tracker.get_metrics = lambda: self.metrics
|
|
50
|
+
|
|
51
|
+
def teardown_method(self):
|
|
52
|
+
"""Clean up temp files and restore state."""
|
|
53
|
+
self.metrics.end_session()
|
|
54
|
+
os.chdir(self.original_cwd)
|
|
55
|
+
|
|
56
|
+
# Restore get_metrics
|
|
57
|
+
import command_tracker
|
|
58
|
+
command_tracker.get_metrics = self._original_get_metrics
|
|
59
|
+
|
|
60
|
+
# Clean up temp files
|
|
61
|
+
if self.db_path.exists():
|
|
62
|
+
self.db_path.unlink()
|
|
63
|
+
if self.test_command.exists():
|
|
64
|
+
self.test_command.unlink()
|
|
65
|
+
self.cmd_dir.rmdir()
|
|
66
|
+
(Path(self.temp_dir) / ".claude").rmdir()
|
|
67
|
+
os.rmdir(self.temp_dir)
|
|
68
|
+
|
|
69
|
+
def test_track_command_load_with_source_path(self):
|
|
70
|
+
"""Should track command load when source path is provided."""
|
|
71
|
+
tokens = track_command_load("test-cmd", source_path=str(self.test_command))
|
|
72
|
+
|
|
73
|
+
assert tokens is not None
|
|
74
|
+
assert tokens > 0
|
|
75
|
+
|
|
76
|
+
# Verify it was recorded in the database
|
|
77
|
+
summary = self.metrics.get_session_summary()
|
|
78
|
+
assert summary.total_tokens >= tokens
|
|
79
|
+
assert "command" in summary.component_breakdown
|
|
80
|
+
|
|
81
|
+
def test_track_command_load_with_content(self):
|
|
82
|
+
"""Should track command load when content is provided directly."""
|
|
83
|
+
content = "# Direct Content\n\nThis is test content."
|
|
84
|
+
tokens = track_command_load("direct-cmd", content=content)
|
|
85
|
+
|
|
86
|
+
assert tokens is not None
|
|
87
|
+
assert tokens > 0
|
|
88
|
+
|
|
89
|
+
summary = self.metrics.get_session_summary()
|
|
90
|
+
assert summary.total_tokens >= tokens
|
|
91
|
+
|
|
92
|
+
def test_track_command_load_finds_file_automatically(self):
|
|
93
|
+
"""Should find command file in known locations."""
|
|
94
|
+
tokens = track_command_load("test-cmd")
|
|
95
|
+
|
|
96
|
+
assert tokens is not None
|
|
97
|
+
assert tokens > 0
|
|
98
|
+
|
|
99
|
+
def test_track_command_load_nonexistent_command(self):
|
|
100
|
+
"""Should handle nonexistent commands gracefully."""
|
|
101
|
+
tokens = track_command_load("nonexistent-cmd")
|
|
102
|
+
|
|
103
|
+
# Should return 0 or None when content can't be found
|
|
104
|
+
assert tokens is None or tokens == 0
|
|
105
|
+
|
|
106
|
+
def test_find_command_content(self):
|
|
107
|
+
"""Should find command content from known locations."""
|
|
108
|
+
content = _find_command_content("test-cmd")
|
|
109
|
+
assert content is not None
|
|
110
|
+
assert "Test Command" in content
|
|
111
|
+
|
|
112
|
+
def test_find_command_content_not_found(self):
|
|
113
|
+
"""Should return None for commands that don't exist."""
|
|
114
|
+
content = _find_command_content("nonexistent-cmd")
|
|
115
|
+
assert content is None
|
|
116
|
+
|
|
117
|
+
def test_get_command_token_estimate(self):
|
|
118
|
+
"""Should estimate tokens without recording."""
|
|
119
|
+
estimate = get_command_token_estimate("test-cmd")
|
|
120
|
+
|
|
121
|
+
assert estimate is not None
|
|
122
|
+
assert estimate > 0
|
|
123
|
+
|
|
124
|
+
# Verify nothing was recorded (no change to session)
|
|
125
|
+
summary = self.metrics.get_session_summary()
|
|
126
|
+
assert summary.total_tokens == 0
|
|
127
|
+
|
|
128
|
+
def test_track_command_multiple_loads(self):
|
|
129
|
+
"""Should track multiple command loads."""
|
|
130
|
+
tokens1 = track_command_load("test-cmd")
|
|
131
|
+
tokens2 = track_command_load("test-cmd")
|
|
132
|
+
|
|
133
|
+
assert tokens1 is not None
|
|
134
|
+
assert tokens2 is not None
|
|
135
|
+
|
|
136
|
+
summary = self.metrics.get_session_summary()
|
|
137
|
+
assert summary.total_tokens >= tokens1 + tokens2
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestCommandTrackerNoSession:
|
|
141
|
+
"""Tests for command tracking without active session."""
|
|
142
|
+
|
|
143
|
+
def setup_method(self):
|
|
144
|
+
"""Create metrics instance without starting session."""
|
|
145
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
146
|
+
self.db_path = Path(self.temp_dir) / "test_metrics.db"
|
|
147
|
+
|
|
148
|
+
self.metrics = TokenMetrics(db_path=self.db_path)
|
|
149
|
+
# Note: NOT starting session
|
|
150
|
+
|
|
151
|
+
import command_tracker
|
|
152
|
+
self._original_get_metrics = command_tracker.get_metrics
|
|
153
|
+
command_tracker.get_metrics = lambda: self.metrics
|
|
154
|
+
|
|
155
|
+
def teardown_method(self):
|
|
156
|
+
"""Clean up."""
|
|
157
|
+
import command_tracker
|
|
158
|
+
command_tracker.get_metrics = self._original_get_metrics
|
|
159
|
+
|
|
160
|
+
if self.db_path.exists():
|
|
161
|
+
self.db_path.unlink()
|
|
162
|
+
os.rmdir(self.temp_dir)
|
|
163
|
+
|
|
164
|
+
def test_track_without_session_returns_none(self):
|
|
165
|
+
"""Should return None when no session is active."""
|
|
166
|
+
tokens = track_command_load("test-cmd", content="Test content")
|
|
167
|
+
assert tokens is None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
import pytest
|
|
172
|
+
pytest.main([__file__, "-v"])
|
|
@@ -244,6 +244,44 @@ class TestToolCallRecording:
|
|
|
244
244
|
summary = self.metrics.get_session_summary()
|
|
245
245
|
assert summary.total_tokens == 350
|
|
246
246
|
|
|
247
|
+
def test_auto_mark_tool_used_on_output(self):
|
|
248
|
+
"""Should auto-mark tool as used when output_tokens > 0 (ANV-294)."""
|
|
249
|
+
# First, load a component with matching name
|
|
250
|
+
self.metrics.record_component_load("tool", "tool:Bash", 100)
|
|
251
|
+
|
|
252
|
+
# Record tool call with output
|
|
253
|
+
self.metrics.record_tool_call("Bash", 50, 200)
|
|
254
|
+
|
|
255
|
+
# Check that the component was marked as used
|
|
256
|
+
with self.metrics._get_connection() as conn:
|
|
257
|
+
cursor = conn.cursor()
|
|
258
|
+
cursor.execute("""
|
|
259
|
+
SELECT was_used FROM component_loads
|
|
260
|
+
WHERE component_name = 'tool:Bash' AND session_id = ?
|
|
261
|
+
""", (self.metrics._session_id,))
|
|
262
|
+
row = cursor.fetchone()
|
|
263
|
+
assert row is not None
|
|
264
|
+
assert row['was_used'] == 1
|
|
265
|
+
|
|
266
|
+
def test_no_auto_mark_tool_with_zero_output(self):
|
|
267
|
+
"""Should NOT auto-mark tool when output_tokens is 0."""
|
|
268
|
+
# Load a component
|
|
269
|
+
self.metrics.record_component_load("tool", "tool:Bash", 100)
|
|
270
|
+
|
|
271
|
+
# Record tool call WITHOUT output (0 output tokens)
|
|
272
|
+
self.metrics.record_tool_call("Bash", 50, 0)
|
|
273
|
+
|
|
274
|
+
# Check that the component was NOT marked as used
|
|
275
|
+
with self.metrics._get_connection() as conn:
|
|
276
|
+
cursor = conn.cursor()
|
|
277
|
+
cursor.execute("""
|
|
278
|
+
SELECT was_used FROM component_loads
|
|
279
|
+
WHERE component_name = 'tool:Bash' AND session_id = ?
|
|
280
|
+
""", (self.metrics._session_id,))
|
|
281
|
+
row = cursor.fetchone()
|
|
282
|
+
assert row is not None
|
|
283
|
+
assert row['was_used'] == 0
|
|
284
|
+
|
|
247
285
|
|
|
248
286
|
class TestBudgetManagement:
|
|
249
287
|
"""Tests for token budget functionality."""
|
package/global/tools/README.md
CHANGED
|
@@ -176,3 +176,156 @@ uv run global/tools/anvil-hud.py
|
|
|
176
176
|
**Context not updating**
|
|
177
177
|
- Verify `post_tool_use.py` has `--update-agent` flag
|
|
178
178
|
- Check that hook is receiving context_window data from Claude Code
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## ralph-watch
|
|
183
|
+
|
|
184
|
+
Live terminal monitor for Ralph autonomous execution sessions. Shows real-time progress, event stream, and Linear subtask status.
|
|
185
|
+
|
|
186
|
+
### Usage
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Watch current directory session
|
|
190
|
+
python3 global/tools/ralph-watch
|
|
191
|
+
|
|
192
|
+
# Compact single-line display
|
|
193
|
+
python3 global/tools/ralph-watch --compact
|
|
194
|
+
|
|
195
|
+
# Custom state file location
|
|
196
|
+
python3 global/tools/ralph-watch --state /path/to/.claude/ralph-state.json
|
|
197
|
+
|
|
198
|
+
# Hide event stream
|
|
199
|
+
python3 global/tools/ralph-watch --no-events
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Keybindings
|
|
203
|
+
|
|
204
|
+
| Key | Action |
|
|
205
|
+
|-----|--------|
|
|
206
|
+
| `q` | Quit |
|
|
207
|
+
| `r` | Force refresh |
|
|
208
|
+
| `e` | Toggle event stream |
|
|
209
|
+
| `c` | Toggle compact mode |
|
|
210
|
+
|
|
211
|
+
### Display Modes
|
|
212
|
+
|
|
213
|
+
**Full Mode** (default)
|
|
214
|
+
- Progress bar with completion percentage
|
|
215
|
+
- Linear subtask status (if enabled)
|
|
216
|
+
- Recent event stream
|
|
217
|
+
- Warnings for circuit breaker and checkpoints
|
|
218
|
+
|
|
219
|
+
**Compact Mode** (`--compact` or press `c`)
|
|
220
|
+
- Single-line status display
|
|
221
|
+
- Shows: task name, status, iteration, progress, duration
|
|
222
|
+
- Ideal for embedding in tmux status bar
|
|
223
|
+
|
|
224
|
+
### Features
|
|
225
|
+
|
|
226
|
+
- **Real-time Updates**: 1-second polling of Ralph state file
|
|
227
|
+
- **Progress Tracking**: Visual progress bar with subtask counts
|
|
228
|
+
- **Linear Integration**: Shows Linear subtask completion status
|
|
229
|
+
- **Event Stream**: Recent events with timestamps and icons
|
|
230
|
+
- **Warning Indicators**: Circuit breaker and checkpoint alerts
|
|
231
|
+
|
|
232
|
+
### Example Output
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
╔═══════════════════════════════════════════════════════════════════╗
|
|
236
|
+
║ 🐺 Ralph Session Monitor ║
|
|
237
|
+
╠═══════════════════════════════════════════════════════════════════╣
|
|
238
|
+
║ Task: Migration Task
|
|
239
|
+
║ Status: RUNNING │ Iteration: 5 │ Duration: 12:34
|
|
240
|
+
║ Progress: [================ ] 67% (4/6)
|
|
241
|
+
╠───────────────────────────────────────────────────────────────────╣
|
|
242
|
+
║ Linear Subtasks:
|
|
243
|
+
║ ✅ ANV-101: Phase 1 Setup
|
|
244
|
+
║ ✅ ANV-102: Core Implementation
|
|
245
|
+
║ ⬜ ANV-103: Testing
|
|
246
|
+
╠───────────────────────────────────────────────────────────────────╣
|
|
247
|
+
║ Recent Events:
|
|
248
|
+
║ ✅ 2026-01-17T17:30:00 subtask_complete
|
|
249
|
+
║ 🔄 2026-01-17T17:25:00 iteration_complete
|
|
250
|
+
╠═══════════════════════════════════════════════════════════════════╣
|
|
251
|
+
║ q:quit r:refresh e:events c:compact ║
|
|
252
|
+
╚═══════════════════════════════════════════════════════════════════╝
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Integration with Ralph
|
|
256
|
+
|
|
257
|
+
The watcher reads from:
|
|
258
|
+
- `.claude/ralph-state.json` - Current session state
|
|
259
|
+
- `~/.anvil/events/current-session.jsonl` - Event stream
|
|
260
|
+
|
|
261
|
+
Start the watcher in a split terminal while running Ralph:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# Terminal 1: Run Ralph
|
|
265
|
+
while :; do cat PROMPT.md | npx @anthropic-ai/claude-code --print; done
|
|
266
|
+
|
|
267
|
+
# Terminal 2: Watch progress
|
|
268
|
+
python3 global/tools/ralph-watch
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Ralph Notification System
|
|
274
|
+
|
|
275
|
+
Additional tools for Ralph visibility:
|
|
276
|
+
|
|
277
|
+
### ralph_notifier.py
|
|
278
|
+
|
|
279
|
+
Dispatches notifications for Ralph events.
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# Show current config
|
|
283
|
+
python3 global/lib/ralph_notifier.py --config
|
|
284
|
+
|
|
285
|
+
# Toggle notifications
|
|
286
|
+
python3 global/lib/ralph_notifier.py --disable tts
|
|
287
|
+
python3 global/lib/ralph_notifier.py --enable macos
|
|
288
|
+
python3 global/lib/ralph_notifier.py --disable all
|
|
289
|
+
|
|
290
|
+
# Test notifications
|
|
291
|
+
python3 global/lib/ralph_notifier.py --test-macos "Test message"
|
|
292
|
+
python3 global/lib/ralph_notifier.py --test-tts "Hello from Ralph"
|
|
293
|
+
|
|
294
|
+
# Dispatch pending events
|
|
295
|
+
python3 global/lib/ralph_notifier.py --dispatch
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### ralph_webhooks.py
|
|
299
|
+
|
|
300
|
+
Sends Ralph events to Slack/Discord webhooks.
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# Test webhooks
|
|
304
|
+
python3 global/lib/ralph_webhooks.py --test-slack $SLACK_WEBHOOK_URL
|
|
305
|
+
python3 global/lib/ralph_webhooks.py --test-discord $DISCORD_WEBHOOK_URL
|
|
306
|
+
|
|
307
|
+
# Dispatch to configured webhooks
|
|
308
|
+
python3 global/lib/ralph_webhooks.py --dispatch
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### ralph_api.py
|
|
312
|
+
|
|
313
|
+
HTTP API server with SSE for real-time events.
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# Start API server
|
|
317
|
+
python3 global/api/ralph_api.py --port 8765
|
|
318
|
+
|
|
319
|
+
# With API key authentication
|
|
320
|
+
python3 global/api/ralph_api.py --api-key "your-secret-key"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Endpoints:**
|
|
324
|
+
- `GET /health` - Server health check
|
|
325
|
+
- `GET /status` - Current session status
|
|
326
|
+
- `GET /events` - SSE event stream
|
|
327
|
+
- `GET /history` - Recent event history
|
|
328
|
+
- `GET /sessions` - Past session summaries
|
|
329
|
+
- `POST /control` - Control commands (stop)
|
|
330
|
+
|
|
331
|
+
See `global/api/openapi.yaml` for full API documentation.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -82,6 +82,13 @@ try:
|
|
|
82
82
|
except ImportError:
|
|
83
83
|
CodeRabbitService = None
|
|
84
84
|
|
|
85
|
+
# ANV-277: CodeRabbit Metrics for weekly stats in HUD
|
|
86
|
+
try:
|
|
87
|
+
from coderabbit_metrics import CodeRabbitMetrics, WeeklySummary
|
|
88
|
+
except ImportError:
|
|
89
|
+
CodeRabbitMetrics = None
|
|
90
|
+
WeeklySummary = None
|
|
91
|
+
|
|
85
92
|
try:
|
|
86
93
|
from config_service import get_config, get_config_service, HUDConfig
|
|
87
94
|
except ImportError:
|
|
@@ -290,12 +297,13 @@ class TaskPanel(Static):
|
|
|
290
297
|
|
|
291
298
|
|
|
292
299
|
class QualityPanel(Static):
|
|
293
|
-
"""Widget displaying quality gate status (ANV-103/104/105, ANV-111-115)."""
|
|
300
|
+
"""Widget displaying quality gate status (ANV-103/104/105, ANV-111-115, ANV-277)."""
|
|
294
301
|
|
|
295
302
|
agents: reactive[Dict[str, Any]] = reactive({})
|
|
296
303
|
quality_results: reactive[Dict[str, Any]] = reactive({})
|
|
297
304
|
github_status: reactive[Dict[str, Any]] = reactive({}) # ANV-111-113
|
|
298
305
|
coderabbit_status: reactive[Dict[str, Any]] = reactive({}) # ANV-114-115
|
|
306
|
+
weekly_metrics: reactive[Dict[str, Any]] = reactive({}) # ANV-277: Weekly CR metrics
|
|
299
307
|
|
|
300
308
|
def compose(self) -> ComposeResult:
|
|
301
309
|
yield Static(id="quality-panel-content")
|
|
@@ -316,6 +324,10 @@ class QualityPanel(Static):
|
|
|
316
324
|
"""Update display when CodeRabbit status changes."""
|
|
317
325
|
self._refresh_display()
|
|
318
326
|
|
|
327
|
+
def watch_weekly_metrics(self, metrics: Dict[str, Any]) -> None:
|
|
328
|
+
"""Update display when weekly metrics change (ANV-277)."""
|
|
329
|
+
self._refresh_display()
|
|
330
|
+
|
|
319
331
|
def _refresh_display(self) -> None:
|
|
320
332
|
"""Refresh the quality panel display."""
|
|
321
333
|
content = self.query_one("#quality-panel-content", Static)
|
|
@@ -478,6 +490,43 @@ class QualityPanel(Static):
|
|
|
478
490
|
|
|
479
491
|
text.append("\n")
|
|
480
492
|
|
|
493
|
+
# ANV-277: CodeRabbit Weekly Metrics Summary
|
|
494
|
+
if self.weekly_metrics:
|
|
495
|
+
text.append("─" * 36 + "\n", style="dim")
|
|
496
|
+
text.append("CODERABBIT WEEKLY\n", style="bold cyan")
|
|
497
|
+
|
|
498
|
+
total_reviews = self.weekly_metrics.get("total_reviews", 0)
|
|
499
|
+
total_issues = self.weekly_metrics.get("total_issues_found", 0)
|
|
500
|
+
total_fixed = self.weekly_metrics.get("total_issues_fixed", 0)
|
|
501
|
+
avg_issues = self.weekly_metrics.get("avg_issues_per_review", 0)
|
|
502
|
+
pass_rate = self.weekly_metrics.get("pass_rate", 0)
|
|
503
|
+
trend = self.weekly_metrics.get("trend", "stable")
|
|
504
|
+
|
|
505
|
+
text.append(f" Reviews: ", style="dim")
|
|
506
|
+
text.append(f"{total_reviews}\n", style="white")
|
|
507
|
+
|
|
508
|
+
text.append(f" Issues: ", style="dim")
|
|
509
|
+
text.append(f"{total_issues} found", style="yellow" if total_issues > 0 else "green")
|
|
510
|
+
text.append(f" / {total_fixed} fixed\n", style="green" if total_fixed > 0 else "dim")
|
|
511
|
+
|
|
512
|
+
text.append(f" Avg/Review: ", style="dim")
|
|
513
|
+
avg_color = "green" if avg_issues < 2 else "yellow" if avg_issues < 5 else "red"
|
|
514
|
+
text.append(f"{avg_issues:.1f}\n", style=avg_color)
|
|
515
|
+
|
|
516
|
+
text.append(f" Pass Rate: ", style="dim")
|
|
517
|
+
pass_color = "green" if pass_rate >= 50 else "yellow" if pass_rate >= 25 else "red"
|
|
518
|
+
text.append(f"{pass_rate:.0f}%\n", style=pass_color)
|
|
519
|
+
|
|
520
|
+
text.append(f" Trend: ", style="dim")
|
|
521
|
+
if trend == "improving":
|
|
522
|
+
text.append("↑ improving\n", style="green")
|
|
523
|
+
elif trend == "degrading":
|
|
524
|
+
text.append("↓ degrading\n", style="red")
|
|
525
|
+
else:
|
|
526
|
+
text.append("→ stable\n", style="dim")
|
|
527
|
+
|
|
528
|
+
text.append("\n")
|
|
529
|
+
|
|
481
530
|
# Summary
|
|
482
531
|
text.append("─" * 36 + "\n", style="dim")
|
|
483
532
|
ready_count = sum(1 for r in self.quality_results.values() if r.get("ready_to_merge"))
|
|
@@ -2377,6 +2426,7 @@ class AnvilHUD(App):
|
|
|
2377
2426
|
self.coordination_service: Optional["CoordinationService"] = None
|
|
2378
2427
|
self.github_service: Optional["GitHubService"] = None # ANV-111-113
|
|
2379
2428
|
self.coderabbit_service: Optional["CodeRabbitService"] = None # ANV-114-115
|
|
2429
|
+
self.coderabbit_metrics: Optional["CodeRabbitMetrics"] = None # ANV-277: Weekly metrics
|
|
2380
2430
|
self.issue_provider = None # ANV-76: Issue provider for Kanban
|
|
2381
2431
|
self.refresh_timer: Optional[Timer] = None
|
|
2382
2432
|
|
|
@@ -2393,6 +2443,12 @@ class AnvilHUD(App):
|
|
|
2393
2443
|
if not self.demo_mode and CodeRabbitService:
|
|
2394
2444
|
if not self.config or self.config.integrations.enable_coderabbit:
|
|
2395
2445
|
self.coderabbit_service = CodeRabbitService()
|
|
2446
|
+
# ANV-277: Initialize CodeRabbit metrics for weekly stats
|
|
2447
|
+
if not self.demo_mode and CodeRabbitMetrics:
|
|
2448
|
+
try:
|
|
2449
|
+
self.coderabbit_metrics = CodeRabbitMetrics()
|
|
2450
|
+
except Exception:
|
|
2451
|
+
self.coderabbit_metrics = None
|
|
2396
2452
|
# ANV-76: Initialize issue provider
|
|
2397
2453
|
if not self.demo_mode and get_provider:
|
|
2398
2454
|
try:
|
|
@@ -2731,6 +2787,24 @@ class AnvilHUD(App):
|
|
|
2731
2787
|
quality_panel.github_status
|
|
2732
2788
|
)
|
|
2733
2789
|
|
|
2790
|
+
# ANV-277: Update CodeRabbit weekly metrics
|
|
2791
|
+
if self.demo_mode:
|
|
2792
|
+
quality_panel.weekly_metrics = self._get_demo_weekly_metrics()
|
|
2793
|
+
elif self.coderabbit_metrics:
|
|
2794
|
+
try:
|
|
2795
|
+
summary = self.coderabbit_metrics.get_weekly_summary()
|
|
2796
|
+
if summary:
|
|
2797
|
+
quality_panel.weekly_metrics = {
|
|
2798
|
+
"total_reviews": summary.total_reviews,
|
|
2799
|
+
"total_issues_found": summary.total_issues_found,
|
|
2800
|
+
"total_issues_fixed": summary.total_issues_fixed,
|
|
2801
|
+
"avg_issues_per_review": summary.avg_issues_per_review,
|
|
2802
|
+
"pass_rate": summary.pass_rate,
|
|
2803
|
+
"trend": summary.trend,
|
|
2804
|
+
}
|
|
2805
|
+
except Exception:
|
|
2806
|
+
pass # Keep previous metrics on error
|
|
2807
|
+
|
|
2734
2808
|
# Update coordination panel (ANV-106-110)
|
|
2735
2809
|
coordination_panel = self.query_one("#coordination-panel", CoordinationPanel)
|
|
2736
2810
|
coordination_panel.agents = agents
|
|
@@ -3422,6 +3496,17 @@ class AnvilHUD(App):
|
|
|
3422
3496
|
},
|
|
3423
3497
|
}
|
|
3424
3498
|
|
|
3499
|
+
def _get_demo_weekly_metrics(self) -> Dict[str, Any]:
|
|
3500
|
+
"""Generate demo CodeRabbit weekly metrics for testing (ANV-277)."""
|
|
3501
|
+
return {
|
|
3502
|
+
"total_reviews": 12,
|
|
3503
|
+
"total_issues_found": 28,
|
|
3504
|
+
"total_issues_fixed": 24,
|
|
3505
|
+
"avg_issues_per_review": 2.3,
|
|
3506
|
+
"pass_rate": 42.0,
|
|
3507
|
+
"trend": "improving",
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3425
3510
|
def _get_demo_agents(self) -> Dict[str, Any]:
|
|
3426
3511
|
"""Generate demo agent data for testing."""
|
|
3427
3512
|
now = datetime.now(timezone.utc).isoformat()
|