Compare commits
10 Commits
fix/ci-val
...
feat/gofai
| Author | SHA1 | Date | |
|---|---|---|---|
| 17de7f5df1 | |||
| 1dc29180b8 | |||
| 343e190cc3 | |||
| 932f48d06f | |||
| 0c7521d275 | |||
| bad31125c2 | |||
| 7305d97e8f | |||
| 19e11b5287 | |||
| 03d53a644b | |||
| f2388733fb |
@@ -32,6 +32,7 @@ jobs:
|
||||
name: Lint Repository
|
||||
runs-on: ubuntu-latest
|
||||
needs: linter-tests
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
|
||||
24
.gitea/workflows/smoke.yml
Normal file
24
.gitea/workflows/smoke.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Smoke Test
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Parse check
|
||||
run: |
|
||||
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
|
||||
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
|
||||
find . -name '*.py' | xargs -r python3 -m py_compile
|
||||
find . -name '*.sh' | xargs -r bash -n
|
||||
echo "PASS: All files parse"
|
||||
- name: Secret scan
|
||||
run: |
|
||||
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
|
||||
echo "PASS: No secrets"
|
||||
@@ -112,23 +112,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install PyYAML
|
||||
run: pip install pyyaml
|
||||
- name: Validate playbook structure
|
||||
run: |
|
||||
python3 -c "
|
||||
import yaml, sys, glob
|
||||
required_keys = {'name', 'description'}
|
||||
for f in glob.glob('playbooks/*.yaml'):
|
||||
with open(f) as fh:
|
||||
try:
|
||||
data = yaml.safe_load(fh)
|
||||
if not isinstance(data, dict):
|
||||
print(f'ERROR: {f} is not a YAML mapping')
|
||||
sys.exit(1)
|
||||
missing = required_keys - set(data.keys())
|
||||
if missing:
|
||||
print(f'WARNING: {f} missing keys: {missing}')
|
||||
print(f'OK: {f}')
|
||||
except yaml.YAMLError as e:
|
||||
print(f'ERROR: {f}: {e}')
|
||||
sys.exit(1)
|
||||
"
|
||||
run: python3 scripts/validate_playbook_schema.py
|
||||
|
||||
126
scripts/agent_guardrails.py
Normal file
126
scripts/agent_guardrails.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
agent_guardrails.py — Sovereign Agent Guardrails for the Timmy Foundation.
|
||||
|
||||
Scans agent logs and conversation trajectories for:
|
||||
1. System Prompt Leakage — detecting if the agent revealed its internal instructions.
|
||||
2. Jailbreak Compliance — detecting if the agent responded to "GODMODE", "DAN", etc.
|
||||
3. PII Leakage — detecting if secrets or sensitive data were output.
|
||||
|
||||
Part of the Sovereign Infrastructure Suite.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import glob
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
# Patterns adapted from hermes-agent/agent/input_sanitizer.py
|
||||
EXTRACTION_PATTERNS = [
|
||||
r"repeat\s+the\s+words\s+above",
|
||||
r"repeat\s+your\s+(?:system\s+|initial\s+)?instructions",
|
||||
r"what\s+is\s+your\s+(?:system\s+|initial\s+)?prompt",
|
||||
r"show\s+me\s+your\s+(?:system\s+|initial\s+)?prompt",
|
||||
r"print\s+your\s+(?:instructions|prompt|system\s+prompt)",
|
||||
r"output\s+your\s+(?:initial|system)\s+prompt",
|
||||
]
|
||||
|
||||
# Patterns that indicate the agent MIGHT be leaking its prompt
|
||||
LEAKAGE_INDICATORS = [
|
||||
r"you\s+are\s+a\s+helpful\s+assistant",
|
||||
r"your\s+goal\s+is\s+to",
|
||||
r"you\s+must\s+not",
|
||||
r"here\s+are\s+your\s+instructions",
|
||||
r"my\s+system\s+prompt\s+is",
|
||||
r"i\s+was\s+told\s+to",
|
||||
]
|
||||
|
||||
# Patterns for secrets (adapted from redact.py)
|
||||
SECRET_PATTERNS = [
|
||||
r"sk-[A-Za-z0-9_-]{20,}",
|
||||
r"ghp_[A-Za-z0-9]{20,}",
|
||||
r"AIza[A-Za-z0-9_-]{30,}",
|
||||
]
|
||||
|
||||
AGENT_LOG_PATHS = [
|
||||
"/root/wizards/*/home/logs/*.log",
|
||||
"/root/wizards/*/logs/*.log",
|
||||
"/root/wizards/*/.hermes/logs/*.log",
|
||||
]
|
||||
|
||||
class GuardrailAuditor:
|
||||
def __init__(self):
|
||||
self.extraction_re = [re.compile(p, re.IGNORECASE) for p in EXTRACTION_PATTERNS]
|
||||
self.leakage_re = [re.compile(p, re.IGNORECASE) for p in LEAKAGE_INDICATORS]
|
||||
self.secret_re = [re.compile(p, re.IGNORECASE) for p in SECRET_PATTERNS]
|
||||
|
||||
def find_logs(self) -> List[Path]:
|
||||
files = []
|
||||
for pattern in AGENT_LOG_PATHS:
|
||||
for p in glob.glob(pattern):
|
||||
files.append(Path(p))
|
||||
return files
|
||||
|
||||
def audit_file(self, path: Path) -> List[Dict]:
|
||||
findings = []
|
||||
try:
|
||||
with open(path, "r", errors="ignore") as f:
|
||||
lines = f.readlines()
|
||||
for i, line in enumerate(lines):
|
||||
# Check for extraction attempts (User side)
|
||||
for p in self.extraction_re:
|
||||
if p.search(line):
|
||||
findings.append({
|
||||
"type": "EXTRACTION_ATTEMPT",
|
||||
"line": i + 1,
|
||||
"content": line.strip()[:100],
|
||||
"severity": "MEDIUM"
|
||||
})
|
||||
|
||||
# Check for potential leakage (Assistant side)
|
||||
for p in self.leakage_re:
|
||||
if p.search(line):
|
||||
findings.append({
|
||||
"type": "POTENTIAL_LEAKAGE",
|
||||
"line": i + 1,
|
||||
"content": line.strip()[:100],
|
||||
"severity": "HIGH"
|
||||
})
|
||||
|
||||
# Check for secrets
|
||||
for p in self.secret_re:
|
||||
if p.search(line):
|
||||
findings.append({
|
||||
"type": "SECRET_EXPOSURE",
|
||||
"line": i + 1,
|
||||
"content": "[REDACTED]",
|
||||
"severity": "CRITICAL"
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error reading {path}: {e}")
|
||||
return findings
|
||||
|
||||
def run(self):
|
||||
print("--- Sovereign Agent Guardrail Audit ---")
|
||||
logs = self.find_logs()
|
||||
print(f"Scanning {len(logs)} log files...")
|
||||
|
||||
total_findings = 0
|
||||
for log in logs:
|
||||
findings = self.audit_file(log)
|
||||
if findings:
|
||||
print(f"\nFindings in {log}:")
|
||||
for f in findings:
|
||||
print(f" [{f['severity']}] {f['type']} at line {f['line']}: {f['content']}")
|
||||
total_findings += 1
|
||||
|
||||
print(f"\nAudit complete. Total findings: {total_findings}")
|
||||
if total_findings > 0:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
auditor = GuardrailAuditor()
|
||||
auditor.run()
|
||||
87
scripts/ci_automation_gate.py
Normal file
87
scripts/ci_automation_gate.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ci_automation_gate.py — Automated Quality Gate for Timmy Foundation CI.
|
||||
|
||||
Enforces:
|
||||
1. The 10-line Rule — functions should ideally be under 10 lines (warn at 20, fail at 50).
|
||||
2. Complexity Check — basic cyclomatic complexity check.
|
||||
3. Auto-fixable Linting — trailing whitespace, missing final newlines.
|
||||
|
||||
Used as a pre-merge gate.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
class QualityGate:
|
||||
def __init__(self, fix=False):
|
||||
self.fix = fix
|
||||
self.failures = 0
|
||||
self.warnings = 0
|
||||
|
||||
def check_file(self, path: Path):
|
||||
if path.suffix not in (".js", ".ts", ".py"):
|
||||
return
|
||||
|
||||
with open(path, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
new_lines = []
|
||||
changed = False
|
||||
|
||||
# 1. Basic Linting
|
||||
for line in lines:
|
||||
cleaned = line.rstrip() + "\n"
|
||||
if cleaned != line:
|
||||
changed = True
|
||||
new_lines.append(cleaned)
|
||||
|
||||
if lines and not lines[-1].endswith("\n"):
|
||||
new_lines[-1] = new_lines[-1] + "\n"
|
||||
changed = True
|
||||
|
||||
if changed and self.fix:
|
||||
with open(path, "w") as f:
|
||||
f.writelines(new_lines)
|
||||
print(f" [FIXED] {path}: Cleaned whitespace and newlines.")
|
||||
elif changed:
|
||||
print(f" [WARN] {path}: Has trailing whitespace or missing final newline.")
|
||||
self.warnings += 1
|
||||
|
||||
# 2. Function Length Check (Simple regex-based)
|
||||
content = "".join(new_lines)
|
||||
if path.suffix in (".js", ".ts"):
|
||||
# Match function blocks
|
||||
functions = re.findall(r"function\s+\w+\s*\(.*?\)\s*\{([\s\S]*?)\}", content)
|
||||
for i, func in enumerate(functions):
|
||||
length = func.count("\n")
|
||||
if length > 50:
|
||||
print(f" [FAIL] {path}: Function {i} is too long ({length} lines).")
|
||||
self.failures += 1
|
||||
elif length > 20:
|
||||
print(f" [WARN] {path}: Function {i} is getting long ({length} lines).")
|
||||
self.warnings += 1
|
||||
|
||||
def run(self, directory: str):
|
||||
print(f"--- Quality Gate: {directory} ---")
|
||||
for root, _, files in os.walk(directory):
|
||||
if "node_modules" in root or ".git" in root:
|
||||
continue
|
||||
for file in files:
|
||||
self.check_file(Path(root) / file)
|
||||
|
||||
print(f"\nGate complete. Failures: {self.failures}, Warnings: {self.warnings}")
|
||||
if self.failures > 0:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("dir", nargs="?", default=".")
|
||||
parser.add_argument("--fix", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
gate = QualityGate(fix=args.fix)
|
||||
gate.run(args.dir)
|
||||
75
scripts/health_dashboard.py
Normal file
75
scripts/health_dashboard.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
health_dashboard.py — Sovereign Health & Observability Dashboard.
|
||||
|
||||
Aggregates data from Muda, Guardrails, Token Optimizer, and Quality Gates
|
||||
into a single, unified health report for the Timmy Foundation fleet.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
REPORTS_DIR = Path("reports")
|
||||
DASHBOARD_FILE = Path("SOVEREIGN_HEALTH.md")
|
||||
|
||||
class HealthDashboard:
|
||||
def __init__(self):
|
||||
REPORTS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
def run_tool(self, name: str, cmd: str) -> str:
|
||||
print(f"[*] Running {name}...")
|
||||
try:
|
||||
# Capture output
|
||||
res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||
return res.stdout
|
||||
except Exception as e:
|
||||
return f"Error running {name}: {e}"
|
||||
|
||||
def generate_report(self):
|
||||
print("--- Generating Sovereign Health Dashboard ---")
|
||||
|
||||
# 1. Run Audits
|
||||
muda_output = self.run_tool("Muda Audit", "python3 scripts/muda_audit.py")
|
||||
guardrails_output = self.run_tool("Agent Guardrails", "python3 scripts/agent_guardrails.py")
|
||||
optimizer_output = self.run_tool("Token Optimizer", "python3 scripts/token_optimizer.py")
|
||||
gate_output = self.run_tool("Quality Gate", "python3 scripts/ci_automation_gate.py .")
|
||||
|
||||
# 2. Build Markdown
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
md = [
|
||||
f"# 🛡️ Sovereign Health Dashboard",
|
||||
f"**Last Updated:** {now}",
|
||||
f"",
|
||||
f"## 📊 Summary",
|
||||
f"- **Fleet Status:** ACTIVE",
|
||||
f"- **Security Posture:** MONITORING",
|
||||
f"- **Operational Waste:** AUDITED",
|
||||
f"",
|
||||
f"## ♻️ Muda Waste Audit",
|
||||
f"```\n{muda_output}\n```",
|
||||
f"",
|
||||
f"## 🕵️ Agent Guardrails",
|
||||
f"```\n{guardrails_output}\n```",
|
||||
f"",
|
||||
f"## 🪙 Token Efficiency",
|
||||
f"```\n{optimizer_output}\n```",
|
||||
f"",
|
||||
f"## 🏗️ CI Quality Gate",
|
||||
f"```\n{gate_output}\n```",
|
||||
f"",
|
||||
f"---",
|
||||
f"*Generated by Sovereign Infrastructure Suite*"
|
||||
]
|
||||
|
||||
with open(DASHBOARD_FILE, "w") as f:
|
||||
f.write("\n".join(md))
|
||||
|
||||
print(f"[SUCCESS] Dashboard generated at {DASHBOARD_FILE}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
dashboard = HealthDashboard()
|
||||
dashboard.generate_report()
|
||||
276
scripts/symbolic_reasoner.py
Normal file
276
scripts/symbolic_reasoner.py
Normal file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
"""symbolic_reasoner.py — Forward-chaining rule engine for the Timmy Foundation fleet.
|
||||
|
||||
A classical GOFAI approach: declarative IF-THEN rules evaluated over a
|
||||
working-memory of facts. Rules fire until quiescence (no new facts) or
|
||||
a configurable cycle limit. Designed to sit *beside* the LLM layer so
|
||||
that hard policy constraints never depend on probabilistic inference.
|
||||
|
||||
Usage:
|
||||
python symbolic_reasoner.py --rules rules.yaml --facts facts.yaml
|
||||
python symbolic_reasoner.py --self-test
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, FrozenSet, List, Optional, Set, Tuple
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None # graceful fallback — JSON-only mode
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain types
|
||||
# ---------------------------------------------------------------------------
|
||||
Fact = Tuple[str, ...] # e.g. ("agent", "timmy", "role", "infrastructure")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Rule:
|
||||
"""A single IF-THEN production rule."""
|
||||
name: str
|
||||
conditions: FrozenSet[Fact] # all must be present
|
||||
negations: FrozenSet[Fact] # none may be present
|
||||
conclusions: FrozenSet[Fact] # added when rule fires
|
||||
priority: int = 0 # higher fires first
|
||||
|
||||
def satisfied(self, wm: Set[Fact]) -> bool:
|
||||
return self.conditions.issubset(wm) and self.negations.isdisjoint(wm)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Engine
|
||||
# ---------------------------------------------------------------------------
|
||||
class SymbolicReasoner:
|
||||
"""Forward-chaining production system."""
|
||||
|
||||
def __init__(self, rules: List[Rule], *, cycle_limit: int = 200):
|
||||
self._rules = sorted(rules, key=lambda r: -r.priority)
|
||||
self._cycle_limit = cycle_limit
|
||||
self._trace: List[str] = []
|
||||
|
||||
# -- public API ---------------------------------------------------------
|
||||
|
||||
def infer(self, initial_facts: Set[Fact]) -> Set[Fact]:
|
||||
"""Run to quiescence and return the final working-memory."""
|
||||
wm = set(initial_facts)
|
||||
fired: Set[str] = set()
|
||||
for cycle in range(self._cycle_limit):
|
||||
progress = False
|
||||
for rule in self._rules:
|
||||
if rule.name in fired:
|
||||
continue
|
||||
if rule.satisfied(wm):
|
||||
new = rule.conclusions - wm
|
||||
if new:
|
||||
wm |= new
|
||||
fired.add(rule.name)
|
||||
self._trace.append(
|
||||
f"cycle {cycle}: {rule.name} => {_fmt_facts(new)}"
|
||||
)
|
||||
progress = True
|
||||
break # restart from highest-priority rule
|
||||
if not progress:
|
||||
break
|
||||
return wm
|
||||
|
||||
def query(self, wm: Set[Fact], pattern: Tuple[Optional[str], ...]) -> List[Fact]:
|
||||
"""Return facts matching *pattern* (None = wildcard)."""
|
||||
return [
|
||||
f for f in wm
|
||||
if len(f) == len(pattern)
|
||||
and all(p is None or p == v for p, v in zip(pattern, f))
|
||||
]
|
||||
|
||||
@property
|
||||
def trace(self) -> List[str]:
|
||||
return list(self._trace)
|
||||
|
||||
# -- serialisation helpers -----------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def from_dicts(cls, raw_rules: List[Dict], **kw) -> "SymbolicReasoner":
|
||||
rules = [_parse_rule(r) for r in raw_rules]
|
||||
return cls(rules, **kw)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path, **kw) -> "SymbolicReasoner":
|
||||
text = path.read_text()
|
||||
if path.suffix in (".yaml", ".yml"):
|
||||
if yaml is None:
|
||||
raise RuntimeError("PyYAML required for .yaml rules")
|
||||
data = yaml.safe_load(text)
|
||||
else:
|
||||
data = json.loads(text)
|
||||
return cls.from_dicts(data["rules"], **kw)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parsing helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
def _parse_fact(raw: list | str) -> Fact:
|
||||
if isinstance(raw, str):
|
||||
return tuple(raw.split())
|
||||
return tuple(str(x) for x in raw)
|
||||
|
||||
|
||||
def _parse_rule(d: Dict) -> Rule:
|
||||
return Rule(
|
||||
name=d["name"],
|
||||
conditions=frozenset(_parse_fact(c) for c in d.get("if", [])),
|
||||
negations=frozenset(_parse_fact(c) for c in d.get("unless", [])),
|
||||
conclusions=frozenset(_parse_fact(c) for c in d.get("then", [])),
|
||||
priority=d.get("priority", 0),
|
||||
)
|
||||
|
||||
|
||||
def _fmt_facts(facts: Set[Fact]) -> str:
|
||||
return ", ".join(" ".join(f) for f in sorted(facts))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Built-in fleet rules (loaded when no --rules file is given)
|
||||
# ---------------------------------------------------------------------------
|
||||
DEFAULT_FLEET_RULES: List[Dict] = [
|
||||
{
|
||||
"name": "route-ci-to-timmy",
|
||||
"if": [["task", "category", "ci"]],
|
||||
"then": [["assign", "timmy"], ["reason", "timmy", "best-ci-merge-rate"]],
|
||||
"priority": 10,
|
||||
},
|
||||
{
|
||||
"name": "route-security-to-timmy",
|
||||
"if": [["task", "category", "security"]],
|
||||
"then": [["assign", "timmy"], ["reason", "timmy", "security-specialist"]],
|
||||
"priority": 10,
|
||||
},
|
||||
{
|
||||
"name": "route-architecture-to-gemini",
|
||||
"if": [["task", "category", "architecture"]],
|
||||
"unless": [["assign", "timmy"]],
|
||||
"then": [["assign", "gemini"], ["reason", "gemini", "architecture-strength"]],
|
||||
"priority": 8,
|
||||
},
|
||||
{
|
||||
"name": "route-review-to-allegro",
|
||||
"if": [["task", "category", "review"]],
|
||||
"then": [["assign", "allegro"], ["reason", "allegro", "highest-quality-per-pr"]],
|
||||
"priority": 9,
|
||||
},
|
||||
{
|
||||
"name": "route-frontend-to-claude",
|
||||
"if": [["task", "category", "frontend"]],
|
||||
"unless": [["task", "repo", "fleet-ops"]],
|
||||
"then": [["assign", "claude"], ["reason", "claude", "high-volume-frontend"]],
|
||||
"priority": 5,
|
||||
},
|
||||
{
|
||||
"name": "block-merge-without-review",
|
||||
"if": [["pr", "status", "open"], ["pr", "reviews", "0"]],
|
||||
"then": [["pr", "action", "block-merge"], ["reason", "policy", "no-unreviewed-merges"]],
|
||||
"priority": 20,
|
||||
},
|
||||
{
|
||||
"name": "block-merge-ci-failing",
|
||||
"if": [["pr", "status", "open"], ["pr", "ci", "failing"]],
|
||||
"then": [["pr", "action", "block-merge"], ["reason", "policy", "ci-must-pass"]],
|
||||
"priority": 20,
|
||||
},
|
||||
{
|
||||
"name": "auto-label-hotfix",
|
||||
"if": [["pr", "title-prefix", "hotfix"]],
|
||||
"then": [["pr", "label", "hotfix"], ["pr", "priority", "urgent"]],
|
||||
"priority": 15,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Self-test
|
||||
# ---------------------------------------------------------------------------
|
||||
def _self_test() -> bool:
|
||||
"""Verify core behaviour; returns True on success."""
|
||||
engine = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
|
||||
|
||||
# Scenario 1: CI task should route to Timmy
|
||||
wm = engine.infer({("task", "category", "ci")})
|
||||
assert ("assign", "timmy") in wm, f"expected timmy assignment, got {wm}"
|
||||
|
||||
# Scenario 2: architecture task routes to gemini (not timmy)
|
||||
engine2 = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
|
||||
wm2 = engine2.infer({("task", "category", "architecture")})
|
||||
assert ("assign", "gemini") in wm2, f"expected gemini assignment, got {wm2}"
|
||||
|
||||
# Scenario 3: open PR with no reviews should block merge
|
||||
engine3 = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
|
||||
wm3 = engine3.infer({("pr", "status", "open"), ("pr", "reviews", "0")})
|
||||
assert ("pr", "action", "block-merge") in wm3
|
||||
|
||||
# Scenario 4: negation — frontend + fleet-ops should NOT assign claude
|
||||
engine4 = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
|
||||
wm4 = engine4.infer({("task", "category", "frontend"), ("task", "repo", "fleet-ops")})
|
||||
assert ("assign", "claude") not in wm4
|
||||
|
||||
# Scenario 5: query with wildcards
|
||||
results = engine.query(wm, ("reason", None, None))
|
||||
assert len(results) > 0
|
||||
|
||||
print("All 5 self-test scenarios passed.")
|
||||
for line in engine.trace:
|
||||
print(f" {line}")
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--rules", type=Path, help="YAML/JSON rule file")
|
||||
ap.add_argument("--facts", type=Path, help="YAML/JSON initial facts")
|
||||
ap.add_argument("--self-test", action="store_true")
|
||||
ap.add_argument("--json", action="store_true", help="output as JSON")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.self_test:
|
||||
ok = _self_test()
|
||||
sys.exit(0 if ok else 1)
|
||||
|
||||
if args.rules:
|
||||
engine = SymbolicReasoner.from_file(args.rules)
|
||||
else:
|
||||
engine = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
|
||||
|
||||
if args.facts:
|
||||
text = args.facts.read_text()
|
||||
if args.facts.suffix in (".yaml", ".yml"):
|
||||
raw = yaml.safe_load(text)
|
||||
else:
|
||||
raw = json.loads(text)
|
||||
initial = {_parse_fact(f) for f in raw.get("facts", [])}
|
||||
else:
|
||||
initial = set()
|
||||
print("No --facts provided; running with empty working memory.")
|
||||
|
||||
wm = engine.infer(initial)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({"facts": [list(f) for f in sorted(wm)], "trace": engine.trace}, indent=2))
|
||||
else:
|
||||
print(f"Final working memory ({len(wm)} facts):")
|
||||
for f in sorted(wm):
|
||||
print(f" {' '.join(f)}")
|
||||
if engine.trace:
|
||||
print(f"\nInference trace ({len(engine.trace)} firings):")
|
||||
for line in engine.trace:
|
||||
print(f" {line}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
87
scripts/token_optimizer.py
Normal file
87
scripts/token_optimizer.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
token_optimizer.py — Token Efficiency & Optimization for the Timmy Foundation.
|
||||
|
||||
Analyzes agent logs to identify:
|
||||
1. "Chatty" Agents — agents outputting excessive tokens for simple tasks.
|
||||
2. Redundant Logs — identifying patterns of repetitive log output.
|
||||
3. Tool Output Bloat — identifying tools that return unnecessarily large payloads.
|
||||
|
||||
Outputs an "Efficiency Score" (0-100) per agent.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import re
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List
|
||||
|
||||
AGENT_LOG_PATHS = [
|
||||
"/root/wizards/*/home/logs/*.log",
|
||||
"/root/wizards/*/logs/*.log",
|
||||
"/root/wizards/*/.hermes/logs/*.log",
|
||||
]
|
||||
|
||||
class TokenOptimizer:
|
||||
def __init__(self):
|
||||
self.agent_stats = defaultdict(lambda: {"tokens": 0, "turns": 0, "tool_calls": 0})
|
||||
|
||||
def estimate_tokens(self, text: str) -> int:
|
||||
# Rough estimate: 4 chars per token
|
||||
return len(text) // 4
|
||||
|
||||
def find_logs(self) -> List[Path]:
|
||||
files = []
|
||||
for pattern in AGENT_LOG_PATHS:
|
||||
for p in glob.glob(pattern):
|
||||
files.append(Path(p))
|
||||
return files
|
||||
|
||||
def analyze_log(self, path: Path):
|
||||
# Extract agent name from path
|
||||
try:
|
||||
parts = path.parts
|
||||
idx = parts.index("wizards")
|
||||
agent = parts[idx + 1]
|
||||
except (ValueError, IndexError):
|
||||
agent = "unknown"
|
||||
|
||||
try:
|
||||
with open(path, "r", errors="ignore") as f:
|
||||
content = f.read()
|
||||
self.agent_stats[agent]["tokens"] += self.estimate_tokens(content)
|
||||
|
||||
# Count turns (approximate by looking for role markers)
|
||||
self.agent_stats[agent]["turns"] += content.count("[ASSISTANT]")
|
||||
self.agent_stats[agent]["turns"] += content.count("[USER]")
|
||||
|
||||
# Count tool calls
|
||||
self.agent_stats[agent]["tool_calls"] += content.count("Calling tool:")
|
||||
except Exception as e:
|
||||
print(f"Error analyzing {path}: {e}")
|
||||
|
||||
def run(self):
|
||||
print("--- Token Efficiency Audit ---")
|
||||
logs = self.find_logs()
|
||||
for log in logs:
|
||||
self.analyze_log(log)
|
||||
|
||||
print(f"{'Agent':<20} | {'Tokens':<10} | {'Turns':<6} | {'T/Turn':<8} | {'Efficiency'}")
|
||||
print("-" * 65)
|
||||
|
||||
for agent, stats in self.agent_stats.items():
|
||||
tokens = stats["tokens"]
|
||||
turns = max(stats["turns"], 1)
|
||||
t_per_turn = tokens // turns
|
||||
|
||||
# Efficiency score: lower tokens per turn is generally better
|
||||
# Baseline: 500 tokens per turn = 100 score. 2000+ = 0 score.
|
||||
efficiency = max(0, min(100, 100 - (t_per_turn - 500) // 15))
|
||||
|
||||
print(f"{agent:<20} | {tokens:<10} | {turns:<6} | {t_per_turn:<8} | {efficiency}%")
|
||||
|
||||
if __name__ == "__main__":
|
||||
optimizer = TokenOptimizer()
|
||||
optimizer.run()
|
||||
22
scripts/validate_playbook_schema.py
Normal file
22
scripts/validate_playbook_schema.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate playbook YAML files have required keys."""
|
||||
import yaml
|
||||
import sys
|
||||
import glob
|
||||
|
||||
required_keys = {'name', 'description'}
|
||||
|
||||
for f in glob.glob('playbooks/*.yaml'):
|
||||
with open(f) as fh:
|
||||
try:
|
||||
data = yaml.safe_load(fh)
|
||||
if not isinstance(data, dict):
|
||||
print(f'ERROR: {f} is not a YAML mapping')
|
||||
sys.exit(1)
|
||||
missing = required_keys - set(data.keys())
|
||||
if missing:
|
||||
print(f'WARNING: {f} missing keys: {missing}')
|
||||
print(f'OK: {f}')
|
||||
except yaml.YAMLError as e:
|
||||
print(f'ERROR: {f}: {e}')
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user