Compare commits
2 Commits
4b494eb2f2
...
dedeedf024
| Author | SHA1 | Date | |
|---|---|---|---|
| dedeedf024 | |||
| bf9380f2ad |
64
CLAUDE.md
64
CLAUDE.md
@@ -145,25 +145,41 @@ 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 !approve
|
||||
Marius → !approve <slug> (Discord/Telegram/WhatsApp → router.py → approved-tasks.json status: approved)
|
||||
23:00 night-execute → citește approved, clonează repo dacă lipsește, generează PRD, lansează ralph.sh
|
||||
actualizează approved-tasks.json (status: running, pid: PID)
|
||||
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/ralph.html (polling 5s) — cards per proiect cu status, iter, ETA, log, stop
|
||||
```
|
||||
|
||||
**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 |
|
||||
| `/l` | Status toate proiectele (PID, stories done/total) |
|
||||
| `/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 |
|
||||
|
||||
Pe **Discord** sunt slash commands native cu autocomplete dinamic: `/a <tab>` listează proiectele pending, `/k <tab>` listează proiectele running. Pe **Telegram** apar în meniul `/` cu descriere. Pe **WhatsApp** sunt parsate ca text plain.
|
||||
**UX interactiv** (Discord/Telegram):
|
||||
- `/l` deschide `RalphRootView` (Discord) / InlineKeyboardMarkup (Telegram) cu butoane per workspace project
|
||||
- Click pe proiect → submeniu cu acțiuni: ➕ 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.
|
||||
|
||||
**Aliasuri legacy** (funcționează încă pentru backwards compat): `!propose`, `!approve`, `!status`, `!stop`.
|
||||
|
||||
@@ -171,16 +187,25 @@ Pe **Discord** sunt slash commands native cu autocomplete dinamic: `/a <tab>` li
|
||||
|
||||
| Path | Rol |
|
||||
|------|-----|
|
||||
| `approved-tasks.json` | Coordonare între cele 3 cron jobs. Schema: `{name, description, status, proposed_at, approved_at, started_at, pid}` |
|
||||
| `tools/ralph/ralph.sh` | Bash loop: N iterații × `claude` CLI per story din prd.json |
|
||||
| `tools/ralph/prompt.md` | Instrucțiuni Claude Code per iterație Ralph |
|
||||
| `tools/ralph/prd-template.json` | Template pentru prd.json generat de Opus |
|
||||
| `tools/ralph_prd_generator.py` | Generează PRD + prd.json cu model Opus |
|
||||
| `~/workspace/<name>/scripts/ralph/prd.json` | PRD per proiect — user stories cu câmp `passes` |
|
||||
| `approved-tasks.json` | Coordonare între cron jobs + UX. Schema: `{name, description, status, planning_session_id, final_plan_path, 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` cu 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; coordinează prin disk artifacts gstack convention; 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 Python (testabile): `infer_tags_from_paths`, `force_include_tags`, `topological_eligible`, `mark_failed`, blocked propagation iterativă. CLI subcommands chemate din ralph.sh (`infer-tags`, `next-story`, `mark-failed`, `incr-retry`) |
|
||||
| `~/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` |
|
||||
| `dashboard/ralph.html` | UI live cards, polling 5s, status badges, ETA, butoane log/prd/stop |
|
||||
| `dashboard/.env` | `GITEA_TOKEN` pentru clone HTTPS la `gitea.romfast.ro` |
|
||||
|
||||
**Status flow:** `pending` → `approved` → `running` → `complete` / `failed` / `stopped`
|
||||
**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
|
||||
|
||||
@@ -211,9 +236,16 @@ Import-uri absolute via `sys.path.insert(0, PROJECT_ROOT)`: `from src.config imp
|
||||
| `dashboard/constants.py` | Path-uri centralizate + config Gitea pentru dashboard |
|
||||
| `dashboard/echo-taskboard.service` | Template systemd user unit |
|
||||
| `cron/jobs.json` | Job-uri APScheduler (schemă plată, Europe/Bucharest) |
|
||||
| `approved-tasks.json` | Fișier coordonare Ralph — status proiecte autonome |
|
||||
| `approved-tasks.json` | Fișier coordonare Ralph — status proiecte autonome (extins cu `planning_session_id`, `final_plan_path`) |
|
||||
| `tasks/lessons.md` | Lecții capturate din corectările lui Marius (citit la session start) |
|
||||
| `tools/ralph/ralph.sh` | Bash loop Ralph (N iter × claude CLI per story) |
|
||||
| `tasks/spike-planning-findings.md` | Validare empirică Spike Step 0 (subprocess `claude -p` + skills gstack + `--resume` round-trip) |
|
||||
| `prompts/planning_agent.md` | System prompt pentru planning agent multi-fază (W2) |
|
||||
| `src/ralph_flow.py` | State per `(adapter, chat, user)` pentru UX flow (TTL 10min) |
|
||||
| `src/planning_session.py` | Wrapper Claude subprocess pentru planning agent |
|
||||
| `src/planning_orchestrator.py` | Orchestrare faze gstack skills (W2) |
|
||||
| `src/adapters/discord_views.py` | Discord Views/Modal pentru UX interactiv (W1) |
|
||||
| `tools/ralph/ralph.sh` | Bash loop DAG-aware (W3): N iter × claude CLI per story |
|
||||
| `tools/ralph_dag.py` | DAG helpers + CLI (W3) |
|
||||
| `tools/ralph_prd_generator.py` | Generează PRD + prd.json cu Opus |
|
||||
|
||||
## gstack
|
||||
|
||||
@@ -1007,8 +1007,8 @@ def create_bot(config: Config) -> discord.Client:
|
||||
slug: str,
|
||||
description: str | None = None,
|
||||
) -> None:
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
# Resolve description: explicit param wins, else look up in approved-tasks.
|
||||
# Done BEFORE defer so we can fall back to a modal if missing.
|
||||
desc = (description or "").strip()
|
||||
if not desc:
|
||||
try:
|
||||
@@ -1020,13 +1020,12 @@ def create_bot(config: Config) -> discord.Client:
|
||||
except Exception:
|
||||
logger.exception("approved-tasks lookup failed")
|
||||
if not desc:
|
||||
await interaction.followup.send(
|
||||
f"Nu am descriere pentru `{slug}`. Adaugă cu `/p {slug} <descriere>` "
|
||||
"sau pasează `description` la `/plan`.",
|
||||
ephemeral=True,
|
||||
)
|
||||
# No description anywhere — ask via modal and start planning on submit.
|
||||
from src.adapters.discord_views import RalphPlanModal
|
||||
await interaction.response.send_modal(RalphPlanModal(slug))
|
||||
return
|
||||
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
channel_id = str(interaction.channel_id)
|
||||
await interaction.followup.send(
|
||||
f"🧠 Pornesc planning pentru `{slug}`… (durează ~60s)", ephemeral=True
|
||||
|
||||
@@ -116,6 +116,46 @@ class RalphProposeModal(discord.ui.Modal, title="Propune feature Ralph"):
|
||||
await interaction.response.send_message(result, ephemeral=True)
|
||||
|
||||
|
||||
class RalphPlanModal(discord.ui.Modal, title="Planifică Ralph"):
|
||||
"""Modal asking for a description and starting a planning session immediately."""
|
||||
|
||||
description: discord.ui.TextInput = discord.ui.TextInput(
|
||||
label="Descriere",
|
||||
placeholder="Ce vrei să planificăm? (1-3 propoziții)",
|
||||
style=discord.TextStyle.paragraph,
|
||||
required=True,
|
||||
max_length=1000,
|
||||
)
|
||||
|
||||
def __init__(self, slug: str) -> None:
|
||||
super().__init__(timeout=VIEW_TIMEOUT)
|
||||
self.slug = slug
|
||||
self.title = f"Planifică: {slug}"[:45]
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||
from src.router import start_planning_session
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
description = str(self.description.value).strip()
|
||||
await interaction.followup.send(
|
||||
f"🧠 Pornesc planning pentru `{self.slug}`… (durează ~60s)",
|
||||
ephemeral=True,
|
||||
)
|
||||
channel_id = str(interaction.channel_id)
|
||||
try:
|
||||
first = start_planning_session(self.slug, description, channel_id, "discord")
|
||||
except Exception as e:
|
||||
log.exception("start_planning_session via modal failed for %s", self.slug)
|
||||
await interaction.followup.send(f"Planning blocat: {e}", ephemeral=True)
|
||||
return
|
||||
for chunk in [first[i:i + 1900] for i in range(0, len(first), 1900)] or [first]:
|
||||
await interaction.followup.send(chunk, ephemeral=True)
|
||||
await interaction.followup.send(
|
||||
"Răspunde aici. Apasă **Continuă faza** când ești gata să trec la următoarea.",
|
||||
view=PlanningActiveView(),
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RalphProjectView — per-project action buttons
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -175,7 +215,6 @@ class RalphProjectView(discord.ui.View):
|
||||
|
||||
@discord.ui.button(label="🧠 Planifică", style=discord.ButtonStyle.primary, row=2)
|
||||
async def plan(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
# Look up description from approved-tasks.json
|
||||
description = ""
|
||||
try:
|
||||
@@ -187,12 +226,11 @@ class RalphProjectView(discord.ui.View):
|
||||
except Exception:
|
||||
log.exception("approved-tasks lookup failed")
|
||||
if not description:
|
||||
await interaction.followup.send(
|
||||
f"Nu am descriere pentru `{self.slug}`. "
|
||||
f"Adaugă mai întâi cu `/p {self.slug} <descriere>`.",
|
||||
ephemeral=True,
|
||||
)
|
||||
# No description yet — ask for one and start planning on submit.
|
||||
# Modal must be sent BEFORE response.defer (Discord constraint).
|
||||
await interaction.response.send_modal(RalphPlanModal(self.slug))
|
||||
return
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
channel_id = str(interaction.channel_id)
|
||||
await interaction.followup.send(
|
||||
f"🧠 Pornesc planning pentru `{self.slug}`… (durează ~60s)",
|
||||
|
||||
@@ -684,12 +684,17 @@ async def callback_ralph(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
description = p.get("description") or ""
|
||||
break
|
||||
if not description:
|
||||
# No description yet — set state and prompt with ForceReply.
|
||||
# Next message in this chat will start the planning session.
|
||||
ralph_flow.set_state(
|
||||
ADAPTER_NAME, chat_id, user_id,
|
||||
step=ralph_flow.STEP_INPUT_DESCRIPTION_THEN_PLAN,
|
||||
project=slug,
|
||||
)
|
||||
await context.bot.send_message(
|
||||
chat_id=int(chat_id),
|
||||
text=(
|
||||
f"Nu am descriere pentru `{slug}`. "
|
||||
f"Adaugă mai întâi cu `/p {slug} <descriere>`."
|
||||
),
|
||||
text=f"📝 Descriere pentru *{slug}* — pornesc planning după ce trimiți:",
|
||||
reply_markup=ForceReply(selective=True),
|
||||
parse_mode="Markdown",
|
||||
)
|
||||
return
|
||||
@@ -915,16 +920,39 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
chat_id, text[:100],
|
||||
)
|
||||
|
||||
# Ralph multi-step state: if user is replying to a "Descriere pentru X" prompt,
|
||||
# route this text to _ralph_propose instead of Claude.
|
||||
# Ralph multi-step state: route the next message based on the recorded step.
|
||||
state = ralph_flow.get_state(ADAPTER_NAME, str(chat_id), str(user_id))
|
||||
if state and state.get("step") == ralph_flow.STEP_INPUT_DESCRIPTION:
|
||||
if state:
|
||||
step = state.get("step")
|
||||
slug = state.get("project")
|
||||
if slug:
|
||||
if slug and step == ralph_flow.STEP_INPUT_DESCRIPTION:
|
||||
ralph_flow.clear_state(ADAPTER_NAME, str(chat_id), str(user_id))
|
||||
result = await asyncio.to_thread(_ralph_propose, slug, text)
|
||||
await message.reply_text(result)
|
||||
return
|
||||
if slug and step == ralph_flow.STEP_INPUT_DESCRIPTION_THEN_PLAN:
|
||||
ralph_flow.clear_state(ADAPTER_NAME, str(chat_id), str(user_id))
|
||||
await message.reply_text(
|
||||
f"🧠 Pornesc planning pentru *{slug}*… (durează ~60s)",
|
||||
parse_mode="Markdown",
|
||||
)
|
||||
try:
|
||||
first = await asyncio.to_thread(
|
||||
start_planning_session, slug, text, str(chat_id), ADAPTER_NAME,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("start_planning_session failed for %s", slug)
|
||||
await message.reply_text(f"Planning blocat: {e}")
|
||||
return
|
||||
for chunk in split_planning_chunks(first):
|
||||
await context.bot.send_message(chat_id=chat_id, text=chunk)
|
||||
await context.bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text="Răspunde aici. Apasă _Continuă faza_ când ești gata să trec la următoarea.",
|
||||
reply_markup=_build_planning_active_keyboard(),
|
||||
parse_mode="Markdown",
|
||||
)
|
||||
return
|
||||
|
||||
# Show typing indicator
|
||||
await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
|
||||
|
||||
@@ -22,7 +22,8 @@ DEFAULT_TTL_SECONDS = 600 # 10 minutes
|
||||
|
||||
# Step values used across adapters
|
||||
STEP_INPUT_DESCRIPTION = "input_description"
|
||||
STEP_IN_PLANNING = "in_planning" # reserved for W2 (planning agent)
|
||||
STEP_INPUT_DESCRIPTION_THEN_PLAN = "input_description_then_plan"
|
||||
STEP_IN_PLANNING = "in_planning" # planning agent active in this channel
|
||||
|
||||
|
||||
def _key(adapter: str, chat_id: str, user_id: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user