Phase 1: Routing Layer Changes to control_api.py
Changes made
1. Added ROUTING constants and helpers (after NOTE_RE)
ROUTES_PATH = os.path.join(WIKI, "control-plane", "model-routing.json")
VALID_TYPES = {"planning", "design", "audit", "review", "implementation",
"infra", "email", "admin", "coding", "bugfix"}
# Title keywords → task type inference (Phase 1: best-effort)
TYPE_KW = {
"planning": [re.compile(r"\b(planning|roadmap|plan\b)\b", re.I)],
"design": [re.compile(r"\b(design|specification|architecture)\b", re.I)],
"audit": [re.compile(r"\b(audit|review)\b", re.I)],
"infra": [re.compile(r"\b(infra|deploy|kubernetes|pod|node)\b", re.I)],
"email": [re.compile(r"\b(email|mail|smtp)\b", re.I)],
}
def _infer_type(title):
"""Best-effort type inference from title text. Returns first match or None."""
for t, pats in TYPE_KW.items():
if any(p.search(title) for p in pats):
return t
return None
def _load_routing():
"""Load model-routing.json; create defaults if missing."""
try:
return json.load(open(ROUTES_PATH, encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError):
return {"type_to_tier": {}, "tiers": {}}
def _save_routing(data):
os.makedirs(os.path.dirname(ROUTES_PATH), exist_ok=True)
json.dump(data, open(ROUTES_PATH, "w", encoding="utf-8"), indent=2)2. Updated _parse_task to include type field
In the return dict of _parse_task(), added:
"type": fm.get("type") or _infer_title_type(fm.get("title", "")),3. Updated create_task to accept and auto-infer type
# Inside create_task(), after priority line:
task_type = payload.get("type") or _infer_type(title) or "implementation"
task_type = task_type if task_type in VALID_TYPES else "implementation"
# Add to frontmatter generation:
f"type: {task_type}\n"4. Updated patch_task to support type and tier_override fields
Added "type" and "tier_override" to the patchable fields list.
5. New /api/routing endpoints
@app.get("/api/routing")
def get_routing():
return _load_routing()
@app.put("/api/routing")
def update_routing(payload: dict = Body(...)):
r = _load_routing()
# Merge provided fields into existing config
if "type_to_tier" in payload:
r["type_to_tier"].update(payload["type_to_tier"])
if "tiers" in payload:
for k, v in payload.get("tiers", {}).items():
if isinstance(v, dict):
r["tiers"][k].update(v)
_save_routing(r)
return {"ok": True}
@app.get("/api/tasks/{task_id}/tier")
def task_tier(task_id: str):
"""Return the tier assigned to a specific task, respecting override."""
f, status = _find_file(task_id)
if not f:
raise HTTPException(404, "task not found")
t = _parse_task(f, status)
routing = _load_routing()
# tier_override takes precedence
override = t.get("tier_override")
if override and override in routing.get("tiers", {}):
return {"task_id": task_id, "type": t.get("type"),
"tier": override, "overridden": True}
tt = t.get("type", "implementation")
tier = routing.get("type_to_tier", {}).get(tt, "builder")
return {"task_id": task_id, "type": tt, "tier": tier, "overridden": False}
@app.post("/api/tasks/{task_id}/dispatch")
def dispatch_task(task_id: str):
"""Dispatch a task to its routed agent (Phase 1 wire)."""
f, status = _find_file(task_id)
if not f:
raise HTTPException(404, "task not found")
t = _parse_task(f, status)
routing = _load_routing()
override = t.get("tier_override")
tt = t.get("type", "implementation")
tier_name = override if override in routing.get("tiers", {}) else \
routing.get("type_to_tier", {}).get(tt, "builder")
tier_info = routing.get("tiers", {}).get(tier_name, {})
# Phase 1: wire infra→mercury (operator tier), everything else→hermes (builder)
agent = tier_info.get("agent", "hermes").lower()
# Determine target endpoint based on agent name
if "mercury" in agent or tier_name == "operator":
target = "https://mercury.hermes.svc.cluster.local:9080/api/task"
else:
target = "https://hermes-api-server.hermes.svc.cluster.local:443/api/v1/tasks"
return {"task_id": task_id, "type": tt, "tier": tier_name,
"target_agent": agent, "dispatch_url": target}Verification steps
- GET /api/routing returns the existing model-routing.json content
- GET /api/tasks/{id}/tier returns the assigned tier for a task
- PUT /api/routing updates model-routing.json on disk
- Task creation auto-infers type from title keywords
- Infra tasks route to mercury, all others to hermes (builder)