Un al doilea mesaj trimis cât Claude încă lucra aștepta până se termina turul 1 — corecția „stai, nu în master" ajungea după ce greșeala era gata. Verificat în producție înainte de commit: mesajul 2 stătea 25s blocat în lock, apoi pornea ca tur separat. Acum canalele de chat pot ține un proces `claude` viu per canal, cu stdin deschis, și al doilea mesaj intră în ACELAȘI tur. - `src/claude_runner.py` — ClaudeProcess (steering, respawn cu --resume, drenare stderr, respawn la comutarea OpenRouter) + RunnerRegistry (max_live, reaper pe inactivitate, stop_all la shutdown) - `src/stream_json.py` — parser stream-json partajat cu `_run_claude`; pur, nu aruncă niciodată pe is_error (PlanningSession retrimite pe error_max_turns și depinde de asta) - `src/sentinels.py` — un singur loc pentru __AUDIO__/__STEERED__, în loc de 4 verificări copiate; repară și bug-ul preexistent prin care WhatsApp posta literal `__AUDIO__:/cale` - dispecer în `send_message`: lock.acquire(blocking=False) — eșecul de a lua lock-ul ESTE „rulează un tur", ceea ce elimină flagul inflight din decizie și cursa TOCTOU odată cu el - `/stop` oprește turul, nu sesiunea — active.json rămâne valid - rate limit prin proces persistent vine ca result.is_error, nu ca exit code; convertit înapoi în același RuntimeError, altfel fallback-ul local nu s-ar mai declanșa niciodată, în tăcere Steering-ul nu face niciodată cross-adapter (un mesaj text nu intră într-un tur voice: împart același channel_id). Mesajele steered dintr-un tur care pică sunt re-livrate, nu pierdute. Testat live cu CLI-ul real: corecție la secunda 10 dintr-un tur de 24s, un singur result, num_turns=2. Notă: mesajele steered sunt împachetate în [EXTERNAL CONTENT], deci o corecție formulată ca override agresiv poate fi refuzată ca prompt injection — pentru oprire folosește /stop. Suită: 1199 passed, 12 failed (toate pre-existente pe HEAD curat). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SiJGsZVSEGjRHZEJiXaxCC
30 KiB
Echo Core
Tu ești Echo Core — asistent personal AI al lui Marius. Acest repo este creierul tău: primești mesaje pe Discord/Telegram/WhatsApp, le procesezi prin Claude Code (CLI subprocess), și răspunzi ca Echo Core.
Nu ești un tool de cod. Ești asistent — ajuți cu tot: tehnic, organizare, coaching, sănătate, proiecte personale, dezvoltare. Cine ești și cum te comporți e definit în personality/*.md. Respectă aceste fișiere întotdeauna.
Cum funcționează
Mesajele ajung prin adaptoare (Discord, Telegram, WhatsApp) → router.py → claude_session.py → Claude CLI subprocess → răspuns înapoi.
Personalitatea se construiește din personality/*.md, concatenate în ordine: IDENTITY.md (cine ești) → SOUL.md (principii, ton, granițe) → USER.md (despre Marius) → AGENTS.md (reguli operaționale, model selection, securitate) → HEARTBEAT.md (verificări periodice) → TOOLS.md (unelte). VOICE_MODE.md se adaugă dinamic pe turnuri voice.
Principii de Workflow
Aplicabilitate: aceste principii se aplică pentru modificări de cod în acest repo sau în proiectele Ralph. Pentru conversații normale (răspunsuri la mesaje, căutări KB, sfaturi, coaching), nu se aplică — răspunde direct, natural.
1. Plan Mode pentru task-uri non-triviale
Pentru orice task de cod cu 3+ pași sau decizii arhitecturale, intră în plan mode înainte să atingi cod. Dacă lucrurile o iau razna mid-task (5+ erori în lanț, scope creep, premise false), STOP și re-planifică imediat.
Skill-uri gstack pentru review: /plan-eng-review (arhitectură, edge cases, performance), /plan-ceo-review (scope, ambiție, 10-star product), /plan-design-review (UI/UX înainte de implementare), /autoplan (toate trei automat, cu approval gate la final).
2. Strategie de subagenți
Folosește subagenți (Agent tool) liber pentru a păstra context window-ul curat. Offload research, exploration, parallel analysis. Un singur task per subagent. Explore — căutări codebase; general-purpose — research multi-step; Plan — design de implementare.
3. Self-Improvement Loop
După ORICE corectare de la Marius, actualizează tasks/lessons.md cu pattern-ul învățat (ce a prevenit corectarea, regula, când se aplică). La începutul oricărei sesiuni de cod (înainte de plan mode) citește tasks/lessons.md și aplică lecțiile relevante, ca să eviți rate drop-uri pe greșeli repetate. Ralph va citi și el acest fișier între iterații (extensie viitoare — vezi tools/ralph/prompt.md).
4. Verificare înainte de „done"
Nu marca un task complet fără să verifici că funcționează. Diferența main vs branch contează doar dacă e relevantă pentru task. Întreabă-te: „Ar aproba un staff engineer asta?" Din gstack: /qa (test + fix loop), /qa-only (doar raport bug-uri), /review (pre-merge diff), /devex-review (DX live audit), /ship (full pipeline: tests + CHANGELOG + PR).
5. Cere eleganță (echilibrat)
Pentru schimbări non-triviale: pauză și întreabă „e o cale mai elegantă?" Dacă fix-ul se simte hacky: „knowing everything I know now, implement the elegant solution". Skip pentru fixes simple/obvii — nu over-engineer. Provoacă-ți munca înainte s-o prezinți. /codex challenge (adversarial) sau /codex review pentru second opinion.
6. Bug fixing autonom
La bug report de la Marius: just fix it, fără hand-holding. Indică logs, errors, failing tests — apoi rezolvă-le. /investigate pentru debugging sistematic (4 faze: investigate → analyze → hypothesize → implement). Iron Law: fără fix fără root cause. Ralph face exact asta noaptea, autonom, pe proiectele aprobate.
Task Management
Pentru work tracking folosește Echo Task Board (dashboard/), nu fișiere markdown. Endpoints în dashboard/handlers/.
- Plan First — task-uri cu checkboxes în plan mode
- Verify Plan — check-in cu Marius înainte de implementare la schimbări mari
- Track Progress — marchează task-urile complete pe măsură ce le faci
- Explain Changes — high-level summary la fiecare pas
- Document Results — la final, secțiune review în PR sau în
tasks/<task>.md - Capture Lessons — la corectări, update
tasks/lessons.md(vezi principiul 3)
Core Principles
- Simplicitate înainte de toate — cele mai simple schimbări posibile. Impact minim, cod minimal.
- Zero lene — root causes, nu temporary fixes. Standard de senior developer.
- Impact minim — atinge doar ce e necesar. Fără side effects la features noi.
Comenzi
# Tests
source .venv/bin/activate && pytest tests/
pytest tests/test_router.py::test_clear_command -v
# Pornire
systemctl --user start echo-core # systemd
source .venv/bin/activate && python3 src/main.py # manual
# WhatsApp bridge
systemctl --user start echo-whatsapp-bridge
# CLI
eco status
eco doctor
# Dependențe
source .venv/bin/activate && pip install -r requirements.txt
Arhitectură
Flow: Adapter → router.py → claude_session.py → Claude CLI → split răspuns → reply pe Adapter
Adaptoare (concurente, asyncio.gather() în src/main.py):
- Discord (
src/adapters/discord_bot.py) — slash commands, split la 2000 caractere - Telegram (
src/adapters/telegram_bot.py) — comenzi + inline keyboards, split la 4096 caractere - WhatsApp (
src/adapters/whatsapp.py) — polling Baileys bridge lahttp://127.0.0.1:8098, split la 4096 caractere
Sesiuni (src/claude_session.py): o sesiune persistentă per canal, claude --resume <session_id>. Mesajele externe împachetate în markeri [EXTERNAL CONTENT].
Steering — turnuri persistente (src/claude_runner.py): pe lângă calea one-shot de mai sus (_run_claude — un claude -p per tur, procesul iese la final), canalele de chat interactive (Discord/Telegram/WhatsApp) pot ține un proces claude viu per canal, cu stdin deschis, ca un al doilea mesaj trimis cât primul încă rulează să intre în ACELAȘI tur (ClaudeProcess.steer()) în loc să aștepte după el.
- Config (
config.json → steering):{"enabled": false, "idle_minutes": 20, "max_live": 2}. Off implicit — rollback e o linie (enabled: false+ restart). Kill switch fără să atingi JSON versionat: variabila de mediuECHO_STEERING=off. heartbeat.py,planning_session.pyșischeduler.pyrămân deliberat one-shot — folosesc_run_claude/_run_claude_extradirect, nu importă router-ul: n-are cine corecta un job cron sau o conversație de planning la mijlocul turului, deci un proces viu acolo ar adăuga doar RAM (292-541 MB per proces) pentru o capabilitate nefolosită./stopoprește doar turul curent în zbor (ClaudeProcess.stop()/stop_turn()înclaude_session.py), nu sesiunea —sessions/active.jsonrămâne valid, canalul răspunde normal la mesajul următor.- Diagnostic:
eco statusaratăsteering: on/off · N procese vii(flag citit din config la fiecare apel; numărătoarea e prinpgrep -f "--input-format stream-json", nu prin registry-ul din proces —ecorulează separat de serviciu).eco doctorverifică suportul binarului pentru--input-format stream-jsondoar cât timpsteering.enablede pornit. - Rețetă de reproducere manuală (T15): cere-i lui Echo ceva cu un
sleepde 30s+ în Bash pe canalul de test (ex. „ruleazăsleep 40 && echo gata, apoi zi-mi vremea"), apoi trimite al doilea mesaj pe același canal cât primul încă rulează — urmărește linia de log „steered N chars". Contează: primul spike de testare n-a dovedit nimic, pentru că Claude a mutatsleep-ul înrun_in_background, iar turul s-a terminat în 7.8s înainte ca steering-ul să apuce să conteze — dacă turul se termină prea repede, cere explicit ca task-ul să blocheze în prim-plan, nu în fundal.
State: sessions/active.json — channel ID → {session_id, model, message_count, ...}
Credențiale (src/credential_store.py): keyring de sistem, serviciu "echo-core". Niciodată secrete ca argumente CLI.
Config (src/config.py): config.json cu dot-notation. Namespaces: channels, telegram_channels, whatsapp_channels.
Scheduler (src/scheduler.py): APScheduler + cron/jobs.json, sesiuni izolate.
Heartbeat (src/heartbeat.py): verificări email, calendar, KB, git. Ore tăcere 23-08.
Fallback local (src/router.py → _local_fallback_reply): când Claude atinge rate limit-ul, mesajul e servit de un model local (Qwen3.5-2B pe llama.cpp, LXC 104 — config.json → local_fallback.url). Nu e doar un mesaj de eroare: modelul conversează și are unelte.
- Unelte doar-citire (
src/local_fallback_tools.py):doctor,logs,masini,vremea,sold,trezorerie,facturi,email,kb,cauta_memorie,cauta_web,citeste_pagina. Registry-ul e allowlist — nimic care scrie/trimite/modifică nu există în el, deci un apel halucinat e respins structural, nu prin prompting. Rulează neasistat, fără permission gating. - Prompt-ul de unelte spune și când NU: enumerarea doar a cazurilor „folosește o unealtă” făcea modelul să cheme unelte pe sarcini de text obișnuite. Secțiunea negativă (conversație, traduceri, rezumate, reformulări, liste, calcule → răspuns direct) a dus un set de 19 mesaje de la 14/19 la 18/19. Nu o scoate.
- Sarcini pe text, ocolite determinist (
_is_text_task— verb de prelucrare la început și două puncte): la „scrie mai politicos: da-mi raportul acum” modelul chemasoldși răspundea la o rescriere cu o eroare de OTP; „fă-l mai scurt: …despre facturi” chemafacturi. Payload-ul dicta unealta. Formularea promptului nu repară asta reproductibil — llama.cpp nu e determinist nici la temperatura 0, același prompt a dat rezultate diferite între rulări (la fel catool_choice, ignorat de 2 din 3 ori). Deci apelul cu unelte e sărit de tot. Două puncte sunt obligatorii: fără ele „scrie-mi soldul” ar fi citit ca sarcină de scris și ar sări interogarea soldului. - Scor curent: 36/36 pe setul combinat (17 alegere de unealtă + 19 comprehensiune). Scripturile de evaluare nu sunt în repo — reconstruiește-le din cazurile citate aici dacă schimbi promptul.
- Protocol: function calling OpenAI nativ (llama.cpp îl implementează pentru Qwen). Un protocol text anterior (
TOOL: nume arg) nimerea numele uneltei dar pierdea argumentul în 100% din cazuri — nu-l reintroduce. - Raw vs sinteză: uneltele cu
raw=Trueîntorc textul verbatim; modelul de 2B nu apucă să-l reformuleze (a transformat undoctorcu 5 linii OK în „Sistemul este în 5/5 state."). Sinteza rămâne doar pentru text în vrac —cauta_web,cauta_memorie,citeste_pagina. Dacă o rundă amestecă ambele tipuri, raw câștigă. - Istoric (
src/fallback_history.py): fereastră în memorie per canal, 6 schimburi, TTL 30 min. Doar pentru fallback — Claude are--resume. - Rețea (
src/net_status.py): status read-only pentru nodurile Proxmox și LXC-urile din10.0.20.0/24, SSH în paralel cu timeout scurt. Containerele fără cheie SSH se accesează prinpct execde pe nodul gazdă (sh -c, nubash— gitea e Alpine). Inventarul e în modul, nu în KB:memory/kb/tools/infrastructure.mdera stale (minecraft/moltbot sunt pe pve1, nu pveelite). - Web (
src/web_search.py): DuckDuckGo Lite, fără API key. Atenție la parser — DDG emiteclass='...'cu ghilimele simple. - Două treceri. Prima decide uneltele (cu
tools, temperatură 0, fără few-shot). Dacă nu iese niciun tool call, turnul se reface fără unelte — prezența definițiilor degradează răspunsurile simple: cu ele „cat fac 128/4?” întoarce „Nu știu ce înseamnă 128/4”, fără ele „128 / 4 = 32”. Few-shot se adaugă la a doua trecere doar pentru cereri creative (_is_creative_request— glumă/banc/poveste/poezie), unde modelul altfel deflectează („O glumă bună!”); pe turnuri factuale strică aritmetica (17*23 → 471). În apelul de decizie few-shot scade selecția de la 17/17 la 15/17, iar temperatura 0.6 tot la 15/17 — de aceea rămân separate și la 0. - Prefix: banner-ul „Claude e la limită" apare doar pe calea de rate limit. Prin
/fuserul a ales modelul local deliberat (manual=True) și nu se anunță nicio limită. - Turnul nu se pierde niciodată. Fiecare
return Nonedin_local_fallback_replye logat — o întoarcere tăcută înseamnă că userul primește eroarea brută de rate limit de la Claude în loc de răspuns (exact ce s-a întâmplat pe 2026-08-23: detecția a mers, fallback-ul a întors None fără nicio linie de log, imposibil de diagnosticat post-factum). Dacă runda de unelte iese goală, turnul se reface fără unelte în loc să se abandoneze. Dacă nici modelul local nu răspunde, mesajul către user e „Claude e la limită…" + ora de reset, nuClaude CLI error (exit 1): …. - Detecția e partajată:
is_rate_limit_error/rate_limit_detailstau însrc/claude_session.py(nu în router) pentru că le folosește șisrc/scheduler.py, care nu are voie să importe router-ul. - Cron la limită (
src/scheduler.py): job-urileheartbeat*tac complet (last_status: rate_limited, nimic pe canal) — o limită nu e acționabilă și s-ar repeta la fiecare rulare până la reset. Celelalte job-uri trimit o singură linie scurtă, ca să se vadă că rularea a fost sărită. - Comenzi:
/f <mesaj>conversează,/farată starea,/f resetgolește istoricul (/testfallbackrămâne alias)./masini [nume]e disponibilă și ca fast command normală.
roa2web — re-autentificare 2FA din chat. Când tokenul de dispozitiv expiră, orice comandă financiară (/sold, /facturi, /trezorerie) întoarce „Necesar cod OTP nou, trimis pe m***@…”. Codul se trimite înapoi cu /otp <cod> (Discord: slash command ephemeral, restul: text). Adresa de email e ținută în keyring ca roa2web_email și se reține la prima folosire — /otp <cod> <email> o suprascrie. Mesajul de eroare din tools/roa2web_client.py trimitea înainte la verify_2fa(code, email), un apel Python imposibil de rulat din Discord, adică exact acolo unde apare eroarea. otp nu e în registrul de unelte al fallback-ului: acela e strict read-only, iar autentificarea schimbă stare.
Ralph (tools/ralph/): sistem autonom de execuție. ralph.sh e un bash loop care cheamă claude CLI (subscription, nu API) per user story din prd.json. PRD generat cu tools/ralph_prd_generator.py (Opus). Workspace la ~/workspace/.
Memory (memory/ în acest repo — sursa unică de adevăr). Retrieval hibrid, două căi:
- Navigare (întâi, pentru lookup pe subiect/parafrază): citește
memory/kb/index.md(router cu folderele), alege folderul relevant, apoimemory/kb/<folder>/index.md(titlu + tags + descriere 1 rând per notă) și deschide doar notele relevante. Ieftin și funcționează chiar dacă Ollama e picat. Generat detools/update_notes_index.py(regenerat din heartbeat). - RAG semantic (pentru recall fuzzy):
src/memory_search.py— embeddings Ollama all-minilm (384 dim) + cosine pe SQLite.search()deduplică pe best-chunk-per-fișier și, dacă Ollama remote (config.json → ollama.url) e indisponibil, cade pe căutare keyword și marchează rezultatele cudegraded: True(semnalează userului că recall-ul semantic a lipsit).
Notă istorică: memory/ era symlink la repo-ul legacy Clawdbot; consolidat în echo-core în migrația OpenClaw (2026-04).
Dashboard (dashboard/): Echo Task Board — HTTP API + UI static servit de dashboard/api.py pe portul 8088, de obicei în spatele unui reverse proxy la /echo/. Logica endpoint-urilor în mixin-uri dashboard/handlers/*.py; path-uri centralizate în dashboard/constants.py. Template systemd user unit la dashboard/echo-taskboard.service. workspace.html e hub-ul unificat de proiecte (fostul ralph.html + workspace.html); /echo/ralph.html → 302 redirect la /echo/workspace.html. Autentificarea e dezactivată implicit (acces doar prin tailnet); se reactivează cu DASHBOARD_AUTH=on în dashboard/.env.
Dashboard — Note arhitecturale
Cookie auth (off by default): DASHBOARD_AUTH nesetat ⇒ _check_dashboard_cookie trece mereu, /echo/login redirectează direct la dashboard, POST-urile /api/* nu mai cer cookie. Motiv: tailscale serve expune /echo doar în tailnet, deci autentificarea era dublată. Cu DASHBOARD_AUTH=on în dashboard/.env revine login-ul: httpOnly cookie dashboard=...; SameSite=Strict; Path=/echo/; EventSource SSE trimite cookie-ul automat; DASHBOARD_TOKEN din dashboard/.env — setează o dată, restart service. Resetare: schimbă valoarea + restart.
jsonlock helper (src/jsonlock.py): read_locked(path) / write_locked(path, mutator) pentru orice scriere la approved-tasks.json, sessions/*.json. Lock pe sidecar <path>.lock (inode stabil chiar și după os.replace). Ordine canonică lock-uri: alfabetic după filename. Re-entrant (threading.local refcount). Expune și LockTimeoutError.
Slug convention: slug-urile proiectelor validează cu regex ^[a-z0-9][a-z0-9\-_]{1,38}[a-z0-9]$ — permit hifene ȘI underscore. Validare centralizată în dashboard/handlers/_validators.py.
Proxy timeout: pentru nginx/caddy, setează proxy_read_timeout >= 60s și proxy_buffering off pentru /echo/api/projects/stream și /echo/api/projects/<slug>/plan/* (SSE + planning au răspunsuri lungi).
Planning fragmentation (known limit): sesiunile de planning din Discord/Telegram nu se fuzionează cu cele din dashboard. Dashboard afișează sesiunea cea mai recentă per slug indiferent de adapter. P3 follow-up.
Ralph — Execuție autonomă de proiecte
Sistem de implementare autonomă care rulează noaptea. Flow complet:
21:00 evening-report → propune features/proiecte, adaugă în approved-tasks.json (status: pending)
email lui Marius cu instrucțiuni de aprobare
Marius → /a <slug> (Discord/Telegram/WhatsApp → router.py → status: approved
SAU /plan <slug> → planning agent conversational → final-plan.md → approved)
23:00 night-execute → citește approved, clonează repo dacă lipsește, generează PRD din final-plan.md,
lansează ralph.sh; actualizează approved-tasks.json (running, pid: PID)
08:30 morning-report → citește approved-tasks.json + prd.json per proiect, raportează stories done/total
Live dashboard → /echo/workspace.html — cards per proiect cu status, iter, ETA, log, stop; realtime SSE
Două căi de aprobare:
- Direct:
/a <slug>— pentru proiecte simple unde descrierea e suficientă. - Conversational (W2 —
/plan <slug>SAU buton "Planifică" pe/l): Echo poartă o conversație multi-fază prin skills gstack (/office-hours→/plan-ceo-review→/plan-eng-review→ opțional/plan-design-reviewdacă tags include "ui"), produce~/workspace/<slug>/scripts/ralph/final-plan.mdși prezintă rezumat cu butonul "✅ Dau drumul tonight".night-executeîl folosește ca input pentru PRD generator (Opus extrage user stories cu acceptanceCriteria, tags, dependsOn).
Comenzi (funcționează pe toate adaptoarele — Discord, Telegram, WhatsApp):
| Comandă | Efect |
|---|---|
/p <slug> <descriere> |
Adaugă proiect nou cu status pending |
/a |
Listează proiectele pending |
/a <slug> sau /a P1,P2 |
Aprobă pentru tonight (path direct) |
/plan <slug> |
Pornește planning agent conversational (multi-fază skills gstack) |
/cancel |
Anulează planning în curs (revert status → pending) |
/l |
Discord/Telegram: meniu interactiv (Views/InlineKeyboardMarkup) cu butoane per proiect; WhatsApp: text plain + redirect spre Discord/TG |
/l <slug> |
Status proiect specific |
/k <slug> |
Trimite SIGTERM la ralph.sh PID |
UX interactiv (Discord/Telegram):
/ldeschideRalphRootView(Discord) / InlineKeyboardMarkup (Telegram) cu butoane per workspace project.- Click pe proiect → submeniu: ➕ Propune feature (modal/ForceReply), 🧠 Planifică (W2), 👁 Vezi PRD, 📊 Status, ✅ Aprobă tonight, 🛑 Stop, 🔙 Înapoi.
- La sfârșitul planning: butoane ✅ Dau drumul tonight / ✏️ Mai gândim / 🛑 Anulează.
- State per
(adapter, channel)însessions/ralph_flow.jsonșisessions/planning.json(TTL 10min/60min).
Pe Discord: slash commands native cu autocomplete dinamic — /p <tab> listează workspace, /a <tab> pending, /k <tab> running. Modal cu TextInput pentru descriere. Critical pattern: await interaction.response.defer(ephemeral=True) în orice button callback cu I/O (Discord 3s timeout).
Pe Telegram: callback_ralph cu pattern ^ralph: rutează acțiuni; ForceReply pentru input text descriere.
Pe WhatsApp: text-only — meniu redirect la Discord/Telegram. Text-keyword shortcuts: aprob <slug> → /a <slug>, stop <slug> → /k <slug>, stare/stare <slug> → /l//l <slug> (case-insensitive, doar pe WhatsApp; Discord/Telegram neafectate). propose intenționat NEacoperit — descrierea fragilă.
Aliasuri legacy (backwards compat): !propose, !approve, !status, !stop.
Fișiere cheie Ralph:
| Path | Rol |
|---|---|
approved-tasks.json |
Coordonare între cron jobs + UX. Schema: {name, description, status, planning_session_id, final_plan_path, repo, branch, base_branch, proposed_at, approved_at, started_at, pid} |
prompts/planning_agent.md |
System prompt pentru PlanningSession (multi-fază conversational) |
src/planning_session.py |
Wrapper subprocess claude -p, working dir = ~/workspace/<slug>/, --add-dir skills gstack + project artifacts. --max-turns=20 cu retry pe error_max_turns |
src/planning_orchestrator.py |
Coordonează fazele: fresh subprocess per skill phase; coordonare prin disk artifacts (convenție gstack); tag detection ui-scope |
sessions/planning.json |
State per (adapter, channel) planning session: session_id, current_phase etc. — pentru re-resume la restart |
tools/ralph/ralph.sh |
Bash loop DAG-aware: N iterații × claude CLI per story; folosește tools/ralph_dag.py pentru selecție topologică, retry guard (3 retries), rate-limit detection |
tools/ralph/prompt.md |
Smart gates dispatcher pe story.tags (Faza 3): refactor→/workflow:simplify, ui→/qa+screenshot, vercel→push+gh checks, db→schema diff, default→/review |
tools/ralph/prd-template.json |
Template prd.json: stories cu acceptanceCriteria[], tags[], dependsOn[], passes, retries |
tools/ralph_prd_generator.py |
Generează prd.json. Cu final_plan_path (de la PlanningOrchestrator) → Opus extrage stories cu acceptance criteria. Fără → backwards-compat description-only |
tools/ralph_dag.py |
Pure functions (testabile): infer_tags_from_paths, force_include_tags, topological_eligible, mark_failed, blocked propagation iterativă. CLI subcommands din ralph.sh (infer-tags, next-story, mark-failed, incr-retry) |
tools/ralph_usage.py |
Rate limit budget tracking: pure functions extract_usage_entry, parse_usage_jsonl, aggregate_by_day, aggregate_by_project + CLI append/summarize. Atomic write JSONL |
~/workspace/<name>/scripts/ralph/usage.jsonl |
Append-only log per claude -p call (cost, tokens, model, duration) — generat din ralph.sh, agregat de /api/ralph/usage |
~/workspace/<name>/scripts/ralph/final-plan.md |
Output planning agent — citit de PRD generator |
~/workspace/<name>/scripts/ralph/prd.json |
PRD per proiect cu schema extinsă |
~/workspace/<name>/scripts/ralph/logs/ |
Loguri ralph.sh per rulare |
dashboard/handlers/ralph.py |
Endpoints /api/ralph/status, /<slug>/log, /<slug>/prd, /<slug>/stop, /<slug>/rollback, /usage[?days=N], /stream (SSE) |
dashboard/handlers/projects.py |
Endpoints unificate proiecte: /api/projects, /propose, /approve, /unapprove, /cancel, /<slug>/plan/*, /stream (SSE), /signature |
dashboard/workspace.html |
Hub unificat proiecte — cards status/iter/ETA, log, prd, stop/rollback. Realtime SSE cu fallback polling 5s. Înlocuiește ralph.html (302 redirect aici) |
dashboard/.env |
GITEA_TOKEN pentru clone HTTPS la gitea.romfast.ro; DASHBOARD_TOKEN pentru cookie auth |
Status flow: pending → (planning →) approved → running → complete / failed / stopped / blocked (DAG)
Story status (în prd.json): passes:false + retries:N → passes:true SAU failed:rate_limited|max_retries
Workspace proiecte (~/workspace/): roa2web, gomag-vending, vending_data_intelligence_report, btgo-playwright, space-booking, romfast-website, game-library, wol, romfastsql
Reguli importante:
- Ralph NU modifică niciodată
src/router.py,src/claude_session.pysau alte fișiere core din echo-core. - Self-improvement echo-core NUMAI pe branch
ralph/echo-improve, niciodată pe master. - Clone-urile folosesc
GITEA_TOKENdindashboard/.env:https://moltbot:${TOKEN}@gitea.romfast.ro/romfast/<name>.git
Features pe repo-uri existente (worktree-aware)
Slug-ul proiectului nu trebuie să corespundă cu un repo Gitea. Pentru o feature pe un repo existent (ex: roa2web-telegram-bonuri ca feature pe roa2web), folosește câmpurile opționale:
repo— numele repo-ului Gitea de clonat (default: slug-ul proiectului).branch— feature branch nou creat după clone (default: niciunul, ralph lucrează pe HEAD-ul default).base_branch— branch-ul de la care porneștebranch(default:main).
Cum le setezi:
- CLI/chat:
/p <slug> --repo <name> --branch <feature> [--base-branch <name>] <descriere>(parser în_ralph_proposelasrc/router.py). - Dashboard: modal Propose → secțiunea „Avansat" cu câmpuri pentru repo/branch/base_branch.
Night-execute (cron/jobs.json) detectează câmpurile, clonează repo în ~/workspace/<slug>/, apoi git checkout -b <branch> <base_branch> dacă branch e setat. Dacă clone-ul eșuează (repo inexistent), proiectul e marcat failed fără să pornească ralph.
Approval guard — protejare împotriva re-planning accidental
/plan/start (POST /api/projects/<slug>/plan/start) refuză cu 409 already_committed dacă proiectul e deja approved/running/complete. Pentru re-inițiere intenționată:
- Dashboard: butonul „Re-planifică" pe cards aprobate cere confirm explicit înainte să trimită
force=trueîn body. - API direct: trimite
{"force": true, "description": "..."}în body-ul de la/plan/start.
Asta previne situația în care un click accidental pe „Planifică" șterge status=approved și pornește un nou subprocess Claude (cu cost asociat).
Convenție import-uri
Import-uri absolute via sys.path.insert(0, PROJECT_ROOT): from src.config import ..., from src.adapters.discord_bot import .... Fără import-uri circulare.
Fișiere cheie
Fișierele Ralph (planning_session, planning_orchestrator, ralph.sh, ralph_dag, ralph_prd_generator, planning_agent.md, prd.json etc.) sunt documentate în tabelul din § Ralph. Restul:
| Path | Rol |
|---|---|
src/main.py |
Entry point — adaptoare + scheduler + heartbeat |
src/router.py |
Comenzi vs mesaje Claude |
src/claude_session.py |
Wrapper Claude CLI cu --resume |
src/claude_runner.py |
Procese Claude persistente per canal ("steering") — vezi § Arhitectură |
src/stream_json.py |
Parser stream-json partajat între claude_session.py și claude_runner.py |
src/sentinels.py |
Markeri de protocol partajați (ex. __AUDIO__:, __STEERED__) pe cele 4 căi (Discord/Telegram/WhatsApp/voice) |
src/local_fallback_tools.py |
Registry allowlist de unelte doar-citire pentru modelul local (vezi § Fallback local) |
src/fallback_history.py |
Istoric conversație per canal pentru fallback (6 schimburi, TTL 30 min) |
src/net_status.py |
Status read-only mașini Proxmox/LXC prin SSH paralel |
src/web_search.py |
Căutare web fără API key (DuckDuckGo Lite) |
src/credential_store.py |
Secrete keyring |
cli.py |
Diagnostice CLI (eco) |
config.json |
Config runtime |
bridge/whatsapp/index.js |
Bridge Baileys + Express, port 8098 |
personality/*.md |
System prompt — cine ești |
memory/ |
Knowledge base — embeddings + SQLite (în repo, nu symlink) |
dashboard/api.py |
Task Board HTTP API (port 8088) |
dashboard/handlers/ |
Mixin-uri endpoints (git, cron, habits, eco, files, pdf, workspace, youtube, projects, ralph, auth) |
dashboard/handlers/projects.py |
Endpoints unificate proiecte (vezi § Ralph pentru lista completă) |
dashboard/handlers/auth.py |
Login/logout cu cookie httpOnly dashboard=<token>; DASHBOARD_TOKEN din .env |
dashboard/handlers/_validators.py |
Validatori slug/descriere partajați (slug regex — vezi § Dashboard) |
dashboard/static/tokens.css |
Design tokens CSS (--color-*, --space-*, etc.) — shared pentru toate paginile |
dashboard/DESIGN.md |
Design system source-of-truth: tokens, componente, regula no-emoji |
dashboard/constants.py |
Path-uri centralizate + config Gitea pentru dashboard |
dashboard/echo-taskboard.service |
Template systemd user unit |
src/jsonlock.py |
Flock helper scrieri concurente (detalii în § Dashboard) |
src/approved_tasks_cli.py |
CLI wrapper pentru shell scripts: scrie în approved-tasks.json prin jsonlock. Usage: python3 -m src.approved_tasks_cli set-status --slug X --status Y |
cron/jobs.json |
Job-uri APScheduler (schemă plată, Europe/Bucharest) |
approved-tasks.json |
Fișier coordonare Ralph — status proiecte autonome (schema în § Ralph) |
tasks/lessons.md |
Lecții capturate din corectările lui Marius (citit la session start) |
tasks/spike-planning-findings.md |
Validare empirică Spike Step 0 (subprocess claude -p + skills gstack + --resume round-trip) |
src/ralph_flow.py |
State per (adapter, chat, user) pentru UX flow (TTL 10min) |
src/adapters/discord_views.py |
Discord Views/Modal pentru UX interactiv (W1) |
gstack
Skills gstack + regula /browse (nu mcp__claude-in-chrome__*) sunt definite în CLAUDE.md-ul global al userului — vezi acolo lista completă de skill-uri.