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

@@ -325,12 +325,40 @@ def _try_ralph_dispatch(text: str, adapter_name: str | None = None) -> str | Non
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:
"""Adaugă un proiect cu status pending în approved-tasks.json.
Schema includes the W2 planning fields (`planning_session_id`,
`final_plan_path`) so the orchestrator and PRD generator can find them.
Description may be prefixed with optional flags:
--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()
for p in data["projects"]:
@@ -343,13 +371,22 @@ def _ralph_propose(slug: str, description: str) -> str:
"status": "pending",
"planning_session_id": 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(),
"approved_at": None,
"started_at": None,
"pid": None,
})
_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: