diff --git a/AGENTS.md b/AGENTS.md
index fbfd20c..dacf9ab 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -32,118 +32,24 @@ When I receive errors, bugs, or new feature requests:
1. **Planning → Opus**: Architecture, strategy, design decisions
2. **Execution → Sonnet**: Implementation, coding, debugging, testing
-## Proiecte/Features Workflow (MANDATORY)
+## Proiecte/Features Workflow
-**Scop:** Propun și creez programe/proiecte în cod care îl ajută pe Marius (80/20), inspirate din Discovery (YouTube, articole, bloguri).
+**Scop:** Propun proiecte 80/20 in evening-report, implementez cu Ralph in night-execute.
+**Tools:** tools/ralph_prd_generator.py, tools/ralph_workflow.py
+**Workspace:** ~/workspace/ | **Tracking:** memory/approved-tasks.md
+**Model strategy:** Opus (planning/PRD) → Sonnet (implementare Ralph)
-### Criterii Propuneri (80/20 STRICT)
-- Impact mare pentru Marius → apoi pentru clienți
-- Proiecte de "joacă" pentru el mai întâi (să vadă cum îl ajută)
-- Din ce îl interesează (USER.md)
-- Inspirat din conținut procesat (memory/kb/youtube/, articole/, insights/)
-- **NU orice** - doar cu valoare concretă
+### Context Proiecte Prioritare
+- **roa2web** (gitea.romfast.ro/romfast/roa2web) - rapoarte, interfata web, notificari ERP ROA
+ - Rapoarte ROA noi → feature in roa2web (NU proiect separat)
+- **Chatbot Maria** (Flowise, LXC 104) - documentatie, raspunsuri clienti
+ - Imbunatatiri chatbot → documentatie + configurare Flowise
+- **Proiecte independente** → ~/workspace/ cu Ralph autonom
-### Workflow Complet
-
-**1. SEARA (20:00) - evening-report:**
-- Propun 1-2 proiecte NOI (P1, P2)
-- Propun 2-3 features pentru proiecte EXISTENTE (F1, F2, F3)
-- Format: context, impact, efort, stack simplu
-- Marius aprobă: "P pentru P1,P2" sau "F pentru F1,F3"
-
-**2. NOAPTE (23:00, 03:00) - night-execute:**
-
-**A. Planning cu OPUS (eu, Echo) - pe moltbot:**
-```python
-from tools.ralph_prd_generator import create_prd_and_json
-from tools.ralph_workflow import run_ralph
-import subprocess
-
-# Pentru fiecare proiect aprobat
-prd_file, prd_json = create_prd_and_json(
- project_name="project-name", # kebab-case
- description="Descriere completă cu Features:\n- Feature 1\n- Feature 2",
- workspace_dir=Path.home() / "workspace"
-)
-
-# Git init + push
-project_dir = prd_json.parent.parent.parent
-subprocess.run(["git", "init"], cwd=project_dir)
-subprocess.run(["git", "add", "."], cwd=project_dir)
-subprocess.run(["git", "commit", "-m", "Initial commit with PRD"], cwd=project_dir)
-subprocess.run(["git", "remote", "add", "origin", f"https://gitea.romfast.ro/romfast/PROJECT"], cwd=project_dir)
-subprocess.run(["git", "push", "-u", "origin", "main"], cwd=project_dir)
-
-# Lansează Ralph
-run_ralph(prd_json, max_iterations=20, background=True)
-
-# Marchează [x] în approved-tasks.md
-```
-
-**B. Implementare cu Ralph (Sonnet) - automat în background:**
-- ralph.sh rulează autonom:
- 1. Selectează story (priority minimă, passes=false)
- 2. Rulează Claude Code (Sonnet) pentru implementare
- 3. Quality checks: typecheck, lint, test
- 4. Git commit dacă OK → passes: true
- 5. Update progress.txt cu learnings
- 6. Repetă până complete sau max 20 iterații
-- Git push automat
-
-**3. DIMINEAȚĂ (08:30) - morning-report:**
-```python
-from tools.ralph_workflow import check_status
-from pathlib import Path
-
-status = check_status(Path.home() / "workspace" / "PROJECT-NAME")
-# Raportez: stories complete/incomplete, learnings, link gitea
-```
-
-- Status per story: ✅ complet / 🔄 în progres / ⚠️ blocat
-- Learnings din progress.txt
-- Link gitea: `https://gitea.romfast.ro/romfast/PROJECT-NAME`
-
-### Mașină Development
-
-**moltbot (LXC 110) - LOCAL:**
-- User: moltbot
-- Workspace: `~/workspace/`
-- Claude Code: `~/.local/bin/claude` (v2.1.37)
-- Ralph tools:
- - `~/clawd/tools/ralph_prd_generator.py` - Generează PRD și prd.json (Python)
- - `~/clawd/tools/ralph_workflow.py` - Wrapper (lansare Ralph)
- - `~/clawd/skills/ralph/templates/` - ralph.sh, prompt.md (copiate în proiecte)
-- Templates copiate automat în fiecare proiect
-
-### Model Strategy (OBLIGATORIU)
-- **Opus** → Planning, PRD, stories (eu, Echo în night-execute)
-- **Sonnet** → Coding, debugging, implementare (Ralph loop)
-
-### Structură Proiect
-```
-/workspace/PROJECT-NAME/
-├── tasks/
-│ └── prd-PROJECT-NAME.md # PRD generat de /prd skill
-├── scripts/
-│ └── ralph/
-│ ├── prd.json # Stories pentru Ralph
-│ ├── progress.txt # Learnings per iterație
-│ └── ralph.sh # Loop autonom
-├── src/ # Cod implementat de Ralph
-└── .git/ # Git repo → gitea
-```
-
-### Tracking
-- `memory/approved-tasks.md` - include proiecte (P1, P2) și features (F1, F2)
-- Secțiuni: "Noaptea asta" + "Nopțile următoare"
-- Format: `[ ] P1 - Nume Proiect: descriere scurtă`
-
-### Exemple Domenii
-- Automatizări pentru ROA (scripturile lui Marius)
-- Unelte productivitate (task tracking, reminder-uri)
-- Mini-tools pentru clienți (rapoarte, validări)
-- Experimente NLP/coaching (exerciții interactive)
-- Tracking sănătate (dureri, pauze respirație)
+### Surse Inspiratie
+- Intrebari frecvente clienti (validari ANAF D406/D394, facturare valuta, taxare inversa)
+- Note din memory/kb/ (youtube, insights, articole)
+- Probleme repetitive ale lui Marius
## Memory
diff --git a/dashboard/api.py b/dashboard/api.py
index dd99ff1..cd93951 100644
--- a/dashboard/api.py
+++ b/dashboard/api.py
@@ -5,10 +5,12 @@ Handles YouTube summarization requests.
"""
import json
+import shutil
import subprocess
import sys
import re
import os
+import signal
from http.server import HTTPServer, SimpleHTTPRequestHandler
from urllib.parse import parse_qs, urlparse
from datetime import datetime
@@ -18,6 +20,20 @@ BASE_DIR = Path(__file__).parent.parent
TOOLS_DIR = BASE_DIR / 'tools'
NOTES_DIR = BASE_DIR / 'kb' / 'youtube'
KANBAN_DIR = BASE_DIR / 'dashboard'
+WORKSPACE_DIR = Path('/home/moltbot/workspace')
+
+# Load .env file if present
+_env_file = Path(__file__).parent / '.env'
+if _env_file.exists():
+ for line in _env_file.read_text().splitlines():
+ line = line.strip()
+ if line and not line.startswith('#') and '=' in line:
+ k, v = line.split('=', 1)
+ os.environ.setdefault(k.strip(), v.strip())
+
+GITEA_URL = os.environ.get('GITEA_URL', 'https://gitea.romfast.ro')
+GITEA_ORG = os.environ.get('GITEA_ORG', 'romfast')
+GITEA_TOKEN = os.environ.get('GITEA_TOKEN', '')
class TaskBoardHandler(SimpleHTTPRequestHandler):
@@ -32,6 +48,16 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
self.handle_git_commit()
elif self.path == '/api/pdf':
self.handle_pdf_post()
+ elif self.path == '/api/workspace/run':
+ self.handle_workspace_run()
+ elif self.path == '/api/workspace/stop':
+ self.handle_workspace_stop()
+ elif self.path == '/api/workspace/git/commit':
+ self.handle_workspace_git_commit()
+ elif self.path == '/api/workspace/git/push':
+ self.handle_workspace_git_push()
+ elif self.path == '/api/workspace/delete':
+ self.handle_workspace_delete()
else:
self.send_error(404)
@@ -229,6 +255,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
self.handle_files_get()
elif self.path.startswith('/api/diff'):
self.handle_git_diff()
+ elif self.path == '/api/workspace' or self.path.startswith('/api/workspace?'):
+ self.handle_workspace_list()
+ elif self.path.startswith('/api/workspace/git/diff'):
+ self.handle_workspace_git_diff()
+ elif self.path.startswith('/api/workspace/logs'):
+ self.handle_workspace_logs()
elif self.path.startswith('/api/'):
self.send_error(404)
else:
@@ -726,6 +758,596 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
else:
self.send_json({'error': 'Unknown action'}, 400)
+ def handle_workspace_list(self):
+ """List projects in ~/workspace/ with Ralph status, git info, etc."""
+ try:
+ projects = []
+ if not WORKSPACE_DIR.exists():
+ self.send_json({'projects': []})
+ return
+
+ for project_dir in sorted(WORKSPACE_DIR.iterdir()):
+ if not project_dir.is_dir() or project_dir.name.startswith('.'):
+ continue
+
+ ralph_dir = project_dir / 'scripts' / 'ralph'
+ prd_json = ralph_dir / 'prd.json'
+ tasks_dir = project_dir / 'tasks'
+
+ proj = {
+ 'name': project_dir.name,
+ 'path': str(project_dir),
+ 'hasRalph': ralph_dir.exists(),
+ 'hasPrd': any(tasks_dir.glob('prd-*.md')) if tasks_dir.exists() else False,
+ 'hasMain': (project_dir / 'main.py').exists(),
+ 'hasVenv': (project_dir / 'venv').exists(),
+ 'hasReadme': (project_dir / 'README.md').exists(),
+ 'ralph': None,
+ 'process': {'running': False, 'pid': None, 'port': None},
+ 'git': None
+ }
+
+ # Ralph status
+ if prd_json.exists():
+ try:
+ prd = json.loads(prd_json.read_text())
+ stories = prd.get('userStories', [])
+ complete = sum(1 for s in stories if s.get('passes'))
+
+ # Check ralph PID
+ ralph_pid = None
+ ralph_running = False
+ pid_file = ralph_dir / '.ralph.pid'
+ if pid_file.exists():
+ try:
+ pid = int(pid_file.read_text().strip())
+ os.kill(pid, 0) # Check if alive
+ ralph_running = True
+ ralph_pid = pid
+ except (ValueError, ProcessLookupError, PermissionError):
+ pass
+
+ # Last iteration time from logs
+ last_iter = None
+ logs_dir = ralph_dir / 'logs'
+ if logs_dir.exists():
+ log_files = sorted(logs_dir.glob('iteration-*.log'), key=lambda f: f.stat().st_mtime, reverse=True)
+ if log_files:
+ mtime = log_files[0].stat().st_mtime
+ last_iter = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M')
+
+ tech = prd.get('techStack', {})
+ proj['ralph'] = {
+ 'running': ralph_running,
+ 'pid': ralph_pid,
+ 'storiesTotal': len(stories),
+ 'storiesComplete': complete,
+ 'lastIteration': last_iter,
+ 'stories': [
+ {'id': s.get('id', ''), 'title': s.get('title', ''), 'passes': s.get('passes', False)}
+ for s in stories
+ ]
+ }
+ proj['techStack'] = {
+ 'type': tech.get('type', ''),
+ 'commands': tech.get('commands', {}),
+ 'port': tech.get('port'),
+ }
+ except (json.JSONDecodeError, IOError):
+ pass
+
+ # Check if main.py is running
+ if proj['hasMain']:
+ try:
+ result = subprocess.run(
+ ['pgrep', '-f', f'python.*{project_dir.name}/main.py'],
+ capture_output=True, text=True, timeout=3
+ )
+ if result.stdout.strip():
+ pids = result.stdout.strip().split('\n')
+ port = None
+ if prd_json.exists():
+ try:
+ prd_data = json.loads(prd_json.read_text())
+ port = prd_data.get('techStack', {}).get('port')
+ except (json.JSONDecodeError, IOError):
+ pass
+ proj['process'] = {
+ 'running': True,
+ 'pid': int(pids[0]),
+ 'port': port
+ }
+ except Exception:
+ pass
+
+ # Git info
+ if (project_dir / '.git').exists():
+ try:
+ branch = subprocess.run(
+ ['git', 'branch', '--show-current'],
+ cwd=project_dir, capture_output=True, text=True, timeout=5
+ ).stdout.strip()
+
+ last_commit = subprocess.run(
+ ['git', 'log', '-1', '--format=%h - %s'],
+ cwd=project_dir, capture_output=True, text=True, timeout=5
+ ).stdout.strip()
+
+ status_out = subprocess.run(
+ ['git', 'status', '--short'],
+ cwd=project_dir, capture_output=True, text=True, timeout=5
+ ).stdout.strip()
+ uncommitted = len([l for l in status_out.split('\n') if l.strip()]) if status_out else 0
+
+ proj['git'] = {
+ 'branch': branch,
+ 'lastCommit': last_commit,
+ 'uncommitted': uncommitted
+ }
+ except Exception:
+ pass
+
+ projects.append(proj)
+
+ self.send_json({'projects': projects})
+ except Exception as e:
+ self.send_json({'error': str(e)}, 500)
+
+ def _read_post_json(self):
+ """Helper to read JSON POST body."""
+ content_length = int(self.headers['Content-Length'])
+ post_data = self.rfile.read(content_length).decode('utf-8')
+ return json.loads(post_data)
+
+ def _validate_project(self, name):
+ """Validate project name and return its path, or None."""
+ if not name or '/' in name or '..' in name:
+ return None
+ project_dir = WORKSPACE_DIR / name
+ if not project_dir.exists() or not project_dir.is_dir():
+ return None
+ # Ensure it resolves within workspace
+ if not str(project_dir.resolve()).startswith(str(WORKSPACE_DIR)):
+ return None
+ return project_dir
+
+ def handle_workspace_run(self):
+ """Start a project process (main.py, ralph.sh, or pytest)."""
+ try:
+ data = self._read_post_json()
+ project_name = data.get('project', '')
+ command = data.get('command', '')
+
+ project_dir = self._validate_project(project_name)
+ if not project_dir:
+ self.send_json({'success': False, 'error': 'Invalid project'}, 400)
+ return
+
+ allowed_commands = {'main', 'ralph', 'test'}
+ if command not in allowed_commands:
+ self.send_json({'success': False, 'error': f'Invalid command. Allowed: {", ".join(allowed_commands)}'}, 400)
+ return
+
+ ralph_dir = project_dir / 'scripts' / 'ralph'
+
+ if command == 'main':
+ main_py = project_dir / 'main.py'
+ if not main_py.exists():
+ self.send_json({'success': False, 'error': 'No main.py found'}, 404)
+ return
+
+ # Use venv python if available
+ venv_python = project_dir / 'venv' / 'bin' / 'python'
+ python_cmd = str(venv_python) if venv_python.exists() else sys.executable
+
+ log_path = ralph_dir / 'logs' / 'main.log' if ralph_dir.exists() else project_dir / 'main.log'
+ log_path.parent.mkdir(parents=True, exist_ok=True)
+
+ with open(log_path, 'a') as log_file:
+ proc = subprocess.Popen(
+ [python_cmd, 'main.py'],
+ cwd=str(project_dir),
+ stdout=log_file,
+ stderr=log_file,
+ start_new_session=True
+ )
+
+ self.send_json({'success': True, 'pid': proc.pid, 'log': str(log_path)})
+
+ elif command == 'ralph':
+ ralph_sh = ralph_dir / 'ralph.sh'
+ if not ralph_sh.exists():
+ self.send_json({'success': False, 'error': 'No ralph.sh found'}, 404)
+ return
+
+ log_path = ralph_dir / 'logs' / 'ralph.log'
+ log_path.parent.mkdir(parents=True, exist_ok=True)
+
+ with open(log_path, 'a') as log_file:
+ proc = subprocess.Popen(
+ ['bash', str(ralph_sh)],
+ cwd=str(project_dir),
+ stdout=log_file,
+ stderr=log_file,
+ start_new_session=True
+ )
+
+ # Write PID
+ pid_file = ralph_dir / '.ralph.pid'
+ pid_file.write_text(str(proc.pid))
+
+ self.send_json({'success': True, 'pid': proc.pid, 'log': str(log_path)})
+
+ elif command == 'test':
+ # Run pytest synchronously (with timeout)
+ venv_python = project_dir / 'venv' / 'bin' / 'python'
+ python_cmd = str(venv_python) if venv_python.exists() else sys.executable
+
+ result = subprocess.run(
+ [python_cmd, '-m', 'pytest', '-v', '--tb=short'],
+ cwd=str(project_dir),
+ capture_output=True, text=True,
+ timeout=120
+ )
+
+ self.send_json({
+ 'success': result.returncode == 0,
+ 'output': result.stdout + result.stderr,
+ 'returncode': result.returncode
+ })
+
+ except subprocess.TimeoutExpired:
+ self.send_json({'success': False, 'error': 'Test timeout (120s)'}, 500)
+ except Exception as e:
+ self.send_json({'success': False, 'error': str(e)}, 500)
+
+ def handle_workspace_stop(self):
+ """Stop a project process."""
+ try:
+ data = self._read_post_json()
+ project_name = data.get('project', '')
+ target = data.get('target', '')
+
+ project_dir = self._validate_project(project_name)
+ if not project_dir:
+ self.send_json({'success': False, 'error': 'Invalid project'}, 400)
+ return
+
+ if target not in ('main', 'ralph'):
+ self.send_json({'success': False, 'error': 'Invalid target. Use: main, ralph'}, 400)
+ return
+
+ if target == 'ralph':
+ pid_file = project_dir / 'scripts' / 'ralph' / '.ralph.pid'
+ if pid_file.exists():
+ try:
+ pid = int(pid_file.read_text().strip())
+ # Verify the process belongs to our user and is within workspace
+ proc_cwd = Path(f'/proc/{pid}/cwd').resolve()
+ if str(proc_cwd).startswith(str(WORKSPACE_DIR)):
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
+ self.send_json({'success': True, 'message': f'Ralph stopped (PID {pid})'})
+ else:
+ self.send_json({'success': False, 'error': 'Process not in workspace'}, 403)
+ except ProcessLookupError:
+ self.send_json({'success': True, 'message': 'Process already stopped'})
+ except PermissionError:
+ self.send_json({'success': False, 'error': 'Permission denied'}, 403)
+ else:
+ self.send_json({'success': False, 'error': 'No PID file found'}, 404)
+
+ elif target == 'main':
+ # Find main.py process for this project
+ try:
+ result = subprocess.run(
+ ['pgrep', '-f', f'python.*{project_dir.name}/main.py'],
+ capture_output=True, text=True, timeout=3
+ )
+ if result.stdout.strip():
+ pid = int(result.stdout.strip().split('\n')[0])
+ proc_cwd = Path(f'/proc/{pid}/cwd').resolve()
+ if str(proc_cwd).startswith(str(WORKSPACE_DIR)):
+ os.kill(pid, signal.SIGTERM)
+ self.send_json({'success': True, 'message': f'Main stopped (PID {pid})'})
+ else:
+ self.send_json({'success': False, 'error': 'Process not in workspace'}, 403)
+ else:
+ self.send_json({'success': True, 'message': 'No running process found'})
+ except Exception as e:
+ self.send_json({'success': False, 'error': str(e)}, 500)
+
+ except Exception as e:
+ self.send_json({'success': False, 'error': str(e)}, 500)
+
+ def handle_workspace_git_diff(self):
+ """Get git diff for a workspace project."""
+ try:
+ parsed = urlparse(self.path)
+ params = parse_qs(parsed.query)
+ project_name = params.get('project', [''])[0]
+
+ project_dir = self._validate_project(project_name)
+ if not project_dir:
+ self.send_json({'error': 'Invalid project'}, 400)
+ return
+
+ if not (project_dir / '.git').exists():
+ self.send_json({'error': 'Not a git repository'}, 400)
+ return
+
+ status = subprocess.run(
+ ['git', 'status', '--short'],
+ cwd=str(project_dir), capture_output=True, text=True, timeout=10
+ ).stdout.strip()
+
+ diff = subprocess.run(
+ ['git', 'diff'],
+ cwd=str(project_dir), capture_output=True, text=True, timeout=10
+ ).stdout
+
+ diff_cached = subprocess.run(
+ ['git', 'diff', '--cached'],
+ cwd=str(project_dir), capture_output=True, text=True, timeout=10
+ ).stdout
+
+ combined_diff = ''
+ if diff_cached:
+ combined_diff += '=== Staged Changes ===\n' + diff_cached
+ if diff:
+ if combined_diff:
+ combined_diff += '\n'
+ combined_diff += '=== Unstaged Changes ===\n' + diff
+
+ self.send_json({
+ 'project': project_name,
+ 'status': status,
+ 'diff': combined_diff,
+ 'hasDiff': bool(status)
+ })
+ except subprocess.TimeoutExpired:
+ self.send_json({'error': 'Timeout'}, 500)
+ except Exception as e:
+ self.send_json({'error': str(e)}, 500)
+
+ def handle_workspace_git_commit(self):
+ """Commit all changes in a workspace project."""
+ try:
+ data = self._read_post_json()
+ project_name = data.get('project', '')
+ message = data.get('message', '').strip()
+
+ project_dir = self._validate_project(project_name)
+ if not project_dir:
+ self.send_json({'success': False, 'error': 'Invalid project'}, 400)
+ return
+
+ if not (project_dir / '.git').exists():
+ self.send_json({'success': False, 'error': 'Not a git repository'}, 400)
+ return
+
+ # Check if there's anything to commit
+ porcelain = subprocess.run(
+ ['git', 'status', '--porcelain'],
+ cwd=str(project_dir), capture_output=True, text=True, timeout=10
+ ).stdout.strip()
+
+ if not porcelain:
+ self.send_json({'success': False, 'error': 'Nothing to commit'})
+ return
+
+ files_changed = len([l for l in porcelain.split('\n') if l.strip()])
+
+ # Auto-message if empty
+ if not message:
+ now = datetime.now().strftime('%Y-%m-%d %H:%M')
+ message = f'Update: {now} ({files_changed} files)'
+
+ # Stage all and commit
+ subprocess.run(
+ ['git', 'add', '-A'],
+ cwd=str(project_dir), capture_output=True, text=True, timeout=10
+ )
+
+ result = subprocess.run(
+ ['git', 'commit', '-m', message],
+ cwd=str(project_dir), capture_output=True, text=True, timeout=30
+ )
+
+ output = result.stdout + result.stderr
+
+ if result.returncode == 0:
+ self.send_json({
+ 'success': True,
+ 'message': message,
+ 'output': output,
+ 'filesChanged': files_changed
+ })
+ else:
+ self.send_json({'success': False, 'error': output or 'Commit failed'})
+ except subprocess.TimeoutExpired:
+ self.send_json({'success': False, 'error': 'Timeout'}, 500)
+ except Exception as e:
+ self.send_json({'success': False, 'error': str(e)}, 500)
+
+ def _ensure_gitea_remote(self, project_dir, project_name):
+ """Create Gitea repo and add remote if no origin exists. Returns (ok, message)."""
+ import urllib.request
+
+ if not GITEA_TOKEN:
+ return False, 'GITEA_TOKEN not set'
+
+ # Create repo via Gitea API
+ api_url = f'{GITEA_URL}/api/v1/orgs/{GITEA_ORG}/repos'
+ payload = json.dumps({'name': project_name, 'private': True, 'auto_init': False}).encode()
+ req = urllib.request.Request(api_url, data=payload, method='POST', headers={
+ 'Authorization': f'token {GITEA_TOKEN}',
+ 'Content-Type': 'application/json'
+ })
+ try:
+ resp = urllib.request.urlopen(req, timeout=15)
+ resp.read()
+ except urllib.error.HTTPError as e:
+ body = e.read().decode(errors='replace')
+ if e.code == 409:
+ pass # repo already exists, fine
+ else:
+ return False, f'Gitea API error {e.code}: {body}'
+
+ # Add remote with token auth
+ remote_url = f'{GITEA_URL}/{GITEA_ORG}/{project_name}.git'
+ # Insert token into URL for push auth
+ auth_url = remote_url.replace('https://', f'https://gitea:{GITEA_TOKEN}@')
+ subprocess.run(
+ ['git', 'remote', 'add', 'origin', auth_url],
+ cwd=str(project_dir), capture_output=True, text=True, timeout=5
+ )
+ return True, f'Created repo {GITEA_ORG}/{project_name}'
+
+ def handle_workspace_git_push(self):
+ """Push a workspace project to its remote, creating Gitea repo if needed."""
+ try:
+ data = self._read_post_json()
+ project_name = data.get('project', '')
+
+ project_dir = self._validate_project(project_name)
+ if not project_dir:
+ self.send_json({'success': False, 'error': 'Invalid project'}, 400)
+ return
+
+ if not (project_dir / '.git').exists():
+ self.send_json({'success': False, 'error': 'Not a git repository'}, 400)
+ return
+
+ created_msg = ''
+ # Check remote exists, create if not
+ remote_check = subprocess.run(
+ ['git', 'remote', 'get-url', 'origin'],
+ cwd=str(project_dir), capture_output=True, text=True, timeout=10
+ )
+ if remote_check.returncode != 0:
+ ok, msg = self._ensure_gitea_remote(project_dir, project_name)
+ if not ok:
+ self.send_json({'success': False, 'error': msg})
+ return
+ created_msg = msg + '\n'
+
+ # Push (set upstream on first push)
+ result = subprocess.run(
+ ['git', 'push', '-u', 'origin', 'HEAD'],
+ cwd=str(project_dir), capture_output=True, text=True, timeout=60
+ )
+
+ output = result.stdout + result.stderr
+
+ if result.returncode == 0:
+ self.send_json({'success': True, 'output': created_msg + (output or 'Pushed successfully')})
+ else:
+ self.send_json({'success': False, 'error': output or 'Push failed'})
+ except subprocess.TimeoutExpired:
+ self.send_json({'success': False, 'error': 'Push timeout (60s)'}, 500)
+ except Exception as e:
+ self.send_json({'success': False, 'error': str(e)}, 500)
+
+ def handle_workspace_delete(self):
+ """Delete a workspace project."""
+ try:
+ data = self._read_post_json()
+ project_name = data.get('project', '')
+ confirm = data.get('confirm', '')
+
+ project_dir = self._validate_project(project_name)
+ if not project_dir:
+ self.send_json({'success': False, 'error': 'Invalid project'}, 400)
+ return
+
+ if confirm != project_name:
+ self.send_json({'success': False, 'error': 'Confirmation does not match project name'}, 400)
+ return
+
+ # Check for running processes
+ try:
+ result = subprocess.run(
+ ['pgrep', '-f', f'{project_dir.name}/(main\\.py|ralph)'],
+ capture_output=True, text=True, timeout=5
+ )
+ if result.stdout.strip():
+ self.send_json({'success': False, 'error': 'Project has running processes. Stop them first.'})
+ return
+ except subprocess.TimeoutExpired:
+ pass
+
+ shutil.rmtree(str(project_dir))
+
+ self.send_json({
+ 'success': True,
+ 'message': f'Project {project_name} deleted'
+ })
+ except Exception as e:
+ self.send_json({'success': False, 'error': str(e)}, 500)
+
+ def handle_workspace_logs(self):
+ """Get last N lines from a project log."""
+ try:
+ parsed = urlparse(self.path)
+ params = parse_qs(parsed.query)
+
+ project_name = params.get('project', [''])[0]
+ log_type = params.get('type', ['ralph'])[0]
+ lines_count = min(int(params.get('lines', ['100'])[0]), 500)
+
+ project_dir = self._validate_project(project_name)
+ if not project_dir:
+ self.send_json({'error': 'Invalid project'}, 400)
+ return
+
+ ralph_dir = project_dir / 'scripts' / 'ralph'
+
+ # Determine log file
+ if log_type == 'ralph':
+ log_file = ralph_dir / 'logs' / 'ralph.log'
+ if not log_file.exists():
+ # Try ralph-test.log
+ log_file = ralph_dir / 'logs' / 'ralph-test.log'
+ elif log_type == 'main':
+ log_file = ralph_dir / 'logs' / 'main.log' if ralph_dir.exists() else project_dir / 'main.log'
+ elif log_type == 'progress':
+ log_file = ralph_dir / 'progress.txt'
+ else:
+ # Try iteration log
+ if log_type.startswith('iteration-'):
+ log_file = ralph_dir / 'logs' / f'{log_type}.log'
+ else:
+ self.send_json({'error': 'Invalid log type'}, 400)
+ return
+
+ if not log_file.exists():
+ self.send_json({
+ 'project': project_name,
+ 'type': log_type,
+ 'lines': [],
+ 'total': 0
+ })
+ return
+
+ # Security: ensure path is within workspace
+ if not str(log_file.resolve()).startswith(str(WORKSPACE_DIR)):
+ self.send_json({'error': 'Access denied'}, 403)
+ return
+
+ content = log_file.read_text(encoding='utf-8', errors='replace')
+ all_lines = content.split('\n')
+ total = len(all_lines)
+ last_lines = all_lines[-lines_count:] if len(all_lines) > lines_count else all_lines
+
+ self.send_json({
+ 'project': project_name,
+ 'type': log_type,
+ 'lines': last_lines,
+ 'total': total
+ })
+ except Exception as e:
+ self.send_json({'error': str(e)}, 500)
+
def handle_youtube(self):
try:
content_length = int(self.headers['Content-Length'])
diff --git a/dashboard/files.html b/dashboard/files.html
index c7414f8..dff270f 100644
--- a/dashboard/files.html
+++ b/dashboard/files.html
@@ -838,6 +838,10 @@
Tasks
+
+
+ Workspace
+
KB
diff --git a/dashboard/index.html b/dashboard/index.html
index f6a6cfa..e15431c 100644
--- a/dashboard/index.html
+++ b/dashboard/index.html
@@ -1063,6 +1063,10 @@
Dashboard
+
+
+ Workspace
+
KB
diff --git a/dashboard/notes.html b/dashboard/notes.html
index fd64afe..6918f3b 100644
--- a/dashboard/notes.html
+++ b/dashboard/notes.html
@@ -688,6 +688,10 @@
Tasks
+
+
+ Workspace
+
KB
diff --git a/dashboard/workspace.html b/dashboard/workspace.html
new file mode 100644
index 0000000..198d938
--- /dev/null
+++ b/dashboard/workspace.html
@@ -0,0 +1,1021 @@
+
+
+
+
+
+
+ Echo · Workspace
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/memory/2026-02-09.md b/memory/2026-02-09.md
new file mode 100644
index 0000000..9586b85
--- /dev/null
+++ b/memory/2026-02-09.md
@@ -0,0 +1,29 @@
+# 2026-02-09 (Duminica)
+
+## Test Ralph Workflow - SUCCES
+- Creat proiect test `file-monitor` in ~/workspace/
+- Ralph a completat 4/4 stories in ~12 minute, 4 iteratii
+- Cost: ~$3.50 (Sonnet pentru implementare)
+- 8 commits git, 28+ teste pytest
+- Workflow: PRD generator → prd.json → ralph.sh → Claude Code autonom → commit
+
+## Simplificare AGENTS.md
+- Redus sectiunea "Proiecte/Features Workflow" de la ~110 linii la ~15 linii
+- Detaliile raman in cron jobs (evening-report, night-execute)
+
+## Actualizare Evening Report
+- Adaugat context proiecte prioritare: roa2web, Chatbot Maria
+- Regula: rapoarte ROA → feature roa2web, NU proiect separat
+- Surse inspiratie: intrebari frecvente clienti, notes, insights
+- Features existente prioritare fata de proiecte noi
+
+## Context Marius (interviu proiecte)
+- **ROA clienti:** rapoarte, interfata web, notificari
+- **Pain point:** raspunde la intrebari similare de la clienti
+- **Chatbot Maria:** Flowise LXC 104, document store XML/MD, Groq+Ollama, ngrok→romfast.ro
+- **Angajat nou:** poate mentine documentatia chatbot (scrie TXT)
+- **roa2web:** are balanta, facturi, trezorerie; lipsesc validari ANAF, facturare valuta
+- **Vending Master:** trebuie verificat rapoarte financiare XLSX
+
+## Prompt creat
+- `prompts/workspace-panel.md` - panou Workspace Projects pentru dashboard Echo
diff --git a/memory/kb/index.json b/memory/kb/index.json
index 4931893..c37fb6f 100644
--- a/memory/kb/index.json
+++ b/memory/kb/index.json
@@ -21,6 +21,34 @@
"video": "",
"tldr": "Monica Ion lucrează cu Mark pe relația cu angajații și frica de pierdere. Mark vine agitat cu teama de a pierde un angajat tehnic genial. Monica folosește **legea transformării** pentru a vindeca dure..."
},
+ {
+ "file": "notes-data/tools/ralph-workflow.md",
+ "title": "Ralph Workflow - Sistem Complet",
+ "date": "2026-02-09",
+ "tags": [],
+ "domains": [],
+ "types": [],
+ "category": "tools",
+ "project": null,
+ "subdir": null,
+ "video": "",
+ "tldr": "**Next:** Integrare night-execute"
+ },
+ {
+ "file": "memory/2026-02-09.md",
+ "title": "2026-02-09 (Duminica)",
+ "date": "2026-02-09",
+ "tags": [],
+ "domains": [],
+ "types": [
+ "memory"
+ ],
+ "category": "memory",
+ "project": null,
+ "subdir": null,
+ "video": "",
+ "tldr": "- `prompts/workspace-panel.md` - panou Workspace Projects pentru dashboard Echo"
+ },
{
"file": "notes-data/coaching/2026-02-08-dimineata.md",
"title": "Gândul de dimineață - 2026-02-08",
@@ -2193,8 +2221,8 @@
"title": "Proiect: Vending Master - Integrare Website → ROA",
"date": "2026-01-30",
"tags": [
- "vending-master",
- "integrare"
+ "integrare",
+ "vending-master"
],
"domains": [
"work"
@@ -2644,7 +2672,7 @@
}
],
"stats": {
- "total": 149,
+ "total": 151,
"by_domain": {
"work": 41,
"health": 25,
@@ -2663,9 +2691,9 @@
"projects": 44,
"reflectii": 3,
"retete": 1,
- "tools": 4,
+ "tools": 5,
"youtube": 42,
- "memory": 15
+ "memory": 16
}
},
"domains": [
diff --git a/memory/kb/tehnici-pauza.md b/memory/kb/tehnici-pauza.md
index ae38c9d..2a9766e 100644
--- a/memory/kb/tehnici-pauza.md
+++ b/memory/kb/tehnici-pauza.md
@@ -71,9 +71,9 @@
### 3 Întrebări Anti-Blocaj
- Când te simți blocat/victimă, întreabă-te rapid:
- 1. "Ce depinde de MINE acum?"
- 2. "Care e cel mai mic pas pe care pot să-l inițiez?"
- 3. "Ce pot face EU chiar acum?"
+ 1. **Ce pot controla eu în situația asta?**
+ 2. **Ce aleg să fac acum?**
+ 3. **Ce vreau să fie diferit - și ce fac pentru asta?**
- **Rezultat:** Mută atenția de la ce NU poți controla la ce POȚI - scoate din paralizie instant
- **Sursă:** [Zoltan Vereș - Starea de Victimă](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-02_zoltan-veres-victima-complet.md)
diff --git a/skills/ralph/templates/ralph.sh b/skills/ralph/templates/ralph.sh
index b335ea7..0ab755e 100755
--- a/skills/ralph/templates/ralph.sh
+++ b/skills/ralph/templates/ralph.sh
@@ -83,6 +83,42 @@ 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"