Merge branch 'ralph/instrumentation' — rate limit budget + WhatsApp keywords

Rate limit budget tracking (TODO P2):
- tools/ralph_usage.py — pure functions extract/parse/aggregate; CLI subcomenzi
  append/summarize. Atomic write JSONL.
- tools/ralph/ralph.sh: după fiecare claude -p, append usage entry la
  workspace/<slug>/scripts/ralph/usage.jsonl (best-effort)
- dashboard/handlers/ralph.py: GET /api/ralph/usage[?days=N] cross-project
  aggregation cu today_cost, today_runs, by_project, by_day

WhatsApp text-keyword commands (TODO P3):
- src/router.py: helper _translate_whatsapp_text — `aprob <slug>` → `/a <slug>`,
  `stop <slug>` → `/k <slug>`, `stare`/`stare <slug>` → `/l`/`/l <slug>`. Aplicat
  DOAR pe adapter whatsapp în _try_ralph_dispatch (Discord/TG nu sunt afectate).
  Propose intentionally NOT covered (descrierea fragilă).

Tests: 53 noi (28 ralph_usage + 21 whatsapp_keywords + 4 dashboard endpoint extend)
+ 0 regressions pe modulele atinse.
This commit is contained in:
2026-04-26 19:12:43 +00:00
8 changed files with 1072 additions and 1 deletions

View File

@@ -163,6 +163,8 @@ class TaskBoardHandler(
self.handle_eco_doctor()
elif self.path == '/api/ralph/status' or self.path.startswith('/api/ralph/status?'):
self.handle_ralph_status()
elif self.path == '/api/ralph/usage' or self.path.startswith('/api/ralph/usage?'):
self.handle_ralph_usage()
elif self.path.startswith('/api/ralph/'):
# /api/ralph/<slug>/log or /api/ralph/<slug>/prd
parts = self.path.split('?', 1)[0].split('/')

View File

@@ -1,9 +1,10 @@
"""Ralph live dashboard endpoints (W3).
"""Ralph live dashboard endpoints (W3 + instrumentation).
Endpoints:
GET /api/ralph/status — toate proiectele Ralph (cards data)
GET /api/ralph/<slug>/log — tail progress.txt (default 100 lines)
GET /api/ralph/<slug>/prd — full prd.json content
GET /api/ralph/usage[?days=N] — rate limit budget summary (cross-project)
POST /api/ralph/<slug>/stop — SIGTERM la Ralph PID
Polling: 5s din ralph.html (suficient pentru iter 8-15min Ralph).
@@ -14,18 +15,30 @@ Citește status din `~/workspace/<slug>/scripts/ralph/`:
- progress.txt → log human-readable
- logs/iteration-*.log → mtime ultimului iter
- .ralph.pid → PID activ (verificat cu os.kill 0)
- usage.jsonl → token/cost log per iter (instrumentation MVP)
Reuse path constants din `dashboard/constants.py` (WORKSPACE_DIR).
"""
import json
import os
import signal
import sys
from datetime import datetime
from pathlib import Path
from urllib.parse import unquote
import constants
# Best-effort import of pure functions for /api/ralph/usage (instrumentation MVP).
# Helper lives at <repo>/tools/ralph_usage.py — sibling of `dashboard/`.
_TOOLS_DIR = Path(__file__).resolve().parents[2] / "tools"
if str(_TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(_TOOLS_DIR))
try:
import ralph_usage # type: ignore
except ImportError: # pragma: no cover — diagnostic only
ralph_usage = None # type: ignore
# Path Ralph per proiect (mereu în scripts/ralph/)
def _ralph_dir(project_dir: Path) -> Path:
@@ -259,6 +272,58 @@ class RalphHandlers:
except Exception as exc:
self.send_json({"error": str(exc)}, 500)
# ── /api/ralph/usage (GET) ─────────────────────────────────
def handle_ralph_usage(self):
"""Returnează rate limit budget summary cross-project.
Citește toate `~/workspace/<slug>/scripts/ralph/usage.jsonl`, le concatenează,
rulează `ralph_usage.summarize` cu `?days=N` (default 7).
Răspuns:
{
"today": "YYYY-MM-DD",
"today_cost": float,
"today_runs": int,
"window_days": N,
"window_cost": float,
"window_runs": int,
"by_project": {...},
"by_day": {...},
"total_cost": float,
"total_runs": int
}
"""
try:
from urllib.parse import parse_qs, urlparse
qs = parse_qs(urlparse(self.path).query)
try:
days = int(qs.get("days", ["7"])[0])
if days <= 0:
days = 7
if days > 365:
days = 365
except ValueError:
days = 7
if ralph_usage is None:
self.send_json({"error": "ralph_usage helper unavailable"}, 500)
return
entries: list[dict] = []
if constants.WORKSPACE_DIR.exists():
for entry in sorted(constants.WORKSPACE_DIR.iterdir()):
if not entry.is_dir() or entry.name.startswith("."):
continue
usage_path = _ralph_dir(entry) / "usage.jsonl"
if usage_path.exists():
entries.extend(ralph_usage.parse_usage_jsonl(usage_path))
summary = ralph_usage.summarize(entries, days=days)
summary["fetchedAt"] = datetime.now().isoformat()
self.send_json(summary)
except Exception as exc:
self.send_json({"error": str(exc)}, 500)
# ── /api/ralph/<slug>/stop (POST) ──────────────────────────
def handle_ralph_stop(self, slug: str):
"""Trimite SIGTERM la Ralph PID. Verifică că PID-ul e în WORKSPACE_DIR."""