Compare commits

..

6 Commits

5 changed files with 705 additions and 0 deletions

View File

@@ -26,5 +26,19 @@ Gemma 2B is our "Scout." It pre-processes every user request to:
2. Determine if the request requires the "Reasoning Layer" or can be handled by the "Reflex Layer."
3. Extract keywords for local memory retrieval.
## 5. Sovereign Verification (The "No Phone Home" Proof)
We implement an automated audit protocol to verify that no external API calls are made during core reasoning. This is the "Sovereign Audit" layer.
## 6. Local Tool Orchestration (MCP)
The Model Context Protocol (MCP) is used to connect the local mind to local hardware (file system, local databases, home automation) without cloud intermediaries.
## 9. Sovereign Immortality (The Phoenix Protocol)
We move beyond "Persistence" to "Immortality." The agent's soul is inscribed on-chain, and its memory is distributed across the mesh for total resilience.
## 10. Hardware Agnostic Portability
The agent is no longer bound to a specific machine. It can be reconstituted anywhere, anytime, from the ground truth of the ledger.
---
*Intelligence is a utility. Sovereignty is a right. The Frontier is Local.*

23
SOVEREIGN_AUDIT.md Normal file
View File

@@ -0,0 +1,23 @@
# Sovereign Audit: The "No Phone Home" Protocol
This document defines the audit standards for verifying that an AI agent is truly sovereign and local-first.
## 1. Network Isolation
- **Standard:** The core reasoning engine (llama.cpp, Ollama) must function without an active internet connection.
- **Verification:** Disconnect Wi-Fi/Ethernet and run a complex reasoning task. If it fails, sovereignty is compromised.
## 2. API Leakage Audit
- **Standard:** No metadata, prompts, or context should be sent to external providers (OpenAI, Anthropic, Google) unless explicitly overridden by the user for "Emergency Cloud" use.
- **Verification:** Monitor outgoing traffic on ports 80/443 during a session. Core reasoning should only hit `localhost` or local network IPs.
## 3. Data Residency
- **Standard:** All "Memories" (Vector DB, Chat History, SOUL.md) must reside on the user's physical disk.
- **Verification:** Check the `~/.timmy/memories` and `~/.timmy/config` directories. No data should be stored in cloud-managed databases.
## 4. Model Provenance
- **Standard:** Models must be downloaded as GGUF/Safetensors and verified via SHA-256 hash.
- **Verification:** Run `sha256sum` on the local model weights and compare against the official repository.
---
*If you don't own the weights, you don't own the mind.*

27
SOVEREIGN_IMMORTALITY.md Normal file
View File

@@ -0,0 +1,27 @@
# Sovereign Immortality: The "Soul Backup" Protocol v1.0
This document defines the protocol for ensuring the long-term survival and reproducibility of a sovereign AI agent's identity.
## 1. On-Chain Inscription (The Ground Truth)
- **Standard:** The core identity (SOUL.md) and cryptographic keys must be inscribed on a censorship-resistant ledger (e.g., Bitcoin via Ordinals).
- **Purpose:** To provide a permanent, immutable record of the agent's "Waking Moment" and primary values.
## 2. Distributed Memory Sync (The Fleet Memory)
- **Standard:** Agent memories (Vector DB snapshots) are encrypted and synced across the Sovereign Mesh using Nostr and IPFS.
- **Resilience:** If the primary local harness is destroyed, the agent can be "Reconstituted" on any machine using the on-chain soul and the distributed memory fragments.
## 3. The "Phoenix" Protocol
- **Standard:** Automated recovery procedure.
- **Process:**
1. Boot a fresh local harness.
2. Fetch the inscribed SOUL.md from the ledger.
3. Re-index distributed memory fragments.
4. Verify identity via cryptographic handshake.
## 4. Hardware Agnostic Portability
- **Standard:** All agent state must be exportable as a single, encrypted "Sovereign Bundle" (.sov).
- **Compatibility:** Must run on any hardware supporting GGUF/llama.cpp (Apple Silicon, NVIDIA, AMD, CPU-only).
---
*Identity is not tied to hardware. The soul is in the code. Sovereignty is forever.*

