Ralph workflow functional: Python PRD generator + ralph.sh autonomous loop
This commit is contained in:
488
tools/ralph_prd_generator.py
Normal file
488
tools/ralph_prd_generator.py
Normal file
@@ -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)
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
Ralph Workflow Helper - pentru Echo
|
Ralph Workflow Helper - pentru Echo
|
||||||
Gestionează workflow-ul complet de creare și execuție proiecte cu Ralph + Claude Code
|
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
|
import os
|
||||||
@@ -10,144 +11,24 @@ import json
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
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"
|
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):
|
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:
|
Args:
|
||||||
prd_json: Path către prd.json
|
prd_json: Path către prd.json
|
||||||
max_iterations: Max iterații Ralph
|
max_iterations: Max iterații Ralph
|
||||||
background: Dacă True, rulează în background (nohup)
|
background: Dacă True, rulează în background
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
subprocess.Popen object dacă background, altfel None
|
subprocess.Popen object dacă background, altfel exit code
|
||||||
"""
|
"""
|
||||||
project_dir = prd_json.parent.parent.parent
|
project_dir = prd_json.parent.parent.parent
|
||||||
ralph_script = prd_json.parent / "ralph.sh"
|
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}")
|
print(f"❌ ralph.sh nu există în {ralph_script.parent}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
print(f"🚀 Lansez Ralph loop")
|
print(f"\n🚀 Lansez Ralph loop")
|
||||||
print(f"📁 Project: {project_dir}")
|
print(f"📁 Project: {project_dir}")
|
||||||
print(f"🔁 Max iterations: {max_iterations}")
|
print(f"🔁 Max iterations: {max_iterations}")
|
||||||
print(f"🌙 Background: {background}")
|
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)]
|
cmd = [str(ralph_script), str(max_iterations)]
|
||||||
|
|
||||||
if background:
|
if background:
|
||||||
# Rulează în background cu nohup
|
|
||||||
log_file = ralph_script.parent / "logs" / "ralph.log"
|
log_file = ralph_script.parent / "logs" / "ralph.log"
|
||||||
log_file.parent.mkdir(exist_ok=True)
|
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,
|
cwd=project_dir,
|
||||||
stdout=f,
|
stdout=f,
|
||||||
stderr=subprocess.STDOUT,
|
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"✅ Ralph pornit în background (PID: {process.pid})")
|
||||||
print(f"📋 Log: {log_file}")
|
print(f"📋 Log: {log_file}")
|
||||||
|
|
||||||
# Salvează PID pentru tracking
|
|
||||||
pid_file = ralph_script.parent / ".ralph.pid"
|
pid_file = ralph_script.parent / ".ralph.pid"
|
||||||
with open(pid_file, 'w') as f:
|
with open(pid_file, 'w') as f:
|
||||||
f.write(str(process.pid))
|
f.write(str(process.pid))
|
||||||
|
|
||||||
return process
|
return process
|
||||||
else:
|
else:
|
||||||
# Rulează în foreground (pentru debug)
|
print("⚠️ Rulare în foreground")
|
||||||
print("⚠️ Rulare în foreground (pentru debug)")
|
|
||||||
result = subprocess.run(cmd, cwd=project_dir)
|
result = subprocess.run(cmd, cwd=project_dir)
|
||||||
return None
|
return result.returncode
|
||||||
|
|
||||||
|
|
||||||
def check_status(project_dir: Path):
|
def check_status(project_dir: Path):
|
||||||
"""
|
"""Verifică status Ralph pentru un proiect"""
|
||||||
Verifică status Ralph pentru un proiect
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_dir: Director proiect
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict cu status (stories complete/incomplete, learnings)
|
|
||||||
"""
|
|
||||||
prd_json = project_dir / "scripts" / "ralph" / "prd.json"
|
prd_json = project_dir / "scripts" / "ralph" / "prd.json"
|
||||||
progress_file = project_dir / "scripts" / "ralph" / "progress.txt"
|
progress_file = project_dir / "scripts" / "ralph" / "progress.txt"
|
||||||
pid_file = project_dir / "scripts" / "ralph" / ".ralph.pid"
|
pid_file = project_dir / "scripts" / "ralph" / ".ralph.pid"
|
||||||
@@ -215,18 +85,16 @@ def check_status(project_dir: Path):
|
|||||||
"learnings": []
|
"learnings": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verifică dacă rulează
|
|
||||||
if pid_file.exists():
|
if pid_file.exists():
|
||||||
with open(pid_file) as f:
|
with open(pid_file) as f:
|
||||||
pid = int(f.read().strip())
|
pid = int(f.read().strip())
|
||||||
try:
|
try:
|
||||||
os.kill(pid, 0) # Verifică dacă procesul există
|
os.kill(pid, 0)
|
||||||
status["running"] = True
|
status["running"] = True
|
||||||
status["pid"] = pid
|
status["pid"] = pid
|
||||||
except OSError:
|
except OSError:
|
||||||
status["running"] = False
|
status["running"] = False
|
||||||
|
|
||||||
# Citește stories
|
|
||||||
if prd_json.exists():
|
if prd_json.exists():
|
||||||
with open(prd_json) as f:
|
with open(prd_json) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
@@ -243,7 +111,6 @@ def check_status(project_dir: Path):
|
|||||||
"priority": story.get('priority', 999)
|
"priority": story.get('priority', 999)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Citește learnings (ultimele 10 linii)
|
|
||||||
if progress_file.exists():
|
if progress_file.exists():
|
||||||
with open(progress_file) as f:
|
with open(progress_file) as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
@@ -270,24 +137,31 @@ def main():
|
|||||||
project_name = sys.argv[2]
|
project_name = sys.argv[2]
|
||||||
description = sys.argv[3]
|
description = sys.argv[3]
|
||||||
|
|
||||||
# Pas 1: Creează PRD
|
print("=" * 70)
|
||||||
prd_file = create_prd(project_name, description)
|
print(f"🧪 Ralph workflow: {project_name}")
|
||||||
if not prd_file:
|
print("=" * 70)
|
||||||
print("❌ Eroare la creare PRD")
|
|
||||||
|
# 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)
|
sys.exit(1)
|
||||||
|
|
||||||
# Pas 2: Convertește în prd.json
|
# Lansează Ralph în background
|
||||||
prd_json = convert_prd(prd_file)
|
process = run_ralph(prd_json, max_iterations=20, background=True)
|
||||||
if not prd_json:
|
|
||||||
print("❌ Eroare la conversie")
|
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)
|
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":
|
elif command == "status":
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 3:
|
||||||
print("Usage: python ralph_workflow.py status PROJECT_NAME")
|
print("Usage: python ralph_workflow.py status PROJECT_NAME")
|
||||||
@@ -302,7 +176,9 @@ def main():
|
|||||||
|
|
||||||
status = check_status(project_dir)
|
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"🔄 Running: {'DA (PID: ' + str(status.get('pid', '')) + ')' if status['running'] else 'NU'}")
|
||||||
print(f"✅ Complete: {len(status['complete'])}")
|
print(f"✅ Complete: {len(status['complete'])}")
|
||||||
print(f"🔄 Incomplete: {len(status['incomplete'])}")
|
print(f"🔄 Incomplete: {len(status['incomplete'])}")
|
||||||
@@ -322,6 +198,8 @@ def main():
|
|||||||
for l in status['learnings'][-5:]:
|
for l in status['learnings'][-5:]:
|
||||||
print(f" {l}")
|
print(f" {l}")
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f"❌ Comandă necunoscută: {command}")
|
print(f"❌ Comandă necunoscută: {command}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user