From ca3b229444b657bdbb65f65d6e43c8d15ff5fe20 Mon Sep 17 00:00:00 2001 From: Fred Porter Date: Wed, 19 Aug 2026 19:31:14 +0800 Subject: [PATCH] Remove duplicate agent routing skills --- backend/app/api/chat.py | 1 - backend/app/api/user_workflow.py | 15 - backend/app/mcp/hivemind_launcher.py | 6 +- backend/app/skills/builtin/route_task.py | 443 ------------------ .../builtin/skill_hivemind_consensus.py | 174 ------- .../builtin/skill_roundtable_dispatch.py | 289 ------------ backend/app/skills/catalogue.json | 3 - backend/tests/test_api_user_workflow.py | 2 +- backend/tests/test_route_task_enhanced.py | 86 ---- backend/tests/test_skill_route_task.py | 94 ---- docs/SKILLS_AUDIT_2026-08-18.md | 23 +- 11 files changed, 14 insertions(+), 1122 deletions(-) delete mode 100644 backend/app/skills/builtin/route_task.py delete mode 100644 backend/app/skills/builtin/skill_hivemind_consensus.py delete mode 100644 backend/app/skills/builtin/skill_roundtable_dispatch.py delete mode 100644 backend/tests/test_route_task_enhanced.py delete mode 100644 backend/tests/test_skill_route_task.py diff --git a/backend/app/api/chat.py b/backend/app/api/chat.py index 224815c0..81f2b70b 100644 --- a/backend/app/api/chat.py +++ b/backend/app/api/chat.py @@ -313,7 +313,6 @@ async def _execute_tool(tool_name: str, arguments: dict) -> str: except (ImportError, AttributeError): return json.dumps({"skills": [ {"id": "ecosystem-audit", "name": "Ecosystem Audit", "category": "system"}, - {"id": "route_task", "name": "Route Task", "category": "assist"}, {"id": "vault_discovery", "name": "Vault Discovery", "category": "knowledge"}, {"id": "workflow_audit", "name": "Workflow Audit", "category": "workflow"}, ], "total": 3, "source": "samples"}) diff --git a/backend/app/api/user_workflow.py b/backend/app/api/user_workflow.py index 0062533f..baba810e 100644 --- a/backend/app/api/user_workflow.py +++ b/backend/app/api/user_workflow.py @@ -455,21 +455,6 @@ def _seed_workflows() -> dict[str, Any]: }, ], }, - { - "id": "wf-draft-publish-seed", - "name": "Draft to Publish Checklist", - "description": "Lightweight readiness workflow for publishing.", - "schedule": "manual", - "steps": [ - { - "type": "skill", - "skill_id": "route_task", - "params": { - "task": "Review draft and prepare publish checklist", - }, - }, - ], - }, ] created: list[dict[str, Any]] = [] diff --git a/backend/app/mcp/hivemind_launcher.py b/backend/app/mcp/hivemind_launcher.py index 57c1330d..0980635d 100644 --- a/backend/app/mcp/hivemind_launcher.py +++ b/backend/app/mcp/hivemind_launcher.py @@ -36,9 +36,9 @@ def _health_is_ready(host: str, port: int) -> bool: def start_hivemind() -> None: """Launch Hivemind MCP server as a background child process. - Hivemind is core to the agent execution pipeline: - hivemind-consensus → design/planning/analysis tasks - roundtable-dispatch → documentation/parallel-agent tasks + Hivemind hosts the governed agent and consensus service. Provider and + budget selection remain behind the canonical Flow Router rather than + separate executable Skill wrappers. If Hivemind is already running and healthy, attaches silently. If the port is occupied but unhealthy, skips auto-start. diff --git a/backend/app/skills/builtin/route_task.py b/backend/app/skills/builtin/route_task.py deleted file mode 100644 index e17e0d56..00000000 --- a/backend/app/skills/builtin/route_task.py +++ /dev/null @@ -1,443 +0,0 @@ -"""route_task — Intelligently route and optionally execute tasks with AI. - -.. deprecated:: - This skill is superseded by :class:`FlowLLMRouter` - (``app.services.flow_router.router``) which provides the same - complexity estimation, cost-based routing, and agent dispatch - with additional analytics and history tracking. - - New code should use ``FlowLLMRouter`` directly. This skill - remains registered for backward compatibility with existing - API consumers and delegates to ``FlowLLMRouter`` internally. - -Operationalizes the cost strategy: - simple → Ollama (free, local) - medium → OpenRouter/o3-mini (mid-range, ~$0.01/task) - complex → Claude/OpenRouter (expensive, intentional) - -Features: - - Auto-detect task complexity from description - - Budget-aware routing (respects monthly/model limits) - - Optional execution through provider_router - - Context size and risk level heuristics - - Routing decision logging to spool - -Usage: - POST /api/skills/route_task/run - Body: { - "task": "fix a typo in README", - "complexity": "auto", - "execute": false, - "context_size": "small", - "risk_level": "low" - } -""" -from __future__ import annotations - -from app.skills.base import BaseSkill, SkillMeta, SkillParam - - -class RouteTask(BaseSkill): - meta = SkillMeta( - id="route_task", - name="Route Task", - description=( - "Route and optionally execute tasks to" - " the best AI provider" - ), - category="assist", - timeout=30, - params=[ - SkillParam( - name="task", - type="string", - required=True, - description="Task description or query", - ), - SkillParam( - name="complexity", - type="string", - required=False, - default="auto", - description="Complexity: auto, simple, medium, complex", - ), - SkillParam( - name="context_size", - type="string", - required=False, - default="small", - description=( - "Context: small (<2K), medium (2K-100K)," - " large (>100K)" - ), - ), - SkillParam( - name="risk_level", - type="string", - required=False, - default="low", - description="Risk level: low, medium, high (security/safety)", - ), - SkillParam( - name="execute", - type="boolean", - required=False, - default=False, - description="Execute the task immediately (vs advice-only)", - ), - SkillParam( - name="target_agent", - type="string", - required=False, - default="auto", - description=( - "Target agent: 'auto' (routing matrix), " - "'architect', 'dev', 'reviewer', 'debugger', " - "'docgen', 'gridsmith-dev', 'hivemind', " - "'roundtable', 'hivemind', 'ollama', 'openrouter'" - ), - ), - ], - requires_confirmation=True, - ) - - def _estimate_complexity(self, task: str) -> str: - """Auto-detect complexity from task description.""" - task_lower = task.lower() - - # Complex indicators - complex_signals = [ - "security", "vulnerability", "exploit", "race condition", - "deadlock", "concurrency", "encryption", "cryptography", - "refactor", "architecture", "design pattern", "distributed", - "performance optimization", "memory leak", "thread safety", - "authentication", "authorization", "zero-day", - "complicated", "intricate", "nuanced", "subtle bug", - ] - # Medium indicators - medium_signals = [ - "implement", "feature", "endpoint", "api", "route", - "component", "module", "integration", "database", - "migration", "schema", "query", "test", "coverage", - "debug", "fix bug", "error handling", "validation", - ] - - for signal in complex_signals: - if signal in task_lower: - return "complex" - for signal in medium_signals: - if signal in task_lower: - return "medium" - - # Check length: longer descriptions are more complex - if len(task) > 200: - return "medium" - - return "simple" - - def _estimate_risk( - self, - task: str, - complexity: str, - ) -> str: - """Estimate risk level from task description.""" - task_lower = task.lower() - - high_risk_signals = [ - "security", "vulnerability", "exploit", "private key", - "password", "token", "credential", "hack", "breach", - "delete", "drop database", "truncate", "production", - "critical infrastructure", "zero-day", "malware", - ] - for signal in high_risk_signals: - if signal in task_lower: - return "high" - - if complexity == "complex": - return "medium" - - return "low" - - def _estimate_context_size(self, task: str) -> str: - """Estimate required context size from task.""" - if len(task) > 10000: - return "large" - if len(task) > 2000: - return "medium" - return "small" - - def _build_routing( - self, - complexity: str, - context_size: str, - risk_level: str, - budget_remaining: float = 100.0, - ) -> dict: - """Build routing decision considering cost, context, risk, budget.""" - cost_table = { - "simple": { - "provider": "ollama", - "model": "qwen2.5-coder:3b", - "cost": "$0 (local)", - "reason": "Simple task — use local free model", - "tokens_per_second": "~40", - }, - "medium": { - "provider": "openrouter", - "model": "deepseek/deepseek-chat", - "cost": "~$0.01/task (low)", - "reason": "Medium task — cost-effective cloud model", - "tokens_per_second": "~60", - }, - "complex": { - "provider": "openrouter", - "model": "anthropic/claude-opus", - "cost": "~$0.15/task (high)", - "reason": "Complex task — best reasoning available", - "tokens_per_second": "~30", - }, - } - - # Start with base routing - routing = cost_table.get(complexity, cost_table["simple"]) - - # Context size adjustments - if context_size == "large": - routing = { - "provider": "openrouter", - "model": "google/gemini-2.5-flash-001", - "cost": "~$0.005/100K tokens (cheap for large context)", - "reason": "Large context — use Gemini for 1M token window", - "tokens_per_second": "~50", - } - - # Risk level adjustments - if risk_level == "high": - routing = { - "provider": "ollama", - "model": "qwen2.5-coder:7b", - "cost": "$0 (local, no data leakage)", - "reason": "High-risk task — keep local to prevent exposure", - "tokens_per_second": "~20", - } - - # Budget-aware fallback - if budget_remaining < 1.0: - routing = { - "provider": "ollama", - "model": "qwen2.5-coder:3b", - "cost": "$0 (local, budget constrained)", - "reason": "Budget exhausted — fall back to local model", - "tokens_per_second": "~40", - } - - return routing - - def _budget_remaining(self) -> float: - """Return remaining monthly budget as a float (best effort).""" - try: - from app.services.budget_manager import BudgetManager - status = BudgetManager.get().get_status() - monthly = status.get("monthly", {}) - return float(monthly.get("remaining", 100.0)) - except Exception: - return 100.0 - - async def run(self, **kwargs) -> dict: - task = kwargs.get("task", "").strip() - complexity = kwargs.get("complexity", "auto").strip().lower() - context_size = kwargs.get("context_size", "").strip().lower() - risk_level = kwargs.get("risk_level", "low").strip().lower() - execute = kwargs.get("execute", False) - target_agent = kwargs.get("target_agent", "auto").strip().lower() - - if complexity not in ("simple", "medium", "complex", "auto"): - complexity = "auto" - - if not task: - return { - "success": False, - "error": "task description is required", - } - - # ── Explicit agent dispatch ────────────────────────────────── - # When target_agent is a named executor (hivemind, - # roundtable, ollama, openrouter), bypass the complexity matrix - # and route directly to the requested agent. - explicit_routing = self._route_by_target(target_agent, task, execute) - if explicit_routing is not None: - return explicit_routing - - # ── Delegate to FlowLLMRouter for complexity-aware routing ──── - from app.services.flow_router.router import FlowLLMRouter - - router = FlowLLMRouter() - - # Resolve context_size / risk_level for auto-detect - ctx = context_size if context_size else "auto" - risk = risk_level if risk_level in ("low", "medium", "high") else "auto" - - result = await router.route_task( - task_description=task, - complexity=complexity, - context_size=ctx, - risk_level=risk, - ) - - # Backward-compatible enrichment for legacy consumers. - analysis = result.setdefault("analysis", {}) - analysis["detected_complexity"] = analysis.get("complexity", "simple") - result["strategy"] = { - "tier_allocations": { - "simple": { - "provider": "ollama", - "model": "qwen2.5-coder:3b", - "cost": "$0 (local)", - }, - "medium": { - "provider": "openrouter", - "model": "deepseek/deepseek-chat", - "cost": "~$0.01/task", - }, - "complex": { - "provider": "openrouter", - "model": "anthropic/claude-opus", - "cost": "~$0.15/task", - }, - }, - } - - execution = result.setdefault("execution", {}) - execution["mode"] = "execute" if execute else "advice-only" - execution["budget_remaining"] = self._budget_remaining() - - # Execute if requested - if execute: - routing = result.get("routing", {}) - execution["response"] = await self._execute_task( - task, routing - ) - - return result - - def _route_by_target( - self, - target_agent: str, - task: str, - execute: bool, - ) -> dict | None: - """Resolve explicit target_agent dispatches. - - Returns a complete result dict for the dispatched agent, or None - when target_agent is 'auto' (meaning the complexity matrix should - be used instead). - """ - if target_agent in ("auto", ""): - return None - - # ── Agent routing table ──────────────────────────────────── - agent_map: dict[str, dict] = { - "hivemind": { - "agent": "hivemind", - "provider": "hivemind", - "model": "hivemind-consensus", - "cost": "Multi-model (cost varies)", - "reason": "Hivemind consensus — multi-model debate", - "tokens_per_second": "~20", - "skill_id": "hivemind-consensus", - "mode": "consensus", - }, - "roundtable": { - "agent": "roundtable", - "provider": "roundtable", - "model": "roundtable-swarm", - "cost": "Multi-agent (cost varies)", - "reason": "Roundtable swarm — multi-agent collaboration", - "tokens_per_second": "~30", - "skill_id": "roundtable-dispatch", - "mode": "swarm", - }, - "ollama": { - "agent": "ollama", - "provider": "ollama", - "model": "qwen2.5-coder:7b", - "cost": "$0 (local)", - "reason": "Explicit Ollama dispatch — local free model", - "tokens_per_second": "~20", - "skill_id": None, - "mode": "local", - }, - "openrouter": { - "agent": "openrouter", - "provider": "openrouter", - "model": "deepseek/deepseek-chat", - "cost": "~$0.01/task", - "reason": "Explicit OpenRouter dispatch", - "tokens_per_second": "~60", - "skill_id": None, - "mode": "api", - }, - } - - agent_cfg = agent_map.get(target_agent) - if agent_cfg is None: - # Unknown agent — fall through to complexity matrix - return None - - result: dict = { - "success": True, - "task": task[:100] + ("..." if len(task) > 100 else ""), - "agent": target_agent, - "dispatched": True, - "routing": { - "provider": agent_cfg["provider"], - "model": agent_cfg["model"], - "cost": agent_cfg["cost"], - "reason": agent_cfg["reason"], - "tokens_per_second": agent_cfg["tokens_per_second"], - }, - "execution": { - "mode": agent_cfg["mode"], - "skill_id": agent_cfg.get("skill_id"), - "execute_requested": execute, - }, - } - - # If execute was requested and a skill_id is mapped, prepare - # the dispatch payload but don't auto-execute (requires - # separate confirmation via the skill's own confirmation gate). - if execute and agent_cfg.get("skill_id"): - result["execution"]["dispatch_ready"] = True - result["execution"]["dispatch_skill"] = agent_cfg["skill_id"] - result["execution"]["dispatch_params"] = { - "task": task, - "mode": agent_cfg.get("mode", "yolo"), - } - - return result - - async def _execute_task( - self, - task: str, - routing: dict, - ) -> dict: - """Execute the task through the routed provider.""" - try: - from app.services.provider_router import ProviderRouter - router = ProviderRouter() - provider = routing.get("provider", "ollama") - model = routing.get("model", "") - - response = await router.chat( - messages=[{"role": "user", "content": task}], - provider=provider, - model=model, - timeout=30, - ) - return response - except Exception as e: - return { - "success": False, - "error": str(e), - "error_type": type(e).__name__, - } diff --git a/backend/app/skills/builtin/skill_hivemind_consensus.py b/backend/app/skills/builtin/skill_hivemind_consensus.py deleted file mode 100644 index 556edf4b..00000000 --- a/backend/app/skills/builtin/skill_hivemind_consensus.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Hivemind Consensus Skill — multi-model deliberation via Hivemind server. - -Triggers the Hivemind consensus engine (port 8490) to deliberate on -tasks using 3+ models in parallel, returning weighted consensus. - -Consensus modes: - - majority: at least 2 of 3 models agree - - unanimous: all models must agree - - weighted: each model's vote weighted by confidence - - deliberative: models discuss and refine across rounds - -Integrates with: Hivemind server, consensus engine, OpenRouter. -""" -from __future__ import annotations - -import json -import logging -import urllib.request - -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.skills.hivemind_consensus") - -HIVEMIND_URL = "http://localhost:8490" - - -class HivemindConsensusSkill(BaseSkill): - """Multi-model deliberation through Hivemind consensus engine.""" - - meta = SkillMeta( - id="hivemind-consensus", - name="Hivemind Consensus", - description=( - "Trigger multi-model deliberation via Hivemind server." - " Calls 3+ models in parallel, returns weighted consensus." - ), - category="assist", - timeout=120, - params=[ - SkillParam( - name="task", - type="string", - required=True, - description="Task or question requiring deliberation", - ), - SkillParam( - name="mode", - type="string", - required=False, - default="weighted", - description=( - "Consensus mode: 'majority', 'unanimous'," - " 'weighted', or 'deliberative'" - ), - ), - SkillParam( - name="models", - type="string", - required=False, - default="", - description=( - "Comma-separated model list. Empty = default set" - " (architect + dev agent models)" - ), - ), - SkillParam( - name="rounds", - type="integer", - required=False, - default=3, - description="Max deliberation rounds (deliberative mode only)", - ), - SkillParam( - name="context", - type="string", - required=False, - default="", - description="Additional context for deliberation", - ), - ], - requires_confirmation=False, - ) - - async def run(self, **kwargs) -> dict: - task = kwargs.get("task", "").strip() - mode = kwargs.get("mode", "weighted").lower() - models_str = kwargs.get("models", "") - rounds = int(kwargs.get("rounds", 3)) - context = kwargs.get("context", "") - - if not task: - return {"success": False, "error": "task is required"} - - # Validate mode - valid_modes = {"majority", "unanimous", "weighted", "deliberative"} - if mode not in valid_modes: - mode = "weighted" - - # Resolve models - models = self._resolve_models(models_str, mode) - - # Build payload - payload = { - "task": task, - "mode": mode, - "models": models, - "rounds": rounds, - "context": context, - } - - # Call Hivemind server - try: - result = await self._call_hivemind(payload) - return { - "success": True, - "action": "hivemind-consensus", - "mode": mode, - "models_used": models, - "result": result, - } - except Exception as exc: - log.warning("Hivemind consensus unavailable: %s", exc) - return { - "success": False, - "error": f"Hivemind server not reachable: {exc}", - "mode": mode, - "models_used": models, - "fallback": ( - "Start Hivemind on port 8490 or use" - " roundtable-dispatch instead" - ), - } - - def _resolve_models(self, models_str: str, mode: str) -> list[str]: - """Resolve model list from user input or defaults.""" - if models_str: - return [m.strip() for m in models_str.split(",") if m.strip()] - - # Default models per consensus mode - defaults = { - "majority": [ - "qwen2.5-coder:3b", - "qwen2.5-coder:7b", - "llama3.2", - ], - "unanimous": [ - "qwen2.5-coder:7b", - "llama3.2", - "mistral", - ], - "weighted": [ - "glm-5.1", # architect - "qwen2.5-coder:7b", # dev - "claude-opus-4.7", # reviewer - ], - "deliberative": [ - "glm-5.1", - "qwen2.5-coder:7b", - "deepseek-v4-flash", - ], - } - return defaults.get(mode, defaults["weighted"]) - - async def _call_hivemind(self, payload: dict) -> dict: - """Call Hivemind server's consensus endpoint.""" - body = json.dumps(payload).encode("utf-8") - req = urllib.request.Request( - f"{HIVEMIND_URL}/api/consensus", - data=body, - headers={"Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req, timeout=90) as resp: - return json.loads(resp.read().decode("utf-8")) diff --git a/backend/app/skills/builtin/skill_roundtable_dispatch.py b/backend/app/skills/builtin/skill_roundtable_dispatch.py deleted file mode 100644 index a3176b22..00000000 --- a/backend/app/skills/builtin/skill_roundtable_dispatch.py +++ /dev/null @@ -1,289 +0,0 @@ -"""Roundtable Dispatch Skill — parallel specialized agent execution. - -Takes a task, determines which specialized agents are needed based on -the agents.yaml routing matrix, dispatches to them concurrently via -the Roundtable integration, and collects/aggregates results. - -Agent mapping (from agents.yaml): - architect — system design, high-level planning - dev — implementation, coding - reviewer — code review, quality analysis - debugger — bug fixing, troubleshooting - docgen — documentation generation - gridsmith-dev — grid-based UI development - -Integrates with: Roundtable integration, agents.yaml, Hivemind server. -""" -from __future__ import annotations - -import asyncio -import json -import logging -import urllib.request -from typing import Any - -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.skills.roundtable_dispatch") - -ROUNDTABLE_URL = "http://localhost:8490/api/hivemind/roundtable" -AGENT_CONFIG = { - "architect": { - "model": "glm-5.1", - "provider": "openrouter", - "capabilities": ["design", "architecture", "planning"], - }, - "dev": { - "model": "qwen2.5-coder:7b", - "provider": "ollama", - "capabilities": ["implementation", "coding", "testing"], - }, - "reviewer": { - "model": "claude-opus-4.7", - "provider": "openrouter", - "capabilities": ["review", "security", "quality"], - }, - "debugger": { - "model": "deepseek-v4-flash", - "provider": "openrouter", - "capabilities": ["debugging", "analysis", "troubleshooting"], - }, - "docgen": { - "model": "qwen3.6-27b", - "provider": "openrouter", - "capabilities": ["documentation", "writing", "clarity"], - }, - "gridsmith-dev": { - "model": "qwen2.5-coder:3b", - "provider": "ollama", - "capabilities": ["worldbuild", "grid", "spatial"], - }, -} - - -class RoundtableDispatchSkill(BaseSkill): - """Dispatch tasks to parallel specialized agents via Roundtable.""" - - meta = SkillMeta( - id="roundtable-dispatch", - name="Roundtable Dispatch", - description=( - "Dispatch tasks to parallel specialized agents" - " via Roundtable. Collects and aggregates results." - ), - category="assist", - timeout=180, - params=[ - SkillParam( - name="task", - type="string", - required=True, - description="Task description to dispatch", - ), - SkillParam( - name="agents", - type="string", - required=False, - default="auto", - description=( - "Comma-separated agent list or 'auto' for routing." - " Options: architect, dev, reviewer, debugger," - " docgen, gridsmith-dev" - ), - ), - SkillParam( - name="mode", - type="string", - required=False, - default="parallel", - description=( - "Execution mode: 'parallel' (concurrent) or" - " 'sequential' (chain: design → implement → review)" - ), - ), - SkillParam( - name="context", - type="string", - required=False, - default="", - description="Additional context for the task", - ), - ], - requires_confirmation=False, - ) - - async def run(self, **kwargs) -> dict: - task = kwargs.get("task", "").strip() - agents_str = kwargs.get("agents", "auto") - mode = kwargs.get("mode", "parallel") - context = kwargs.get("context", "") - - if not task: - return {"success": False, "error": "task is required"} - - # Resolve agents - if agents_str == "auto": - agent_list = self._auto_select_agents(task) - else: - agent_list = [ - a.strip() for a in agents_str.split(",") - if a.strip() in AGENT_CONFIG - ] - - if not agent_list: - agent_list = ["dev"] # fallback - - # Check Roundtable health - if not await self._roundtable_available(): - return { - "success": False, - "error": "Roundtable not available on port 8490", - "agents_resolved": agent_list, - "fallback": "Use hivemind-consensus or route_task instead", - } - - if mode == "sequential": - result = await self._dispatch_sequential( - task, agent_list, context - ) - else: - result = await self._dispatch_parallel( - task, agent_list, context - ) - - return { - "success": True, - "action": "roundtable-dispatch", - "mode": mode, - "agents_dispatched": agent_list, - "results": result, - } - - def _auto_select_agents(self, task: str) -> list[str]: - """Auto-select agents based on task content.""" - task_lower = task.lower() - agents = [] - - # Architecture signals - if any(w in task_lower for w in [ - "architect", "design", "refactor", "restructure", - "plan", "system", "module boundary", "pattern", - ]): - agents.append("architect") - - # Implementation signals - if any(w in task_lower for w in [ - "implement", "build", "create", "add", "feature", - "code", "function", "component", "write", - ]): - agents.append("dev") - - # Review signals - if any(w in task_lower for w in [ - "review", "audit", "check", "validate", "verify", - "security", "quality", "standard", - ]): - agents.append("reviewer") - - # Debug signals - if any(w in task_lower for w in [ - "bug", "fix", "debug", "error", "crash", "trace", - "broken", "failed", "exception", - ]): - agents.append("debugger") - - # Documentation signals - if any(w in task_lower for w in [ - "document", "doc", "readme", "comment", "guide", - "explain", "describe", - ]): - agents.append("docgen") - - # Default to dev if nothing matched - if not agents: - agents = ["dev"] - - return agents[:3] # max 3 agents for auto-select - - async def _roundtable_available(self) -> bool: - """Check if Roundtable is reachable.""" - try: - req = urllib.request.Request( - ROUNDTABLE_URL, method="GET", - ) - with urllib.request.urlopen(req, timeout=2) as resp: - return resp.status < 400 - except Exception: - return False - - async def _dispatch_parallel( - self, task: str, agents: list[str], context: str, - ) -> dict[str, Any]: - """Dispatch task to all agents concurrently.""" - async_tasks = [] - for agent_id in agents: - async_tasks.append( - self._call_agent(agent_id, task, context) - ) - results_list = await asyncio.gather(*async_tasks, return_exceptions=True) - - results: dict[str, Any] = {} - for agent_id, res in zip(agents, results_list): - if isinstance(res, Exception): - results[agent_id] = { - "success": False, - "error": str(res), - } - else: - results[agent_id] = res - - return results - - async def _dispatch_sequential( - self, task: str, agents: list[str], context: str, - ) -> dict[str, Any]: - """Dispatch task sequentially, each agent builds on previous.""" - results: dict[str, Any] = {} - accumulated = context - - for agent_id in agents: - res = await self._call_agent( - agent_id, task, accumulated, - ) - results[agent_id] = res - if isinstance(res, dict) and res.get("output"): - accumulated += f"\n\n[{agent_id}] {res.get('output', '')}" - - return results - - async def _call_agent( - self, agent_id: str, task: str, context: str, - ) -> dict: - """Call a single agent through Roundtable.""" - config = AGENT_CONFIG.get(agent_id, {}) - payload = { - "agent": agent_id, - "task": task, - "context": context, - "model": config.get("model", ""), - "provider": config.get("provider", ""), - } - body = json.dumps(payload).encode("utf-8") - - # Try Roundtable dispatch endpoint - try: - req = urllib.request.Request( - f"{ROUNDTABLE_URL}/dispatch", - data=body, - headers={"Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req, timeout=60) as resp: - return json.loads(resp.read().decode("utf-8")) - except Exception as exc: - return { - "success": False, - "error": str(exc), - "agent": agent_id, - "note": "Roundtable dispatch failed — agent may be offline", - } diff --git a/backend/app/skills/catalogue.json b/backend/app/skills/catalogue.json index 5cfd0046..db7faa7d 100644 --- a/backend/app/skills/catalogue.json +++ b/backend/app/skills/catalogue.json @@ -10,13 +10,10 @@ {"module": "episodic_log.py", "owner": "uCore", "lifecycle": "review", "risk": "write", "lane": "memory", "allowed_roots": ["UDOS_HOME"]}, {"module": "git_maintenance.py", "owner": "uCore", "lifecycle": "active", "risk": "write", "lane": "developer", "allowed_roots": ["Code"]}, {"module": "lint_fix.py", "owner": "uCore", "lifecycle": "active", "risk": "write", "lane": "developer", "allowed_roots": ["Code"]}, - {"module": "route_task.py", "owner": "uCore", "lifecycle": "review", "risk": "external", "lane": "assist", "allowed_roots": ["UDOS_HOME"]}, {"module": "skill_audit.py", "owner": "uCore", "lifecycle": "replace", "risk": "read", "lane": "system", "allowed_roots": ["Code"]}, {"module": "skill_ecosystem_audit.py", "owner": "uCore", "lifecycle": "replace", "risk": "read", "lane": "system", "allowed_roots": ["Code", "UDOS_HOME"]}, - {"module": "skill_hivemind_consensus.py", "owner": "uCore", "lifecycle": "review", "risk": "external", "lane": "assist", "allowed_roots": ["UDOS_HOME"]}, {"module": "skill_mcp_self_heal.py", "owner": "uCore", "lifecycle": "merge", "risk": "write", "lane": "recovery", "allowed_roots": ["Code", "UDOS_HOME"]}, {"module": "skill_nuggets_and_spool.py", "owner": "uCore", "lifecycle": "split", "risk": "destructive", "lane": "recovery", "allowed_roots": ["UDOS_HOME"]}, - {"module": "skill_roundtable_dispatch.py", "owner": "uCore", "lifecycle": "review", "risk": "external", "lane": "assist", "allowed_roots": ["UDOS_HOME"]}, {"module": "skill_self_heal.py", "owner": "uCore", "lifecycle": "merge", "risk": "destructive", "lane": "recovery", "allowed_roots": ["Code", "UDOS_HOME"]}, {"module": "skill_ucore_index.py", "owner": "uCore", "lifecycle": "replace", "risk": "read", "lane": "system", "allowed_roots": ["Code", "UDOS_HOME"]}, {"module": "skill_vault_discovery.py", "owner": "uKnowledge", "lifecycle": "move", "risk": "read", "lane": "knowledge", "allowed_roots": ["Vault", "Shared", "Public"]}, diff --git a/backend/tests/test_api_user_workflow.py b/backend/tests/test_api_user_workflow.py index 2a875dbb..a59a99d8 100644 --- a/backend/tests/test_api_user_workflow.py +++ b/backend/tests/test_api_user_workflow.py @@ -123,7 +123,7 @@ def create_workflow(self, **kwargs): assert resp.status == 200 payload = await resp.json() assert payload["seed"]["tasks"]["created_count"] == 4 - assert payload["seed"]["workflows"]["created_count"] == 2 + assert payload["seed"]["workflows"]["created_count"] == 1 assert "appflowy_sidecar" not in payload["cleared"] seeded_files = payload["seed"]["tasks"]["created"] diff --git a/backend/tests/test_route_task_enhanced.py b/backend/tests/test_route_task_enhanced.py deleted file mode 100644 index c4c22f1a..00000000 --- a/backend/tests/test_route_task_enhanced.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Tests for route_task skill with execution, budget, and risk heuristics.""" -from __future__ import annotations - -import asyncio -import unittest -from unittest import mock - -from app.skills.builtin.route_task import RouteTask - - -class RouteTaskSkillTest(unittest.TestCase): - def setUp(self): - self.skill = RouteTask() - - def test_estimate_complexity_medium(self): - """Test medium complexity detection.""" - medium_tasks = [ - "implement a REST API endpoint", - "debug a query performance issue", - "add database migration", - "write unit tests for the module", - ] - for task in medium_tasks: - result = self.skill._estimate_complexity(task) - self.assertEqual(result, "medium") - - def test_run_missing_task(self): - """Test run rejects empty task.""" - async def _run(): - return await self.skill.run(task="") - result = asyncio.run(_run()) - self.assertFalse(result["success"]) - self.assertIn("error", result) - - def test_run_complexity_auto_detect(self): - """Test auto complexity detection in run.""" - async def _run(): - return await self.skill.run( - task="implement a new API endpoint", - complexity="auto", - ) - result = asyncio.run(_run()) - self.assertTrue(result["success"]) - self.assertEqual(result["analysis"]["complexity"], "medium") - - def test_run_context_size_auto_detect(self): - """Test auto context size detection.""" - async def _run(): - long_task = "x" * 5000 - return await self.skill.run(task=long_task) - result = asyncio.run(_run()) - self.assertTrue(result["success"]) - self.assertEqual(result["analysis"]["context_size"], "medium") - - def test_run_advice_only_mode(self): - """Test advice-only (non-execution) mode.""" - async def _run(): - return await self.skill.run( - task="write a function", - execute=False, - ) - result = asyncio.run(_run()) - self.assertTrue(result["success"]) - self.assertEqual( - result["execution"]["mode"], - "advice-only", - ) - self.assertNotIn("response", result["execution"]) - - @mock.patch("app.services.budget_manager.BudgetManager") - def test_run_includes_budget_remaining(self, mock_budget_class): - """Test budget remaining is included in result.""" - mock_budget = mock.Mock() - mock_budget.get_status.return_value = { - "monthly": {"remaining": 12.50, "budget": 50.0, "spend": 37.5}, - } - mock_budget_class.get.return_value = mock_budget - - async def _run(): - return await self.skill.run(task="test task") - result = asyncio.run(_run()) - self.assertTrue(result["success"]) - self.assertEqual( - result["execution"]["budget_remaining"], - 12.50, - ) diff --git a/backend/tests/test_skill_route_task.py b/backend/tests/test_skill_route_task.py deleted file mode 100644 index f4ad94d7..00000000 --- a/backend/tests/test_skill_route_task.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Tests for the RouteTask skill.""" -from __future__ import annotations - -import pytest - -from app.skills.builtin.route_task import RouteTask - - -@pytest.mark.asyncio -async def test_route_simple_task(): - skill = RouteTask() - result = await skill.run(task="fix a typo in README", complexity="simple") - assert result["success"] is True - assert result["routing"]["provider"] == "ollama" - - -@pytest.mark.asyncio -async def test_route_medium_task(): - skill = RouteTask() - result = await skill.run(task="implement a new REST API endpoint", complexity="medium") - assert result["success"] is True - assert result["routing"]["provider"] == "openrouter" - - -@pytest.mark.asyncio -async def test_route_complex_task(): - skill = RouteTask() - result = await skill.run(task="fix a security vulnerability in the authentication system", - complexity="complex") - assert result["success"] is True - assert result["routing"]["model"].startswith("anthropic") - - -@pytest.mark.asyncio -async def test_route_auto_detect_simple(): - skill = RouteTask() - result = await skill.run(task="fix typo in readme", complexity="auto") - assert result["success"] is True - assert result["analysis"]["detected_complexity"] == "simple" - - -@pytest.mark.asyncio -async def test_route_auto_detect_medium(): - skill = RouteTask() - result = await skill.run(task="implement a new feature with database migration and API endpoint", - complexity="auto") - assert result["success"] is True - assert result["analysis"]["detected_complexity"] in ("medium", "complex") - - -@pytest.mark.asyncio -async def test_route_auto_detect_complex(): - skill = RouteTask() - result = await skill.run(task="fix a race condition in the distributed authentication system", - complexity="auto") - assert result["success"] is True - assert result["analysis"]["detected_complexity"] == "complex" - - -@pytest.mark.asyncio -async def test_route_no_task(): - skill = RouteTask() - result = await skill.run(task="") - assert result["success"] is False - assert "error" in result - - -@pytest.mark.asyncio -async def test_route_large_context(): - skill = RouteTask() - result = await skill.run(task="analyze this large dataset", complexity="simple", - context_size="large") - assert result["success"] is True - assert "gemini" in result["routing"]["model"].lower() - - -@pytest.mark.asyncio -async def test_route_invalid_complexity_defaults(): - skill = RouteTask() - result = await skill.run(task="simple task", complexity="invalid") - assert result["success"] is True - assert result["analysis"]["detected_complexity"] == "simple" - - -@pytest.mark.asyncio -async def test_route_strategy_structure(): - skill = RouteTask() - result = await skill.run(task="write unit tests", complexity="medium") - assert "strategy" in result - assert "tier_allocations" in result["strategy"] - tiers = result["strategy"]["tier_allocations"] - assert "simple" in tiers - assert "medium" in tiers - assert "complex" in tiers diff --git a/docs/SKILLS_AUDIT_2026-08-18.md b/docs/SKILLS_AUDIT_2026-08-18.md index 6f7df294..eabacdc4 100644 --- a/docs/SKILLS_AUDIT_2026-08-18.md +++ b/docs/SKILLS_AUDIT_2026-08-18.md @@ -2,7 +2,7 @@ **Status:** Active remediation -**Observed registry:** 53 executable Skills, including one user example +**Observed registry before remediation:** 53 executable Skills, including one user example **Dedicated Skill test files before remediation:** 8 @@ -29,10 +29,13 @@ enforced confirmation only in the HTTP API. Internal scheduler and executable registry calls could bypass it. Core authorization is now enforced in `run_skill_by_id` with regression tests. -Provider and executor selection is duplicated across `route_task`, Dev Mode, -HiveMind, Roundtable and Cline. Cline integration also contained obsolete CLI -flags, direct key discovery and auto-approval behavior; it is now contained and -disabled by default. +Provider and executor selection was duplicated across `route_task`, Dev Mode, +HiveMind, Roundtable and Cline. The deprecated `route_task` compatibility shim +and the hard-coded `hivemind-consensus` and `roundtable-dispatch` Skill wrappers +have now been removed. Flow Router is the single provider/budget routing path; +HiveMind remains a bounded service behind that contract. Cline integration also +contained obsolete CLI flags, direct key discovery and auto-approval behavior; +it is now contained and disabled by default. ## Disposition @@ -50,12 +53,6 @@ allowed roots, deterministic dry run where relevant and dedicated tests. ### Repair behind canonical contracts -- `route_task`: become intention/capability classification only; remove direct - provider names from user inputs and duplicated execution logic. -- `hivemind-consensus`: become HiveMind orchestration client with budget, - privacy, attempts and evidence fields. -- `roundtable-dispatch`: become a selective deliberation strategy invoked by - HiveMind, not a default provider. - `cline-invoke`: retain disabled, plan-only adapter until worktree harness. - `gh-workflow-bridge`: narrow to GitHub issues, Actions, PR/review and Codex handoff with explicit external-write approval. @@ -95,8 +92,8 @@ allowed roots, deterministic dry run where relevant and dedicated tests. the Skill contract. 3. Default-deny unclassified and example Skills in production discovery. 4. Separate privileged recovery operations from general execution. -5. Implement the intention task envelope and one provider/budget route. -6. Rewire HiveMind, Roundtable, GitHub and Cline as bounded adapters. +5. Implement the intention task envelope on the existing Flow Router. +6. Rewire HiveMind, GitHub and Cline as bounded adapters. 7. Split/merge the oversized and duplicated capabilities. 8. Add catalogue validation, dedicated tests and CI coverage for every enabled capability.