84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from app.agents.isolation.strategy_selector import IsolationDecision
|
|
|
|
|
|
class WorktreeIsolationError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _slugify(value: str, *, fallback: str) -> str:
|
|
slug = re.sub(r"[^a-zA-Z0-9._-]+", "-", (value or "").strip()).strip("-").lower()
|
|
return slug or fallback
|
|
|
|
|
|
def _resolve_git_root() -> Path:
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "--show-toplevel"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
except subprocess.CalledProcessError as exc:
|
|
raise WorktreeIsolationError(exc.stderr.strip() or exc.stdout.strip() or "git_root_unavailable") from exc
|
|
git_root = Path(result.stdout.strip())
|
|
if not git_root.exists():
|
|
raise WorktreeIsolationError("git_root_not_found")
|
|
return git_root
|
|
|
|
|
|
def prepare_worktree_isolation(
|
|
*,
|
|
state: dict[str, Any],
|
|
decision: IsolationDecision,
|
|
role_value: str,
|
|
sub_commander: str,
|
|
create_workspace: bool = True,
|
|
) -> dict[str, Any]:
|
|
isolation_id = f"worktree-{uuid4().hex[:8]}"
|
|
conversation_slug = _slugify(str(state.get("conversation_id") or "conversation"), fallback="conversation")
|
|
role_slug = _slugify(role_value, fallback="agent")
|
|
git_root = _resolve_git_root()
|
|
workspace_root = git_root / ".worktrees" / "jarvis" / conversation_slug
|
|
workspace_path = workspace_root / f"{role_slug}-{isolation_id}"
|
|
branch = f"jarvis/{conversation_slug}/{role_slug}-{isolation_id}"
|
|
|
|
if create_workspace and not workspace_path.exists():
|
|
workspace_root.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
subprocess.run(
|
|
["git", "-C", str(git_root), "worktree", "add", "-b", branch, str(workspace_path), "HEAD"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
except subprocess.CalledProcessError as exc:
|
|
raise WorktreeIsolationError(exc.stderr.strip() or exc.stdout.strip() or "worktree_add_failed") from exc
|
|
|
|
return {
|
|
"mode": "worktree",
|
|
"isolation_id": isolation_id,
|
|
"workspace_path": str(workspace_path),
|
|
"parent_conversation_id": str(state.get("conversation_id") or "") or None,
|
|
"metadata": {
|
|
**dict(decision.metadata or {}),
|
|
"reason": decision.reason,
|
|
"role": role_value,
|
|
"sub_commander": sub_commander,
|
|
"tool_names": list(decision.tool_names),
|
|
"capability_ids": list(decision.capability_ids),
|
|
"repo_root": str(git_root),
|
|
"branch": branch,
|
|
"workspace_strategy": "ephemeral_worktree",
|
|
"cleanup_status": "pending",
|
|
"materialized": workspace_path.exists(),
|
|
},
|
|
}
|