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.
Files changed (117) hide show
  1. package/README.md +48 -18
  2. package/VERSION +1 -1
  3. package/docs/command-reference.md +97 -16
  4. package/docs/system-architecture.md +15 -0
  5. package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
  6. package/global/api/openapi.yaml +357 -0
  7. package/global/api/ralph_api.py +528 -0
  8. package/global/commands/anvil-settings.md +44 -18
  9. package/global/commands/coderabbit-fix.md +282 -0
  10. package/global/commands/evidence.md +23 -6
  11. package/global/commands/hud.md +24 -0
  12. package/global/commands/orient.md +22 -21
  13. package/global/commands/weekly-review.md +21 -1
  14. package/global/config/notifications.yaml.template +50 -0
  15. package/global/hooks/ralph_stop.sh +33 -1
  16. package/global/hooks/statusline.sh +67 -2
  17. package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
  18. package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
  19. package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
  20. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  21. package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
  22. package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
  23. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  24. package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
  25. package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
  26. package/global/lib/coderabbit_metrics.py +647 -0
  27. package/global/lib/command_tracker.py +147 -0
  28. package/global/lib/log_rotation.py +287 -0
  29. package/global/lib/ralph_events.py +398 -0
  30. package/global/lib/ralph_notifier.py +366 -0
  31. package/global/lib/ralph_webhooks.py +470 -0
  32. package/global/lib/state_manager.py +121 -0
  33. package/global/lib/token_analyzer.py +28 -2
  34. package/global/lib/token_metrics.py +49 -3
  35. package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
  36. package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
  37. package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
  38. package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  39. package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
  40. package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
  41. package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
  42. package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
  43. package/global/tests/test_command_tracker.py +172 -0
  44. package/global/tests/test_token_metrics.py +38 -0
  45. package/global/tools/README.md +153 -0
  46. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  47. package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
  48. package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
  49. package/global/tools/anvil-hud.py +86 -1
  50. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
  51. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
  52. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
  53. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
  54. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
  55. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
  56. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
  57. package/global/tools/anvil-memory/src/commands/context.ts +322 -0
  58. package/global/tools/anvil-memory/src/db.ts +108 -0
  59. package/global/tools/anvil-memory/src/index.ts +2 -8
  60. package/global/tools/orient_linear.py +159 -0
  61. package/global/tools/ralph-watch +423 -0
  62. package/package.json +2 -1
  63. package/project/.anvil-project.yaml.template +93 -0
  64. package/project/CLAUDE.md.template +343 -0
  65. package/project/agents/README.md +119 -0
  66. package/project/agents/cross-layer-debugger.md +217 -0
  67. package/project/agents/security-code-reviewer.md +162 -0
  68. package/project/constitution.md.template +235 -0
  69. package/project/coordination.md +103 -0
  70. package/project/docs/background-tasks.md +258 -0
  71. package/project/docs/skills-frontmatter.md +243 -0
  72. package/project/examples/README.md +106 -0
  73. package/project/examples/api-route-template.ts +171 -0
  74. package/project/examples/component-template.tsx +110 -0
  75. package/project/examples/hook-template.ts +152 -0
  76. package/project/examples/service-template.ts +207 -0
  77. package/project/examples/test-template.test.tsx +249 -0
  78. package/project/hooks/README.md +491 -0
  79. package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
  80. package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
  81. package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
  82. package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  83. package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  84. package/project/hooks/notification.py +183 -0
  85. package/project/hooks/permission_request.py +438 -0
  86. package/project/hooks/post_tool_use.py +397 -0
  87. package/project/hooks/pre_compact.py +126 -0
  88. package/project/hooks/pre_tool_use.py +454 -0
  89. package/project/hooks/session_start.py +656 -0
  90. package/project/hooks/stop.py +356 -0
  91. package/project/hooks/subagent_start.py +223 -0
  92. package/project/hooks/subagent_stop.py +215 -0
  93. package/project/hooks/user_prompt_submit.py +110 -0
  94. package/project/hooks/utils/llm/anth.py +114 -0
  95. package/project/hooks/utils/llm/oai.py +114 -0
  96. package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
  97. package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
  98. package/project/hooks/utils/tts/openai_tts.py +92 -0
  99. package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
  100. package/project/linear.yaml.template +23 -0
  101. package/project/product.md.template +238 -0
  102. package/project/retros/README.md +126 -0
  103. package/project/rules/README.md +90 -0
  104. package/project/rules/debugging.md +139 -0
  105. package/project/rules/security-review.md +115 -0
  106. package/project/settings.yaml.template +185 -0
  107. package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
  108. package/project/templates/api-python/CLAUDE.md +547 -0
  109. package/project/templates/generic/CLAUDE.md +260 -0
  110. package/project/templates/saas/CLAUDE.md +478 -0
  111. package/project/tests/README.md +140 -0
  112. package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
  113. package/project/tests/fixtures/sample-transcript.jsonl +21 -0
  114. package/project/tests/test-hooks.sh +259 -0
  115. package/project/tests/test-lib.sh +248 -0
  116. package/project/tests/test-statusline.sh +165 -0
  117. 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
- self._cumulative_tokens += tokens
379
- self._peak_tokens = max(self._peak_tokens, self._cumulative_tokens)
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.
@@ -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."""
@@ -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.
@@ -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()