From 094c6be5a97831288e52af039ab13f6084b7f6a0 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Sun, 26 Apr 2026 17:46:52 +0000 Subject: [PATCH] feat(ralph): unified slash commands /p /a /l /k cu legacy aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructurează comenzile Ralph într-un dispatcher unificat (_try_ralph_dispatch) care suportă atât comenzile noi scurte (/p /a /l /k) cât și aliasurile legacy (!propose !approve !status !stop). Pe Discord adaugă slash commands native cu autocomplete dinamic pentru pending (/a) și running (/k). Pe Telegram apar în meniul /. WhatsApp le parsează ca text plain. Activează cron jobs morning-report (08:30) și evening-report (21:00) și adaugă night-execute (23:00) pentru execuția autonomă a proiectelor aprobate. Foundation pentru W1 din planul "Echo Core conversational planning agent". Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 18 +-- cron/jobs.json | 28 ++++- src/adapters/discord_bot.py | 77 ++++++++++++- src/adapters/telegram_bot.py | 70 +++++++++++- src/router.py | 209 ++++++++++++++++++++--------------- 5 files changed, 296 insertions(+), 106 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1bfc794..4207869 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -152,16 +152,20 @@ Marius → !approve (Discord/Telegram/WhatsApp → router.py → appro 08:30 morning-report → citește approved-tasks.json + prd.json per proiect, raportează stories done/total ``` -**Comenzi text** (funcționează pe toate adaptoarele — Discord, Telegram, WhatsApp): +**Comenzi** (funcționează pe toate adaptoarele — Discord, Telegram, WhatsApp): | Comandă | Efect | |---------|-------| -| `!propose ` | Adaugă proiect nou cu status `pending` | -| `!approve` | Listează proiectele pending | -| `!approve ` sau `!approve P1,P2` | Aprobă pentru tonight | -| `!status` | Status toate proiectele (PID, stories done/total) | -| `!status ` | Status proiect specific | -| `!stop ` | Trimite SIGTERM la ralph.sh PID | +| `/p ` | Adaugă proiect nou cu status `pending` | +| `/a` | Listează proiectele pending | +| `/a ` sau `/a P1,P2` | Aprobă pentru tonight | +| `/l` | Status toate proiectele (PID, stories done/total) | +| `/l ` | Status proiect specific | +| `/k ` | Trimite SIGTERM la ralph.sh PID | + +Pe **Discord** sunt slash commands native cu autocomplete dinamic: `/a ` listează proiectele pending, `/k ` listează proiectele running. Pe **Telegram** apar în meniul `/` cu descriere. Pe **WhatsApp** sunt parsate ca text plain. + +**Aliasuri legacy** (funcționează încă pentru backwards compat): `!propose`, `!approve`, `!status`, `!stop`. **Fișiere cheie Ralph:** diff --git a/cron/jobs.json b/cron/jobs.json index b15c7cd..34cafca 100644 --- a/cron/jobs.json +++ b/cron/jobs.json @@ -179,12 +179,12 @@ }, { "name": "morning-report", - "cron": "30 9 * * *", + "cron": "30 8 * * *", "channel": "echo-work", "model": "sonnet", - "prompt": "RAPORT DIMINEAȚĂ - trimite pe EMAIL (Gmail: mmarius28@gmail.com)\n\n## CALENDAR\nVerifică calendarul:\n```bash\ncd ~/echo-core && source venv/bin/activate && python3 tools/calendar_check.py today\npython3 tools/calendar_check.py travel\npython3 tools/calendar_check.py week\n```\n\n## CITEȘTE CONTEXT\n- USER.md pentru programul lui Marius (luni-joi 15-16 liber)\n- memory/kb/insights/ pentru propuneri (ultimele 3 zile)\n- memory/approved-tasks.md pentru status proiecte/features\n\n## FORMAT EMAIL HTML\n- Font: 16px text, 18px titluri\n- Culori: albastru (#dbeafe) DONE, gri (#f3f4f6) PROGRAMAT, verde (#d1fae5) PROJECTS\n- Link-uri vizibile\n\n## STRUCTURA RAPORT\n\n### 1. CALENDAR\n- 📅 **AZI:** [evenimente]\n- 📅 **MÂINE:** [evenimente]\n- 📅 **PESTE 2 ZILE:** [dacă e GRUP, NLP, meeting mare]\n- 🚂 **TRAVEL:** Reminders bilete+cazare\n\n### 2. PROIECTE/FEATURES NOAPTEA 💻\n\nCitesc approved-tasks.md și raportez ce s-a realizat:\n\n**Format pentru fiecare proiect/feature [x]:**\n\n```html\n
\n

✅ P1 - Nume Proiect

\n \n

Status: X/Y stories complete

\n \n

Stories realizate:

\n
    \n
  • ✅ US-001: Titlu story - implementat cu succes
  • \n
  • ✅ US-002: Titlu story - quality checks pass
  • \n
  • 🔄 US-003: Titlu story - în progres (blocat pe dependency)
  • \n
\n \n

Link: gitea.romfast.ro/romfast/PROJECT-NAME

\n \n

Learnings: [din progress.txt - ce patterns am descoperit]

\n \n

Next steps: [ce rămâne de făcut]

\n
\n```\n\n**Dacă NU s-au executat proiecte/features:**\n- Sari peste această secțiune\n\n### 3. STATUS GENERAL\n- Ce s-a făcut ieri (joburi, taskuri)\n- Git status ~/clawd\n- Joburi executate (YouTube, insights, etc.)\n\n### 4. PROPUNERI CU ZI ȘI ORĂ!\n\n**OBLIGATORIU:** Fiecare propunere TU+EU sau FAC TU trebuie să aibă ZI și ORĂ concrete!\n\nCategorii:\n- 🤖 **FAC EU** (0 efort) - execut singur\n- 🤝 **TU+EU** (eu pregătesc) - cu zi/oră!\n- 👤 **FAC TU** (template gata) - cu zi/oră!\n\nExemplu:\n- **A1 - Sesiune Dizolvare Vină** 🤝 TU+EU\n 📅 **Marți 3 feb, 15:00-15:30**\n Context + link sursă\n\nReguli programare:\n- Luni-Joi 15:00-16:00 = slot liber\n- Vineri-Duminică = NLP, evită\n- Verifică calendar să nu fie ocupat\n\n### 5. INSIGHTS DISPONIBILE\n\nListează insights-uri [ ] nepropuse încă (format scurt).\n\n### 6. CUM RĂSPUNZI\n- DA = aprob toate (cu zilele/orele propuse)\n- 1 pentru A1,A2 = execut ACUM\n- 2 pentru A3 = programez noapte\n- 3 pentru A5 = skip\n- Alt orar = \"A1 miercuri nu marți\"\n\n## TRIMITERE\npython3 /home/moltbot/echo-core/tools/email_send.py \"mmarius28@gmail.com\" \"Raport Dimineata DATA\" \"HTML_CONTENT\"\n\nNU trimite pe Discord - doar email.", + "enabled": true, + "prompt": "RAPORT DIMINEAȚĂ - trimite pe EMAIL (Gmail: mmarius28@gmail.com)\n\n## CALENDAR\nVerifică calendarul:\n```bash\ncd ~/echo-core && source venv/bin/activate && python3 tools/calendar_check.py today\npython3 tools/calendar_check.py travel\npython3 tools/calendar_check.py week\n```\n\n## CITEȘTE CONTEXT\n- USER.md pentru programul lui Marius (luni-joi 15-16 liber)\n- memory/kb/insights/ pentru propuneri (ultimele 3 zile)\n- /home/moltbot/echo-core/approved-tasks.json pentru status proiecte/features (câmpurile: name, status, started_at, pid)\n\n## FORMAT EMAIL HTML\n- Font: 16px text, 18px titluri\n- Culori: albastru (#dbeafe) DONE, gri (#f3f4f6) PROGRAMAT, verde (#d1fae5) PROJECTS\n- Link-uri vizibile\n\n## STRUCTURA RAPORT\n\n### 1. CALENDAR\n- 📅 **AZI:** [evenimente]\n- 📅 **MÂINE:** [evenimente]\n- 📅 **PESTE 2 ZILE:** [dacă e GRUP, NLP, meeting mare]\n- 🚂 **TRAVEL:** Reminders bilete+cazare\n\n### 2. PROIECTE/FEATURES NOAPTEA 💻\n\nCitesc /home/moltbot/echo-core/approved-tasks.json și raportez ce s-a realizat:\n(statusuri: pending, approved, running, complete, failed, stopped)\nPentru stories done/total: citesc /home/moltbot/workspace/{name}/scripts/ralph/prd.json\n\n**Format pentru fiecare proiect/feature [x]:**\n\n```html\n
\n

✅ P1 - Nume Proiect

\n \n

Status: X/Y stories complete

\n \n

Stories realizate:

\n
    \n
  • ✅ US-001: Titlu story - implementat cu succes
  • \n
  • ✅ US-002: Titlu story - quality checks pass
  • \n
  • 🔄 US-003: Titlu story - în progres (blocat pe dependency)
  • \n
\n \n

Link: gitea.romfast.ro/romfast/PROJECT-NAME

\n \n

Learnings: [din progress.txt - ce patterns am descoperit]

\n \n

Next steps: [ce rămâne de făcut]

\n
\n```\n\n**Dacă NU s-au executat proiecte/features:**\n- Sari peste această secțiune\n\n### 3. STATUS GENERAL\n- Ce s-a făcut ieri (joburi, taskuri)\n- Git status ~/clawd\n- Joburi executate (YouTube, insights, etc.)\n\n### 4. PROPUNERI CU ZI ȘI ORĂ!\n\n**OBLIGATORIU:** Fiecare propunere TU+EU sau FAC TU trebuie să aibă ZI și ORĂ concrete!\n\nCategorii:\n- 🤖 **FAC EU** (0 efort) - execut singur\n- 🤝 **TU+EU** (eu pregătesc) - cu zi/oră!\n- 👤 **FAC TU** (template gata) - cu zi/oră!\n\nExemplu:\n- **A1 - Sesiune Dizolvare Vină** 🤝 TU+EU\n 📅 **Marți 3 feb, 15:00-15:30**\n Context + link sursă\n\nReguli programare:\n- Luni-Joi 15:00-16:00 = slot liber\n- Vineri-Duminică = NLP, evită\n- Verifică calendar să nu fie ocupat\n\n### 5. INSIGHTS DISPONIBILE\n\nListează insights-uri [ ] nepropuse încă (format scurt).\n\n### 6. CUM RĂSPUNZI\n- DA = aprob toate (cu zilele/orele propuse)\n- 1 pentru A1,A2 = execut ACUM\n- 2 pentru A3 = programez noapte\n- 3 pentru A5 = skip\n- Alt orar = \"A1 miercuri nu marți\"\n\n## TRIMITERE\npython3 /home/moltbot/echo-core/tools/email_send.py \"mmarius28@gmail.com\" \"Raport Dimineata DATA\" \"HTML_CONTENT\"\n\nNU trimite pe Discord - doar email.", "allowed_tools": [], - "enabled": false, "last_run": null, "last_status": null, "next_run": null @@ -194,9 +194,9 @@ "cron": "0 21 * * *", "channel": "echo-work", "model": "sonnet", - "prompt": "RAPORT SEARĂ - trimite pe EMAIL (Gmail: mmarius28@gmail.com)\n\n## CALENDAR\nVerifică ce ai mâine și săptămâna:\n```bash\ncd ~/echo-core && source venv/bin/activate && python3 tools/calendar_check.py today\npython3 tools/calendar_check.py week\n```\n\n## CITEȘTE CONTEXT\n- USER.md pentru programul lui Marius (luni-joi 15-16 liber, vineri-dum NLP)\n- memory/kb/insights/YYYY-MM-DD.md pentru propuneri insights\n- memory/kb/youtube/ și memory/kb/articole/ pentru inspirație proiecte\n- memory/approved-tasks.md pentru status proiecte existente\n\n## FORMAT EMAIL HTML\n- Font: 16px text, 18px titluri\n- Culori: albastru (#dbeafe) DONE, gri (#f3f4f6) PROGRAMAT, verde (#d1fae5) PROJECTS\n- Link-uri vizibile\n\n## STRUCTURA RAPORT\n\n### 1. MÂINE\n- 📅 Evenimente calendar\n- 🚂 Travel reminders\n\n### 2. STATUS\n- Ce s-a făcut azi\n- Git status\n\n### 3. PROPUNERI CU ZI ȘI ORĂ!\n\n**OBLIGATORIU:** Fiecare propunere TU+EU sau FAC TU trebuie să aibă ZI și ORĂ concrete!\n\nReguli programare:\n- Luni-Joi 15:00-16:00 = slot liber\n- Vineri-Duminică = NLP, evită\n- Verifică calendar să nu fie ocupat\n- Sesiuni scurte: 15-30 min\n\n### 4. PROGRAME/PROIECTE PRACTICE 💻\n\n**CONTEXT OBLIGATORIU - citește înainte de a propune:**\n\n**Proiecte existente (PRIORITARE pentru features):**\n- **roa2web** (gitea.romfast.ro/romfast/roa2web) - FastAPI+Vue.js+Telegram bot\n - Are deja: balanță, facturi, trezorerie\n - Lipsesc: validări declarații ANAF, facturare valută/taxare inversă, notificări\n - Rapoarte ROA noi → FEATURE în roa2web, NU proiect separat!\n- **Chatbot Maria** (Flowise pe LXC 104, ngrok → romfast.ro/chatbot_maria.html)\n - Document store: XML, MD | Groq gratuit + Ollama embeddings + FAISS\n - Problema: răspunsuri nu sunt suficient de bune\n - Angajatul nou poate menține documentația (scrie TXT, trebuie converter)\n - Clientii îl accesează din programele ROA direct\n\n**Întrebări frecvente clienți (surse de proiecte):**\n- Erori validare declarații ANAF (D406, D394, D100 etc.)\n- Cum facturez în valută cu taxare inversă?\n- Probleme la instalări, inițializări firme noi, configurări\n\n**Reguli propuneri (80/20 STRICT):**\n- Impact mare pentru Marius → apoi pentru clienți ERP ROA\n- Inspirat din discovery (YouTube, articole, insights procesate)\n- Features roa2web > proiecte noi (integrare în existent)\n- Proiecte independente doar dacă NU se potrivesc în roa2web/Flowise\n\n**A. FEATURES PROIECTE EXISTENTE (2-3, PRIORITAR):**\n\nFormat:\n```\n### ⚡ F1 - Feature pentru [roa2web/chatbot]\n**Ce face:** Descriere scurtă\n**De ce:** Ce problemă rezolvă (ex: \"clienții întreabă X de 5 ori/săptămână\")\n**Complexitate:** S/M/L\n**Proiect:** roa2web / chatbot-maria\n```\n\n**B. PROIECTE NOI (max 1, doar dacă nu se integrează în existente):**\n\nFormat:\n```\n### 💻 P1 - Nume Proiect\n**De ce:** Cum se leagă de nevoile lui Marius/clienți\n**Impact:** Pentru Marius + pentru clienți\n**Efort:** Ore/zile realist\n**Stack:** Simplu (80/20)\n**Sursă:** [Link nota KB]\n```\n\n**NU propune:**\n- Proiecte complexe fără beneficiu clar\n- Proiecte duplicat cu ce există deja\n- Rapoarte ROA ca proiect separat (→ feature roa2web)\n\n### 5. INSIGHTS DISPONIBILE\nListează insights-uri [ ] nepropuse încă (format scurt).\n\n### 6. CUM RĂSPUNZI\n- DA = aprob toate (cu zilele/orele propuse)\n- 1 pentru A1,A2 = execut ACUM\n- 2 pentru A3 = programez noapte\n- 3 pentru A5 = skip\n- **F pentru F1,F3** = implementează features (joburi noapte)\n- **P pentru P1** = creează proiect nou (job noapte)\n- Alt orar = \"A1 miercuri nu marți\"\n\n## IMPLEMENTARE PROIECTE APROBATE\n\nCând Marius aprobă cu F sau P:\n1. Adaugă în memory/approved-tasks.md secțiunea \"Noaptea asta\"\n2. Night-execute (23:00) va:\n - Pentru proiecte noi: genera PRD cu ralph_prd_generator.py → ralph.sh\n - Pentru features roa2web: adaugă stories în prd.json existent → ralph.sh\n - Pentru chatbot: generează documentație nouă și uploadează în Flowise\n\n## TRIMITERE\npython3 /home/moltbot/echo-core/tools/email_send.py \"mmarius28@gmail.com\" \"Raport Seara DATA\" \"HTML_CONTENT\"\n\nNU trimite pe Discord - doar email.", + "enabled": true, + "prompt": "RAPORT SEARĂ - trimite pe EMAIL (Gmail: mmarius28@gmail.com)\n\n## CALENDAR\nVerifică ce ai mâine și săptămâna:\n```bash\ncd ~/echo-core && source venv/bin/activate && python3 tools/calendar_check.py today\npython3 tools/calendar_check.py week\n```\n\n## CITEȘTE CONTEXT\n- USER.md pentru programul lui Marius (luni-joi 15-16 liber, vineri-dum NLP)\n- memory/kb/insights/YYYY-MM-DD.md pentru propuneri insights\n- memory/kb/youtube/ și memory/kb/articole/ pentru inspirație proiecte\n- /home/moltbot/echo-core/approved-tasks.json pentru status proiecte existente (câmpurile: name, status, proposed_at)\n\n## FORMAT EMAIL HTML\n- Font: 16px text, 18px titluri\n- Culori: albastru (#dbeafe) DONE, gri (#f3f4f6) PROGRAMAT, verde (#d1fae5) PROJECTS\n- Link-uri vizibile\n\n## STRUCTURA RAPORT\n\n### 1. MÂINE\n- 📅 Evenimente calendar\n- 🚂 Travel reminders\n\n### 2. STATUS\n- Ce s-a făcut azi\n- Git status\n\n### 3. PROPUNERI CU ZI ȘI ORĂ!\n\n**OBLIGATORIU:** Fiecare propunere TU+EU sau FAC TU trebuie să aibă ZI și ORĂ concrete!\n\nReguli programare:\n- Luni-Joi 15:00-16:00 = slot liber\n- Vineri-Duminică = NLP, evită\n- Verifică calendar să nu fie ocupat\n- Sesiuni scurte: 15-30 min\n\n### 4. PROGRAME/PROIECTE PRACTICE 💻\n\n**CONTEXT OBLIGATORIU - citește înainte de a propune:**\n\n**Proiecte existente (PRIORITARE pentru features):**\n- **roa2web** (gitea.romfast.ro/romfast/roa2web) - FastAPI+Vue.js+Telegram bot\n - Are deja: balanță, facturi, trezorerie\n - Lipsesc: validări declarații ANAF, facturare valută/taxare inversă, notificări\n - Rapoarte ROA noi → FEATURE în roa2web, NU proiect separat!\n- **Chatbot Maria** (Flowise pe LXC 104, ngrok → romfast.ro/chatbot_maria.html)\n - Document store: XML, MD | Groq gratuit + Ollama embeddings + FAISS\n - Problema: răspunsuri nu sunt suficient de bune\n - Angajatul nou poate menține documentația (scrie TXT, trebuie converter)\n - Clientii îl accesează din programele ROA direct\n\n**Întrebări frecvente clienți (surse de proiecte):**\n- Erori validare declarații ANAF (D406, D394, D100 etc.)\n- Cum facturez în valută cu taxare inversă?\n- Probleme la instalări, inițializări firme noi, configurări\n\n**Reguli propuneri (80/20 STRICT):**\n- Impact mare pentru Marius → apoi pentru clienți ERP ROA\n- Inspirat din discovery (YouTube, articole, insights procesate)\n- Features roa2web > proiecte noi (integrare în existent)\n- Proiecte independente doar dacă NU se potrivesc în roa2web/Flowise\n\n**A. FEATURES PROIECTE EXISTENTE (2-3, PRIORITAR):**\n\nFormat:\n```\n### ⚡ F1 - Feature pentru [roa2web/chatbot]\n**Ce face:** Descriere scurtă\n**De ce:** Ce problemă rezolvă (ex: \"clienții întreabă X de 5 ori/săptămână\")\n**Complexitate:** S/M/L\n**Proiect:** roa2web / chatbot-maria\n```\n\n**B. PROIECTE NOI (max 1, doar dacă nu se integrează în existente):**\n\nFormat:\n```\n### 💻 P1 - Nume Proiect\n**De ce:** Cum se leagă de nevoile lui Marius/clienți\n**Impact:** Pentru Marius + pentru clienți\n**Efort:** Ore/zile realist\n**Stack:** Simplu (80/20)\n**Sursă:** [Link nota KB]\n```\n\n**NU propune:**\n- Proiecte complexe fără beneficiu clar\n- Proiecte duplicat cu ce există deja\n- Rapoarte ROA ca proiect separat (→ feature roa2web)\n\n### 5. INSIGHTS DISPONIBILE\nListează insights-uri [ ] nepropuse încă (format scurt).\n\n### 6. CUM RĂSPUNZI\n- DA = aprob toate (cu zilele/orele propuse)\n- 1 pentru A1,A2 = execut ACUM\n- 2 pentru A3 = programez noapte\n- 3 pentru A5 = skip\n- **F pentru F1,F3** = implementează features (joburi noapte)\n- **P pentru P1** = creează proiect nou (job noapte)\n- Alt orar = \"A1 miercuri nu marți\"\n\n## IMPLEMENTARE PROIECTE APROBATE\n\nCând propui features (F) sau proiecte (P), adaugă-le automat în /home/moltbot/echo-core/approved-tasks.json cu status 'pending':\n```bash\npython3 -c \"\nimport json, datetime\nf = open('/home/moltbot/echo-core/approved-tasks.json')\ndata = json.load(f); f.close()\ndata['projects'].append({'name': 'SLUG-PROIECT', 'description': 'DESCRIERE', 'status': 'pending', 'proposed_at': datetime.datetime.utcnow().isoformat(), 'approved_at': None, 'started_at': None, 'pid': None})\ndata['last_updated'] = datetime.datetime.utcnow().isoformat()\nopen('/home/moltbot/echo-core/approved-tasks.json', 'w').write(json.dumps(data, indent=2))\n\"\n```\n\nÎn email, arată lui Marius comanda de aprobare:\n`!approve SLUG-PROIECT` (trimite pe Discord/Telegram la Echo)\n\nNight-execute (23:00) va:\n - genera PRD cu ralph_prd_generator.py dacă nu există prd.json\n - lansa ralph.sh 15 iterații pentru fiecare proiect aprobat\n\n## TRIMITERE\npython3 /home/moltbot/echo-core/tools/email_send.py \"mmarius28@gmail.com\" \"Raport Seara DATA\" \"HTML_CONTENT\"\n\nNU trimite pe Discord - doar email.", "allowed_tools": [], - "enabled": false, "last_run": null, "last_status": null, "next_run": null @@ -272,5 +272,21 @@ "last_run": "2026-04-26T16:00:00.003767+00:00", "last_status": "ok", "next_run": "2026-04-26T18:00:00+00:00" + }, + { + "name": "night-execute", + "cron": "0 23 * * *", + "channel": "echo-work", + "model": "opus", + "enabled": true, + "prompt": "NIGHT-EXECUTE - Implementare autonoma proiecte aprobate\n\n## PASUL 1: Citeste proiectele aprobate\n\nCiteste /home/moltbot/echo-core/approved-tasks.json\nSelecteaza proiectele cu status='approved'\nDaca nu sunt proiecte aprobate: raporteaza pe Discord si opreste-te.\n\n## PASUL 2: Pentru fiecare proiect aprobat\n\n1. Verifica daca workspace-ul exista: /home/moltbot/workspace/{name}\n - Daca nu: TOKEN=$(grep GITEA_TOKEN /home/moltbot/echo-core/dashboard/.env | cut -d= -f2) && git clone https://moltbot:${TOKEN}@gitea.romfast.ro/romfast/{name}.git /home/moltbot/workspace/{name}\n\n2. Verifica daca prd.json exista: /home/moltbot/workspace/{name}/scripts/ralph/prd.json\n - Daca nu: ruleaza generatorul PRD:\n source .venv/bin/activate\n python3 tools/ralph_prd_generator.py \"{name}\" \"{description}\" /home/moltbot/workspace\n\n3. Lanseaza Ralph loop:\n cd /home/moltbot/workspace/{name}\n chmod +x scripts/ralph/ralph.sh\n mkdir -p scripts/ralph/logs\n nohup ./scripts/ralph/ralph.sh 15 > scripts/ralph/logs/ralph-$(date +%Y%m%d).log 2>&1 &\n echo $! > scripts/ralph/.ralph.pid\n\n4. Actualizeaza approved-tasks.json:\n - status: 'running'\n - started_at: timestamp curent\n - pid: PID din .ralph.pid\n\n## PASUL 3: Raport Discord\n\nTrimite pe echo-work:\n- Cate proiecte au pornit\n- PID-urile lor\n- 'morning-report va raporta progresul la 08:30'\n\n## REGULI IMPORTANTE\n\n- Nu modifica niciodata src/router.py, src/claude_session.py sau alte fisiere core echo-core prin Ralph\n- echo-core self-improvement NUMAI pe branch ralph/echo-improve, nu pe master\n- Daca ralph.sh esueaza: log in approved-tasks.json (status: failed, error: mesaj)\n- Delay 5 secunde intre proiecte pentru a evita rate limiting\n", + "allowed_tools": [ + "Bash", + "Read", + "Write" + ], + "last_run": null, + "last_status": null, + "next_run": null } -] +] \ No newline at end of file diff --git a/src/adapters/discord_bot.py b/src/adapters/discord_bot.py index 0797cba..5e95579 100644 --- a/src/adapters/discord_bot.py +++ b/src/adapters/discord_bot.py @@ -16,7 +16,14 @@ from src.claude_session import ( VALID_MODELS, ) from src.fast_commands import dispatch as fast_dispatch -from src.router import route_message +from src.router import ( + route_message, + _ralph_propose, + _ralph_approve, + _ralph_status, + _ralph_stop, + _load_approved_tasks, +) logger = logging.getLogger("echo-core.discord") _security_log = logging.getLogger("echo-core.security") @@ -150,6 +157,12 @@ def create_bot(config: Config) -> discord.Client: "`/heartbeat` — Health checks", "`/restart` — Restart bot (owner)", "", + "**Ralph (autonomous projects)**", + "`/p ` — Propose new project", + "`/a [slug]` — Approve for tonight (autocomplete)", + "`/l` — List projects status", + "`/k ` — Stop a running project (autocomplete)", + "", "**Admin**", "`/setup` — Claim ownership", "`/channel add ` — Register channel", @@ -886,6 +899,68 @@ def create_bot(config: Config) -> discord.Client: f"Error reading logs: {e}", ephemeral=True ) + # --- Ralph commands (autonomous project execution) --- + + async def _autocomplete_by_status( + interaction: discord.Interaction, current: str, statuses: tuple[str, ...] + ) -> list[app_commands.Choice[str]]: + try: + data = _load_approved_tasks() + except Exception: + return [] + current_low = (current or "").lower() + choices: list[app_commands.Choice[str]] = [] + for p in data.get("projects", []): + if p.get("status") not in statuses: + continue + name = p.get("name", "") + if current_low and current_low not in name.lower(): + continue + desc = (p.get("description") or "").strip() + label = f"{name} — {desc}"[:100] if desc else name + choices.append(app_commands.Choice(name=label, value=name)) + if len(choices) >= 25: + break + return choices + + async def _ralph_autocomplete_pending( + interaction: discord.Interaction, current: str + ) -> list[app_commands.Choice[str]]: + return await _autocomplete_by_status(interaction, current, ("pending",)) + + async def _ralph_autocomplete_running( + interaction: discord.Interaction, current: str + ) -> list[app_commands.Choice[str]]: + return await _autocomplete_by_status(interaction, current, ("running", "approved")) + + @tree.command(name="p", description="Propose new Ralph project") + @app_commands.describe(slug="Project slug (e.g. game-library)", description="Short description of what to do") + async def ralph_p( + interaction: discord.Interaction, slug: str, description: str + ) -> None: + await interaction.response.send_message(_ralph_propose(slug, description)) + + @tree.command(name="a", description="Approve Ralph project for tonight (no slug = list pending)") + @app_commands.describe(slug="Project slug to approve (leave empty to list pending)") + @app_commands.autocomplete(slug=_ralph_autocomplete_pending) + async def ralph_a( + interaction: discord.Interaction, slug: str | None = None + ) -> None: + slugs = [slug] if slug else [] + await interaction.response.send_message(_ralph_approve(slugs)) + + @tree.command(name="l", description="List Ralph projects status") + async def ralph_l(interaction: discord.Interaction) -> None: + await interaction.response.send_message(_ralph_status()) + + @tree.command(name="k", description="Stop a running Ralph project") + @app_commands.describe(slug="Project slug to stop") + @app_commands.autocomplete(slug=_ralph_autocomplete_running) + async def ralph_k( + interaction: discord.Interaction, slug: str + ) -> None: + await interaction.response.send_message(_ralph_stop(slug)) + # --- Events --- @client.event diff --git a/src/adapters/telegram_bot.py b/src/adapters/telegram_bot.py index d174ad2..7850574 100644 --- a/src/adapters/telegram_bot.py +++ b/src/adapters/telegram_bot.py @@ -22,7 +22,13 @@ from src.claude_session import ( VALID_MODELS, ) from src.fast_commands import dispatch as fast_dispatch -from src.router import route_message +from src.router import ( + route_message, + _ralph_propose, + _ralph_approve, + _ralph_status, + _ralph_stop, +) logger = logging.getLogger("echo-core.telegram") _security_log = logging.getLogger("echo-core.security") @@ -141,6 +147,12 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: "/logs [N] — Log lines", "/doctor — Diagnostics", "/heartbeat — Health checks", + "", + "*Ralph (autonomous projects)*", + "/p — Propose new project", + "/a [slug] — Approve for tonight (no slug = list pending)", + "/l — List projects status", + "/k — Stop a running project", ] await update.message.reply_text("\n".join(lines), parse_mode="Markdown") @@ -299,6 +311,52 @@ async def cmd_register(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No ) +# --- Ralph commands (autonomous project execution) --- + + +async def cmd_ralph_p(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """/p — propune proiect Ralph.""" + args = list(context.args or []) + if len(args) < 2: + await update.message.reply_text( + "Folosire: /p \nEx: /p roa2web Homepage redesign cu hero section" + ) + return + slug = args[0] + description = " ".join(args[1:]) + result = await asyncio.to_thread(_ralph_propose, slug, description) + await update.message.reply_text(result) + + +async def cmd_ralph_a(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """/a [slug] — aprobă proiect (fără arg = listă pending).""" + args = list(context.args or []) + slugs: list[str] = [] + if args: + for a in args: + slugs.extend(s.strip() for s in a.replace(",", " ").split() if s.strip()) + result = await asyncio.to_thread(_ralph_approve, slugs) + await update.message.reply_text(result) + + +async def cmd_ralph_l(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """/l — status proiecte Ralph.""" + args = list(context.args or []) + filter_slug = args[0].lower() if args else None + result = await asyncio.to_thread(_ralph_status, filter_slug) + await update.message.reply_text(result) + + +async def cmd_ralph_k(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """/k — oprește proiect Ralph.""" + args = list(context.args or []) + if not args: + await update.message.reply_text("Folosire: /k ") + return + result = await asyncio.to_thread(_ralph_stop, args[0]) + await update.message.reply_text(result) + + # --- Fast command handlers --- @@ -529,6 +587,12 @@ def create_telegram_bot(config: Config, token: str) -> Application: app.add_handler(CommandHandler("register", cmd_register)) app.add_handler(CallbackQueryHandler(callback_model, pattern="^model:")) + # Ralph commands + app.add_handler(CommandHandler("p", cmd_ralph_p)) + app.add_handler(CommandHandler("a", cmd_ralph_a)) + app.add_handler(CommandHandler("l", cmd_ralph_l)) + app.add_handler(CommandHandler("k", cmd_ralph_k)) + # Fast commands app.add_handler(CommandHandler("email", cmd_email)) app.add_handler(CommandHandler("emailsend", cmd_emailsend)) @@ -579,6 +643,10 @@ def create_telegram_bot(config: Config, token: str) -> Application: BotCommand("logs", "Show log lines"), BotCommand("doctor", "Diagnostics"), BotCommand("heartbeat", "Health checks"), + BotCommand("p", "Ralph: propose new project"), + BotCommand("a", "Ralph: approve project for tonight"), + BotCommand("l", "Ralph: list projects status"), + BotCommand("k", "Ralph: stop running project"), ]) app.post_init = post_init diff --git a/src/router.py b/src/router.py index 8aa434d..981511a 100644 --- a/src/router.py +++ b/src/router.py @@ -52,18 +52,10 @@ def route_message( """ text = text.strip() - # Ralph commands (!approve, !status, !stop, !propose) - if text.lower().startswith("!approve ") or text.lower() == "!approve": - return _ralph_approve(text), True - - if text.lower() == "!status" or text.lower().startswith("!status "): - return _ralph_status(text), True - - if text.lower().startswith("!stop "): - return _ralph_stop(text), True - - if text.lower().startswith("!propose "): - return _ralph_propose(text), True + # Ralph commands — short form (/p /a /l /k) and legacy aliases (!propose !approve !status !stop) + ralph_response = _try_ralph_dispatch(text) + if ralph_response is not None: + return ralph_response, True # Text-based commands (not slash commands — these work in any adapter) if text.lower() == "/clear": @@ -168,81 +160,146 @@ def _save_approved_tasks(data: dict) -> None: APPROVED_TASKS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False)) -def _ralph_approve(text: str) -> str: - """!approve P1,P2 sau !approve roa2web — aprobă proiecte pentru night-execute.""" - parts = text.split(None, 1) - if len(parts) < 2: - data = _load_approved_tasks() +RALPH_CMDS = { + "propose": ("/p", "!propose"), + "approve": ("/a", "!approve"), + "list": ("/l", "!status"), + "stop": ("/k", "!stop"), +} + + +def _try_ralph_dispatch(text: str) -> str | None: + """Parse and dispatch Ralph commands. Returns response string or None if no match.""" + low = text.lower() + first = low.split(None, 1)[0] if low else "" + + if first in ("/p", "!propose"): + parts = text.split(None, 2) + if len(parts) < 3: + return "Folosire: /p \nEx: /p roa2web Homepage redesign cu hero section" + return _ralph_propose(parts[1].strip(), parts[2].strip()) + + if first in ("/a", "!approve"): + parts = text.split(None, 1) + slugs = [] + if len(parts) > 1: + slugs = [s.strip() for s in parts[1].replace(",", " ").split() if s.strip()] + return _ralph_approve(slugs) + + if first in ("/l", "!status"): + parts = text.split(None, 1) + filter_slug = parts[1].strip().lower() if len(parts) > 1 else None + return _ralph_status(filter_slug) + + if first in ("/k", "!stop"): + parts = text.split(None, 1) + if len(parts) < 2: + return "Folosire: /k " + return _ralph_stop(parts[1].strip()) + + return None + + +def _ralph_propose(slug: str, description: str) -> str: + """Adaugă un proiect cu status pending în approved-tasks.json.""" + data = _load_approved_tasks() + + for p in data["projects"]: + if p["name"].lower() == slug.lower(): + return f"Proiectul '{slug}' există deja cu status: {p.get('status', 'unknown')}." + + data["projects"].append({ + "name": slug, + "description": description, + "status": "pending", + "proposed_at": datetime.now(timezone.utc).isoformat(), + "approved_at": None, + "started_at": None, + "pid": None, + }) + _save_approved_tasks(data) + return f"📋 Adăugat: {slug}\n └ {description}\n\nAprobă cu: /a {slug}" + + +def _ralph_approve(slugs: list[str]) -> str: + """Aprobă unul sau mai multe proiecte. Listă goală = listează pending.""" + data = _load_approved_tasks() + + if not slugs: pending = [p for p in data["projects"] if p.get("status") == "pending"] if not pending: - return "Niciun proiect pending. Folosește !propose pentru a adăuga." - lines = [f"Proiecte pending (aprobă cu !approve ):"] + return "Niciun proiect pending. Adaugă cu /p ." + lines = ["📋 Proiecte pending (aprobă cu /a ):"] for p in pending: - lines.append(f" - {p['name']}: {p['description'][:60]}") + lines.append(f" • {p['name']}") + lines.append(f" └ {p['description'][:80]}") return "\n".join(lines) - names_raw = parts[1].strip() - names = [n.strip() for n in names_raw.replace(",", " ").split() if n.strip()] + approved_info: list[tuple[str, str]] = [] + not_found: list[str] = [] - data = _load_approved_tasks() - approved = [] - not_found = [] - - for name in names: + for slug in slugs: found = False for p in data["projects"]: - if p["name"].lower() == name.lower(): + if p["name"].lower() == slug.lower(): p["status"] = "approved" p["approved_at"] = datetime.now(timezone.utc).isoformat() - approved.append(p["name"]) + approved_info.append((p["name"], p.get("description", ""))) found = True break if not found: - not_found.append(name) + not_found.append(slug) if not_found: - return f"Nu am găsit proiectele: {', '.join(not_found)}. Verifică !status pentru lista completă." + return f"Nu am găsit: {', '.join(not_found)}. Verifică /l pentru lista completă." _save_approved_tasks(data) - names_str = ", ".join(approved) - return f"✅ Aprobat pentru tonight: {names_str}\nNight-execute rulează la 23:00 și va implementa stories autonom." + lines = ["✅ Aprobat pentru tonight:"] + for name, desc in approved_info: + lines.append(f" • {name}") + lines.append(f" └ {desc[:80]}") + lines.append("\nNight-execute rulează la 23:00 și implementează stories autonom.") + return "\n".join(lines) -def _ralph_status(text: str) -> str: - """!status sau !status — status Ralph pentru proiecte.""" - parts = text.split(None, 1) - filter_name = parts[1].strip().lower() if len(parts) > 1 else None - +def _ralph_status(filter_slug: str | None = None) -> str: + """Status Ralph pentru proiecte. Optional filter pe slug.""" data = _load_approved_tasks() projects = data.get("projects", []) - if filter_name: - projects = [p for p in projects if filter_name in p["name"].lower()] + if filter_slug: + projects = [p for p in projects if filter_slug in p["name"].lower()] if not projects: - return "Niciun proiect în approved-tasks.json. Adaugă cu !propose." + return "Niciun proiect. Adaugă cu /p ." - lines = ["📊 Status proiecte Ralph:"] + status_labels = { + "approved": "⏳ aștept 23:00", + "pending": "📋 pending", + "complete": "✅ complet", + "failed": "❌ eșuat", + "stopped": "⏹ oprit", + } + + lines = ["📊 Proiecte Ralph:"] for p in projects: status = p.get("status", "unknown") name = p["name"] + desc = p.get("description", "") pid = p.get("pid") - started = p.get("started_at", "")[:16] if p.get("started_at") else "-" + started = p.get("started_at", "")[:16].replace("T", " ") if p.get("started_at") else "-" - # Verifică dacă procesul mai rulează if pid and status == "running": try: os.kill(pid, 0) - running_indicator = f"🟢 PID {pid}" + indicator = f"🟢 PID {pid}" except (ProcessLookupError, PermissionError): - running_indicator = "🔴 PID mort" + indicator = "🔴 PID mort" p["status"] = "stopped" _save_approved_tasks(data) else: - running_indicator = {"approved": "⏳ aștept 23:00", "pending": "📋 pending", - "complete": "✅ complet", "failed": "❌ eșuat", "stopped": "⏹ oprit"}.get(status, status) + indicator = status_labels.get(status, status) - # Stories complete din prd.json prd_path = Path(f"/home/moltbot/workspace/{name}/scripts/ralph/prd.json") stories_info = "" if prd_path.exists(): @@ -250,26 +307,24 @@ def _ralph_status(text: str) -> str: prd = json.loads(prd_path.read_text()) total = len(prd.get("userStories", [])) done = sum(1 for s in prd.get("userStories", []) if s.get("passes")) - stories_info = f" | Stories: {done}/{total}" + stories_info = f" | {done}/{total} stories" except Exception: pass - lines.append(f" {name}: {running_indicator}{stories_info} | Start: {started}") + lines.append(f"\n {name} {indicator}{stories_info} | Start: {started}") + if desc: + lines.append(f" └ {desc[:80]}") return "\n".join(lines) -def _ralph_stop(text: str) -> str: - """!stop — oprește Ralph loop pentru un proiect.""" - parts = text.split(None, 1) - if len(parts) < 2: - return "Folosire: !stop " - - name = parts[1].strip() +def _ralph_stop(slug: str) -> str: + """Oprește Ralph loop (SIGTERM) pentru un proiect.""" data = _load_approved_tasks() for p in data["projects"]: - if p["name"].lower() == name.lower(): + if p["name"].lower() == slug.lower(): + desc = p.get("description", "") pid = p.get("pid") if pid: try: @@ -277,45 +332,17 @@ def _ralph_stop(text: str) -> str: p["status"] = "stopped" p["stopped_at"] = datetime.now(timezone.utc).isoformat() _save_approved_tasks(data) - return f"⏹ Ralph oprit pentru {name} (PID {pid} terminat)." + return f"⏹ Oprit: {p['name']} (PID {pid})\n └ {desc[:80]}" except ProcessLookupError: p["status"] = "stopped" _save_approved_tasks(data) - return f"PID {pid} nu mai rula pentru {name}. Status actualizat." + return f"PID {pid} nu mai rula pentru {p['name']}. Status actualizat." except PermissionError: return f"❌ Nu am permisiune să opresc PID {pid}." else: - return f"{name} nu are un PID activ (status: {p.get('status', 'unknown')})." + return f"{p['name']} nu are PID activ (status: {p.get('status', 'unknown')})." - return f"Proiect '{name}' nu găsit în approved-tasks.json." - - -def _ralph_propose(text: str) -> str: - """!propose — adaugă un proiect pentru aprobare.""" - parts = text.split(None, 2) - if len(parts) < 3: - return "Folosire: !propose \nEx: !propose roa2web Homepage redesign cu hero section și animații" - - name = parts[1].strip() - description = parts[2].strip() - - data = _load_approved_tasks() - - for p in data["projects"]: - if p["name"].lower() == name.lower(): - return f"Proiectul '{name}' există deja cu status: {p.get('status', 'unknown')}." - - data["projects"].append({ - "name": name, - "description": description, - "status": "pending", - "proposed_at": datetime.now(timezone.utc).isoformat(), - "approved_at": None, - "started_at": None, - "pid": None - }) - _save_approved_tasks(data) - return f"📋 Adăugat: {name}\n{description}\n\nAprobă cu: !approve {name}" + return f"Proiect '{slug}' nu găsit. Verifică /l pentru lista completă." def _get_channel_config(channel_id: str) -> dict | None: