arkaos 2.17.1 → 2.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/arka/SKILL.md +9 -67
- package/arka/skills/comfyui/SKILL.md +50 -12
- package/arka/skills/conclave/SKILL.md +43 -141
- package/arka/skills/conclave/references/advisors.md +36 -0
- package/arka/skills/human-writing/SKILL.md +15 -100
- package/arka/skills/human-writing/references/forbidden-patterns.md +32 -0
- package/config/hooks/post-tool-use.sh +72 -0
- package/config/hooks/session-start.sh +16 -0
- package/config/hooks/user-prompt-submit.ps1 +2 -2
- package/config/hooks/user-prompt-submit.sh +102 -26
- package/core/agents/__pycache__/behavior_enforcer.cpython-313.pyc +0 -0
- package/core/agents/__pycache__/dna_registry.cpython-313.pyc +0 -0
- package/core/agents/adapters/__pycache__/disc_adapter.cpython-313.pyc +0 -0
- package/core/agents/adapters/disc_adapter.py +149 -0
- package/core/agents/behavior_enforcer.py +255 -0
- package/core/agents/dna_registry.py +235 -0
- package/core/forge/__init__.py +36 -0
- package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/runtime_dispatcher.cpython-313.pyc +0 -0
- package/core/forge/orchestrator.py +770 -0
- package/core/forge/runtime_dispatcher.py +465 -0
- package/core/governance/__pycache__/quality_api.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/quality_router.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/review_workflow.cpython-313.pyc +0 -0
- package/core/governance/quality_api.py +280 -0
- package/core/governance/quality_router.py +304 -0
- package/core/governance/review_workflow.py +386 -0
- package/core/memory/__pycache__/compressor.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/rehydrator.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/session_store.cpython-313.pyc +0 -0
- package/core/memory/compressor.py +269 -0
- package/core/memory/rehydrator.py +204 -0
- package/core/memory/session_store.py +256 -0
- package/core/runtime/__pycache__/context_compactor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/subagent.cpython-313.pyc +0 -0
- package/core/synapse/__init__.py +10 -3
- package/core/synapse/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/engine.py +27 -16
- package/core/synapse/kb_cache.py +382 -0
- package/core/synapse/layers.py +253 -50
- package/core/sync/__pycache__/self_healing.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/announcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/dashboard.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/state.cpython-313.pyc +0 -0
- package/core/workflow/announcer.py +246 -0
- package/core/workflow/dashboard.py +194 -0
- package/core/workflow/enforcer.py +234 -0
- package/core/workflow/recovery.py +196 -0
- package/core/workflow/rules_registry.py +484 -0
- package/core/workflow/session_summary.py +204 -0
- package/core/workflow/state.py +12 -2
- package/departments/dev/SKILL.md +10 -42
- package/departments/dev/skills/agent-design/SKILL.md +6 -26
- package/departments/dev/skills/ci-cd-pipeline/SKILL.md +6 -29
- package/departments/dev/skills/db-schema/SKILL.md +6 -24
- package/departments/dev/skills/dependency-audit/SKILL.md +1 -3
- package/departments/dev/skills/incident/SKILL.md +6 -24
- package/departments/dev/skills/mcp-builder/SKILL.md +4 -17
- package/departments/dev/skills/observability/SKILL.md +6 -29
- package/departments/dev/skills/performance-profiler/SKILL.md +5 -26
- package/departments/dev/skills/rag-architect/SKILL.md +5 -23
- package/departments/dev/skills/release/SKILL.md +4 -17
- package/departments/dev/skills/spec/SKILL.md +47 -148
- package/departments/landing/skills/landing-gen/SKILL.md +3 -15
- package/departments/marketing/skills/cold-email/SKILL.md +5 -17
- package/departments/marketing/skills/programmatic-seo/SKILL.md +6 -21
- package/departments/strategy/skills/board-advisor/SKILL.md +7 -21
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/synapse-bridge.py +27 -3
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Quality Gate API — submit deliverables, query status, record verdicts.
|
|
2
|
+
|
|
3
|
+
Provides a simple programmatic interface for the Quality Gate system:
|
|
4
|
+
- submit() — submit a deliverable for review
|
|
5
|
+
- query() — check status of a submission
|
|
6
|
+
- verdict() — record a reviewer's verdict
|
|
7
|
+
- list_pending() — get all pending reviews
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from core.governance.quality_router import (
|
|
15
|
+
QualityRouter,
|
|
16
|
+
ReviewAssignment,
|
|
17
|
+
ReviewerType,
|
|
18
|
+
DeliverableType,
|
|
19
|
+
ReviewPriority,
|
|
20
|
+
route_deliverable,
|
|
21
|
+
)
|
|
22
|
+
from core.governance.review_workflow import (
|
|
23
|
+
ReviewWorkflowEngine,
|
|
24
|
+
ReviewWorkflow,
|
|
25
|
+
ReviewPhase,
|
|
26
|
+
Verdict,
|
|
27
|
+
ReviewerOpinion,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_STATE_DIR = Path.home() / ".arkaos" / "quality-gate"
|
|
32
|
+
_QUEUE_FILE = _STATE_DIR / "queue.json"
|
|
33
|
+
_WORKFLOWS_FILE = _STATE_DIR / "workflows.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ensure_state_dir() -> None:
|
|
37
|
+
_STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_queue() -> list[dict]:
|
|
41
|
+
_ensure_state_dir()
|
|
42
|
+
if not _QUEUE_FILE.exists():
|
|
43
|
+
return []
|
|
44
|
+
try:
|
|
45
|
+
return json.loads(_QUEUE_FILE.read_text())
|
|
46
|
+
except (json.JSONDecodeError, OSError):
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _save_queue(queue: list[dict]) -> None:
|
|
51
|
+
_ensure_state_dir()
|
|
52
|
+
_QUEUE_FILE.write_text(json.dumps(queue, indent=2))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_workflows() -> list[dict]:
|
|
56
|
+
_ensure_state_dir()
|
|
57
|
+
if not _WORKFLOWS_FILE.exists():
|
|
58
|
+
return []
|
|
59
|
+
try:
|
|
60
|
+
return json.loads(_WORKFLOWS_FILE.read_text())
|
|
61
|
+
except (json.JSONDecodeError, OSError):
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _save_workflows(workflows: list[dict]) -> None:
|
|
66
|
+
_ensure_state_dir()
|
|
67
|
+
_WORKFLOWS_FILE.write_text(json.dumps(workflows, indent=2))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def submit(
|
|
71
|
+
title: str,
|
|
72
|
+
description: str,
|
|
73
|
+
deliverable_type: str | None = None,
|
|
74
|
+
submitter: str = "unknown",
|
|
75
|
+
metadata: dict | None = None,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""Submit a deliverable for Quality Gate review.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
title: Deliverable title
|
|
81
|
+
description: Brief description
|
|
82
|
+
deliverable_type: Type as string (auto-detected if None)
|
|
83
|
+
submitter: Agent ID of submitter
|
|
84
|
+
metadata: Additional context
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Dict with submission_id, reviewers, priority, and estimated_sla
|
|
88
|
+
"""
|
|
89
|
+
detype = DeliverableType(deliverable_type) if deliverable_type else None
|
|
90
|
+
|
|
91
|
+
router = QualityRouter()
|
|
92
|
+
assignment = router.route(title, description, detype, submitter, metadata)
|
|
93
|
+
|
|
94
|
+
queue = _load_queue()
|
|
95
|
+
queue.append(
|
|
96
|
+
{
|
|
97
|
+
"id": assignment.id,
|
|
98
|
+
"title": assignment.title,
|
|
99
|
+
"deliverable_type": assignment.deliverable_type.value,
|
|
100
|
+
"submitter": assignment.submitter,
|
|
101
|
+
"reviewers": [r.value for r in assignment.reviewers],
|
|
102
|
+
"priority": assignment.priority.value,
|
|
103
|
+
"status": assignment.status,
|
|
104
|
+
"submitted_at": assignment.submitted_at,
|
|
105
|
+
"sla_due": assignment.sla_due,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
_save_queue(queue)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"submission_id": assignment.id,
|
|
112
|
+
"reviewers": [r.value for r in assignment.reviewers],
|
|
113
|
+
"priority": assignment.priority.value,
|
|
114
|
+
"estimated_sla": assignment.sla_due,
|
|
115
|
+
"deliverable_type": assignment.deliverable_type.value,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def query(submission_id: str) -> dict[str, Any] | None:
|
|
120
|
+
"""Query the status of a submission.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
submission_id: ID returned from submit()
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Status dict or None if not found
|
|
127
|
+
"""
|
|
128
|
+
queue = _load_queue()
|
|
129
|
+
for item in queue:
|
|
130
|
+
if item["id"] == submission_id:
|
|
131
|
+
return item
|
|
132
|
+
|
|
133
|
+
workflows = _load_workflows()
|
|
134
|
+
for wf in workflows:
|
|
135
|
+
if wf["id"] == submission_id:
|
|
136
|
+
return wf
|
|
137
|
+
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def record_verdict(
|
|
142
|
+
submission_id: str,
|
|
143
|
+
reviewer: str,
|
|
144
|
+
verdict: str,
|
|
145
|
+
feedback: str = "",
|
|
146
|
+
flagged_rules: list[str] | None = None,
|
|
147
|
+
) -> bool:
|
|
148
|
+
"""Record a reviewer's verdict for a submission.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
submission_id: ID of the submission
|
|
152
|
+
reviewer: Agent ID of reviewer
|
|
153
|
+
verdict: APPROVED, REJECTED, REQUEST_CHANGES, or ESCALATE
|
|
154
|
+
feedback: Feedback for the submitter
|
|
155
|
+
flagged_rules: List of Constitution rules violated
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if verdict recorded successfully
|
|
159
|
+
"""
|
|
160
|
+
reviewer_type = ReviewerType(reviewer) if reviewer in [r.value for r in ReviewerType] else None
|
|
161
|
+
|
|
162
|
+
queue = _load_queue()
|
|
163
|
+
for item in queue:
|
|
164
|
+
if item["id"] == submission_id:
|
|
165
|
+
item["status"] = "reviewed"
|
|
166
|
+
item["verdict"] = verdict
|
|
167
|
+
item["feedback"] = feedback
|
|
168
|
+
item["flagged_rules"] = flagged_rules or []
|
|
169
|
+
_save_queue(queue)
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def list_pending(reviewer: str | None = None) -> list[dict]:
|
|
176
|
+
"""List all pending submissions.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
reviewer: If set, only show items pending for this reviewer
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of pending submission dicts
|
|
183
|
+
"""
|
|
184
|
+
queue = _load_queue()
|
|
185
|
+
pending = [item for item in queue if item["status"] == "pending"]
|
|
186
|
+
|
|
187
|
+
if reviewer:
|
|
188
|
+
pending = [item for item in pending if reviewer in item.get("reviewers", [])]
|
|
189
|
+
|
|
190
|
+
return pending
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def list_approved(limit: int = 10) -> list[dict]:
|
|
194
|
+
"""List recently approved submissions.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
limit: Maximum number to return
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of approved submission dicts
|
|
201
|
+
"""
|
|
202
|
+
queue = _load_queue()
|
|
203
|
+
approved = [
|
|
204
|
+
item
|
|
205
|
+
for item in queue
|
|
206
|
+
if item.get("status") == "reviewed" and item.get("verdict") == "APPROVED"
|
|
207
|
+
]
|
|
208
|
+
return approved[:limit]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def clear_resolved(older_than_days: int = 7) -> int:
|
|
212
|
+
"""Remove resolved submissions older than specified days.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
older_than_days: Remove items resolved more than this many days ago
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Number of items removed
|
|
219
|
+
"""
|
|
220
|
+
from datetime import datetime, timedelta, timezone
|
|
221
|
+
|
|
222
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=older_than_days)
|
|
223
|
+
queue = _load_queue()
|
|
224
|
+
original_count = len(queue)
|
|
225
|
+
|
|
226
|
+
remaining = []
|
|
227
|
+
for item in queue:
|
|
228
|
+
if item["status"] == "pending":
|
|
229
|
+
remaining.append(item)
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
verdict_at = item.get("verdict_at", item.get("submitted_at", ""))
|
|
233
|
+
try:
|
|
234
|
+
dt = datetime.fromisoformat(verdict_at.replace("Z", "+00:00"))
|
|
235
|
+
if dt > cutoff:
|
|
236
|
+
remaining.append(item)
|
|
237
|
+
except (ValueError, TypeError, AttributeError):
|
|
238
|
+
remaining.append(item)
|
|
239
|
+
|
|
240
|
+
_save_queue(remaining)
|
|
241
|
+
return original_count - len(remaining)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class QualityGateAPI:
|
|
245
|
+
"""Class-based API for Quality Gate operations."""
|
|
246
|
+
|
|
247
|
+
def __init__(self):
|
|
248
|
+
_ensure_state_dir()
|
|
249
|
+
|
|
250
|
+
def submit(
|
|
251
|
+
self,
|
|
252
|
+
title: str,
|
|
253
|
+
description: str,
|
|
254
|
+
deliverable_type: str | None = None,
|
|
255
|
+
submitter: str = "unknown",
|
|
256
|
+
metadata: dict | None = None,
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
return submit(title, description, deliverable_type, submitter, metadata)
|
|
259
|
+
|
|
260
|
+
def query(self, submission_id: str) -> dict[str, Any] | None:
|
|
261
|
+
return query(submission_id)
|
|
262
|
+
|
|
263
|
+
def record_verdict(
|
|
264
|
+
self,
|
|
265
|
+
submission_id: str,
|
|
266
|
+
reviewer: str,
|
|
267
|
+
verdict: str,
|
|
268
|
+
feedback: str = "",
|
|
269
|
+
flagged_rules: list[str] | None = None,
|
|
270
|
+
) -> bool:
|
|
271
|
+
return record_verdict(submission_id, reviewer, verdict, feedback, flagged_rules)
|
|
272
|
+
|
|
273
|
+
def list_pending(self, reviewer: str | None = None) -> list[dict]:
|
|
274
|
+
return list_pending(reviewer)
|
|
275
|
+
|
|
276
|
+
def list_approved(self, limit: int = 10) -> list[dict]:
|
|
277
|
+
return list_approved(limit)
|
|
278
|
+
|
|
279
|
+
def clear_resolved(self, older_than_days: int = 7) -> int:
|
|
280
|
+
return clear_resolved(older_than_days)
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Quality Gate Router — auto-routes deliverables to appropriate reviewers.
|
|
2
|
+
|
|
3
|
+
Routes deliverables to:
|
|
4
|
+
- Marta (CQO): Architecture, security, technical decisions
|
|
5
|
+
- Eduardo (Copy): All text, copy, documentation, content
|
|
6
|
+
- Francisca (Tech UX): UI/UX, frontend, user-facing code
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Automatic routing based on deliverable type
|
|
10
|
+
- Priority queuing
|
|
11
|
+
- SLA tracking (24h default)
|
|
12
|
+
- Escalation on BLOCK violations
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import uuid
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime, timezone, timedelta
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReviewerType(str, Enum):
|
|
24
|
+
CQO = "cqo-marta" # Marta - Chief Quality Officer
|
|
25
|
+
COPY = "copy-eduardo" # Eduardo - Copy Director
|
|
26
|
+
TECH_UX = "tech-francisca" # Francisca - Tech UX
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DeliverableType(str, Enum):
|
|
30
|
+
CODE = "code" # Backend/API code
|
|
31
|
+
FRONTEND = "frontend" # UI/UX, Vue/React
|
|
32
|
+
ARCHITECTURE = "architecture" # ADRs, system design
|
|
33
|
+
SECURITY = "security" # Security audits, auth
|
|
34
|
+
COPY = "copy" # Text, docs, content
|
|
35
|
+
DOCUMENTATION = "documentation" # Docs, READMEs
|
|
36
|
+
STRATEGY = "strategy" # Plans, roadmaps
|
|
37
|
+
DATA = "data" # Models, migrations
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReviewPriority(str, Enum):
|
|
41
|
+
LOW = "low"
|
|
42
|
+
NORMAL = "normal"
|
|
43
|
+
HIGH = "high"
|
|
44
|
+
URGENT = "urgent"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ReviewAssignment:
|
|
49
|
+
"""A deliverable assigned for review."""
|
|
50
|
+
|
|
51
|
+
id: str
|
|
52
|
+
deliverable_type: DeliverableType
|
|
53
|
+
title: str
|
|
54
|
+
description: str
|
|
55
|
+
submitter: str
|
|
56
|
+
submitted_at: str
|
|
57
|
+
reviewers: list[ReviewerType]
|
|
58
|
+
priority: ReviewPriority = ReviewPriority.NORMAL
|
|
59
|
+
status: str = "pending"
|
|
60
|
+
verdict: str = ""
|
|
61
|
+
feedback: str = ""
|
|
62
|
+
sla_due: str = ""
|
|
63
|
+
metadata: dict = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
def __post_init__(self):
|
|
66
|
+
if not self.sla_due:
|
|
67
|
+
self.sla_due = (datetime.now(timezone.utc) + timedelta(hours=24)).isoformat()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class RoutingRule:
|
|
72
|
+
"""A rule for routing deliverables to reviewers."""
|
|
73
|
+
|
|
74
|
+
deliverable_types: list[DeliverableType]
|
|
75
|
+
keywords: list[str]
|
|
76
|
+
file_extensions: list[str]
|
|
77
|
+
reviewers: list[ReviewerType]
|
|
78
|
+
priority: ReviewPriority = ReviewPriority.NORMAL
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
DEFAULT_ROUTING_RULES: list[RoutingRule] = [
|
|
82
|
+
RoutingRule(
|
|
83
|
+
deliverable_types=[DeliverableType.ARCHITECTURE],
|
|
84
|
+
keywords=["architecture", "adr", "system design", "decision"],
|
|
85
|
+
file_extensions=[".md"],
|
|
86
|
+
reviewers=[ReviewerType.CQO],
|
|
87
|
+
priority=ReviewPriority.HIGH,
|
|
88
|
+
),
|
|
89
|
+
RoutingRule(
|
|
90
|
+
deliverable_types=[DeliverableType.SECURITY],
|
|
91
|
+
keywords=["security", "auth", "permission", "oauth", "jwt"],
|
|
92
|
+
file_extensions=[],
|
|
93
|
+
reviewers=[ReviewerType.CQO, ReviewerType.TECH_UX],
|
|
94
|
+
priority=ReviewPriority.URGENT,
|
|
95
|
+
),
|
|
96
|
+
RoutingRule(
|
|
97
|
+
deliverable_types=[DeliverableType.FRONTEND],
|
|
98
|
+
keywords=["vue", "react", "ui", "component", "tailwind", "css"],
|
|
99
|
+
file_extensions=[".vue", ".tsx", ".jsx"],
|
|
100
|
+
reviewers=[ReviewerType.TECH_UX],
|
|
101
|
+
priority=ReviewPriority.NORMAL,
|
|
102
|
+
),
|
|
103
|
+
RoutingRule(
|
|
104
|
+
deliverable_types=[DeliverableType.CODE],
|
|
105
|
+
keywords=["api", "backend", "service", "model", "controller"],
|
|
106
|
+
file_extensions=[".py", ".php", ".java", ".go"],
|
|
107
|
+
reviewers=[ReviewerType.CQO],
|
|
108
|
+
priority=ReviewPriority.NORMAL,
|
|
109
|
+
),
|
|
110
|
+
RoutingRule(
|
|
111
|
+
deliverable_types=[DeliverableType.COPY],
|
|
112
|
+
keywords=["copy", "headline", "cta", "landing", "sales"],
|
|
113
|
+
file_extensions=[],
|
|
114
|
+
reviewers=[ReviewerType.COPY],
|
|
115
|
+
priority=ReviewPriority.NORMAL,
|
|
116
|
+
),
|
|
117
|
+
RoutingRule(
|
|
118
|
+
deliverable_types=[DeliverableType.DOCUMENTATION],
|
|
119
|
+
keywords=["doc", "readme", "guide", "docs"],
|
|
120
|
+
file_extensions=[".md", ".txt"],
|
|
121
|
+
reviewers=[ReviewerType.COPY, ReviewerType.TECH_UX],
|
|
122
|
+
priority=ReviewPriority.LOW,
|
|
123
|
+
),
|
|
124
|
+
RoutingRule(
|
|
125
|
+
deliverable_types=[DeliverableType.STRATEGY],
|
|
126
|
+
keywords=["roadmap", "strategy", "plan", "okr", "kpi"],
|
|
127
|
+
file_extensions=[".md"],
|
|
128
|
+
reviewers=[ReviewerType.CQO, ReviewerType.COPY],
|
|
129
|
+
priority=ReviewPriority.HIGH,
|
|
130
|
+
),
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class QualityRouter:
|
|
135
|
+
"""Routes deliverables to appropriate Quality Gate reviewers."""
|
|
136
|
+
|
|
137
|
+
def __init__(self, rules: list[RoutingRule] | None = None):
|
|
138
|
+
self.rules = rules or DEFAULT_ROUTING_RULES
|
|
139
|
+
self._queue: list[ReviewAssignment] = []
|
|
140
|
+
|
|
141
|
+
def route(
|
|
142
|
+
self,
|
|
143
|
+
title: str,
|
|
144
|
+
description: str,
|
|
145
|
+
deliverable_type: DeliverableType | None = None,
|
|
146
|
+
submitter: str = "unknown",
|
|
147
|
+
metadata: dict | None = None,
|
|
148
|
+
) -> ReviewAssignment:
|
|
149
|
+
"""Route a deliverable to appropriate reviewers.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
title: Deliverable title
|
|
153
|
+
description: Brief description
|
|
154
|
+
deliverable_type: Explicit type (auto-detected if None)
|
|
155
|
+
submitter: Agent ID of submitter
|
|
156
|
+
metadata: Additional context
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
ReviewAssignment with assigned reviewers
|
|
160
|
+
"""
|
|
161
|
+
if deliverable_type is None:
|
|
162
|
+
deliverable_type = self._detect_type(title, description, metadata)
|
|
163
|
+
|
|
164
|
+
reviewers = self._find_reviewers(deliverable_type, title, description, metadata)
|
|
165
|
+
priority = self._find_priority(deliverable_type, title, description)
|
|
166
|
+
|
|
167
|
+
assignment = ReviewAssignment(
|
|
168
|
+
id=str(uuid.uuid4()),
|
|
169
|
+
deliverable_type=deliverable_type,
|
|
170
|
+
title=title,
|
|
171
|
+
description=description,
|
|
172
|
+
submitter=submitter,
|
|
173
|
+
submitted_at=datetime.now(timezone.utc).isoformat(),
|
|
174
|
+
reviewers=reviewers,
|
|
175
|
+
priority=priority,
|
|
176
|
+
metadata=metadata or {},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
self._queue.append(assignment)
|
|
180
|
+
return assignment
|
|
181
|
+
|
|
182
|
+
def _detect_type(
|
|
183
|
+
self,
|
|
184
|
+
title: str,
|
|
185
|
+
description: str,
|
|
186
|
+
metadata: dict | None,
|
|
187
|
+
) -> DeliverableType:
|
|
188
|
+
"""Auto-detect deliverable type from content."""
|
|
189
|
+
text = f"{title} {description}".lower()
|
|
190
|
+
metadata = metadata or {}
|
|
191
|
+
|
|
192
|
+
if any(kw in text for kw in ["security", "auth", "permission", "oauth"]):
|
|
193
|
+
return DeliverableType.SECURITY
|
|
194
|
+
if any(kw in text for kw in ["architecture", "adr", "system design"]):
|
|
195
|
+
return DeliverableType.ARCHITECTURE
|
|
196
|
+
if any(kw in text for kw in ["vue", "react", "ui", "component", "frontend"]):
|
|
197
|
+
return DeliverableType.FRONTEND
|
|
198
|
+
if any(kw in text for kw in ["api", "backend", "service", "model"]):
|
|
199
|
+
return DeliverableType.CODE
|
|
200
|
+
if any(kw in text for kw in ["copy", "headline", "cta", "sales", "landing"]):
|
|
201
|
+
return DeliverableType.COPY
|
|
202
|
+
if any(kw in text for kw in ["doc", "readme", "guide"]):
|
|
203
|
+
return DeliverableType.DOCUMENTATION
|
|
204
|
+
if any(kw in text for kw in ["roadmap", "strategy", "plan"]):
|
|
205
|
+
return DeliverableType.STRATEGY
|
|
206
|
+
|
|
207
|
+
if metadata.get("file_path"):
|
|
208
|
+
ext = Path(metadata["file_path"]).suffix.lower()
|
|
209
|
+
if ext in [".vue", ".tsx", ".jsx"]:
|
|
210
|
+
return DeliverableType.FRONTEND
|
|
211
|
+
if ext in [".py", ".php", ".java", ".go"]:
|
|
212
|
+
return DeliverableType.CODE
|
|
213
|
+
|
|
214
|
+
return DeliverableType.CODE
|
|
215
|
+
|
|
216
|
+
def _find_reviewers(
|
|
217
|
+
self,
|
|
218
|
+
deliverable_type: DeliverableType,
|
|
219
|
+
title: str,
|
|
220
|
+
description: str,
|
|
221
|
+
metadata: dict | None,
|
|
222
|
+
) -> list[ReviewerType]:
|
|
223
|
+
"""Find appropriate reviewers for this deliverable."""
|
|
224
|
+
for rule in self.rules:
|
|
225
|
+
if deliverable_type in rule.deliverable_types:
|
|
226
|
+
return rule.reviewers
|
|
227
|
+
|
|
228
|
+
return [ReviewerType.CQO]
|
|
229
|
+
|
|
230
|
+
def _find_priority(
|
|
231
|
+
self,
|
|
232
|
+
deliverable_type: DeliverableType,
|
|
233
|
+
title: str,
|
|
234
|
+
description: str,
|
|
235
|
+
) -> ReviewPriority:
|
|
236
|
+
"""Determine review priority."""
|
|
237
|
+
text = f"{title} {description}".lower()
|
|
238
|
+
|
|
239
|
+
if any(kw in text for kw in ["urgent", "asap", "critical", "blocker"]):
|
|
240
|
+
return ReviewPriority.URGENT
|
|
241
|
+
|
|
242
|
+
for rule in self.rules:
|
|
243
|
+
if deliverable_type in rule.deliverable_types:
|
|
244
|
+
return rule.priority
|
|
245
|
+
|
|
246
|
+
return ReviewPriority.NORMAL
|
|
247
|
+
|
|
248
|
+
def get_queue(self) -> list[ReviewAssignment]:
|
|
249
|
+
"""Get current review queue."""
|
|
250
|
+
return self._queue.copy()
|
|
251
|
+
|
|
252
|
+
def get_pending_for_reviewer(self, reviewer: ReviewerType) -> list[ReviewAssignment]:
|
|
253
|
+
"""Get pending items for a specific reviewer."""
|
|
254
|
+
return [a for a in self._queue if reviewer in a.reviewers and a.status == "pending"]
|
|
255
|
+
|
|
256
|
+
def get_assignment(self, assignment_id: str) -> ReviewAssignment | None:
|
|
257
|
+
"""Get a specific assignment by ID."""
|
|
258
|
+
for a in self._queue:
|
|
259
|
+
if a.id == assignment_id:
|
|
260
|
+
return a
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
def update_verdict(
|
|
264
|
+
self,
|
|
265
|
+
assignment_id: str,
|
|
266
|
+
verdict: str,
|
|
267
|
+
feedback: str,
|
|
268
|
+
reviewer: ReviewerType,
|
|
269
|
+
) -> ReviewAssignment | None:
|
|
270
|
+
"""Record a verdict for an assignment."""
|
|
271
|
+
assignment = self.get_assignment(assignment_id)
|
|
272
|
+
if not assignment:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
if reviewer not in assignment.reviewers:
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
assignment.verdict = verdict
|
|
279
|
+
assignment.feedback = feedback
|
|
280
|
+
assignment.status = "reviewed"
|
|
281
|
+
return assignment
|
|
282
|
+
|
|
283
|
+
def is_sla_breached(self, assignment: ReviewAssignment) -> bool:
|
|
284
|
+
"""Check if an assignment has breached SLA."""
|
|
285
|
+
try:
|
|
286
|
+
due = datetime.fromisoformat(assignment.sla_due)
|
|
287
|
+
return datetime.now(timezone.utc) > due
|
|
288
|
+
except (ValueError, TypeError):
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
def get_breached_items(self) -> list[ReviewAssignment]:
|
|
292
|
+
"""Get all assignments that have breached SLA."""
|
|
293
|
+
return [a for a in self._queue if self.is_sla_breached(a) and a.status == "pending"]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def route_deliverable(
|
|
297
|
+
title: str,
|
|
298
|
+
description: str,
|
|
299
|
+
deliverable_type: DeliverableType | None = None,
|
|
300
|
+
submitter: str = "unknown",
|
|
301
|
+
) -> ReviewAssignment:
|
|
302
|
+
"""Convenience function to route a deliverable."""
|
|
303
|
+
router = QualityRouter()
|
|
304
|
+
return router.route(title, description, deliverable_type, submitter)
|