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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user