Compare commits
1 Commits
step35/443
...
step35/476
| Author | SHA1 | Date | |
|---|---|---|---|
| d94016458a |
@@ -129,15 +129,35 @@ Preserved by timmy-orchestrator to prevent loss." 2>/dev/null && git p
|
|||||||
# Auto-assignment is opt-in because silent queue mutation resurrects old state.
|
# Auto-assignment is opt-in because silent queue mutation resurrects old state.
|
||||||
if [ "$unassigned_count" -gt 0 ]; then
|
if [ "$unassigned_count" -gt 0 ]; then
|
||||||
if [ "$AUTO_ASSIGN_UNASSIGNED" = "1" ]; then
|
if [ "$AUTO_ASSIGN_UNASSIGNED" = "1" ]; then
|
||||||
log "Assigning $unassigned_count issues to claude..."
|
log "Routing $unassigned_count issues via dispatch_router..."
|
||||||
|
DISPATCHER="$SCRIPT_DIR/../tools/dispatch_router.py"
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
local repo=$(echo "$line" | sed 's/.*REPO=\([^ ]*\).*/\1/')
|
local repo=$(echo "$line" | sed 's/.*REPO=\([^ ]*\).*/\1/')
|
||||||
local num=$(echo "$line" | sed 's/.*NUM=\([^ ]*\).*/\1/')
|
local num=$(echo "$line" | sed 's/.*NUM=\([^ ]*\).*/\1/')
|
||||||
|
local title=$(echo "$line" | sed 's/.*TITLE=//')
|
||||||
|
# Get routing recommendation from dispatch_router
|
||||||
|
local agent="timmy" category="unknown" score="N/A" reason="unavailable"
|
||||||
|
if [ -x "$DISPATCHER" ]; then
|
||||||
|
local result
|
||||||
|
result=$(python3 "$DISPATCHER" "$title" 2>/dev/null)
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
agent=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('primary',{}).get('agent','timmy'))" 2>/dev/null)
|
||||||
|
category=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('category','unknown'))" 2>/dev/null)
|
||||||
|
local score_raw
|
||||||
|
score_raw=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('primary',{}).get('score','N/A'))" 2>/dev/null)
|
||||||
|
score=$(printf "%.2f" "$score_raw" 2>/dev/null || echo "$score_raw")
|
||||||
|
reason=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('primary',{}).get('reason',''))" 2>/dev/null)
|
||||||
|
[ -z "$agent" ] && agent="timmy"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "WARNING: dispatch_router.py not found or not executable at $DISPATCHER"
|
||||||
|
fi
|
||||||
|
# Apply assignment
|
||||||
curl -sf -X PATCH "$GITEA_URL/api/v1/repos/$repo/issues/$num" \
|
curl -sf -X PATCH "$GITEA_URL/api/v1/repos/$repo/issues/$num" \
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"assignees":["claude"]}' >/dev/null 2>&1 && \
|
-d "{\"assignees\":[\"$agent\"]}" >/dev/null 2>&1 && \
|
||||||
log " Assigned #$num ($repo) to claude"
|
log " Routed #$num ($repo) to $agent | category=$category score=$score | $reason"
|
||||||
done < "$state_dir/unassigned.txt"
|
done < "$state_dir/unassigned.txt"
|
||||||
else
|
else
|
||||||
log "Auto-assign disabled: leaving $unassigned_count unassigned issues untouched"
|
log "Auto-assign disabled: leaving $unassigned_count unassigned issues untouched"
|
||||||
|
|||||||
224
tools/dispatch_router.py
Normal file
224
tools/dispatch_router.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
tools/dispatch_router.py — Audit-backed agent dispatch router
|
||||||
|
|
||||||
|
Routes incoming tasks to the best agent based on proven merge rates
|
||||||
|
from the Perplexity contribution audit (April 2026).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python tools/dispatch_router.py "fix: broken CI pipeline"
|
||||||
|
python tools/dispatch_router.py "add feature architecture for user vault"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentRecommendation:
|
||||||
|
"""Single agent recommendation with confidence score."""
|
||||||
|
agent: str
|
||||||
|
score: float
|
||||||
|
reviewer: str
|
||||||
|
reason: str
|
||||||
|
category: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DispatchResult:
|
||||||
|
"""Complete routing result."""
|
||||||
|
task: str
|
||||||
|
category: str
|
||||||
|
primary: AgentRecommendation
|
||||||
|
candidates: List[AgentRecommendation]
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"task": self.task,
|
||||||
|
"category": self.category,
|
||||||
|
"primary": asdict(self.primary),
|
||||||
|
"candidates": [asdict(c) for c in self.candidates],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Agent profiles ──────────────────────────────────────────────────────────
|
||||||
|
# Score percentages are from the April 2026 audit (SOVEREIGN_AUDIT.md)
|
||||||
|
AGENTS = {
|
||||||
|
"timmy": {
|
||||||
|
"categories": ["ci_cd", "infra", "security", "hotfix", "governance"],
|
||||||
|
"merge_rate": 0.95,
|
||||||
|
"reviewer": "allegro",
|
||||||
|
"notes": "Sovereign review, architecture, release judgment",
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"categories": ["feature_architecture", "research", "design"],
|
||||||
|
"merge_rate": 0.70,
|
||||||
|
"reviewer": "timmy",
|
||||||
|
"notes": "Frontier architecture, research-heavy prototypes",
|
||||||
|
},
|
||||||
|
"perplexity": {
|
||||||
|
"categories": ["code_review", "qa", "integration", "research_triage"],
|
||||||
|
"merge_rate": 0.82,
|
||||||
|
"reviewer": "timmy",
|
||||||
|
"notes": "Research triage, integration evaluation, architecture memos",
|
||||||
|
},
|
||||||
|
"allegro": {
|
||||||
|
"categories": ["spec_writing", "docs", "triage", "queue_hygiene"],
|
||||||
|
"merge_rate": 0.824,
|
||||||
|
"reviewer": "timmy",
|
||||||
|
"notes": "Tempo-and-dispatch, queue hygiene, operational next-move",
|
||||||
|
},
|
||||||
|
"codex": {
|
||||||
|
"categories": ["high_precision", "migration", "verification", "cleanup"],
|
||||||
|
"merge_rate": 1.00,
|
||||||
|
"reviewer": "timmy",
|
||||||
|
"notes": "Workflow hardening, cleanup, migration verification",
|
||||||
|
},
|
||||||
|
"claude": {
|
||||||
|
"categories": ["frontend_3d", "nexus", "refactor", "test_heavy"],
|
||||||
|
"merge_rate": 0.52,
|
||||||
|
"reviewer": "perplexity",
|
||||||
|
"notes": "Hard refactors, deep implementation, test-heavy changes",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Keyword → category mapping ──────────────────────────────────────────────
|
||||||
|
KEYWORD_MAP = {
|
||||||
|
# CI / CD / Infra / Security
|
||||||
|
"ci": "ci_cd",
|
||||||
|
"cd": "ci_cd",
|
||||||
|
"pipeline": "ci_cd",
|
||||||
|
"build": "ci_cd",
|
||||||
|
"deploy": "ci_cd",
|
||||||
|
"infra": "infra",
|
||||||
|
"infrastructure": "infra",
|
||||||
|
"server": "infra",
|
||||||
|
"vps": "infra",
|
||||||
|
"ssh": "infra",
|
||||||
|
"security": "security",
|
||||||
|
"vulnerability": "security",
|
||||||
|
"auth": "security",
|
||||||
|
"authentication": "security",
|
||||||
|
"hotfix": "hotfix",
|
||||||
|
"emergency": "hotfix",
|
||||||
|
"governance": "governance",
|
||||||
|
"policy": "governance",
|
||||||
|
# Architecture / Design
|
||||||
|
"architecture": "feature_architecture",
|
||||||
|
"architectural": "feature_architecture",
|
||||||
|
"design": "feature_architecture",
|
||||||
|
"prototype": "feature_architecture",
|
||||||
|
"research": "research",
|
||||||
|
# Code review / QA
|
||||||
|
"review": "code_review",
|
||||||
|
"qa": "qa",
|
||||||
|
"test": "qa",
|
||||||
|
"quality": "qa",
|
||||||
|
"bug": "qa",
|
||||||
|
"integration": "integration",
|
||||||
|
# Docs / Specs
|
||||||
|
"spec": "spec_writing",
|
||||||
|
"specification": "spec_writing",
|
||||||
|
"documentation": "docs",
|
||||||
|
"docs": "docs",
|
||||||
|
"readme": "docs",
|
||||||
|
"triage": "triage",
|
||||||
|
"dispatch": "triage",
|
||||||
|
# High-precision
|
||||||
|
"migration": "migration",
|
||||||
|
"verify": "verification",
|
||||||
|
"verification": "verification",
|
||||||
|
"cleanup": "cleanup",
|
||||||
|
"refactor": "refactor",
|
||||||
|
# 3D / Frontend
|
||||||
|
"nexus": "nexus",
|
||||||
|
"three": "frontend_3d",
|
||||||
|
"3d": "frontend_3d",
|
||||||
|
"frontend": "frontend_3d",
|
||||||
|
"ui": "frontend_3d",
|
||||||
|
# Misc
|
||||||
|
"agent": "high_precision",
|
||||||
|
"orchestrat": "high_precision",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def categorize(task: str) -> str:
|
||||||
|
"""Categorize a task description into one of the known categories."""
|
||||||
|
task_lower = task.lower()
|
||||||
|
# Score keywords by presence
|
||||||
|
scores = {}
|
||||||
|
for keyword, category in KEYWORD_MAP.items():
|
||||||
|
if keyword in task_lower:
|
||||||
|
scores[category] = scores.get(category, 0) + 1
|
||||||
|
if scores:
|
||||||
|
return max(scores, key=scores.get)
|
||||||
|
# Default: treat as generic task → Timmy (sovereign decision-maker)
|
||||||
|
return "ci_cd"
|
||||||
|
|
||||||
|
|
||||||
|
def agents_for_category(category: str) -> List[str]:
|
||||||
|
"""Return agent names that handle the given category, ordered by preference."""
|
||||||
|
agents_in_category = []
|
||||||
|
for agent, profile in AGENTS.items():
|
||||||
|
if category in profile["categories"]:
|
||||||
|
agents_in_category.append(agent)
|
||||||
|
# If no agents match, default to timmy
|
||||||
|
return agents_in_category or ["timmy"]
|
||||||
|
|
||||||
|
|
||||||
|
def rank_agents(task: str, category: str) -> List[AgentRecommendation]:
|
||||||
|
"""Rank all agents by suitability for this task."""
|
||||||
|
candidates = []
|
||||||
|
for agent, profile in AGENTS.items():
|
||||||
|
score = profile["merge_rate"]
|
||||||
|
# Boost score if agent handles the category
|
||||||
|
if category in profile["categories"]:
|
||||||
|
score += 0.1 # category fit bonus
|
||||||
|
else:
|
||||||
|
score *= 0.5 # penalty for wrong category
|
||||||
|
candidates.append(
|
||||||
|
AgentRecommendation(
|
||||||
|
agent=agent,
|
||||||
|
score=round(score, 3),
|
||||||
|
reviewer=profile["reviewer"],
|
||||||
|
reason=profile["notes"],
|
||||||
|
category=category,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Sort descending by score
|
||||||
|
candidates.sort(key=lambda x: x.score, reverse=True)
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch(task: str) -> DispatchResult:
|
||||||
|
"""Main entry: route a task description to the best agent."""
|
||||||
|
category = categorize(task)
|
||||||
|
ranked = rank_agents(task, category)
|
||||||
|
primary = ranked[0] if ranked else AgentRecommendation(
|
||||||
|
agent="timmy", score=0.5, reviewer="allegro",
|
||||||
|
reason="default fallback", category=category
|
||||||
|
)
|
||||||
|
return DispatchResult(
|
||||||
|
task=task,
|
||||||
|
category=category,
|
||||||
|
primary=primary,
|
||||||
|
candidates=ranked,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python tools/dispatch_router.py "<task description>"")
|
||||||
|
sys.exit(1)
|
||||||
|
task = " ".join(sys.argv[1:])
|
||||||
|
result = dispatch(task)
|
||||||
|
print(json.dumps(result.to_dict(), indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user