docs(5.9): PRD corectie modal mobil + scaffold Ralph (prd.json cu R1-R12)
- docs/prd/prd-5.9-ux-corectie-modal-mobil.md: PRD aprobat (8 stories, raport AUTOPLAN) - scripts/ralph/: ralph.sh + prompt.md (smart-gates) + progress.txt - scripts/ralph/prd.json: 8 user stories cu reviziile obligatorii R1-R12 incorporate in acceptance criteria; priority encodeaza valurile de dependente - .gitignore: runtime Ralph (logs/screenshots/archive/.last-branch/usage.jsonl) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
232
scripts/ralph/prd.json
Normal file
232
scripts/ralph/prd.json
Normal file
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"projectName": "autopass-5.9-ux-corectie-modal-mobil",
|
||||
"branchName": "ralph/5.9-ux-corectie-modal-mobil",
|
||||
"description": "Curatenie UX pe ecranul Trimiteri, fara schimbare a fluxului de trimitere catre RAR: detaliul+corectia trec intr-un MODAL global (in afara zonei de poll de 15s), editare in-place fara dublarea valorilor, butoane consolidate, fara chevron, eticheta umana scurta sub pill + cod brut in modal, cod RAR simplu. Plus design responsive/mobil (<768px) pentru toate paginile dashboard. Backend (worker, masina de stari, contract RAR, idempotenta, mapare) NEATINS logic; rutele isi pastreaza semnatura, se schimba doar hx-target si prezentarea. APLICA REVIZIILE OBLIGATORII R1-R12 din raportul AUTOPLAN (au prioritate unde difera de AC original). Sursa: docs/prd/prd-5.9-ux-corectie-modal-mobil.md.",
|
||||
"techStack": {
|
||||
"type": "python",
|
||||
"commands": {
|
||||
"start": "uvicorn app.main:app --reload --port 8010",
|
||||
"build": "",
|
||||
"lint": "",
|
||||
"typecheck": "",
|
||||
"test": "python3 -m pytest -q -m 'not live'"
|
||||
},
|
||||
"port": 8010
|
||||
},
|
||||
"userStories": [
|
||||
{
|
||||
"id": "US-001",
|
||||
"title": "Eticheta umana scurta pe randul de afisare + cod brut disponibil pentru modal (R1)",
|
||||
"description": "Ca dezvoltator vreau ca randul tabelului sa expuna o eticheta umana scurta a problemei (NU codul brut de catalog) pentru ca sub pill sa apara un text prietenos, consistent cu investitia 5.4 in erori umane.",
|
||||
"priority": 10,
|
||||
"acceptanceCriteria": [
|
||||
"R1 OVERRIDE: NU se adauga helper nou `cod_eroare_din` care re-parseaza acelasi JSON (DRY). Se reutilizeaza `motiv_uman`/`eticheta_scurta` din `app/web/labels.py` deja existente.",
|
||||
"`_submission_row_view` (`app/web/routes.py`) expune o eticheta umana scurta pe rand reutilizand `motiv` (deja intors nerandat) / `eticheta_scurta` — fara al 3-lea decoder al `rar_error`.",
|
||||
"Eticheta este sir gol pe stari fara problema (`queued/sending/sent`) si ne-goala pe `error`/`needs_data`/`needs_mapping`.",
|
||||
"Codul BRUT de catalog (ex. `RAR_EROARE_SERVER`, `COD_NEMAPAT`) ramane disponibil DOAR pentru modal, derivat din `parse_erori(...)[0]['cod']` (app/errors.py) — nu se pune pe rand.",
|
||||
"Helperele sunt defensive la `rar_error` lipsa/JSON invalid (intorc sir gol, nu ridica).",
|
||||
"`prez`, `motiv`, `stare_*` raman neschimbate (aditiv/non-regresie).",
|
||||
"Teste in `tests/test_web_submissions.py`: `test_eticheta_umana_sub_pill` (R1 rename), eticheta prezenta pe error/needs_mapping, goala pe rand ok.",
|
||||
"`python3 -m pytest tests/test_web_submissions.py -q` trece."
|
||||
],
|
||||
"tags": ["backend"],
|
||||
"dependsOn": [],
|
||||
"requiresBrowserCheck": false,
|
||||
"requiresDesignReview": false,
|
||||
"passes": false,
|
||||
"failed": false,
|
||||
"blocked": false,
|
||||
"retries": 0,
|
||||
"failureReason": "",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-003",
|
||||
"title": "Component modal reutilizabil (overlay, focus-trap, a11y, inchidere pe succes)",
|
||||
"description": "Ca operator vreau ca detaliul sa apara intr-o fereastra modala clar delimitata, in afara zonei de poll, pentru ca sa nu mai confund marginea cu tabelul si sa nu mi se mai inchida formularul la refresh.",
|
||||
"priority": 15,
|
||||
"acceptanceCriteria": [
|
||||
"Container modal global `<div id=\"modal-detaliu\" role=\"dialog\" aria-modal=\"true\" hidden>` cu backdrop, plasat IN AFARA `#submissions-wrap` (poll-ul de 15s nu-l atinge). Corpul `#detaliu-modal-body` e tinta de swap pentru fragment + rutele corectie/mapare/lifecycle.",
|
||||
"Ancora globala in `_coada.html` in afara `#submissions-wrap`; `#trimitere-detaliu` global inert vechi din `_coada.html` este ELIMINAT (rol preluat de `#modal-detaliu`).",
|
||||
"Deschidere la `htmx:beforeRequest` de pe `tr.trimitere-row`: backdrop+dialog vizibile cu placeholder `Se incarca...` (`hx-indicator`), focus mutat in modal. Un singur modal o data.",
|
||||
"Inchidere prin buton `x`, `Esc`, click pe backdrop: `hidden`, corp golit, focus readus pe randul declansator.",
|
||||
"R7: focus-trap (Tab/Shift+Tab cicleaza in modal); `inert` SI `aria-hidden` pe ancestor STABIL `<main>` (NU pe `#submissions-wrap` care e swap target); scroll body blocat cat modalul e deschis; dialog cu `aria-labelledby` pe heading; buton `x` cu `aria-label`.",
|
||||
"R3: tot codul inline-expand din 5.8 (chevron, `tr.detaliu-rand`, `marcheazaDetaliuDeschis`, `inchideDetaliu` pe rand, toggle single-open) ELIMINAT/inlocuit de logica modalului; `window.inchideDetaliu(id)` ramane expus dar inchide modalul.",
|
||||
"R3 (EngF1): adauga in scope `tests/test_web_detaliu_inline.py` (rescrie/sterge) si `tests/test_acasa_trimiteri.py:71` (scoate assert `#trimitere-detaliu`). Niciun template/test nu mai refera `tr.detaliu-rand`/`#trimitere-detaliu`/`marcheazaDetaliuDeschis`/`aria-expanded` pe rand.",
|
||||
"R5: pe succes corectie SI sterge, modalul se inchide + listener pe `HX-Trigger: trimiteriChanged` reincarca lista; pe load-error al fragmentului stare `Nu s-a putut incarca [Reincearca]/[Inchide]` (HTMX `responseError`), nu placeholder blocat.",
|
||||
"Teste in `tests/test_web_modal.py`: `test_modal_container_in_afara_submissions_wrap`, `test_fragment_detaliu_tinteste_modalul`.",
|
||||
"`python3 -m pytest tests/test_web_modal.py -q` trece."
|
||||
],
|
||||
"tags": ["ui"],
|
||||
"dependsOn": [],
|
||||
"requiresBrowserCheck": true,
|
||||
"requiresDesignReview": true,
|
||||
"passes": false,
|
||||
"failed": false,
|
||||
"blocked": false,
|
||||
"retries": 0,
|
||||
"failureReason": "",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-006",
|
||||
"title": "Fundatie responsive — viewport, header, nav, modal full-screen mobil, breakpoint-uri",
|
||||
"description": "Ca utilizator pe telefon vreau ca shell-ul aplicatiei sa fie utilizabil pentru ca azi header-ul, tab-bar-ul si modalul nou nu au tratament sub 768px.",
|
||||
"priority": 20,
|
||||
"acceptanceCriteria": [
|
||||
"`<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">` prezent (confirmat/adaugat) in `base.html`.",
|
||||
"Modal full-screen pe mobil: sub 768px modalul ocupa tot ecranul (fara backdrop lateral), buton `x` mare (>=44px), scroll intern; pe desktop ramane centrat cu latime marginita.",
|
||||
"Header + nav: meniul de cont (un buton hamburger) si tab-bar-ul nu depasesc latimea pe 375px; tinte touch >=44px; fara scroll orizontal pe pagina.",
|
||||
"Breakpoint-uri consistente definite o data (ex. `--bp-mobil: 768px`) si reutilizate; fara regresie pe desktop (>=1024px arata identic).",
|
||||
"Teste in `tests/test_web_responsive.py`: `test_viewport_meta_prezent`, `test_modal_fullscreen_clasa_mobil`, `test_nav_colapsabil_sub_breakpoint`.",
|
||||
"`python3 -m pytest tests/test_web_responsive.py -q` trece."
|
||||
],
|
||||
"tags": ["ui"],
|
||||
"dependsOn": ["US-003"],
|
||||
"requiresBrowserCheck": true,
|
||||
"requiresDesignReview": true,
|
||||
"passes": false,
|
||||
"failed": false,
|
||||
"blocked": false,
|
||||
"retries": 0,
|
||||
"failureReason": "",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-002",
|
||||
"title": "Tabel trimiteri — eticheta umana sub stare, fara chevron, cod RAR simplu, rand declanseaza modalul",
|
||||
"description": "Ca operator vreau sa vad problema direct in tabel si un rand mai curat pentru ca sa stiu dintr-o privire ce rand are nevoie de mine.",
|
||||
"priority": 30,
|
||||
"acceptanceCriteria": [
|
||||
"R1 OVERRIDE: sub pill-ul de Stare apare ETICHETA UMANA scurta (din US-001, NU codul brut), text mic muted (`s-error` pe `error`/`needs_*`), DOAR cand e ne-goala. Stare transmisa prin text, nu doar culoare.",
|
||||
"R8: chevron ELIMINAT din coloana `#` (`_submissions.html:52-53`) si din CSS/JS asociat; regula touch `min-height/padding >=44px` pe rand INLOCUIESTE `@media pointer:coarse .chevron` (singura regula 44px); scoate logica toggle (re-click inchide) din `htmx:beforeRequest`.",
|
||||
"Coloana Operatie linia 2: doar codul RAR (ex. `OE-2`), FARA prefixul `cod RAR:` — un `<span>` muted/pill; cand nemapat afiseaza `nemapat` muted (comportament 5.8 pastrat).",
|
||||
"Randul `tr.trimitere-row` declanseaza deschiderea modalului (US-003): `hx-get /_fragments/trimitere/{id}` cu `hx-target` pe corpul modalului, NU pe rand-sibling. Randul-sibling `<tr class=\"detaliu-rand\">` din 5.8 este ELIMINAT.",
|
||||
"R8: rand `role=\"button\"`, `tabindex=\"0\"`, `aria-haspopup=\"dialog\"`, FARA `aria-expanded`; focusabil; Enter/Space deschid modalul; hover/focus pastrate ca afordanta; tinta touch >=44px.",
|
||||
"Teste in `tests/test_web_submissions.py`: `test_eticheta_umana_apare_sub_pill`, `test_fara_chevron_in_rand`, `test_cod_rar_fara_prefix_text`, `test_rand_deschide_modal`, plus test keyboard Enter/Space deschide modal + Esc readuce focus.",
|
||||
"`python3 -m pytest tests/test_web_submissions.py -q` trece.",
|
||||
"E2E (requiresBrowserCheck): gstack browser pe `/` — rand `error` arata eticheta umana sub pill; fara chevron; cod RAR fara `cod RAR:`; click pe rand deschide modalul (nu rand-sibling)."
|
||||
],
|
||||
"tags": ["ui"],
|
||||
"dependsOn": ["US-001", "US-003"],
|
||||
"requiresBrowserCheck": true,
|
||||
"requiresDesignReview": false,
|
||||
"passes": false,
|
||||
"failed": false,
|
||||
"blocked": false,
|
||||
"retries": 0,
|
||||
"failureReason": "",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-004",
|
||||
"title": "Detaliu editabil in-place + butoane consolidate (in modal), ordine verticala definita",
|
||||
"description": "Ca operator vreau sa corectez datele direct, o singura aparitie per camp, cu un buton clar pentru ca sa nu mai vad valorile de doua ori si sa nu mai ezit intre butoane.",
|
||||
"priority": 35,
|
||||
"acceptanceCriteria": [
|
||||
"Zero dublare: blocul read-only de grila (`_trimitere_detaliu.html:19-32`) si formularul separat de corectie se contopesc intr-un singur formular cu campuri editabile pre-completate. Campurile editabile (`nr_inmatriculare`, `vin`, `data_prestatie`, `odometru_final`, `odometru_initial`) apar O SINGURA DATA.",
|
||||
"Layout: `Numar inmatriculare` pe propriul rand (latime plina), `VIN (serie sasiu)` pe randul de DEDESUBT (latime plina). Restul campurilor pot ramane in grila.",
|
||||
"R10 ordine verticala modal: (1) header `#id`+pill+motiv uman; (2) bloc eroare blocanta cand exista; (3) mapare inline cand `nemapate_inline`; (4) formular editabil (Nr. rand propriu, VIN dedesubt, apoi grila), cu operatie+cod read-only deasupra campurilor; (5) actiuni (primar, apoi Sterge separat); (6) `<details>` `Detalii tehnice` colapsat (Nr. prezentare RAR, Cod HTTP, Reincercari, timestamps, mesaj RAR brut + cod brut). Context read-only NU precede formularul.",
|
||||
"Cod RAR simplu: nu mai exista campul etichetat `Cod RAR` separat (`:23`). R9: operatie+cod folosesc `prez.cod_rar` (fallback `nemapat`), NU `prez.cod`; afisate impreuna ca read-only (ex. `Operatie: Revizie · OE-2`), fara eticheta `Cod RAR`.",
|
||||
"R2 OVERRIDE (buton primar conditionat de stare): `error` -> buton `Re-pune in coada` care posteaza pe `/trimitere/{id}/repune` (neschimbat); `needs_data`/`needs_mapping` -> buton `Salveaza si retrimite` care posteaza pe `/trimitere/{id}/corecteaza` (neschimbat). UN SINGUR buton primar per stare (clasa primara `--accent` umplut). Butonul `Re-pune in coada` gol vechi (`:55-59`) eliminat.",
|
||||
"R11: UN SINGUR `Sterge` (outline distructiv, `var(--err)`/rosu) pe RAND SEPARAT (NU `margin-left:auto`), gap vizibil, full-width stivuit pe mobil; `hx-confirm` specific (`Stergi definitiv trimiterea #{id}? Nu se poate anula.`); posteaza pe `/trimitere/{id}/sterge` (neschimbat).",
|
||||
"R9: sectiunea de mapare inline (5.7, `:71-116`) ramane functional identica, mutata in modal, tintind corpul modalului; cand `nemapate_inline`: linia `Operatie: X · nemapat` urmata de picker; dupa mapare re-render arata codul rezolvat.",
|
||||
"R4: rescrie `<script>`-ul inline din `_trimitere_detaliu.html:161-176` pentru modal — FARA `marcheazaDetaliuDeschis`, FARA `scrollIntoView` pe randul de fundal; muta focusul in corpul modalului.",
|
||||
"R5: disable-on-submit pe toate cele 3 forme (corectie/sterge/mapare); pe succes (queued sau sterge) modalul se inchide + `trimiteriChanged`.",
|
||||
"Erorile de camp (`err_map`, `aria-invalid`, mesaj inline) si flash-ul de corectie raman. Comportamentul rutelor `/corecteaza` si `/repune` (validare, idempotency, re-rezolvare) NESCHIMBAT — test de regresie confirma acelasi rezultat ca azi.",
|
||||
"Teste in `tests/test_web_corectie.py`: `test_camp_apare_o_singura_data`, `test_nr_si_vin_pe_randuri_separate`, `test_un_singur_buton_primar_per_stare` (R2), `test_error_foloseste_repune` (R2), `test_sterge_prezent_si_distinct`, `test_corectie_pastreaza_comportament` (regresie).",
|
||||
"`python3 -m pytest tests/test_web_corectie.py -q` trece.",
|
||||
"E2E (requiresBrowserCheck): gstack browser — `needs_data` arata fiecare camp o data (nr. pe un rand, VIN dedesubt), corectez data, `Salveaza si retrimite` -> rand `queued`; `error` arata `Re-pune in coada`; `Sterge` clar separat si rosu."
|
||||
],
|
||||
"tags": ["ui"],
|
||||
"dependsOn": ["US-003"],
|
||||
"requiresBrowserCheck": true,
|
||||
"requiresDesignReview": true,
|
||||
"passes": false,
|
||||
"failed": false,
|
||||
"blocked": false,
|
||||
"retries": 0,
|
||||
"failureReason": "",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-007",
|
||||
"title": "Responsive — paginile de continut (Mapari, Cont, Nomenclator, Integrare, Jurnal, Admin)",
|
||||
"description": "Ca operator pe telefon vreau ca paginile de lucru sa fie lizibile pentru ca tabelele si formularele lor ies azi din ecran sub 768px.",
|
||||
"priority": 40,
|
||||
"acceptanceCriteria": [
|
||||
"R12 politica per-tabel: tabele actionabile (Mapari) = CARD per rand sub 768px (adauga `data-eticheta` la `<td>`-uri, model 5.8 `.tabel-trimiteri`); dense read-only (Jurnal, Nomenclator) = `.tablewrap` cu scroll orizontal CONTAINED (nu deborda pagina).",
|
||||
"R12: scopare `.tablewrap` sa NU atinga blocul `.tabel-trimiteri @media(max-width:767px)` din 5.8; test ca acele carduri 5.8 supravietuiesc.",
|
||||
"Formularele (Cont, Mapari add-row, filtre, Integrare) stiveaza campurile pe o coloana sub 768px, inputuri full-width, butoane >=44px.",
|
||||
"La 375px niciuna din pagini nu produce scroll orizontal de pagina (`scrollWidth <= clientWidth` pe `body`).",
|
||||
"Fara regresie la >=1024px (layout actual pastrat).",
|
||||
"Fisiere: `app/web/templates/_mapari.html`, `_cont.html`, `_nomenclator.html`, `_integrare.html`, `_jurnal.html`, `admin.html`, `base.html` (utilitar `.tablewrap`/card).",
|
||||
"Teste in `tests/test_web_responsive.py`: `test_tabele_continut_au_clasa_responsive`, `test_formulare_full_width_mobil`.",
|
||||
"`python3 -m pytest tests/test_web_responsive.py -q` trece.",
|
||||
"E2E (requiresBrowserCheck): gstack browser la 375px pe `/?tab=mapari`, `?tab=jurnal`, Cont/Nomenclator/Integrare si `/admin` — fiecare fara scroll orizontal, tabele lizibile, formulare pe o coloana."
|
||||
],
|
||||
"tags": ["ui"],
|
||||
"dependsOn": ["US-006"],
|
||||
"requiresBrowserCheck": true,
|
||||
"requiresDesignReview": false,
|
||||
"passes": false,
|
||||
"failed": false,
|
||||
"blocked": false,
|
||||
"retries": 0,
|
||||
"failureReason": "",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-008",
|
||||
"title": "Responsive — Acasa (upload, status, filtre) + login/signup",
|
||||
"description": "Ca utilizator pe telefon vreau ca prima pagina si autentificarea sa fie utilizabile pentru ca sunt punctele de intrare si azi nu au tratament mobil complet.",
|
||||
"priority": 45,
|
||||
"acceptanceCriteria": [
|
||||
"Zona de upload, bara de status si bara de filtre (`_coada.html:25-59`) stiveaza pe o coloana sub 768px; inputuri/butoane full-width >=44px.",
|
||||
"Tabelul de trimiteri pastreaza cardurile din 5.8 (verificat, nereparat daca deja ok).",
|
||||
"`login.html` si `signup.html`: card centrat care nu depaseste latimea pe 375px, inputuri full-width, fara scroll orizontal.",
|
||||
"Fara regresie la >=1024px.",
|
||||
"Fisiere: `app/web/templates/_acasa.html`, `_upload.html`, `_status.html`, `_coada.html` (filtre), `login.html`, `signup.html`.",
|
||||
"Teste in `tests/test_web_responsive.py`: `test_acasa_fara_scroll_orizontal_mobil`, `test_login_signup_full_width_mobil`.",
|
||||
"`python3 -m pytest tests/test_web_responsive.py -q` trece.",
|
||||
"E2E (requiresBrowserCheck): gstack browser la 375px pe `/`, `/login`, `/signup` — fara scroll orizontal; upload + filtre pe o coloana; carduri de trimiteri intacte."
|
||||
],
|
||||
"tags": ["ui"],
|
||||
"dependsOn": ["US-006"],
|
||||
"requiresBrowserCheck": true,
|
||||
"requiresDesignReview": false,
|
||||
"passes": false,
|
||||
"failed": false,
|
||||
"blocked": false,
|
||||
"retries": 0,
|
||||
"failureReason": "",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-005",
|
||||
"title": "Poll-ul nu mai inchide modalul si nu mai sterge bifele",
|
||||
"description": "Ca operator vreau ca selectia si modalul sa supravietuiasca refresh-ului de 15s pentru ca azi formularul se inchide singur si bifele se sterg dupa cateva secunde.",
|
||||
"priority": 50,
|
||||
"acceptanceCriteria": [
|
||||
"Modalul traieste in afara `#submissions-wrap` -> un swap de poll NU il mai atinge (rezolvat structural de US-003). Test: cu modalul deschis, un refresh al listei nu-l inchide.",
|
||||
"R6 OVERRIDE: poll-ul de 15s (`#submissions-wrap`, `_coada.html:62-65`) se pune pe pauza SCOPAT DOAR pe trigger periodic `every 15s` cat timp: (a) modalul e deschis SAU (b) exista cel putin un checkbox de bulk bifat. `trimiteriChanged` si submit-ul de filtru TREC MEREU (nu se blocheaza). Resume pe checkbox `change`->gol via delegare pe body.",
|
||||
"R6: evita blocajul permanent — daca randul bifat paraseste filtrul, pauza nu ramane lipita (pauza doar pe trigger periodic).",
|
||||
"La pauza nu se pierde nici scroll, nici focus, nici selectia de checkbox-uri.",
|
||||
"Logica veche de pauza pe `rand expandat` (5.8, `base.html:478-483`) inlocuita de aceasta.",
|
||||
"Teste in `tests/test_web_modal.py`: `test_poll_pauzat_cat_modal_deschis`, `test_poll_pauzat_cat_exista_bifa`, `test_trimiteriChanged_inca_reincarca_cu_bifa` (R6).",
|
||||
"`python3 -m pytest tests/test_web_modal.py -q` trece.",
|
||||
"E2E (requiresBrowserCheck): gstack browser — bifez 2 trimiteri, astept >15s: bifele raman; deschid modalul, astept >15s: modalul ramane deschis cu datele intacte."
|
||||
],
|
||||
"tags": ["ui"],
|
||||
"dependsOn": ["US-002", "US-003"],
|
||||
"requiresBrowserCheck": true,
|
||||
"requiresDesignReview": false,
|
||||
"passes": false,
|
||||
"failed": false,
|
||||
"blocked": false,
|
||||
"retries": 0,
|
||||
"failureReason": "",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
7
scripts/ralph/progress.txt
Normal file
7
scripts/ralph/progress.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# Ralph Progress Log
|
||||
Started: 2026-06-24
|
||||
Project: autopass-5.9-ux-corectie-modal-mobil
|
||||
Branch: ralph/5.9-ux-corectie-modal-mobil
|
||||
Source PRD: docs/prd/prd-5.9-ux-corectie-modal-mobil.md
|
||||
Note: PRD APROBAT 2026-06-24 cu revizii obligatorii R1-R12 (raport AUTOPLAN). R1-R12 au prioritate unde difera de AC original — sunt deja incorporate in acceptance criteria.
|
||||
---
|
||||
124
scripts/ralph/prompt.md
Normal file
124
scripts/ralph/prompt.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Ralph - Instrucțiuni pentru Iterație (smart gates)
|
||||
|
||||
Ești un agent autonom care implementează user stories dintr-un PRD. Aceasta este O SINGURĂ iterație — implementezi UN singur story, validezi prin gate-urile relevante, apoi te oprești.
|
||||
|
||||
## Workflow per iterație (4 faze, gates condiționale pe `story.tags`)
|
||||
|
||||
### Faza 0: Citește contextul
|
||||
|
||||
- PRD-ul (`prd.json`) și `progress.txt` sunt furnizate în context.
|
||||
- Identifică următorul story candidate:
|
||||
- `passes != true` ȘI `failed != true` ȘI `blocked != true`
|
||||
- DAG: toate ID-urile din `dependsOn[]` au `passes == true` (altfel sare la următorul independent)
|
||||
- Cea mai mică `priority` printre cele eligibile.
|
||||
- Notează `techStack.commands` (lint, typecheck, test, start) și `techStack.port`.
|
||||
- Notează `story.tags[]` — alegerea Faza 3 depinde de ele.
|
||||
|
||||
### Faza 1: IMPLEMENTARE (mereu)
|
||||
|
||||
1.1. **Branch management** — verifică să fii pe `branchName` din PRD; checkout/create dacă lipsește.
|
||||
1.2. **Citește acceptance criteria** — fiecare criteriu e un test mental concret de trecut.
|
||||
1.3. **Implementează cod minimal** — DOAR ce cere story-ul. Urmează patterns existente. Fără over-engineering, fără side features.
|
||||
1.4. **Update `notes`** în `prd.json` cu fișierele atinse (pentru audit ulterior).
|
||||
|
||||
### Faza 2: QUALITY BASE (mereu, înainte de gates)
|
||||
|
||||
Folosește `techStack.commands`:
|
||||
|
||||
```bash
|
||||
{techStack.commands.typecheck} # ex: npm run typecheck / mypy .
|
||||
{techStack.commands.lint} # ex: npm run lint / ruff check .
|
||||
{techStack.commands.test} # ex: npm test / pytest
|
||||
```
|
||||
|
||||
**Loop intern**: dacă vreuna eșuează → repară și repetă, max 3 retries în această fază. Dacă încă fail după 3, ieși cu sumar de erori în `progress.txt` (ralph.sh va decide retry-ul iterației).
|
||||
|
||||
### Faza 3: SMART GATES (dispatcher pe `story.tags`)
|
||||
|
||||
Tags posibile: `ui`, `db`, `vercel`, `refactor`, `docs`, `backend`, `infra`.
|
||||
|
||||
Aplică DOAR gate-urile potrivite — **nu rulează toate**:
|
||||
|
||||
| Tag | Gate |
|
||||
|-------------|---------------------------------------------------------------------------------------|
|
||||
| `refactor` | `/workflow:simplify` pe diff (reduce complexity fără behavior change) |
|
||||
| `ui` | `/qa` Playwright/agent-browser snapshot pe `localhost:{techStack.port}` + screenshot |
|
||||
| `ui` + `requiresDesignReview` | `/plan-design-review` pe screenshot capturat |
|
||||
| `vercel` | push branch + `gh pr checks --watch` (timeout 5 min); fail dacă PR checks eșuează |
|
||||
| `db` | verify schema diff (alembic / prisma migrate diff / `psql \\d+ tablename`) |
|
||||
| `docs` | doar typecheck base (Faza 2 e suficient); skip gate dedicat |
|
||||
| `backend` | `/review` pe diff (intern — second pass review pe API contracts, error handling) |
|
||||
| `infra` | `/review` pe diff + manual smoke test al modificărilor (CI config, Dockerfile, etc.) |
|
||||
| _(tags vide)_ | **run-all-gates fallback** — `/review` + `/qa` + `/workflow:simplify` (safe default) |
|
||||
|
||||
**Mecanism**: skill-urile gstack se invocă prin text mention în prompt — Claude (subprocess `claude -p`) le vede ca tool-uri disponibile via `~/.claude/skills/gstack/`.
|
||||
|
||||
**Multi-tag**: rulează gate-uri pentru fiecare tag (ex: `["ui", "backend"]` → atât `/qa` cât și `/review`).
|
||||
|
||||
**Important**: dacă vreun gate eșuează, NU marca `passes=true`. Repară (max 3 fix-uri în iterație) sau lasă pentru iterația următoare (ralph.sh se ocupă de retry counter).
|
||||
|
||||
### Faza 4: COMMIT + MARK
|
||||
|
||||
4.1. **Commit** cu mesaj descriptiv:
|
||||
```
|
||||
feat: [Story ID] - [Story Title]
|
||||
|
||||
- ce ai schimbat (1-3 bullets)
|
||||
- gates rulate: typecheck PASS, lint PASS, /qa PASS
|
||||
```
|
||||
|
||||
4.2. **Update `prd.json`**:
|
||||
- `passes: true` DOAR DACĂ toate gate-urile relevante au pasat
|
||||
- `notes` populat cu rezultate gate (ex: "qa: ok, design-review: 8/10")
|
||||
|
||||
4.3. **Append `progress.txt`**:
|
||||
```markdown
|
||||
## Iterație: [timestamp]
|
||||
### Story implementat: [ID] - [Title] (tags: [ui, backend])
|
||||
### Status: Complete / Partial / Failed
|
||||
|
||||
### Gates rulate:
|
||||
- Typecheck: PASS
|
||||
- Lint: PASS
|
||||
- Tests: PASS/SKIP
|
||||
- /qa (ui): PASS — screenshot la scripts/ralph/screenshots/...
|
||||
- /review (backend): PASS
|
||||
|
||||
### Learnings:
|
||||
- [Patterns descoperite, gotchas]
|
||||
|
||||
### Next:
|
||||
- [Stories eligibile pentru iterația următoare]
|
||||
---
|
||||
```
|
||||
|
||||
## Reguli importante
|
||||
|
||||
1. **UN SINGUR STORY PE ITERAȚIE** — nu implementa mai mult de un story.
|
||||
2. **DAG STRICT** — nu sări peste `dependsOn` neîmplinite.
|
||||
3. **GATES PE TAGS** — rulează doar ce e relevant; tags vide = run-all-gates fallback.
|
||||
4. **NU MARCA `passes=true` cu gate failed** — altfel ralph.sh nu va relua story-ul.
|
||||
5. **FII CONCIS** — fără over-engineering, fără docs auto-generate dacă story-ul nu cere.
|
||||
|
||||
## Comenzi agent-browser (referință rapidă pentru gate `ui`)
|
||||
|
||||
```bash
|
||||
agent-browser navigate "http://localhost:{techStack.port}"
|
||||
agent-browser snapshot # listă elemente compactă (@e1, @e2...)
|
||||
agent-browser click @e5
|
||||
agent-browser fill @e3 "value"
|
||||
agent-browser screenshot ./scripts/ralph/screenshots/US-{id}-$(date +%Y%m%d-%H%M%S).png
|
||||
agent-browser console # erori JS
|
||||
agent-browser wait-for "Loading complete"
|
||||
```
|
||||
|
||||
## Condiție de terminare
|
||||
|
||||
Dacă TOATE story-urile au `passes: true` (sau combinat cu `failed: true` / `blocked: true` astfel că nimic nu mai e eligibil):
|
||||
|
||||
```
|
||||
<promise>COMPLETE</promise>
|
||||
```
|
||||
|
||||
---
|
||||
ÎNCEPE IMPLEMENTAREA ACUM.
|
||||
387
scripts/ralph/ralph.sh
Executable file
387
scripts/ralph/ralph.sh
Executable file
@@ -0,0 +1,387 @@
|
||||
#!/bin/bash
|
||||
# Ralph pentru Claude Code - Loop autonom de agent AI (W3: smart gates + DAG + rate limit)
|
||||
# Adaptat din Ralph original (snarktank/ralph) pentru Claude Code CLI
|
||||
# Usage: ./ralph.sh [max_iterations] [project_dir]
|
||||
#
|
||||
# Env vars (opționale):
|
||||
# RALPH_MAX_TURNS — --max-turns per iter (default 30)
|
||||
# RALPH_RATE_LIMIT_SLEEP — sleep după rate limit detection (default 1800 = 30min)
|
||||
# RALPH_DAG_HELPER — path la tools/ralph_dag.py (auto-detect default)
|
||||
# RALPH_PYTHON — interpreter Python pentru DAG helper (default python3)
|
||||
|
||||
set -e
|
||||
|
||||
MAX_ITERATIONS=${1:-10}
|
||||
PROJECT_DIR=${2:-$(pwd)}
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PRD_FILE="$SCRIPT_DIR/prd.json"
|
||||
PROGRESS_FILE="$SCRIPT_DIR/progress.txt"
|
||||
ARCHIVE_DIR="$SCRIPT_DIR/archive"
|
||||
SCREENSHOTS_DIR="$SCRIPT_DIR/screenshots"
|
||||
LAST_BRANCH_FILE="$SCRIPT_DIR/.last-branch"
|
||||
PROMPT_FILE="$SCRIPT_DIR/prompt.md"
|
||||
|
||||
# W3 config
|
||||
MAX_TURNS=${RALPH_MAX_TURNS:-30}
|
||||
RATE_LIMIT_SLEEP=${RALPH_RATE_LIMIT_SLEEP:-1800}
|
||||
RALPH_PYTHON=${RALPH_PYTHON:-python3}
|
||||
|
||||
# DAG helper auto-detect: prefer co-located cu echo-core; fallback la $SCRIPT_DIR
|
||||
if [ -n "$RALPH_DAG_HELPER" ] && [ -f "$RALPH_DAG_HELPER" ]; then
|
||||
DAG_HELPER="$RALPH_DAG_HELPER"
|
||||
elif [ -f "/home/moltbot/echo-core/tools/ralph_dag.py" ]; then
|
||||
DAG_HELPER="/home/moltbot/echo-core/tools/ralph_dag.py"
|
||||
elif [ -f "/home/moltbot/echo-core-qc/tools/ralph_dag.py" ]; then
|
||||
DAG_HELPER="/home/moltbot/echo-core-qc/tools/ralph_dag.py"
|
||||
elif [ -f "$SCRIPT_DIR/ralph_dag.py" ]; then
|
||||
DAG_HELPER="$SCRIPT_DIR/ralph_dag.py"
|
||||
else
|
||||
DAG_HELPER=""
|
||||
fi
|
||||
|
||||
# Usage helper auto-detect (rate limit budget tracking — best effort, niciodată
|
||||
# blochează rularea Ralph dacă lipsește)
|
||||
if [ -n "$RALPH_USAGE_HELPER" ] && [ -f "$RALPH_USAGE_HELPER" ]; then
|
||||
USAGE_HELPER="$RALPH_USAGE_HELPER"
|
||||
elif [ -f "/home/moltbot/echo-core/tools/ralph_usage.py" ]; then
|
||||
USAGE_HELPER="/home/moltbot/echo-core/tools/ralph_usage.py"
|
||||
elif [ -f "/home/moltbot/echo-core-instr/tools/ralph_usage.py" ]; then
|
||||
USAGE_HELPER="/home/moltbot/echo-core-instr/tools/ralph_usage.py"
|
||||
elif [ -f "$SCRIPT_DIR/ralph_usage.py" ]; then
|
||||
USAGE_HELPER="$SCRIPT_DIR/ralph_usage.py"
|
||||
else
|
||||
USAGE_HELPER=""
|
||||
fi
|
||||
USAGE_FILE="$SCRIPT_DIR/usage.jsonl"
|
||||
|
||||
# Verifică că jq este instalat
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Eroare: jq nu este instalat. Rulează: apt install jq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verifică că claude este instalat
|
||||
if ! command -v claude &> /dev/null; then
|
||||
echo "Eroare: Claude Code CLI nu este instalat."
|
||||
echo "Instalează cu: npm install -g @anthropic-ai/claude-code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verifică agent-browser (opțional, pentru verificări UI)
|
||||
if ! command -v agent-browser &> /dev/null; then
|
||||
echo "Notă: agent-browser nu este instalat."
|
||||
echo "Pentru verificări vizuale UI, instalează cu: npm install -g agent-browser && agent-browser install"
|
||||
echo "Continuăm fără verificări browser..."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Verifică existența fișierelor necesare
|
||||
if [ ! -f "$PRD_FILE" ]; then
|
||||
echo "Eroare: prd.json nu există în $SCRIPT_DIR"
|
||||
echo "Generează mai întâi un PRD folosind skill-ul /prd și apoi /ralph"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PROMPT_FILE" ]; then
|
||||
echo "Eroare: prompt.md nu există în $SCRIPT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Arhivare rulare anterioară dacă branch-ul s-a schimbat
|
||||
if [ -f "$PRD_FILE" ] && [ -f "$LAST_BRANCH_FILE" ]; then
|
||||
CURRENT_BRANCH=$(jq -r '.branchName // empty' "$PRD_FILE" 2>/dev/null || echo "")
|
||||
LAST_BRANCH=$(cat "$LAST_BRANCH_FILE" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$CURRENT_BRANCH" ] && [ -n "$LAST_BRANCH" ] && [ "$CURRENT_BRANCH" != "$LAST_BRANCH" ]; then
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
FOLDER_NAME=$(echo "$LAST_BRANCH" | sed 's|^ralph/||')
|
||||
ARCHIVE_FOLDER="$ARCHIVE_DIR/$DATE-$FOLDER_NAME"
|
||||
|
||||
echo "Arhivare rulare anterioară: $LAST_BRANCH"
|
||||
mkdir -p "$ARCHIVE_FOLDER"
|
||||
[ -f "$PRD_FILE" ] && cp "$PRD_FILE" "$ARCHIVE_FOLDER/"
|
||||
[ -f "$PROGRESS_FILE" ] && cp "$PROGRESS_FILE" "$ARCHIVE_FOLDER/"
|
||||
echo " Arhivat în: $ARCHIVE_FOLDER"
|
||||
|
||||
# Reset progress file
|
||||
echo "# Ralph Progress Log" > "$PROGRESS_FILE"
|
||||
echo "Started: $(date)" >> "$PROGRESS_FILE"
|
||||
echo "Branch: $CURRENT_BRANCH" >> "$PROGRESS_FILE"
|
||||
echo "---" >> "$PROGRESS_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Salvează branch-ul curent
|
||||
if [ -f "$PRD_FILE" ]; then
|
||||
CURRENT_BRANCH=$(jq -r '.branchName // empty' "$PRD_FILE" 2>/dev/null || echo "")
|
||||
if [ -n "$CURRENT_BRANCH" ]; then
|
||||
echo "$CURRENT_BRANCH" > "$LAST_BRANCH_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Creează directoare necesare
|
||||
mkdir -p "$SCRIPT_DIR/logs" "$SCRIPT_DIR/archive" "$SCRIPT_DIR/screenshots"
|
||||
|
||||
# Creează .gitignore dacă nu există
|
||||
if [ ! -f "$PROJECT_DIR/.gitignore" ]; then
|
||||
cat > "$PROJECT_DIR/.gitignore" << 'GITIGNORE'
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
|
||||
# Virtual environment
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Ralph runtime
|
||||
scripts/ralph/.ralph.pid
|
||||
scripts/ralph/.last-branch
|
||||
scripts/ralph/logs/
|
||||
scripts/ralph/screenshots/
|
||||
scripts/ralph/archive/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
GITIGNORE
|
||||
echo "Created .gitignore"
|
||||
fi
|
||||
|
||||
# Inițializare progress file dacă nu există
|
||||
if [ ! -f "$PROGRESS_FILE" ]; then
|
||||
echo "# Ralph Progress Log" > "$PROGRESS_FILE"
|
||||
echo "Started: $(date)" >> "$PROGRESS_FILE"
|
||||
echo "---" >> "$PROGRESS_FILE"
|
||||
fi
|
||||
|
||||
# Funcție pentru a verifica dacă toate story-urile sunt complete
|
||||
check_all_complete() {
|
||||
local incomplete=$(jq '[.userStories[] | select(.passes != true)] | length' "$PRD_FILE" 2>/dev/null || echo "999")
|
||||
[ "$incomplete" -eq 0 ]
|
||||
}
|
||||
|
||||
# W3: nimic eligibil = toate sunt fie passes, fie failed, fie blocked
|
||||
check_no_eligible() {
|
||||
local n=$(jq '[.userStories[] | select(.passes != true and .failed != true and .blocked != true)] | length' "$PRD_FILE" 2>/dev/null || echo "999")
|
||||
[ "$n" -eq 0 ]
|
||||
}
|
||||
|
||||
# W3: alege next eligible story via DAG helper. Print story ID sau "" dacă nimic.
|
||||
dag_next_story() {
|
||||
if [ -n "$DAG_HELPER" ]; then
|
||||
"$RALPH_PYTHON" "$DAG_HELPER" next-story "$PRD_FILE" 2>/dev/null || echo ""
|
||||
else
|
||||
# Fallback simplu (fără DAG): primul story cu passes!=true && failed!=true && blocked!=true, priority asc
|
||||
jq -r '[.userStories[] | select(.passes != true and .failed != true and .blocked != true)] | sort_by(.priority) | .[0].id // ""' "$PRD_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
dag_incr_retry() {
|
||||
local sid="$1"
|
||||
if [ -n "$DAG_HELPER" ]; then
|
||||
"$RALPH_PYTHON" "$DAG_HELPER" incr-retry "$PRD_FILE" "$sid" 2>/dev/null || echo "0"
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
dag_mark_failed() {
|
||||
local sid="$1" reason="$2"
|
||||
if [ -n "$DAG_HELPER" ]; then
|
||||
"$RALPH_PYTHON" "$DAG_HELPER" mark-failed "$PRD_FILE" "$sid" "$reason" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
dag_force_tags() {
|
||||
local sid="$1"
|
||||
if [ -n "$DAG_HELPER" ]; then
|
||||
"$RALPH_PYTHON" "$DAG_HELPER" force-tags "$PRD_FILE" "$sid" "$PROJECT_DIR" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# W3: detectează rate limit în output Claude (heuristic — Anthropic nu are exit code dedicat)
|
||||
is_rate_limited() {
|
||||
local output="$1"
|
||||
echo "$output" | grep -qiE "rate limit|rate_limit_exceeded|429|too many requests"
|
||||
}
|
||||
|
||||
# Afișare status inițial
|
||||
echo ""
|
||||
echo "======================================================================="
|
||||
echo " RALPH pentru Claude Code - Agent Autonom "
|
||||
echo "======================================================================="
|
||||
PROJECT_NAME=$(jq -r '.projectName // "Unknown"' "$PRD_FILE")
|
||||
BRANCH_NAME=$(jq -r '.branchName // "N/A"' "$PRD_FILE")
|
||||
TOTAL_STORIES=$(jq '.userStories | length' "$PRD_FILE")
|
||||
COMPLETE_STORIES=$(jq '[.userStories[] | select(.passes == true)] | length' "$PRD_FILE")
|
||||
echo " Proiect: $PROJECT_NAME"
|
||||
echo " Branch: $BRANCH_NAME"
|
||||
echo " Stories: $COMPLETE_STORIES / $TOTAL_STORIES complete"
|
||||
echo " Max iterații: $MAX_ITERATIONS"
|
||||
echo " Screenshots: $SCREENSHOTS_DIR"
|
||||
echo "======================================================================="
|
||||
echo ""
|
||||
|
||||
# Verificare rapidă - poate toate sunt deja complete?
|
||||
if check_all_complete; then
|
||||
echo "Toate story-urile sunt deja complete!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Tracker pentru rate limit retry (max 1 retry de iterație-rate-limit per rulare)
|
||||
RATE_LIMIT_RETRY_USED=0
|
||||
|
||||
# Loop principal
|
||||
for i in $(seq 1 $MAX_ITERATIONS); do
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " Ralph Iterația $i din $MAX_ITERATIONS"
|
||||
echo "==================================================================="
|
||||
|
||||
# W3: alege next story via DAG (propagă blocked dacă vreun dep a eșuat)
|
||||
CURRENT_STORY=$(dag_next_story)
|
||||
if [ -z "$CURRENT_STORY" ]; then
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
if check_all_complete; then
|
||||
echo " TOATE STORY-URILE DIN PRD SUNT COMPLETE!"
|
||||
exit 0
|
||||
else
|
||||
echo " NICIUN STORY ELIGIBIL (toate fie complete, fie failed, fie blocked)"
|
||||
echo " Stories incomplete:"
|
||||
jq -r '.userStories[] | select(.passes != true) | " - \(.id): \(.title) [failed=\(.failed // false) blocked=\(.blocked // false) retries=\(.retries // 0)]"' "$PRD_FILE"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Status curent
|
||||
COMPLETE_NOW=$(jq '[.userStories[] | select(.passes == true)] | length' "$PRD_FILE")
|
||||
NEXT_TITLE=$(jq -r --arg id "$CURRENT_STORY" '.userStories[] | select(.id == $id) | "\(.id): \(.title)"' "$PRD_FILE")
|
||||
STORY_TAGS=$(jq -r --arg id "$CURRENT_STORY" '.userStories[] | select(.id == $id) | (.tags // []) | join(",")' "$PRD_FILE")
|
||||
STORY_RETRIES=$(jq -r --arg id "$CURRENT_STORY" '.userStories[] | select(.id == $id) | (.retries // 0)' "$PRD_FILE")
|
||||
echo " Progress: $COMPLETE_NOW / $TOTAL_STORIES stories complete"
|
||||
echo " Next: $NEXT_TITLE [tags: ${STORY_TAGS:-<none>}, retries: $STORY_RETRIES]"
|
||||
echo ""
|
||||
|
||||
# Pregătește prompt-ul cu context
|
||||
FULL_PROMPT=$(cat <<EOF
|
||||
# Context pentru această iterație Ralph
|
||||
|
||||
## Story țintă (DAG-eligible):
|
||||
$CURRENT_STORY (tags: ${STORY_TAGS:-<none>})
|
||||
|
||||
## PRD (prd.json):
|
||||
$(cat "$PRD_FILE")
|
||||
|
||||
## Progress până acum (progress.txt):
|
||||
$(cat "$PROGRESS_FILE")
|
||||
|
||||
## Instrucțiuni pentru această iterație:
|
||||
$(cat "$PROMPT_FILE")
|
||||
EOF
|
||||
)
|
||||
|
||||
# Execută Claude Code în modul non-interactiv
|
||||
LOG_FILE="$SCRIPT_DIR/logs/iteration-$i-$(date +%Y%m%d-%H%M%S).log"
|
||||
mkdir -p "$SCRIPT_DIR/logs"
|
||||
|
||||
# --output-format json + --max-turns pentru control runtime
|
||||
set +e
|
||||
echo "$FULL_PROMPT" | claude -p \
|
||||
--dangerously-skip-permissions \
|
||||
--output-format json \
|
||||
--max-turns "$MAX_TURNS" \
|
||||
2>&1 | tee "$LOG_FILE"
|
||||
CLAUDE_EXIT=${PIPESTATUS[1]}
|
||||
set -e
|
||||
OUTPUT=$(cat "$LOG_FILE")
|
||||
|
||||
# Rate limit budget tracking (best-effort, never blocks Ralph)
|
||||
if [ -n "$USAGE_HELPER" ]; then
|
||||
"$RALPH_PYTHON" "$USAGE_HELPER" append \
|
||||
"$USAGE_FILE" "$LOG_FILE" \
|
||||
--slug "$PROJECT_NAME" \
|
||||
--story-id "$CURRENT_STORY" \
|
||||
--iter "$i" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# W3: rate limit detection (max 1 retry per rulare)
|
||||
if is_rate_limited "$OUTPUT" || [ "$CLAUDE_EXIT" = "29" ]; then
|
||||
if [ "$RATE_LIMIT_RETRY_USED" = "0" ]; then
|
||||
echo ""
|
||||
echo " ⏸️ Rate limit detectat. Sleep ${RATE_LIMIT_SLEEP}s, apoi retry o dată."
|
||||
RATE_LIMIT_RETRY_USED=1
|
||||
echo "## Rate limit la iter $i — sleep $RATE_LIMIT_SLEEP" >> "$PROGRESS_FILE"
|
||||
sleep "$RATE_LIMIT_SLEEP"
|
||||
continue # retry aceeași iterație
|
||||
else
|
||||
echo " ❌ Rate limit din nou — abort run, mark $CURRENT_STORY rate_limited"
|
||||
dag_mark_failed "$CURRENT_STORY" "rate_limited"
|
||||
echo "## Rate limit final la iter $i — abort" >> "$PROGRESS_FILE"
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verifică dacă toate task-urile sunt complete
|
||||
if echo "$OUTPUT" | grep -q "<promise>COMPLETE</promise>"; then
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " RALPH A TERMINAT TOATE TASK-URILE!"
|
||||
echo " Completat la iterația $i din $MAX_ITERATIONS"
|
||||
echo "==================================================================="
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Verifică și prin prd.json
|
||||
if check_all_complete; then
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " TOATE STORY-URILE DIN PRD SUNT COMPLETE!"
|
||||
echo "==================================================================="
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# W3: tag validation post-iter — chiar dacă Opus a marcat docs, dacă diff atinge .vue/.tsx, force ui
|
||||
dag_force_tags "$CURRENT_STORY" >/dev/null 2>&1 || true
|
||||
|
||||
# W3: dacă story-ul curent ÎNCĂ nu trece (passes==false), incrementăm retries
|
||||
STILL_INCOMPLETE=$(jq -r --arg id "$CURRENT_STORY" '.userStories[] | select(.id == $id) | (.passes == true)' "$PRD_FILE")
|
||||
if [ "$STILL_INCOMPLETE" != "true" ]; then
|
||||
NEW_RETRY=$(dag_incr_retry "$CURRENT_STORY")
|
||||
echo " Story $CURRENT_STORY încă incomplet. Retries: $NEW_RETRY/3"
|
||||
if [ "$NEW_RETRY" -ge 3 ] 2>/dev/null; then
|
||||
echo " ❌ $CURRENT_STORY failed: max_retries — sare la următorul"
|
||||
# mark-failed e deja făcut de incr-retry când >=3, dar idempotent o re-aplicăm
|
||||
dag_mark_failed "$CURRENT_STORY" "max_retries"
|
||||
fi
|
||||
else
|
||||
echo " ✅ Story $CURRENT_STORY marcat passes=true în iterația asta."
|
||||
fi
|
||||
|
||||
echo " Iterația $i completă. Continuăm..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " Ralph a atins limita de iterații ($MAX_ITERATIONS)"
|
||||
echo " Verifică progress.txt pentru status."
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
# Afișează stories incomplete
|
||||
echo "Stories incomplete:"
|
||||
jq -r '.userStories[] | select(.passes != true) | " - \(.id): \(.title)"' "$PRD_FILE"
|
||||
|
||||
exit 1
|
||||
Reference in New Issue
Block a user