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:
2026-05-05 08:27:14 +00:00
parent a5cab9677a
commit 2bcefe1ab4
5 changed files with 180 additions and 15 deletions

View File

@@ -629,6 +629,9 @@ class ProjectsHandlers:
return # parse_json_body already sent 400
slug = (body.get("slug") 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)
desc_err = validate_description(description)
@@ -659,6 +662,9 @@ class ProjectsHandlers:
"status": "pending",
"planning_session_id": None,
"final_plan_path": None,
"repo": repo,
"branch": branch,
"base_branch": base_branch,
"proposed_at": _now_iso(),
"approved_at": None,
"started_at": None,
@@ -825,13 +831,30 @@ class ProjectsHandlers:
if body is None:
return
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
# users can re-open planning on a project they already proposed.
if not description:
existing = _find_project(_read_approved(), slug)
if existing:
description = (existing.get("description") or "").strip()
if existing_proj:
description = (existing_proj.get("description") or "").strip()
desc_err = validate_description(description) if description else "description required"
if desc_err:
self.send_json({"error": "invalid_description", "message": desc_err}, 400)
@@ -847,6 +870,9 @@ class ProjectsHandlers:
"status": "planning",
"planning_session_id": None,
"final_plan_path": None,
"repo": None,
"branch": None,
"base_branch": None,
"proposed_at": _now_iso(),
"approved_at": None,
"started_at": None,