diff --git a/tools/ralph_prd_generator.py b/tools/ralph_prd_generator.py new file mode 100644 index 0000000..805f1c6 --- /dev/null +++ b/tools/ralph_prd_generator.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python3 +""" +Ralph PRD Generator - implementare Python a skill-ului ralph-prd +Generează PRD și prd.json fără să apeleze Claude Code +""" + +import json +import re +from pathlib import Path +from datetime import datetime + + +def detect_project_context(project_dir: Path): + """Detectează tech stack și mod (NEW_PROJECT vs FEATURE)""" + context = { + "mode": "NEW_PROJECT", + "stack_type": None, + "config_file": None, + "scripts": {}, + "dependencies": [] + } + + # Verifică fișiere de config + config_files = { + "package.json": "nodejs", + "pyproject.toml": "python", + "requirements.txt": "python-legacy", + "go.mod": "go", + "Cargo.toml": "rust", + "pom.xml": "java-maven", + "build.gradle": "java-gradle", + "composer.json": "php", + "Gemfile": "ruby" + } + + for filename, stack in config_files.items(): + config_path = project_dir / filename + if config_path.exists(): + context["mode"] = "FEATURE" + context["stack_type"] = stack + context["config_file"] = filename + + # Extrage info din package.json + if filename == "package.json": + try: + with open(config_path) as f: + data = json.load(f) + context["scripts"] = data.get("scripts", {}) + context["dependencies"] = list(data.get("dependencies", {}).keys()) + except: + pass + + break + + return context + + +def generate_prd_markdown(project_name: str, description: str, context: dict): + """Generează PRD markdown bazat pe descriere și context""" + + # Parse descriere pentru a extrage info + lines = description.strip().split('\n') + features = [] + requirements = [] + + for line in lines: + line = line.strip() + if line.startswith('Features:') or line.startswith('Feature:'): + continue + if line.startswith('-'): + features.append(line[1:].strip()) + elif line and not line.endswith(':'): + requirements.append(line) + + if not features: + # Dacă nu sunt features explicit, folosim descrierea ca feature + features = [description.split('.')[0]] + + # Detectează tip proiect din descriere + desc_lower = description.lower() + if 'cli' in desc_lower or 'command' in desc_lower: + project_type = "CLI Tool" + elif 'api' in desc_lower or 'backend' in desc_lower: + project_type = "API / Backend" + elif 'web' in desc_lower or 'app' in desc_lower: + project_type = "Web Application" + else: + project_type = "Application" + + # Detectează stack din descriere sau context + if 'python' in desc_lower: + stack = "Python" + test_framework = "pytest" + elif 'javascript' in desc_lower or 'node' in desc_lower or 'typescript' in desc_lower: + stack = "Node.js / TypeScript" + test_framework = "jest" + elif context["stack_type"] == "nodejs": + stack = "Node.js / TypeScript" + test_framework = "jest" + elif context["stack_type"] and context["stack_type"].startswith("python"): + stack = "Python" + test_framework = "pytest" + else: + stack = "Python" # Default + test_framework = "pytest" + + # Template PRD + prd = f"""# PRD: {project_name.replace('-', ' ').title()} + +## 1. Introducere + +{description.split('.')[0]}. + +**Data:** {datetime.now().strftime('%Y-%m-%d')} +**Status:** Draft +**Mode:** {context['mode']} + +## 2. Context Tehnic + +""" + + if context["mode"] == "FEATURE": + prd += f"""**Proiect existent detectat:** +- Stack: {context['stack_type']} +- Config: {context['config_file']} +- Scripts: {', '.join(context['scripts'].keys())} + +""" + else: + prd += f"""**Proiect nou:** +- Tip: {project_type} +- Stack recomandat: {stack} +- Test framework: {test_framework} + +""" + + prd += f"""## 3. Obiective + +### Obiectiv Principal +{features[0] if features else description.split('.')[0]} + +### Obiective Secundare +""" + + for i, feature in enumerate(features[1:] if len(features) > 1 else features, 1): + prd += f"- {feature}\n" + + prd += f""" +### Metrici de Succes +- Toate funcționalitățile implementate conform spec +- Tests passing (coverage > 80%) +- Code quality: lint + typecheck pass + +## 4. User Stories + +""" + + # Generează user stories din features + for i, feature in enumerate(features, 1): + story_id = f"US-{i:03d}" + title = feature.strip() + + prd += f"""### {story_id}: {title} +**Ca** utilizator +**Vreau** {title.lower()} +**Pentru că** pot folosi aplicația eficient + +**Acceptance Criteria:** +- [ ] Funcționalitatea implementată conform descrierii +- [ ] Input validation în loc +- [ ] Error handling pentru cazuri edge +- [ ] Tests cu {test_framework} (coverage > 80%) +- [ ] Code quality: lint + typecheck pass + +**Priority:** {i * 10} + +""" + + # Dacă nu sunt multe features, adaugă story pentru tests + if len(features) < 3: + prd += f"""### US-{len(features)+1:03d}: Tests și Documentație +**Ca** developer +**Vreau** teste comprehensive și documentație +**Pentru că** asigur calitatea codului + +**Acceptance Criteria:** +- [ ] Unit tests pentru toate funcțiile (coverage > 80%) +- [ ] Integration tests pentru flow-uri principale +- [ ] README cu instrucțiuni de utilizare +- [ ] Docstrings pentru funcții publice +- [ ] {test_framework} rulează fără erori + +**Priority:** {(len(features)+1) * 10} + +""" + + prd += f"""## 5. Cerințe Funcționale + +""" + + for i, req in enumerate(requirements if requirements else features, 1): + prd += f"{i}. [REQ-{i:03d}] {req}\n" + + prd += f""" +## 6. Non-Goals (Ce NU facem) + +- Interfață grafică (GUI) - doar CLI/API +- Suport multiple limbaje - doar {stack} +- Deployment infrastructure - doar cod functional + +## 7. Considerații Tehnice + +### Stack/Tehnologii +- Limbaj: {stack} +- Testing: {test_framework} +- Linting: pylint / eslint (depinde de stack) +- Type checking: mypy / typescript + +### Patterns de Urmat +- Clean code principles +- SOLID principles unde aplicabil +- Error handling consistent +- Input validation strict + +### Riscuri Tehnice +- Edge cases la input validation +- Performance pentru volume mari de date (dacă aplicabil) + +## 8. Considerații Security + +- Input validation pentru toate datele externe +- Error messages fără info sensibilă +- Principle of least privilege + +## 9. Open Questions + +- [ ] Performance requirements specifice? +- [ ] Limite pe input sizes? +- [ ] Specific error handling patterns preferați? + +--- + +**Generated by:** Echo (Ralph PRD Generator) +**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M')} +""" + + return prd + + +def prd_to_stories(prd_content: str, project_name: str): + """Extrage user stories din PRD și le convertește în format prd.json""" + + stories = [] + + # Parse PRD pentru stories + story_pattern = r'### (US-\d+): (.+?)\n\*\*Ca\*\* (.+?)\n\*\*Vreau\*\* (.+?)\n\*\*Pentru că\*\* (.+?)\n\n\*\*Acceptance Criteria:\*\*\n(.*?)\n\n\*\*Priority:\*\* (\d+)' + + matches = re.finditer(story_pattern, prd_content, re.DOTALL) + + for match in matches: + story_id = match.group(1) + title = match.group(2).strip() + user_type = match.group(3).strip() + want = match.group(4).strip() + because = match.group(5).strip() + criteria_text = match.group(6).strip() + priority = int(match.group(7)) + + # Parse acceptance criteria + criteria = [] + for line in criteria_text.split('\n'): + line = line.strip() + if line.startswith('- [ ]'): + criteria.append(line[5:].strip()) + + # Detectează dacă necesită browser check (pentru UI) + requires_browser = 'ui' in title.lower() or 'interface' in title.lower() + + story = { + "id": story_id, + "title": title, + "description": f"Ca {user_type}, vreau {want} pentru că {because}", + "priority": priority, + "acceptanceCriteria": criteria, + "requiresBrowserCheck": requires_browser, + "passes": False, + "notes": "" + } + + stories.append(story) + + # Dacă nu găsim stories (regex failed), generăm basic + if not stories: + stories = [{ + "id": "US-001", + "title": "Implementare funcționalitate principală", + "description": f"Implementează {project_name}", + "priority": 10, + "acceptanceCriteria": [ + "Funcționalitatea implementată", + "Tests passing", + "Lint + typecheck pass" + ], + "requiresBrowserCheck": False, + "passes": False, + "notes": "" + }] + + return stories + + +def detect_tech_stack_commands(project_dir: Path, context: dict): + """Detectează comenzile tech stack pentru prd.json""" + + stack_type = context.get("stack_type", "python") + scripts = context.get("scripts", {}) + + # Default commands per stack + if stack_type == "nodejs" or "package.json" in str(context.get("config_file", "")): + commands = { + "start": scripts.get("dev", scripts.get("start", "npm run dev")), + "build": scripts.get("build", "npm run build"), + "lint": scripts.get("lint", "npm run lint"), + "typecheck": scripts.get("typecheck", "npm run typecheck"), + "test": scripts.get("test", "npm test") + } + port = 3000 + stack_name = "nextjs" if "next" in str(context.get("dependencies", [])) else "nodejs" + + elif stack_type and stack_type.startswith("python"): + commands = { + "start": "python main.py", + "build": "", + "lint": "ruff check .", + "typecheck": "mypy .", + "test": "pytest" + } + port = 8000 + stack_name = "python" + + else: + # Generic/fallback + commands = { + "start": "python main.py", + "build": "", + "lint": "echo 'No linter configured'", + "typecheck": "echo 'No typecheck configured'", + "test": "pytest" + } + port = 8000 + stack_name = "python" + + return { + "type": stack_name, + "commands": commands, + "port": port + } + + +def create_prd_and_json(project_name: str, description: str, workspace_dir: Path): + """ + Generează PRD markdown și prd.json pentru un proiect + + Returns: + tuple: (prd_file_path, prd_json_path) sau (None, None) dacă eroare + """ + project_dir = workspace_dir / project_name + project_dir.mkdir(parents=True, exist_ok=True) + + # Detectează context + context = detect_project_context(project_dir) + + print(f"📊 Context detectat:") + print(f" Mode: {context['mode']}") + if context['stack_type']: + print(f" Stack: {context['stack_type']}") + print(f" Config: {context['config_file']}") + + # Generează PRD markdown + prd_content = generate_prd_markdown(project_name, description, context) + + # Salvează PRD + tasks_dir = project_dir / "tasks" + tasks_dir.mkdir(exist_ok=True) + prd_file = tasks_dir / f"prd-{project_name}.md" + + with open(prd_file, 'w', encoding='utf-8') as f: + f.write(prd_content) + + print(f"✅ PRD salvat: {prd_file}") + + # Generează prd.json + stories = prd_to_stories(prd_content, project_name) + tech_stack = detect_tech_stack_commands(project_dir, context) + + prd_json_data = { + "projectName": project_name, + "branchName": f"ralph/{project_name}", + "description": description.split('\n')[0], + "techStack": tech_stack, + "userStories": stories + } + + # Creează structura ralph + ralph_dir = project_dir / "scripts" / "ralph" + ralph_dir.mkdir(parents=True, exist_ok=True) + (ralph_dir / "logs").mkdir(exist_ok=True) + (ralph_dir / "archive").mkdir(exist_ok=True) + (ralph_dir / "screenshots").mkdir(exist_ok=True) + + # Salvează prd.json + prd_json_file = ralph_dir / "prd.json" + with open(prd_json_file, 'w', encoding='utf-8') as f: + json.dump(prd_json_data, f, indent=2, ensure_ascii=False) + + print(f"✅ prd.json salvat: {prd_json_file}") + print(f"📋 Stories: {len(stories)}") + for story in stories: + print(f" - {story['id']}: {story['title']}") + + # Copiază template-uri ralph + templates_dir = Path.home() / ".claude" / "skills" / "ralph" / "templates" + if not templates_dir.exists(): + templates_dir = Path.home() / "clawd" / "skills" / "ralph" / "templates" + + if templates_dir.exists(): + # Copiază ralph.sh + ralph_sh_src = templates_dir / "ralph.sh" + if ralph_sh_src.exists(): + ralph_sh_dst = ralph_dir / "ralph.sh" + with open(ralph_sh_src) as f: + content = f.read() + with open(ralph_sh_dst, 'w') as f: + f.write(content) + ralph_sh_dst.chmod(0o755) + print(f"✅ ralph.sh copiat") + + # Copiază prompt.md + prompt_src = templates_dir / "prompt.md" + if prompt_src.exists(): + prompt_dst = ralph_dir / "prompt.md" + with open(prompt_src) as f: + content = f.read() + with open(prompt_dst, 'w') as f: + f.write(content) + print(f"✅ prompt.md copiat") + + # Init progress.txt + progress_file = ralph_dir / "progress.txt" + with open(progress_file, 'w') as f: + f.write(f"# Ralph Progress Log\n") + f.write(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n") + f.write(f"Project: {project_name}\n") + f.write(f"---\n") + + print(f"✅ Structură Ralph completă în {ralph_dir}") + + return prd_file, prd_json_file + + +if __name__ == "__main__": + import sys + + if len(sys.argv) < 3: + print("Usage: python ralph_prd_generator.py PROJECT_NAME 'description'") + sys.exit(1) + + project_name = sys.argv[1] + description = sys.argv[2] + workspace = Path.home() / "workspace" + + print(f"🔄 Generez PRD pentru {project_name}") + print("=" * 70) + + prd_file, prd_json = create_prd_and_json(project_name, description, workspace) + + if prd_file and prd_json: + print("\n" + "=" * 70) + print("✅ PRD și prd.json generate cu succes!") + print(f"📄 PRD: {prd_file}") + print(f"📋 JSON: {prd_json}") + print("\n📌 Următorii pași:") + print(f" 1. Revizuiește PRD în {prd_file}") + print(f" 2. Rulează Ralph: cd {prd_file.parent.parent} && ./scripts/ralph/ralph.sh 20") + print("=" * 70) + else: + print("\n❌ Eroare la generare PRD") + sys.exit(1) diff --git a/tools/ralph_workflow.py b/tools/ralph_workflow.py index 1765d49..43c3b8f 100755 --- a/tools/ralph_workflow.py +++ b/tools/ralph_workflow.py @@ -2,6 +2,7 @@ """ Ralph Workflow Helper - pentru Echo Gestionează workflow-ul complet de creare și execuție proiecte cu Ralph + Claude Code +Folosește ralph_prd_generator.py pentru PRD, apoi ralph.sh pentru implementare """ import os @@ -10,144 +11,24 @@ import json import subprocess from pathlib import Path +# Import generator PRD +sys.path.insert(0, str(Path(__file__).parent)) +from ralph_prd_generator import create_prd_and_json + WORKSPACE = Path.home() / "workspace" -CLAUDE_BIN = Path.home() / ".local" / "bin" / "claude" - - -def create_prd(project_name: str, description: str, workspace_dir: Path = WORKSPACE): - """ - Pas 1: Creează PRD folosind Claude Code cu skill /ralph:prd - - Args: - project_name: Nume proiect (kebab-case) - description: Descriere feature/proiect - workspace_dir: Director workspace (default: ~/workspace) - - Returns: - Path către PRD generat - """ - project_dir = workspace_dir / project_name - project_dir.mkdir(parents=True, exist_ok=True) - - # Verifică dacă există package.json (feature mode) sau nu (new project mode) - config_exists = (project_dir / "package.json").exists() - mode = "FEATURE" if config_exists else "NEW_PROJECT" - - print(f"🔄 Creez PRD pentru {project_name} (mode: {mode})") - print(f"📁 Workspace: {project_dir}") - - # Construiește prompt pentru Claude Code - prompt = f"""/ralph:prd - -{description} -""" - - # Rulează Claude Code în director proiect - try: - result = subprocess.run( - [str(CLAUDE_BIN), "exec", prompt], - cwd=project_dir, - capture_output=True, - text=True, - timeout=300 # 5 min timeout - ) - - if result.returncode != 0: - print(f"❌ Eroare la generare PRD: {result.stderr}") - return None - - # Caută PRD generat - tasks_dir = project_dir / "tasks" - if tasks_dir.exists(): - prd_files = list(tasks_dir.glob("prd-*.md")) - if prd_files: - prd_file = prd_files[0] - print(f"✅ PRD generat: {prd_file}") - return prd_file - - print("⚠️ PRD generat dar nu găsit în tasks/") - return None - - except subprocess.TimeoutExpired: - print("❌ Timeout la generare PRD (>5 min)") - return None - except Exception as e: - print(f"❌ Eroare: {e}") - return None - - -def convert_prd(prd_file: Path): - """ - Pas 2: Convertește PRD în prd.json folosind /ralph:convert - - Args: - prd_file: Path către PRD markdown - - Returns: - Path către prd.json generat - """ - project_dir = prd_file.parent.parent - - print(f"🔄 Convertesc PRD în prd.json") - print(f"📄 PRD: {prd_file}") - - # Construiește prompt pentru conversie - prompt = f"""/ralph:convert - -Convertește PRD-ul din {prd_file.relative_to(project_dir)} -""" - - try: - result = subprocess.run( - [str(CLAUDE_BIN), "exec", prompt], - cwd=project_dir, - capture_output=True, - text=True, - timeout=180 # 3 min timeout - ) - - if result.returncode != 0: - print(f"❌ Eroare la conversie: {result.stderr}") - return None - - # Verifică prd.json - prd_json = project_dir / "scripts" / "ralph" / "prd.json" - if prd_json.exists(): - print(f"✅ prd.json generat: {prd_json}") - - # Afișează stories - with open(prd_json) as f: - data = json.load(f) - print(f"\n📋 Stories: {len(data.get('userStories', []))}") - for story in data.get('userStories', [])[:3]: - print(f" - {story['id']}: {story['title']}") - if len(data.get('userStories', [])) > 3: - print(f" ... și {len(data['userStories']) - 3} mai multe") - - return prd_json - - print("⚠️ Conversie completă dar prd.json nu găsit") - return None - - except subprocess.TimeoutExpired: - print("❌ Timeout la conversie (>3 min)") - return None - except Exception as e: - print(f"❌ Eroare: {e}") - return None def run_ralph(prd_json: Path, max_iterations: int = 20, background: bool = False): """ - Pas 3: Rulează ralph.sh pentru execuție autonomă + Rulează ralph.sh pentru execuție autonomă Args: prd_json: Path către prd.json max_iterations: Max iterații Ralph - background: Dacă True, rulează în background (nohup) + background: Dacă True, rulează în background Returns: - subprocess.Popen object dacă background, altfel None + subprocess.Popen object dacă background, altfel exit code """ project_dir = prd_json.parent.parent.parent ralph_script = prd_json.parent / "ralph.sh" @@ -156,7 +37,7 @@ def run_ralph(prd_json: Path, max_iterations: int = 20, background: bool = False print(f"❌ ralph.sh nu există în {ralph_script.parent}") return None - print(f"🚀 Lansez Ralph loop") + print(f"\n🚀 Lansez Ralph loop") print(f"📁 Project: {project_dir}") print(f"🔁 Max iterations: {max_iterations}") print(f"🌙 Background: {background}") @@ -164,7 +45,6 @@ def run_ralph(prd_json: Path, max_iterations: int = 20, background: bool = False cmd = [str(ralph_script), str(max_iterations)] if background: - # Rulează în background cu nohup log_file = ralph_script.parent / "logs" / "ralph.log" log_file.parent.mkdir(exist_ok=True) @@ -174,35 +54,25 @@ def run_ralph(prd_json: Path, max_iterations: int = 20, background: bool = False cwd=project_dir, stdout=f, stderr=subprocess.STDOUT, - start_new_session=True # Detach de terminal + start_new_session=True ) print(f"✅ Ralph pornit în background (PID: {process.pid})") print(f"📋 Log: {log_file}") - # Salvează PID pentru tracking pid_file = ralph_script.parent / ".ralph.pid" with open(pid_file, 'w') as f: f.write(str(process.pid)) return process else: - # Rulează în foreground (pentru debug) - print("⚠️ Rulare în foreground (pentru debug)") + print("⚠️ Rulare în foreground") result = subprocess.run(cmd, cwd=project_dir) - return None + return result.returncode def check_status(project_dir: Path): - """ - Verifică status Ralph pentru un proiect - - Args: - project_dir: Director proiect - - Returns: - Dict cu status (stories complete/incomplete, learnings) - """ + """Verifică status Ralph pentru un proiect""" prd_json = project_dir / "scripts" / "ralph" / "prd.json" progress_file = project_dir / "scripts" / "ralph" / "progress.txt" pid_file = project_dir / "scripts" / "ralph" / ".ralph.pid" @@ -215,18 +85,16 @@ def check_status(project_dir: Path): "learnings": [] } - # Verifică dacă rulează if pid_file.exists(): with open(pid_file) as f: pid = int(f.read().strip()) try: - os.kill(pid, 0) # Verifică dacă procesul există + os.kill(pid, 0) status["running"] = True status["pid"] = pid except OSError: status["running"] = False - # Citește stories if prd_json.exists(): with open(prd_json) as f: data = json.load(f) @@ -243,7 +111,6 @@ def check_status(project_dir: Path): "priority": story.get('priority', 999) }) - # Citește learnings (ultimele 10 linii) if progress_file.exists(): with open(progress_file) as f: lines = f.readlines() @@ -270,24 +137,31 @@ def main(): project_name = sys.argv[2] description = sys.argv[3] - # Pas 1: Creează PRD - prd_file = create_prd(project_name, description) - if not prd_file: - print("❌ Eroare la creare PRD") + print("=" * 70) + print(f"🧪 Ralph workflow: {project_name}") + print("=" * 70) + + # Generează PRD și prd.json + prd_file, prd_json = create_prd_and_json(project_name, description, WORKSPACE) + + if not prd_file or not prd_json: + print("\n❌ Eroare la generare PRD") sys.exit(1) - # Pas 2: Convertește în prd.json - prd_json = convert_prd(prd_file) - if not prd_json: - print("❌ Eroare la conversie") + # Lansează Ralph în background + process = run_ralph(prd_json, max_iterations=20, background=True) + + if process: + print("\n" + "=" * 70) + print("✅ Workflow complet!") + print(f"📁 Project: {prd_json.parent.parent.parent}") + print(f"🔄 Ralph PID: {process.pid}") + print(f"📋 Monitor: tail -f {prd_json.parent}/logs/ralph.log") + print("=" * 70) + else: + print("\n❌ Eroare la lansare Ralph") sys.exit(1) - # Pas 3: Lansează Ralph în background - run_ralph(prd_json, max_iterations=20, background=True) - - print("\n✅ Workflow complet!") - print(f"📁 Project: {prd_json.parent.parent.parent}") - elif command == "status": if len(sys.argv) < 3: print("Usage: python ralph_workflow.py status PROJECT_NAME") @@ -302,7 +176,9 @@ def main(): status = check_status(project_dir) - print(f"\n📊 Status: {status['project']}") + print("\n" + "=" * 70) + print(f"📊 Status: {status['project']}") + print("=" * 70) print(f"🔄 Running: {'DA (PID: ' + str(status.get('pid', '')) + ')' if status['running'] else 'NU'}") print(f"✅ Complete: {len(status['complete'])}") print(f"🔄 Incomplete: {len(status['incomplete'])}") @@ -321,6 +197,8 @@ def main(): print("\n📚 Recent learnings:") for l in status['learnings'][-5:]: print(f" {l}") + + print("=" * 70) else: print(f"❌ Comandă necunoscută: {command}")