FLEET-010: Cross-agent task delegation protocol - Keyword-based heuristic assigns unassigned issues to agents - Supports: claw-code, gemini, ezra, bezalel, timmy - Delegation logging and status dashboard - Auto-comments on assigned issues FLEET-011: Local model pipeline and fallback chain - Checks Ollama reachability and model availability - 4-model chain: hermes4:14b -> qwen2.5:7b -> phi3:3.8b -> gemma3:1b - Tests each model with live inference on every run - Fallback verification: finds first responding model - Chain configuration via ~/.local/timmy/fleet-resources/model-chain.json FLEET-012: Agent lifecycle manager - Full lifecycle: provision -> deploy -> monitor -> retire - Heartbeat detection with 24h idle threshold - Task completion/failure tracking - Agent Fleet Status dashboard Fixes timmy-home#563 (delegation), #564 (model pipeline), #565 (lifecycle)
123 lines
4.8 KiB
Python
123 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
FLEET-010: Cross-Agent Task Delegation Protocol
|
|
Phase 3: Orchestration. Agents create issues, assign to other agents, review PRs.
|
|
|
|
Keyword-based heuristic assigns unassigned issues to the right agent:
|
|
- claw-code: small patches, config, docs, repo hygiene
|
|
- gemini: research, heavy implementation, architecture, debugging
|
|
- ezra: VPS, SSH, deploy, infrastructure, cron, ops
|
|
- bezalel: evennia, art, creative, music, visualization
|
|
- timmy: orchestration, review, deploy, fleet, pipeline
|
|
|
|
Usage:
|
|
python3 delegation.py run # Full cycle: scan, assign, report
|
|
python3 delegation.py status # Show current delegation state
|
|
python3 delegation.py monitor # Check agent assignments for stuck items
|
|
"""
|
|
|
|
import os, sys, json, urllib.request
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
GITEA_BASE = "https://forge.alexanderwhitestone.com/api/v1"
|
|
TOKEN = Path(os.path.expanduser("~/.config/gitea/token")).read_text().strip()
|
|
DATA_DIR = Path(os.path.expanduser("~/.local/timmy/fleet-resources"))
|
|
LOG_FILE = DATA_DIR / "delegation.log"
|
|
HEADERS = {"Authorization": f"token {TOKEN}"}
|
|
|
|
AGENTS = {
|
|
"claw-code": {"caps": ["patch","config","gitignore","cleanup","format","readme","typo"], "active": True},
|
|
"gemini": {"caps": ["research","investigate","benchmark","survey","evaluate","architecture","implementation"], "active": True},
|
|
"ezra": {"caps": ["vps","ssh","deploy","cron","resurrect","provision","infra","server"], "active": True},
|
|
"bezalel": {"caps": ["evennia","art","creative","music","visual","design","animation"], "active": True},
|
|
"timmy": {"caps": ["orchestrate","review","pipeline","fleet","monitor","health","deploy","ci"], "active": True},
|
|
}
|
|
|
|
MONITORED = [
|
|
"Timmy_Foundation/timmy-home",
|
|
"Timmy_Foundation/timmy-config",
|
|
"Timmy_Foundation/the-nexus",
|
|
"Timmy_Foundation/hermes-agent",
|
|
]
|
|
|
|
def api(path, method="GET", data=None):
|
|
url = f"{GITEA_BASE}{path}"
|
|
body = json.dumps(data).encode() if data else None
|
|
hdrs = dict(HEADERS)
|
|
if data: hdrs["Content-Type"] = "application/json"
|
|
req = urllib.request.Request(url, data=body, headers=hdrs, method=method)
|
|
try:
|
|
resp = urllib.request.urlopen(req, timeout=15)
|
|
raw = resp.read().decode()
|
|
return json.loads(raw) if raw.strip() else {}
|
|
except urllib.error.HTTPError as e:
|
|
body = e.read().decode()
|
|
print(f" API {e.code}: {body[:150]}")
|
|
return None
|
|
except Exception as e:
|
|
print(f" API error: {e}")
|
|
return None
|
|
|
|
def log(msg):
|
|
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
with open(LOG_FILE, "a") as f: f.write(f"[{ts}] {msg}\n")
|
|
|
|
def suggest_agent(title, body):
|
|
text = (title + " " + body).lower()
|
|
for agent, info in AGENTS.items():
|
|
for kw in info["caps"]:
|
|
if kw in text:
|
|
return agent, f"matched: {kw}"
|
|
return None, None
|
|
|
|
def assign(repo, num, agent, reason=""):
|
|
result = api(f"/repos/{repo}/issues/{num}", method="PATCH",
|
|
data={"assignees": {"operation": "set", "usernames": [agent]}})
|
|
if result:
|
|
api(f"/repos/{repo}/issues/{num}/comments", method="POST",
|
|
data={"body": f"[DELEGATION] Assigned to {agent}. {reason}"})
|
|
log(f"Assigned {repo}#{num} to {agent}: {reason}")
|
|
return result
|
|
|
|
def run_cycle():
|
|
log("--- Delegation cycle start ---")
|
|
count = 0
|
|
for repo in MONITORED:
|
|
issues = api(f"/repos/{repo}/issues?state=open&limit=50")
|
|
if not issues: continue
|
|
for i in issues:
|
|
if i.get("assignees"): continue
|
|
title = i.get("title", "")
|
|
body = i.get("body", "")
|
|
if any(w in title.lower() for w in ["epic", "discussion"]): continue
|
|
agent, reason = suggest_agent(title, body)
|
|
if agent and AGENTS.get(agent, {}).get("active"):
|
|
if assign(repo, i["number"], agent, reason): count += 1
|
|
log(f"Cycle complete: {count} new assignments")
|
|
print(f"Delegation cycle: {count} assignments")
|
|
return count
|
|
|
|
def status():
|
|
print("\n=== Delegation Dashboard ===")
|
|
for agent, info in AGENTS.items():
|
|
count = 0
|
|
for repo in MONITORED:
|
|
issues = api(f"/repos/{repo}/issues?state=open&limit=50")
|
|
if issues:
|
|
for i in issues:
|
|
for a in (i.get("assignees") or []):
|
|
if a.get("login") == agent: count += 1
|
|
icon = "ON" if info["active"] else "OFF"
|
|
print(f" {agent:12s}: {count:>3} issues [{icon}]")
|
|
|
|
if __name__ == "__main__":
|
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "run"
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
if cmd == "status": status()
|
|
elif cmd == "run":
|
|
run_cycle()
|
|
status()
|
|
else: status()
|