feat(projects): approval guard + worktree-aware ralph execution
Two structural fixes that together let users manage feature-branch
work without manual intervention:
Approval guard — `/plan/start` returns 409 `already_committed` if the
project status is approved/running/complete, unless the body opts in
with `force=true`. Frontend now renders "Re-planifică" instead of
"Planifică" on approved cards and gates it behind a confirm dialog
that threads `force=true` through. Prevents an accidental click from
wiping `status=approved` and burning a fresh planning subprocess.
Worktree awareness — projects can now declare that they target a
feature branch on an existing Gitea repo, not a repo-per-slug clone.
Three optional fields added to approved-tasks.json: `repo` (default
= slug), `branch` (feature branch to create), `base_branch` (default
main). Wired through `/p` flag parser in router.py, the dashboard
Propose modal's new "Avansat" section, and the night-execute prompt
which clones {repo} and creates {branch} from {base_branch} before
running ralph.
CLAUDE.md updated with both flows + the new schema fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
25
CLAUDE.md
25
CLAUDE.md
@@ -199,7 +199,7 @@ Pe **WhatsApp**: text-only — meniu redirect la Discord/Telegram. **Text-keywor
|
|||||||
|
|
||||||
| Path | Rol |
|
| Path | Rol |
|
||||||
|------|-----|
|
|------|-----|
|
||||||
| `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}` |
|
| `approved-tasks.json` | Coordonare între cron jobs + UX. Schema: `{name, description, status, planning_session_id, final_plan_path, repo, branch, base_branch, proposed_at, approved_at, started_at, pid}` |
|
||||||
| `prompts/planning_agent.md` | System prompt pentru `PlanningSession` (multi-fază conversational) |
|
| `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_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 |
|
| `src/planning_orchestrator.py` | Coordonează fazele: fresh subprocess per skill phase; coordinează prin disk artifacts gstack convention; tag detection ui-scope |
|
||||||
@@ -229,6 +229,29 @@ Pe **WhatsApp**: text-only — meniu redirect la Discord/Telegram. **Text-keywor
|
|||||||
- Self-improvement echo-core NUMAI pe branch `ralph/echo-improve`, niciodată pe master
|
- Self-improvement echo-core NUMAI pe branch `ralph/echo-improve`, niciodată pe master
|
||||||
- Clone-urile folosesc `GITEA_TOKEN` din `dashboard/.env`: `https://moltbot:${TOKEN}@gitea.romfast.ro/romfast/<name>.git`
|
- Clone-urile folosesc `GITEA_TOKEN` din `dashboard/.env`: `https://moltbot:${TOKEN}@gitea.romfast.ro/romfast/<name>.git`
|
||||||
|
|
||||||
|
### Features pe repo-uri existente (worktree-aware)
|
||||||
|
|
||||||
|
Slug-ul proiectului nu trebuie să corespundă cu un repo Gitea. Pentru o feature pe un repo existent (ex: `roa2web-telegram-bonuri` ca feature pe `roa2web`), folosește câmpurile opționale `repo`, `branch`, `base_branch`:
|
||||||
|
|
||||||
|
- **`repo`** — numele repo-ului Gitea de clonat (default: slug-ul proiectului).
|
||||||
|
- **`branch`** — feature branch nou care va fi creat după clone (default: niciunul, ralph lucrează pe HEAD-ul default).
|
||||||
|
- **`base_branch`** — branch-ul de la care porneste `branch` (default: `main`).
|
||||||
|
|
||||||
|
Cum le setezi:
|
||||||
|
- **CLI/chat:** `/p <slug> --repo <name> --branch <feature> [--base-branch <name>] <descriere>` (parser în `_ralph_propose` la `src/router.py`).
|
||||||
|
- **Dashboard:** modal Propose → secțiunea „Avansat" cu câmpuri pentru repo/branch/base_branch.
|
||||||
|
|
||||||
|
Night-execute (`cron/jobs.json`) detectează câmpurile și clonează `repo` în `~/workspace/<slug>/`, apoi `git checkout -b <branch> <base_branch>` dacă `branch` e setat. Dacă clone-ul eșuează (repo inexistent), proiectul e marcat `failed` fără să mai pornească ralph.
|
||||||
|
|
||||||
|
### Approval guard — protejare împotriva re-planning accidental
|
||||||
|
|
||||||
|
`/plan/start` (POST `/api/projects/<slug>/plan/start`) refuză cu 409 `already_committed` dacă proiectul e deja `approved`/`running`/`complete`. Pentru a re-iniția planning-ul intenționat:
|
||||||
|
|
||||||
|
- **Dashboard:** butonul „Re-planifică" pe cards aprobate cere confirm explicit înainte să trimită `force=true` în body.
|
||||||
|
- **API direct:** trimite `{"force": true, "description": "..."}` în body-ul de la `/plan/start`.
|
||||||
|
|
||||||
|
Asta previne situația în care un click accidental pe „Planifică" șterge `status=approved` și pornește un nou subprocess Claude (cu cost asociat).
|
||||||
|
|
||||||
## Convenție import-uri
|
## Convenție import-uri
|
||||||
|
|
||||||
Import-uri absolute via `sys.path.insert(0, PROJECT_ROOT)`: `from src.config import ...`, `from src.adapters.discord_bot import ...`. Fără import-uri circulare.
|
Import-uri absolute via `sys.path.insert(0, PROJECT_ROOT)`: `from src.config import ...`, `from src.adapters.discord_bot import ...`. Fără import-uri circulare.
|
||||||
|
|||||||
@@ -269,9 +269,9 @@
|
|||||||
"prompt": "Heartbeat check. Rulează src/heartbeat.py printr-un scurt raport de status.\nDacă nu e nimic de raportat (email=0, calendar nu are evenimente <2h, kb ok), răspunde doar cu HEARTBEAT_OK și oprește-te — nu trimite mesaj.\nDacă e ceva: raport scurt pe Discord #echo-work.",
|
"prompt": "Heartbeat check. Rulează src/heartbeat.py printr-un scurt raport de status.\nDacă nu e nimic de raportat (email=0, calendar nu are evenimente <2h, kb ok), răspunde doar cu HEARTBEAT_OK și oprește-te — nu trimite mesaj.\nDacă e ceva: raport scurt pe Discord #echo-work.",
|
||||||
"allowed_tools": [],
|
"allowed_tools": [],
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-05-05T06:00:00.002794+00:00",
|
"last_run": "2026-05-05T08:00:00.002199+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-05-05T08:00:00+00:00"
|
"next_run": "2026-05-05T10:00:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "night-execute",
|
"name": "night-execute",
|
||||||
@@ -279,7 +279,7 @@
|
|||||||
"channel": "echo-work",
|
"channel": "echo-work",
|
||||||
"model": "opus",
|
"model": "opus",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"prompt": "NIGHT-EXECUTE - Implementare autonoma proiecte aprobate\n\n## PASUL 1: Citeste proiectele aprobate\n\nCiteste /home/moltbot/echo-core/approved-tasks.json\nSelecteaza proiectele cu status='approved'\nDaca nu sunt proiecte aprobate: raporteaza pe Discord si opreste-te.\n\n## PASUL 2: Pentru fiecare proiect aprobat\n\n1. Verifica daca workspace-ul exista: /home/moltbot/workspace/{name}\n - Daca nu: TOKEN=$(grep GITEA_TOKEN /home/moltbot/echo-core/dashboard/.env | cut -d= -f2) && git clone https://moltbot:${TOKEN}@gitea.romfast.ro/romfast/{name}.git /home/moltbot/workspace/{name}\n\n2. Verifica daca prd.json exista: /home/moltbot/workspace/{name}/scripts/ralph/prd.json\n - Daca nu: ruleaza generatorul PRD:\n source .venv/bin/activate\n python3 tools/ralph_prd_generator.py \"{name}\" \"{description}\" /home/moltbot/workspace\n\n3. Lanseaza Ralph loop:\n cd /home/moltbot/workspace/{name}\n chmod +x scripts/ralph/ralph.sh\n mkdir -p scripts/ralph/logs\n nohup ./scripts/ralph/ralph.sh 15 > scripts/ralph/logs/ralph-$(date +%Y%m%d).log 2>&1 &\n echo $! > scripts/ralph/.ralph.pid\n\n4. Actualizeaza approved-tasks.json:\n - status: 'running'\n - started_at: timestamp curent\n - pid: PID din .ralph.pid\n\n## PASUL 3: Raport Discord\n\nTrimite pe echo-work:\n- Cate proiecte au pornit\n- PID-urile lor\n- 'morning-report va raporta progresul la 08:30'\n\n## REGULI IMPORTANTE\n\n- Nu modifica niciodata src/router.py, src/claude_session.py sau alte fisiere core echo-core prin Ralph\n- echo-core self-improvement NUMAI pe branch ralph/echo-improve, nu pe master\n- Daca ralph.sh esueaza: log in approved-tasks.json (status: failed, error: mesaj)\n- Delay 5 secunde intre proiecte pentru a evita rate limiting\n",
|
"prompt": "NIGHT-EXECUTE - Implementare autonoma proiecte aprobate\n\n## PASUL 1: Citeste proiectele aprobate\n\nCiteste /home/moltbot/echo-core/approved-tasks.json\nSelecteaza proiectele cu status='approved'\nDaca nu sunt proiecte aprobate: raporteaza pe Discord si opreste-te.\n\n## PASUL 2: Pentru fiecare proiect aprobat\n\nPentru un proiect cu schema extinsa (campuri optionale {repo, branch, base_branch}):\n - {name} = slug-ul proiectului (cheia 'name' din JSON)\n - {repo} = numele repo-ului Gitea (default = {name} daca nu e setat)\n - {branch} = feature branch nou (None inseamna 'lucreaza pe HEAD-ul default al repo-ului')\n - {base_branch} = branch-ul de la care porneste {branch} (default 'main')\n\n1. Verifica daca workspace-ul exista: /home/moltbot/workspace/{name}\n - Daca NU exista:\n TOKEN=$(grep GITEA_TOKEN /home/moltbot/echo-core/dashboard/.env | cut -d= -f2)\n git clone https://moltbot:${TOKEN}@gitea.romfast.ro/romfast/{repo}.git /home/moltbot/workspace/{name}\n # NOTA: cloneaza {repo}, nu {name}, ca sa suporte features pe repo-uri existente\n # (ex: slug='roa2web-bonuri', repo='roa2web')\n cd /home/moltbot/workspace/{name}\n # Daca {branch} e setat: creeaza branch nou de la {base_branch}\n if [ -n \"{branch}\" ]; then\n git fetch origin {base_branch:-main}\n git checkout {base_branch:-main}\n git checkout -b {branch} 2>/dev/null || git checkout {branch}\n fi\n - Daca EXISTA workspace-ul si {branch} e setat: asigura-te ca esti pe {branch}:\n cd /home/moltbot/workspace/{name}\n git checkout {branch} 2>/dev/null || git checkout -b {branch} {base_branch:-main}\n\n2. Verifica daca prd.json exista: /home/moltbot/workspace/{name}/scripts/ralph/prd.json\n - Daca nu: ruleaza generatorul PRD:\n source .venv/bin/activate\n python3 tools/ralph_prd_generator.py \"{name}\" \"{description}\" /home/moltbot/workspace\n\n3. Lanseaza Ralph loop:\n cd /home/moltbot/workspace/{name}\n chmod +x scripts/ralph/ralph.sh\n mkdir -p scripts/ralph/logs\n nohup ./scripts/ralph/ralph.sh 15 > scripts/ralph/logs/ralph-$(date +%Y%m%d).log 2>&1 &\n echo $! > scripts/ralph/.ralph.pid\n\n4. Actualizeaza approved-tasks.json:\n - status: 'running'\n - started_at: timestamp curent\n - pid: PID din .ralph.pid\n\n## PASUL 3: Raport Discord\n\nTrimite pe echo-work:\n- Cate proiecte au pornit\n- PID-urile lor\n- Pentru cele cu {branch} setat, mentioneaza branch-ul activ\n- 'morning-report va raporta progresul la 08:30'\n\n## REGULI IMPORTANTE\n\n- Nu modifica niciodata src/router.py, src/claude_session.py sau alte fisiere core echo-core prin Ralph\n- echo-core self-improvement NUMAI pe branch ralph/echo-improve, nu pe master\n- Daca ralph.sh esueaza: log in approved-tasks.json (status: failed, error: mesaj)\n- Daca git clone esueaza (repo inexistent): log status='failed' cu mesajul, NU continua cu PRD/ralph\n- Delay 5 secunde intre proiecte pentru a evita rate limiting\n",
|
||||||
"allowed_tools": [
|
"allowed_tools": [
|
||||||
"Bash",
|
"Bash",
|
||||||
"Read",
|
"Read",
|
||||||
|
|||||||
@@ -629,6 +629,9 @@ class ProjectsHandlers:
|
|||||||
return # parse_json_body already sent 400
|
return # parse_json_body already sent 400
|
||||||
slug = (body.get("slug") or "").strip()
|
slug = (body.get("slug") or "").strip()
|
||||||
description = (body.get("description") or "").strip()
|
description = (body.get("description") or "").strip()
|
||||||
|
repo = (body.get("repo") or "").strip() or None
|
||||||
|
branch = (body.get("branch") or "").strip() or None
|
||||||
|
base_branch = (body.get("base_branch") or "").strip() or None
|
||||||
|
|
||||||
slug_err = validate_slug(slug)
|
slug_err = validate_slug(slug)
|
||||||
desc_err = validate_description(description)
|
desc_err = validate_description(description)
|
||||||
@@ -659,6 +662,9 @@ class ProjectsHandlers:
|
|||||||
"status": "pending",
|
"status": "pending",
|
||||||
"planning_session_id": None,
|
"planning_session_id": None,
|
||||||
"final_plan_path": None,
|
"final_plan_path": None,
|
||||||
|
"repo": repo,
|
||||||
|
"branch": branch,
|
||||||
|
"base_branch": base_branch,
|
||||||
"proposed_at": _now_iso(),
|
"proposed_at": _now_iso(),
|
||||||
"approved_at": None,
|
"approved_at": None,
|
||||||
"started_at": None,
|
"started_at": None,
|
||||||
@@ -825,13 +831,30 @@ class ProjectsHandlers:
|
|||||||
if body is None:
|
if body is None:
|
||||||
return
|
return
|
||||||
description = (body.get("description") or "").strip()
|
description = (body.get("description") or "").strip()
|
||||||
|
force = bool(body.get("force"))
|
||||||
|
|
||||||
|
# Approval guard: refuse to silently overwrite approved/running/complete
|
||||||
|
# projects. Caller must opt in with force=true (the "Re-planifică" path).
|
||||||
|
existing_proj = _find_project(_read_approved(), slug)
|
||||||
|
if existing_proj and not force:
|
||||||
|
current_status = (existing_proj.get("status") or "").lower()
|
||||||
|
if current_status in {"approved", "running", "complete"}:
|
||||||
|
self.send_json({
|
||||||
|
"error": "already_committed",
|
||||||
|
"slug": slug,
|
||||||
|
"status": current_status,
|
||||||
|
"message": (
|
||||||
|
f"Proiectul `{slug}` e deja `{current_status}`. "
|
||||||
|
"Folosește «Re-planifică» (cu force=true) ca să reiei planning-ul."
|
||||||
|
),
|
||||||
|
}, 409)
|
||||||
|
return
|
||||||
|
|
||||||
# Description fallback: use the existing approved-tasks description so
|
# Description fallback: use the existing approved-tasks description so
|
||||||
# users can re-open planning on a project they already proposed.
|
# users can re-open planning on a project they already proposed.
|
||||||
if not description:
|
if not description:
|
||||||
existing = _find_project(_read_approved(), slug)
|
if existing_proj:
|
||||||
if existing:
|
description = (existing_proj.get("description") or "").strip()
|
||||||
description = (existing.get("description") or "").strip()
|
|
||||||
desc_err = validate_description(description) if description else "description required"
|
desc_err = validate_description(description) if description else "description required"
|
||||||
if desc_err:
|
if desc_err:
|
||||||
self.send_json({"error": "invalid_description", "message": desc_err}, 400)
|
self.send_json({"error": "invalid_description", "message": desc_err}, 400)
|
||||||
@@ -847,6 +870,9 @@ class ProjectsHandlers:
|
|||||||
"status": "planning",
|
"status": "planning",
|
||||||
"planning_session_id": None,
|
"planning_session_id": None,
|
||||||
"final_plan_path": None,
|
"final_plan_path": None,
|
||||||
|
"repo": None,
|
||||||
|
"branch": None,
|
||||||
|
"base_branch": None,
|
||||||
"proposed_at": _now_iso(),
|
"proposed_at": _now_iso(),
|
||||||
"approved_at": None,
|
"approved_at": None,
|
||||||
"started_at": None,
|
"started_at": None,
|
||||||
|
|||||||
@@ -930,6 +930,30 @@
|
|||||||
<input type="checkbox" id="proposeWithPlanning" checked>
|
<input type="checkbox" id="proposeWithPlanning" checked>
|
||||||
<span>Planifică cu Echo imediat după propunere</span>
|
<span>Planifică cu Echo imediat după propunere</span>
|
||||||
</label>
|
</label>
|
||||||
|
<details class="form-advanced">
|
||||||
|
<summary>Avansat — feature pe repo existent</summary>
|
||||||
|
<p class="form-hint" style="font-size:0.85em;color:var(--text-muted);margin:0.5em 0;">
|
||||||
|
Setează doar dacă slug-ul nu corespunde unui repo Gitea (e o feature pe alt repo).
|
||||||
|
</p>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="proposeRepo">Repo Gitea</label>
|
||||||
|
<input type="text" id="proposeRepo" class="form-input" name="repo"
|
||||||
|
autocomplete="off" autocapitalize="off" spellcheck="false"
|
||||||
|
placeholder="ex: roa2web (default = slug)">
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="proposeBranch">Branch nou</label>
|
||||||
|
<input type="text" id="proposeBranch" class="form-input" name="branch"
|
||||||
|
autocomplete="off" autocapitalize="off" spellcheck="false"
|
||||||
|
placeholder="ex: feature/telegram-bonuri">
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="proposeBaseBranch">Base branch</label>
|
||||||
|
<input type="text" id="proposeBaseBranch" class="form-input" name="base_branch"
|
||||||
|
autocomplete="off" autocapitalize="off" spellcheck="false"
|
||||||
|
placeholder="default: main">
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</form>
|
</form>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn-sm secondary" data-close="propose">Anulează</button>
|
<button type="button" class="btn-sm secondary" data-close="propose">Anulează</button>
|
||||||
@@ -1281,7 +1305,7 @@
|
|||||||
case 'approved':
|
case 'approved':
|
||||||
return [
|
return [
|
||||||
{ label: 'Dezaprobă', type: 'secondary', action: () => unapproveProject(slug) },
|
{ label: 'Dezaprobă', type: 'secondary', action: () => unapproveProject(slug) },
|
||||||
{ label: 'Planifică', type: 'ghost', action: () => startPlanning(slug, p.description) },
|
{ label: 'Re-planifică', type: 'ghost', action: () => replanProject(slug, p.description) },
|
||||||
];
|
];
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return [
|
return [
|
||||||
@@ -1657,6 +1681,9 @@
|
|||||||
clearProposeErrors();
|
clearProposeErrors();
|
||||||
const slug = (proposeSlug.value || '').trim();
|
const slug = (proposeSlug.value || '').trim();
|
||||||
const desc = (proposeDesc.value || '').trim();
|
const desc = (proposeDesc.value || '').trim();
|
||||||
|
const repo = (document.getElementById('proposeRepo')?.value || '').trim();
|
||||||
|
const branch = (document.getElementById('proposeBranch')?.value || '').trim();
|
||||||
|
const baseBranch = (document.getElementById('proposeBaseBranch')?.value || '').trim();
|
||||||
let invalid = false;
|
let invalid = false;
|
||||||
if (!slug || !/^[a-z0-9][a-z0-9_-]{0,49}$/i.test(slug)) {
|
if (!slug || !/^[a-z0-9][a-z0-9_-]{0,49}$/i.test(slug)) {
|
||||||
proposeSlugErr.textContent = 'Slug invalid (folosește litere/cifre/_/-, max 50)';
|
proposeSlugErr.textContent = 'Slug invalid (folosește litere/cifre/_/-, max 50)';
|
||||||
@@ -1675,10 +1702,14 @@
|
|||||||
proposeSubmit.textContent = 'Se trimite…';
|
proposeSubmit.textContent = 'Se trimite…';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const body = { slug, description: desc };
|
||||||
|
if (repo) body.repo = repo;
|
||||||
|
if (branch) body.branch = branch;
|
||||||
|
if (baseBranch) body.base_branch = baseBranch;
|
||||||
const res = await apiFetch('/echo/api/projects/propose', {
|
const res = await apiFetch('/echo/api/projects/propose', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ slug, description: desc }),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
if (res.status === 400) {
|
if (res.status === 400) {
|
||||||
const data = await safeJson(res);
|
const data = await safeJson(res);
|
||||||
@@ -1809,6 +1840,15 @@
|
|||||||
await openPlanModal(slug, true, description);
|
await openPlanModal(slug, true, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function replanProject(slug, description) {
|
||||||
|
const ok = confirm(
|
||||||
|
'Anulezi aprobarea curentă pentru `' + slug + '` și restartezi planning-ul?\n\n' +
|
||||||
|
'Aprobarea (data și final-plan.md) va fi pierdută; vei reintra în Office hours.'
|
||||||
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
await openPlanModal(slug, true, description, /* force */ true);
|
||||||
|
}
|
||||||
|
|
||||||
async function rollbackProject(slug) {
|
async function rollbackProject(slug) {
|
||||||
if (!confirm('Rollback (git revert HEAD) pe ' + slug + '? Decrementează ultima poveste trecută.')) return;
|
if (!confirm('Rollback (git revert HEAD) pe ' + slug + '? Decrementează ultima poveste trecută.')) return;
|
||||||
try {
|
try {
|
||||||
@@ -2075,12 +2115,45 @@
|
|||||||
setPhase(null, []);
|
setPhase(null, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openPlanModal(slug, startIfMissing, descriptionHint) {
|
async function openPlanModal(slug, startIfMissing, descriptionHint, force) {
|
||||||
resetPlanModal();
|
resetPlanModal();
|
||||||
state.planning.slug = slug;
|
state.planning.slug = slug;
|
||||||
planSlugEl.textContent = slug;
|
planSlugEl.textContent = slug;
|
||||||
openModal(planModal, { focusSelector: '#composerInput' });
|
openModal(planModal, { focusSelector: '#composerInput' });
|
||||||
|
|
||||||
|
// Re-planning: skip transcript reuse, go straight to fresh start.
|
||||||
|
if (force) {
|
||||||
|
appendTypingIndicator();
|
||||||
|
startElapsedCounter();
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/echo/api/projects/' + encodeURIComponent(slug) + '/plan/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ description: descriptionHint || '', force: true }),
|
||||||
|
});
|
||||||
|
removeTypingIndicator();
|
||||||
|
stopElapsedCounter();
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await safeJson(res);
|
||||||
|
if (data) {
|
||||||
|
setPhase(data.phase || '/office-hours', []);
|
||||||
|
if (data.message) appendMessage('assistant', data.message);
|
||||||
|
}
|
||||||
|
await fetchProjects();
|
||||||
|
} else {
|
||||||
|
const d = await safeJson(res);
|
||||||
|
appendMessage('assistant', '_Eroare la re-planning: ' + ((d && d.message) || ('HTTP ' + res.status)) + '_');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
removeTypingIndicator();
|
||||||
|
stopElapsedCounter();
|
||||||
|
if (err.message !== 'unauthorized') {
|
||||||
|
appendMessage('assistant', '_Eroare: ' + (err.message || err) + '_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to fetch existing transcript first
|
// Try to fetch existing transcript first
|
||||||
try {
|
try {
|
||||||
const tRes = await apiFetch('/echo/api/projects/' + encodeURIComponent(slug) + '/plan/transcript');
|
const tRes = await apiFetch('/echo/api/projects/' + encodeURIComponent(slug) + '/plan/transcript');
|
||||||
@@ -2133,8 +2206,14 @@
|
|||||||
await fetchProjects();
|
await fetchProjects();
|
||||||
} else {
|
} else {
|
||||||
const d = await safeJson(res);
|
const d = await safeJson(res);
|
||||||
|
if (res.status === 409 && d && d.error === 'already_committed') {
|
||||||
|
appendMessage('assistant',
|
||||||
|
'_' + (d.message || 'Proiectul e deja committed.') +
|
||||||
|
' Folosește «Re-planifică» dacă vrei să o iei de la zero._');
|
||||||
|
} else {
|
||||||
appendMessage('assistant', '_Eroare la pornirea planning: ' + ((d && d.message) || ('HTTP ' + res.status)) + '_');
|
appendMessage('assistant', '_Eroare la pornirea planning: ' + ((d && d.message) || ('HTTP ' + res.status)) + '_');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
removeTypingIndicator();
|
removeTypingIndicator();
|
||||||
stopElapsedCounter();
|
stopElapsedCounter();
|
||||||
|
|||||||
@@ -325,12 +325,40 @@ def _try_ralph_dispatch(text: str, adapter_name: str | None = None) -> str | Non
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_propose_flags(text: str) -> tuple[dict, str]:
|
||||||
|
"""Strip leading --repo/--branch/--base-branch flags from text.
|
||||||
|
|
||||||
|
Returns (flags_dict, remaining_text). Flags are accepted in any order before
|
||||||
|
the description. Unknown tokens are left in remaining_text.
|
||||||
|
"""
|
||||||
|
tokens = text.split()
|
||||||
|
flags: dict[str, str] = {}
|
||||||
|
consumed = 0
|
||||||
|
while consumed < len(tokens):
|
||||||
|
tok = tokens[consumed]
|
||||||
|
if tok in ("--repo", "--branch", "--base-branch") and consumed + 1 < len(tokens):
|
||||||
|
key = tok.lstrip("-").replace("-", "_")
|
||||||
|
flags[key] = tokens[consumed + 1]
|
||||||
|
consumed += 2
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return flags, " ".join(tokens[consumed:]).strip()
|
||||||
|
|
||||||
|
|
||||||
def _ralph_propose(slug: str, description: str) -> str:
|
def _ralph_propose(slug: str, description: str) -> str:
|
||||||
"""Adaugă un proiect cu status pending în approved-tasks.json.
|
"""Adaugă un proiect cu status pending în approved-tasks.json.
|
||||||
|
|
||||||
Schema includes the W2 planning fields (`planning_session_id`,
|
Description may be prefixed with optional flags:
|
||||||
`final_plan_path`) so the orchestrator and PRD generator can find them.
|
--repo <name> Gitea repo to clone (default: slug)
|
||||||
|
--branch <name> Feature branch to create (default: none → main)
|
||||||
|
--base-branch <name> Branch to fork from (default: main)
|
||||||
|
|
||||||
|
Example: /p roa2web-bonuri --repo roa2web --branch feature/bonuri "<descriere>"
|
||||||
"""
|
"""
|
||||||
|
flags, description = _parse_propose_flags(description)
|
||||||
|
if not description:
|
||||||
|
return "Descriere lipsă după flag-uri. Folosire: /p <slug> [--repo X --branch Y] <descriere>"
|
||||||
|
|
||||||
data = _load_approved_tasks()
|
data = _load_approved_tasks()
|
||||||
|
|
||||||
for p in data["projects"]:
|
for p in data["projects"]:
|
||||||
@@ -343,13 +371,22 @@ def _ralph_propose(slug: str, description: str) -> str:
|
|||||||
"status": "pending",
|
"status": "pending",
|
||||||
"planning_session_id": None,
|
"planning_session_id": None,
|
||||||
"final_plan_path": None,
|
"final_plan_path": None,
|
||||||
|
"repo": flags.get("repo"),
|
||||||
|
"branch": flags.get("branch"),
|
||||||
|
"base_branch": flags.get("base_branch"),
|
||||||
"proposed_at": datetime.now(timezone.utc).isoformat(),
|
"proposed_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"approved_at": None,
|
"approved_at": None,
|
||||||
"started_at": None,
|
"started_at": None,
|
||||||
"pid": None,
|
"pid": None,
|
||||||
})
|
})
|
||||||
_save_approved_tasks(data)
|
_save_approved_tasks(data)
|
||||||
return f"📋 Adăugat: {slug}\n └ {description}\n\nAprobă cu: /a {slug}"
|
|
||||||
|
extras = []
|
||||||
|
if flags.get("repo"): extras.append(f"repo={flags['repo']}")
|
||||||
|
if flags.get("branch"): extras.append(f"branch={flags['branch']}")
|
||||||
|
if flags.get("base_branch"): extras.append(f"base={flags['base_branch']}")
|
||||||
|
extras_str = f"\n └ {' · '.join(extras)}" if extras else ""
|
||||||
|
return f"📋 Adăugat: {slug}{extras_str}\n └ {description}\n\nAprobă cu: /a {slug}"
|
||||||
|
|
||||||
|
|
||||||
def _ralph_approve(slugs: list[str]) -> str:
|
def _ralph_approve(slugs: list[str]) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user