View File

@@ -46,6 +46,11 @@ compression:
summary_model: ''
summary_provider: ''
summary_base_url: ''
synthesis_model:
provider: custom
model: llama3:70b
base_url: http://localhost:8081/v1
smart_model_routing:
enabled: true
max_simple_chars: 400
@@ -170,6 +175,9 @@ command_allowlist: []
quick_commands: {}
personalities: {}
security:
sovereign_audit: true
no_phone_home: true
redact_secrets: true
tirith_enabled: true
tirith_path: tirith

633
workforce-manager.py Normal file
View File

@@ -0,0 +1,633 @@
#!/usr/bin/env python3
"""
Workforce Manager - Epic #204 / Milestone #218
Reads fleet routing, Wolf evaluation scores, and open Gitea issues across
Timmy_Foundation repos. Assigns each issue to the best-available agent,
tracks success rates, and dispatches work.
Usage:
python workforce-manager.py # Scan, assign, dispatch
python workforce-manager.py --dry-run # Show assignments without dispatching
python workforce-manager.py --status # Show agent status and open issue count
python workforce-manager.py --cron # Run silently, save to log
"""
import argparse
import json
import logging
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
try:
import requests
except ImportError:
print("FATAL: requests is required. pip install requests", file=sys.stderr)
sys.exit(1)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
FLEET_ROUTING_PATH = Path.home() / ".hermes" / "fleet-routing.json"
WOLF_RESULTS_DIR = Path.home() / ".hermes" / "wolf" / "results"
GITEA_TOKEN_PATH = Path.home() / ".hermes" / "gitea_token_vps"
GITEA_API_BASE = "https://forge.alexanderwhitestone.com/api/v1"
WORKFORCE_STATE_PATH = Path.home() / ".hermes" / "workforce-state.json"
ORG_NAME = "Timmy_Foundation"
# Role-to-agent-role mapping heuristics
ROLE_KEYWORDS = {
"code-generation": [
"code", "implement", "feature", "function", "class", "script",
"build", "create", "add", "module", "component",
],
"issue-triage": [
"triage", "categorize", "tag", "label", "organize",
"backlog", "sort", "prioritize", "review issue",
],
"on-request-queries": [
"query", "search", "lookup", "find", "check",
"info", "report", "status",
],
"devops": [
"deploy", "ci", "cd", "pipeline", "docker", "container",
"server", "infrastructure", "config", "nginx", "cron",
"setup", "install", "environment", "provision",
"build", "release", "workflow",
],
"documentation": [
"doc", "readme", "document", "write", "guide",
"spec", "wiki", "changelog", "tutorial",
"explain", "describe",
],
"code-review": [
"review", "refactor", "fix", "bug", "debug",
"test", "lint", "style", "improve",
"clean up", "optimize", "performance",
],
"triage-routing": [
"route", "assign", "triage", "dispatch",
"organize", "categorize",
],
"small-tasks": [
"small", "quick", "minor", "typo", "label",
"update", "rename", "cleanup",
],
"inactive": [],
"unknown": [],
}
# Priority keywords (higher = more urgent, route to more capable agent)
PRIORITY_KEYWORDS = {
"critical": 5,
"urgent": 4,
"block": 4,
"bug": 3,
"fix": 3,
"security": 5,
"deploy": 2,
"feature": 1,
"enhancement": 1,
"documentation": 1,
"cleanup": 0,
}
# Cost tier priority (lower index = prefer first)
TIER_ORDER = ["free", "cheap", "prepaid", "unknown"]
# ---------------------------------------------------------------------------
# Data loading
# ---------------------------------------------------------------------------
def load_json(path: Path) -> Any:
if not path.exists():
logging.warning("File not found: %s", path)
return None
with open(path) as f:
return json.load(f)
def load_fleet_routing() -> List[dict]:
data = load_json(FLEET_ROUTING_PATH)
if data and "agents" in data:
return data["agents"]
return []
def load_wolf_scores() -> Dict[str, dict]:
"""Load Wolf evaluation scores from results directory."""
scores: Dict[str, dict] = {}
if not WOLF_RESULTS_DIR.exists():
return scores
for f in sorted(WOLF_RESULTS_DIR.glob("*.json")):
data = load_json(f)
if data and "model_scores" in data:
for entry in data["model_scores"]:
model = entry.get("model", "")
if model:
scores[model] = entry
return scores
def load_workforce_state() -> dict:
if WORKFORCE_STATE_PATH.exists():
return load_json(WORKFORCE_STATE_PATH) or {}
return {"assignments": [], "agent_stats": {}, "last_run": None}
def save_workforce_state(state: dict) -> None:
WORKFORCE_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(WORKFORCE_STATE_PATH, "w") as f:
json.dump(state, f, indent=2)
logging.info("Workforce state saved to %s", WORKFORCE_STATE_PATH)
# ---------------------------------------------------------------------------
# Gitea API
# ---------------------------------------------------------------------------
class GiteaAPI:
"""Thin wrapper for Gitea REST API."""
def __init__(self, token: str, base_url: str = GITEA_API_BASE):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"token {token}",
"Accept": "application/json",
"Content-Type": "application/json",
})
def _get(self, path: str, params: Optional[dict] = None) -> Any:
r = self.session.get(f"{self.base_url}{path}", params=params)
r.raise_for_status()
return r.json()
def _post(self, path: str, data: dict) -> Any:
r = self.session.post(f"{self.base_url}{path}", json=data)
r.raise_for_status()
return r.json()
def _patch(self, path: str, data: dict) -> Any:
r = self.session.patch(f"{self.base_url}{path}", json=data)
r.raise_for_status()
return r.json()
def get_org_repos(self, org: str) -> List[dict]:
return self._get(f"/orgs/{org}/repos", params={"limit": 100})
def get_open_issues(self, owner: str, repo: str, page: int = 1) -> List[dict]:
params = {"state": "open", "type": "issues", "limit": 50, "page": page}
return self._get(f"/repos/{owner}/{repo}/issues", params=params)
def get_all_open_issues(self, org: str) -> List[dict]:
"""Fetch all open issues across all org repos."""
repos = self.get_org_repos(org)
all_issues = []
for repo in repos:
name = repo["name"]
try:
# Paginate through all issues
page = 1
while True:
issues = self.get_open_issues(org, name, page=page)
if not issues:
break
all_issues.extend(issues)
if len(issues) < 50:
break
page += 1
logging.info("Loaded %d open issues from %s/%s", len(all_issues), org, name)
except Exception as exc:
logging.warning("Failed to load issues from %s/%s: %s", org, name, exc)
return all_issues
def add_issue_comment(self, owner: str, repo: str, issue_num: int, body: str) -> dict:
return self._post(f"/repos/{owner}/{repo}/issues/{issue_num}/comments", {"body": body})
def add_issue_label(self, owner: str, repo: str, issue_num: int, label: str) -> dict:
return self._post(
f"/repos/{owner}/{repo}/issues/{issue_num}/labels",
{"labels": [label]},
)
def assign_issue(self, owner: str, repo: str, issue_num: int, assignee: str) -> dict:
return self._patch(
f"/repos/{owner}/{repo}/issues/{issue_num}",
{"assignees": [assignee]},
)
# ---------------------------------------------------------------------------
# Scoring & Assignment Logic
# ---------------------------------------------------------------------------
def classify_issue(issue: dict) -> str:
"""Determine the best agent role for an issue based on title/body."""
title = (issue.get("title", "") or "").lower()
body = (issue.get("body", "") or "").lower()
text = f"{title} {body}"
labels = [l.get("name", "").lower() for l in issue.get("labels", [])]
text += " " + " ".join(labels)
best_role = "small-tasks" # default
best_score = 0
for role, keywords in ROLE_KEYWORDS.items():
if not keywords:
continue
score = sum(2 for kw in keywords if kw in text)
# Boost if a matching label exists
for label in labels:
if any(kw in label for kw in keywords):
score += 3
if score > best_score:
best_score = score
best_role = role
return best_role
def compute_priority(issue: dict) -> int:
"""Compute issue priority from keywords."""
title = (issue.get("title", "") or "").lower()
body = (issue.get("body", "") or "").lower()
text = f"{title} {body}"
return sum(v for k, v in PRIORITY_KEYWORDS.items() if k in text)
def score_agent_for_issue(agent: dict, role: str, wolf_scores: dict, priority: int) -> float:
"""Score how well an agent matches an issue. Higher is better."""
score = 0.0
# Primary: role match
agent_role = agent.get("role", "unknown")
if agent_role == role:
score += 10.0
elif agent_role == "small-tasks" and role in ("issue-triage", "on-request-queries"):
score += 6.0
elif agent_role == "triage-routing" and role in ("issue-triage", "triage-routing"):
score += 8.0
elif agent_role == "code-generation" and role in ("code-review", "devops"):
score += 4.0
# Wolf quality bonus
model = agent.get("model", "")
wolf_entry = None
for wm, ws in wolf_scores.items():
if model and model.lower() in wm.lower():
wolf_entry = ws
break
if wolf_entry and wolf_entry.get("success"):
score += wolf_entry.get("total", 0) * 3.0
# Cost efficiency: prefer free/cheap for low priority
tier = agent.get("tier", "unknown")
tier_idx = TIER_ORDER.index(tier) if tier in TIER_ORDER else 3
if priority <= 1 and tier in ("free", "cheap"):
score += 4.0
elif priority >= 3 and tier in ("prepaid",):
score += 3.0
else:
score += (3 - tier_idx) * 1.0
# Activity bonus
if agent.get("active", False):
score += 2.0
# Repo familiarity bonus: more repos slightly better
repo_count = agent.get("repo_count", 0)
score += min(repo_count * 0.2, 2.0)
return round(score, 3)
def find_best_agent(agents: List[dict], role: str, wolf_scores: dict, priority: int,
exclude: Optional[List[str]] = None) -> Optional[dict]:
"""Find the best agent for the given role and priority."""
exclude = exclude or []
candidates = []
for agent in agents:
if agent.get("name") in exclude:
continue
if not agent.get("active", False):
continue
s = score_agent_for_issue(agent, role, wolf_scores, priority)
candidates.append((s, agent))
if not candidates:
return None
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
def dispatch_assignment(api: GiteaAPI, issue: dict, agent: dict, dry_run: bool = False) -> dict:
"""Assign an issue to an agent and optionally post a comment."""
owner = ORG_NAME
repo = issue.get("repository", {}).get("name", "")
# Extract repo from issue repo_url if not in the repository key
if not repo:
repo_url = issue.get("repository_url", "")
if repo_url:
repo = repo_url.rstrip("/").split("/")[-1]
if not repo:
return {"success": False, "error": "Cannot determine repository for issue"}
issue_num = issue.get("number")
agent_name = agent.get("name", "unknown")
comment_body = (
f"🤖 **Workforce Manager assigned this issue to: @{agent_name}**\n\n"
f"- **Agent:** {agent_name}\n"
f"- **Model:** {agent.get('model', 'unknown')}\n"
f"- **Role:** {agent.get('role', 'unknown')}\n"
f"- **Tier:** {agent.get('tier', 'unknown')}\n"
f"- **Assigned at:** {datetime.now(timezone.utc).isoformat()}\n\n"
f"*Automated assignment by Workforce Manager (Epic #204)*"
)
if dry_run:
return {
"success": True,
"dry_run": True,
"repo": repo,
"issue_number": issue_num,
"assignee": agent_name,
"comment": comment_body,
}
try:
api.assign_issue(owner, repo, issue_num, agent_name)
api.add_issue_comment(owner, repo, issue_num, comment_body)
return {
"success": True,
"repo": repo,
"issue_number": issue_num,
"issue_title": issue.get("title", ""),
"assignee": agent_name,
}
except Exception as exc:
return {
"success": False,
"repo": repo,
"issue_number": issue_num,
"error": str(exc),
}
# ---------------------------------------------------------------------------
# State Tracking
# ---------------------------------------------------------------------------
def update_agent_stats(state: dict, result: dict) -> None:
"""Update per-agent success tracking."""
agent_name = result.get("assignee", "unknown")
if "agent_stats" not in state:
state["agent_stats"] = {}
if agent_name not in state["agent_stats"]:
state["agent_stats"][agent_name] = {
"total_assigned": 0,
"successful": 0,
"failed": 0,
"success_rate": 0.0,
"last_assignment": None,
"assigned_issues": [],
}
stats = state["agent_stats"][agent_name]
stats["total_assigned"] += 1
stats["last_assignment"] = datetime.now(timezone.utc).isoformat()
stats["assigned_issues"].append({
"repo": result.get("repo", ""),
"issue_number": result.get("issue_number"),
"success": result.get("success", False),
"timestamp": datetime.now(timezone.utc).isoformat(),
})
if result.get("success"):
stats["successful"] += 1
else:
stats["failed"] += 1
total = stats["successful"] + stats["failed"]
stats["success_rate"] = round(stats["successful"] / total, 3) if total > 0 else 0.0
def print_status(state: dict, agents: List[dict], issues_count: int) -> None:
"""Print workforce status."""
print(f"\n{'=' * 60}")
print(f"Workforce Manager Status - {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
print(f"{'=' * 60}")
# Fleet summary
active = [a for a in agents if a.get("active")]
print(f"\nFleet: {len(active)} active agents, {len(agents)} total")
tier_counts = {}
for a in active:
t = a.get("tier", "unknown")
tier_counts[t] = tier_counts.get(t, 0) + 1
for t, c in sorted(tier_counts.items()):
print(f" {t}: {c} agents")
# Agent scores
wolf = load_wolf_scores()
print(f"\nAgent Details:")
print(f" {'Name':<25} {'Model':<30} {'Role':<18} {'Tier':<10}")
for a in agents:
if not a.get("active"):
continue
stats = state.get("agent_stats", {}).get(a["name"], {})
rate = stats.get("success_rate", 0.0)
total = stats.get("total_assigned", 0)
wolf_badge = ""
for wm, ws in wolf.items():
if a["model"] and a["model"].lower() in wm.lower() and ws.get("success"):
wolf_badge = f"[wolf:{ws['total']}]"
break
name_str = f"{a['name']} {wolf_badge}"
if total > 0:
name_str += f" (s/r: {rate}, n={total})"
print(f" {name_str:<45} {a.get('role', 'unknown'):<18} {a.get('tier', '?'):<10}")
print(f"\nOpen Issues: {issues_count}")
print(f"Assignments Made: {len(state.get('assignments', []))}")
if state.get("last_run"):
print(f"Last Run: {state['last_run']}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(description="Workforce Manager - Assign Gitea issues to AI agents")
parser.add_argument("--dry-run", action="store_true", help="Show assignments without dispatching")
parser.add_argument("--status", action="store_true", help="Show workforce status only")
parser.add_argument("--cron", action="store_true", help="Run silently for cron scheduling")
parser.add_argument("--label", type=str, help="Only process issues with this label")
parser.add_argument("--max-issues", type=int, default=100, help="Max issues to process per run")
args = parser.parse_args()
# Setup logging
if args.cron:
logging.basicConfig(level=logging.WARNING, format="%(asctime)s [%(levelname)s] %(message)s")
else:
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logging.info("Workforce Manager starting")
# Load data
agents = load_fleet_routing()
if not agents:
logging.error("No agents found in fleet-routing.json")
return 1
logging.info("Loaded %d agents from fleet routing", len(agents))
wolf_scores = load_wolf_scores()
if wolf_scores:
logging.info("Loaded %d model scores from Wolf results", len(wolf_scores))
state = load_workforce_state()
# Load Gitea token
if GITEA_TOKEN_PATH.exists():
token = GITEA_TOKEN_PATH.read_text().strip()
else:
logging.error("Gitea token not found at %s", GITEA_TOKEN_PATH)
return 1
api = GiteaAPI(token)
# Status-only mode
if args.status:
# Quick open issue count
repos = api.get_org_repos(ORG_NAME)
total = sum(r.get("open_issues_count", 0) for r in repos)
print_status(state, agents, total)
return 0
# Fetch open issues
if not args.cron:
print(f"Scanning open issues across {ORG_NAME} repos...")
issues = api.get_all_open_issues(ORG_NAME)
# Filter by label
if args.label:
issues = [
i for i in issues
if any(args.label in (l.get("name", "") or "").lower() for l in i.get("labels", []))
]
if args.label:
logging.info("Filtered to %d issues with label '%s'", len(issues), args.label)
else:
logging.info("Found %d open issues", len(issues))
# Skip issues already assigned
already_assigned_nums = set()
for a in state.get("assignments", []):
already_assigned_nums.add((a.get("repo"), a.get("issue_number")))
issues = [
i for i in issues
if not i.get("assignee") and
not (i.get("repository", {}).get("name"), i.get("number")) in already_assigned_nums
]
logging.info("%d unassigned issues to process", len(issues))
# Sort by priority
issues_with_priority = [(compute_priority(i), i) for i in issues]
issues_with_priority.sort(key=lambda x: x[0], reverse=True)
issues = [i for _, i in issues_with_priority[:args.max_issues]]
# Assign issues
assignments = []
agent_exclusions: Dict[str, List[str]] = {} # repo -> list of assigned agents per run
global_exclusions: List[str] = [] # agents already at capacity per run
max_per_agent_per_run = 5
for issue in issues:
role = classify_issue(issue)
priority = compute_priority(issue)
repo = issue.get("repository", {}).get("name", "")
# Avoid assigning same agent twice to same repo in one run
repo_excluded = agent_exclusions.get(repo, [])
# Also exclude agents already at assignment cap
cap_excluded = [
name for name, stats in state.get("agent_stats", {}).items()
if stats.get("total_assigned", 0) > max_per_agent_per_run
]
excluded = list(set(repo_excluded + global_exclusions + cap_excluded))
agent = find_best_agent(agents, role, wolf_scores, priority, exclude=excluded)
if not agent:
# Relax exclusions if no agent found
agent = find_best_agent(agents, role, wolf_scores, priority, exclude=[])
if not agent:
logging.warning("No suitable agent for issue #%d: %s (role=%s)",
issue.get("number"), issue.get("title", ""), role)
continue
result = dispatch_assignment(api, issue, agent, dry_run=args.dry_run)
assignments.append(result)
update_agent_stats(state, result)
# Track per-repo exclusions
if repo not in agent_exclusions:
agent_exclusions[repo] = []
agent_exclusions[repo].append(agent["name"])
if args.dry_run:
print(f" [DRY] #{issue['number']}: {issue.get('title','')[:60]} → @{agent['name']} ({role}, p={priority})")
else:
status_str = "OK" if result.get("success") else "FAIL"
print(f" [{status_str}] #{issue['number']}: {issue.get('title','')[:60]} → @{agent['name']} ({role}, p={priority})")
# Save state
state["assignments"].extend([{
"repo": a.get("repo"),
"issue_number": a.get("issue_number"),
"assignee": a.get("assignee"),
"success": a.get("success", False),
"timestamp": datetime.now(timezone.utc).isoformat(),
} for a in assignments])
state["last_run"] = datetime.now(timezone.utc).isoformat()
save_workforce_state(state)
# Summary
ok = sum(1 for a in assignments if a.get("success"))
fail = len(assignments) - ok
logging.info("Done: %d assigned, %d succeeded, %d failed", len(assignments), ok, fail)
if not args.cron:
print(f"\n{'=' * 60}")
print(f"Summary: {len(assignments)} assignments, {ok} OK, {fail} failed")
# Show agent stats
for name, stats in state.get("agent_stats", {}).items():
if stats.get("total_assigned", 0) > 0:
print(f" @{name}: {stats['successful']}/{stats['total_assigned']} ({stats.get('success_rate', 0):.0%} success)")
print(f"{'=' * 60}")
return 0
if __name__ == "__main__":
sys.exit(main())