Files
echo-core/CLAUDE.md
Marius Mutu 747afbaf9d feat(steering): mesaje mid-tur + /stop pe turul în zbor
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
2026-09-02 11:05:58 +00:00

30 KiB
Raw Blame History

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/.

  1. Plan First — task-uri cu checkboxes în plan mode
  2. Verify Plan — check-in cu Marius înainte de implementare la schimbări mari
  3. Track Progress — marchează task-urile complete pe măsură ce le faci
  4. Explain Changes — high-level summary la fiecare pas
  5. Document Results — la final, secțiune review în PR sau în tasks/<task>.md
  6. 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 la http://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 mediu ECHO_STEERING=off.
  • heartbeat.py, planning_session.py și scheduler.py rămân deliberat one-shot — folosesc _run_claude/_run_claude_extra direct, 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ă.
  • /stop oprește doar turul curent în zbor (ClaudeProcess.stop() / stop_turn() în claude_session.py), nu sesiunea — sessions/active.json rămâne valid, canalul răspunde normal la mesajul următor.
  • Diagnostic: eco status arată steering: on/off · N procese vii (flag citit din config la fiecare apel; numărătoarea e prin pgrep -f "--input-format stream-json", nu prin registry-ul din proces — eco rulează separat de serviciu). eco doctor verifică suportul binarului pentru --input-format stream-json doar cât timp steering.enabled e pornit.
  • Rețetă de reproducere manuală (T15): cere-i lui Echo ceva cu un sleep de 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 mutat sleep-ul în run_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 chema sold și răspundea la o rescriere cu o eroare de OTP; „fă-l mai scurt: …despre facturi” chema facturi. 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 ca tool_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 un doctor cu 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 din 10.0.20.0/24, SSH în paralel cu timeout scurt. Containerele fără cheie SSH se accesează prin pct exec de pe nodul gazdă (sh -c, nu bash — gitea e Alpine). Inventarul e în modul, nu în KB: memory/kb/tools/infrastructure.md era stale (minecraft/moltbot sunt pe pve1, nu pveelite).
  • Web (src/web_search.py): DuckDuckGo Lite, fără API key. Atenție la parser — DDG emite class='...' 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 /f userul a ales modelul local deliberat (manual=True) și nu se anunță nicio limită.
  • Turnul nu se pierde niciodată. Fiecare return None din _local_fallback_reply e 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, nu Claude CLI error (exit 1): ….
  • Detecția e partajată: is_rate_limit_error / rate_limit_detail stau în src/claude_session.py (nu în router) pentru că le folosește și src/scheduler.py, care nu are voie să importe router-ul.
  • Cron la limită (src/scheduler.py): job-urile heartbeat* 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ă, /f arată starea, /f reset golește istoricul (/testfallback ră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:

  1. Navigare (întâi, pentru lookup pe subiect/parafrază): citește memory/kb/index.md (router cu folderele), alege folderul relevant, apoi memory/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 de tools/update_notes_index.py (regenerat din heartbeat).
  2. 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 cu degraded: 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-review dacă 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):

  • /l deschide RalphRootView (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) în sessions/ralph_flow.json și sessions/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.py sau alte fișiere core din echo-core.
  • Self-improvement echo-core NUMAI pe branch ralph/echo-improve, niciodată pe master.
  • Clone-urile folosesc GITEA_TOKEN din dashboard/.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ște branch (default: main).

Cum le setezi:

  • CLI/chat: /p <slug> --repo <name> --branch <feature> [--base-branch <name>] <descriere> (parser în _ralph_propose la src/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.