From 51f7364532cb2b5766706c63eb097f14d6627352 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sun, 31 May 2026 20:15:01 +0300 Subject: [PATCH] feat: integrate Analyst into Plane/Orchestrator pipeline - Add git fetch+checkout in agent launch cmd (ensures correct branch) - Add git fetch+checkout in _monitor_agent before commit/push - Post start comment in Plane when analyst launches - Post :approved: request comment after analyst completes successfully - Branch lookup moved before cmd construction for reuse --- src/agents/launcher.py | 38 +++++++++++++++++++++++++++++++++----- src/db.py | 3 ++- src/webhooks/plane.py | 3 +++ tests/test_webhooks.py | 2 ++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/agents/launcher.py b/src/agents/launcher.py index a9fd3e9..cfbf5f1 100644 --- a/src/agents/launcher.py +++ b/src/agents/launcher.py @@ -8,7 +8,7 @@ from ..db import get_db, get_task_by_repo_branch, update_task_stage from ..stages import get_next_stage, get_qg_for_stage, get_agent_for_stage from ..qg.checks import QG_CHECKS from ..notifications import notify_stage_change, notify_qg_failure -from ..plane_sync import notify_stage_change as plane_notify_stage +from ..plane_sync import notify_stage_change as plane_notify_stage, add_comment as plane_add_comment logger = logging.getLogger("orchestrator.launcher") @@ -94,8 +94,12 @@ class AgentLauncher: system_prompt = config["system_prompt"] allowed_tools = config["allowed_tools"] + # Determine branch for checkout + _br_row = get_db().execute("SELECT branch FROM tasks WHERE id=?", (task_id,)).fetchone() if task_id else None + agent_branch = _br_row[0] if _br_row else "main" + cmd = ( - f'cd {local_repo_path} && ' + f'cd {local_repo_path} && git fetch origin 2>/dev/null; git checkout {agent_branch} 2>/dev/null || git checkout -b {agent_branch} origin/{agent_branch} 2>/dev/null; ' f'{self.CLAUDE_BIN} --print ' f'"$(cat {task_file})" ' f'--system-prompt "$(cat {system_prompt})" ' @@ -137,8 +141,7 @@ class AgentLauncher: t.start() # Start monitor thread (waits for completion, commits, pushes) - task_row = get_db().execute("SELECT branch FROM tasks WHERE id=?", (task_id,)).fetchone() if task_id else None - agent_branch = task_row[0] if task_row else "main" + # agent_branch already computed above m = threading.Thread( target=self._monitor_agent, args=(proc, run_id, agent, repo, agent_branch), @@ -193,6 +196,20 @@ class AgentLauncher: "GIT_COMMITTER_NAME": "claude-bot", "GIT_COMMITTER_EMAIL": "claude-bot@mva154.local", } + # Checkout feature branch before committing + subprocess.run( + ["git", "-C", repo_path, "fetch", "origin"], + capture_output=True, text=True, timeout=30, env=git_env + ) + checkout_result = subprocess.run( + ["git", "-C", repo_path, "checkout", branch], + capture_output=True, text=True, timeout=30, env=git_env + ) + if checkout_result.returncode != 0: + subprocess.run( + ["git", "-C", repo_path, "checkout", "-b", branch, f"origin/{branch}"], + capture_output=True, text=True, timeout=30, env=git_env + ) result = subprocess.run( ["git", "-C", repo_path, "status", "--porcelain"], capture_output=True, text=True, timeout=10, env=git_env @@ -258,7 +275,18 @@ class AgentLauncher: if qg_name and qg_name in QG_CHECKS: check_fn = QG_CHECKS[qg_name] if qg_name in ("check_review_approved", "check_analysis_approved"): - # Skip — requires human approval (handled by webhook comment handler) + # Requires human approval - post request comment if analyst just finished + if agent == "analyst" and qg_name == "check_analysis_approved" and work_item_id: + files_check = QG_CHECKS.get("check_analysis_complete") + if files_check: + files_ok, _ = files_check(repo, work_item_id) + if files_ok: + plane_add_comment( + work_item_id, + "📋 BRD/ТЗ/AC/TestPlan готовы. " + "Прошу review и реакцию :approved: для продвижения в Architecture." + ) + logger.info(f"Task {task_id}: analyst finished, requested :approved: in Plane") return elif qg_name == "check_ci_green": passed, reason = check_fn(repo, branch) diff --git a/src/db.py b/src/db.py index d490856..1a4fb50 100644 --- a/src/db.py +++ b/src/db.py @@ -28,7 +28,8 @@ def init_db(): stage TEXT DEFAULT 'created', agent_running TEXT, created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) + updated_at TEXT DEFAULT (datetime('now')), + plane_issue_id TEXT ); CREATE TABLE IF NOT EXISTS agent_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/src/webhooks/plane.py b/src/webhooks/plane.py index 84cd083..dc8081c 100644 --- a/src/webhooks/plane.py +++ b/src/webhooks/plane.py @@ -130,6 +130,9 @@ async def handle_work_item_created(data: dict): task_desc = f"Work item: {work_item_id}\nRepo: {repo}\nBranch: {branch}\nStage: analysis\nTitle: {name}" run_id = launcher.launch("analyst", repo, task_desc, task_id=task_id) logger.info(f"Task {task_id}: launched analyst (run_id={run_id})") + # Post start comment to Plane + from ..plane_sync import add_comment as _add_comment + _add_comment(work_item_id, "🔍 Analyst запущен. BRD/ТЗ/AC/TestPlan в работе (ожидайте 8-15 мин).") except Exception as e: logger.error(f"Failed to launch analyst for {work_item_id}: {e}") diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index cc5d7d7..0c93649 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -6,6 +6,8 @@ from unittest.mock import patch, MagicMock, AsyncMock # Override DB path before importing app _test_db = os.path.join(tempfile.gettempdir(), "test_orchestrator.db") os.environ["ORCH_DB_PATH"] = _test_db +os.environ["ORCH_PLANE_WEBHOOK_SECRET"] = "" +os.environ["ORCH_GITEA_WEBHOOK_SECRET"] = "" os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir() os.environ["ORCH_HOST_REPOS_DIR"] = "/home/slin/repos" os.environ["ORCH_GITEA_TOKEN"] = "test-token"