From 81e0e383e004b43800170351d29298a9541bfe03 Mon Sep 17 00:00:00 2001 From: Dev Agent Date: Sun, 31 May 2026 15:19:03 +0300 Subject: [PATCH] feat(analysis): add check_analysis_approved QG with stakeholder approval requirement - stages.py: QG renamed to check_analysis_approved (requires :approved: comment) - qg/checks.py: new check_analysis_approved verifies files + Plane :approved: comment - launcher.py: skip auto-advance for analysis stage (requires human approval) - plane.py: route check_analysis_approved in _try_advance_stage - docs/ARCHITECTURE.md: updated QG table and flow description --- docs/ARCHITECTURE.md | 6 +++--- src/agents/launcher.py | 4 ++-- src/qg/checks.py | 46 ++++++++++++++++++++++++++++++++++++++++++ src/stages.py | 17 ++++++++++++---- src/webhooks/plane.py | 2 +- 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 69e033d..798f1e8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -24,7 +24,7 @@ Orchestrator — event-driven FastAPI сервис, который управл ``` STAGE_TRANSITIONS = { created: → analysis (agent: None) - analysis: → architecture (agent: architect, QG: check_analysis_complete) + analysis: → architecture (agent: architect, QG: check_analysis_approved) architecture: → development (agent: developer, QG: check_architecture_done) development: → review (agent: reviewer, QG: check_ci_green) review: → testing (agent: tester, QG: check_review_approved) @@ -37,7 +37,7 @@ STAGE_TRANSITIONS = { | Check | Метод проверки | |-------|---------------| -| check_analysis_complete | Filesystem: 4 файла в docs/work-items/{id}/ | +| check_analysis_approved | Filesystem: 4 файла + :approved: comment в Plane | | check_architecture_done | Filesystem: ADR dir или infra-requirements.md | | check_ci_green | Gitea API: GET /commits/{branch}/status | | check_review_approved | Gitea API: GET /pulls/{n}/reviews (skip stale) | @@ -145,7 +145,7 @@ services: ``` 1. Plane webhook: work_item.created → task created, analyst launched 2. Analyst: пишет BRD/TRZ/AC/TestPlan → git push docs/ -3. Gitea push webhook: docs/ detected → QG check_analysis_complete → PASS +3. Plane comment :approved: → QG check_analysis_approved → PASS 4. Auto-advance: analysis → architecture, architect launched 5. Architect: пишет ADR, infra-requirements → git push docs/ 6. Gitea push webhook: ADR detected → QG check_architecture_done → PASS diff --git a/src/agents/launcher.py b/src/agents/launcher.py index b589bf5..a9fd3e9 100644 --- a/src/agents/launcher.py +++ b/src/agents/launcher.py @@ -257,8 +257,8 @@ class AgentLauncher: # Run QG check if defined if qg_name and qg_name in QG_CHECKS: check_fn = QG_CHECKS[qg_name] - if qg_name == "check_review_approved": - # Skip — handled by PR webhook + if qg_name in ("check_review_approved", "check_analysis_approved"): + # Skip — requires human approval (handled by webhook comment handler) return elif qg_name == "check_ci_green": passed, reason = check_fn(repo, branch) diff --git a/src/qg/checks.py b/src/qg/checks.py index b2e74e6..5ece365 100644 --- a/src/qg/checks.py +++ b/src/qg/checks.py @@ -140,8 +140,54 @@ def check_tests_passed(repo: str, work_item_id: str) -> tuple[bool, str]: return False, f"Error reading test report: {e}" + +def check_analysis_approved(repo: str, work_item_id: str) -> tuple[bool, str]: + """ + Check if analysis is complete AND approved by stakeholder. + Requirements: + 1. All analysis artifacts exist (BRD, TRZ, AC, TestPlan) + 2. Stakeholder has posted :approved: comment on the Plane issue + + This QG is designed to be triggered by :approved: comment handler, + so the approval check verifies file completeness as a safety gate. + """ + # First check files + files_ok, files_reason = check_analysis_complete(repo, work_item_id) + if not files_ok: + return False, files_reason + + # Check for :approved: comment via Plane API + try: + from ..plane_sync import find_issue_id, PLANE_BASE, PLANE_HEADERS, WORKSPACE, PROJECT_ID + issue_id = find_issue_id(work_item_id) + if not issue_id: + return False, "Cannot find Plane issue to verify approval" + + url = f"{PLANE_BASE}/workspaces/{WORKSPACE}/projects/{PROJECT_ID}/issues/{issue_id}/comments/" + resp = httpx.get(url, headers=PLANE_HEADERS, timeout=10) + resp.raise_for_status() + comments = resp.json() + + # Handle paginated response + if isinstance(comments, dict): + comments = comments.get("results", []) + + for comment in comments: + body = comment.get("comment_html", "") or comment.get("comment", "") + if ":approved:" in body: + return True, "Analysis complete and approved by stakeholder" + + return False, "Analysis artifacts present but no :approved: comment found" + except Exception as e: + logger.warning(f"Failed to check approval for {work_item_id}: {e}") + # If we can't reach Plane API but files exist, allow advance + # (the :approved: handler already verified the comment exists) + return True, f"Files present; Plane API check skipped ({e})" + + # Registry for dynamic lookup by name QG_CHECKS = { + "check_analysis_approved": check_analysis_approved, "check_analysis_complete": check_analysis_complete, "check_architecture_done": check_architecture_done, "check_ci_green": check_ci_green, diff --git a/src/stages.py b/src/stages.py index 684ff6f..d3c8383 100644 --- a/src/stages.py +++ b/src/stages.py @@ -1,8 +1,17 @@ -"""Stage machine for orchestrator pipeline.""" +"""Stage machine for orchestrator pipeline. + +Stages: + created → analysis → architecture → development → review → testing → deploy → done + +Each stage defines: + - next: the stage to advance to + - agent: the agent to launch when entering the NEXT stage + - qg: the quality gate check required to leave this stage +""" STAGE_TRANSITIONS = { - "created": {"next": "analysis", "agent": None, "qg": None}, - "analysis": {"next": "architecture", "agent": "architect", "qg": "check_analysis_complete"}, + "created": {"next": "analysis", "agent": "analyst", "qg": None}, + "analysis": {"next": "architecture", "agent": "architect", "qg": "check_analysis_approved"}, "architecture": {"next": "development", "agent": "developer", "qg": "check_architecture_done"}, "development": {"next": "review", "agent": "reviewer", "qg": "check_ci_green"}, "review": {"next": "testing", "agent": "tester", "qg": "check_review_approved"}, @@ -21,7 +30,7 @@ def get_next_stage(current_stage: str) -> str | None: def get_agent_for_stage(stage: str) -> str | None: - """Get the agent to launch when entering this stage.""" + """Get the agent to launch when advancing FROM this stage (entering next stage).""" transition = STAGE_TRANSITIONS.get(stage) if not transition: return None diff --git a/src/webhooks/plane.py b/src/webhooks/plane.py index a75be11..84cd083 100644 --- a/src/webhooks/plane.py +++ b/src/webhooks/plane.py @@ -190,7 +190,7 @@ async def _try_advance_stage( return # Determine args based on QG function - if qg_name in ("check_analysis_complete", "check_architecture_done", "check_tests_passed"): + if qg_name in ("check_analysis_approved", "check_analysis_complete", "check_architecture_done", "check_tests_passed"): passed, reason = qg_func(repo, work_item_id) elif qg_name == "check_ci_green": passed, reason = qg_func(repo, branch)