198 lines
7.6 KiB
Python
198 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Full cross-audit of Timmy Foundation team and system.
|
|
Scans all repos, all agents, all cron jobs, all VPS health, all local state.
|
|
Produces actionable issues with clear acceptance criteria."""
|
|
import subprocess, json, os
|
|
|
|
GITEA_TOK = open(os.path.expanduser('~/.hermes/gitea_token_vps')).read().strip()
|
|
FORGE = 'https://forge.alexanderwhitestone.com/api/v1'
|
|
REPOS = ['timmy-config', 'timmy-home', 'the-nexus', 'hermes-agent', 'wolf', 'the-door', 'turboquant', 'timmy-academy']
|
|
|
|
def curl(url):
|
|
r = subprocess.run(
|
|
['curl', '-s', url, '-H', f'Authorization: token {GITEA_TOK}'],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
return json.loads(r.stdout)
|
|
|
|
def api(method, path, data=None):
|
|
r = subprocess.run(
|
|
['curl', '-s', '-X', method, f'{FORGE}/{path}',
|
|
'-H', f'Authorization: token {GITEA_TOK}',
|
|
'-H', 'Content-Type: application/json']
|
|
+ (['-d', json.dumps(data)] if data else []),
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
return json.loads(r.stdout)
|
|
|
|
# ============================================================
|
|
# 1. INVENTORY: Every repo, every issue, every agent
|
|
# ============================================================
|
|
|
|
print("=" * 60)
|
|
print("CROSS AUDIT — Timmy Foundation")
|
|
print("=" * 60)
|
|
|
|
# All open issues
|
|
all_issues = []
|
|
repos_state = {}
|
|
for repo in REPOS:
|
|
issues = curl(f'{FORGE}/repos/Timmy_Foundation/{repo}/issues?state=open&limit=50')
|
|
if not isinstance(issues, list):
|
|
issues = []
|
|
|
|
pr_count = 0
|
|
issue_count = 0
|
|
unassigned = 0
|
|
timmy_assigned = 0
|
|
|
|
for iss in issues:
|
|
if 'pull_request' in iss:
|
|
pr_count += 1
|
|
continue
|
|
issue_count += 1
|
|
a = iss.get('assignee', {})
|
|
login = a.get('login', 'unassigned') if a else 'unassigned'
|
|
if login == 'unassigned':
|
|
unassigned += 1
|
|
elif login == 'Timmy':
|
|
timmy_assigned += 1
|
|
labels = [l['name'] for l in iss.get('labels', [])]
|
|
all_issues.append({
|
|
'repo': repo,
|
|
'num': iss['number'],
|
|
'title': iss['title'][:80],
|
|
'assignee': login,
|
|
'labels': labels,
|
|
'created': iss.get('created_at', '')[:10],
|
|
})
|
|
repos_state[repo] = {
|
|
'open_issues': issue_count,
|
|
'open_prs': pr_count,
|
|
'unassigned': unassigned,
|
|
'timmy_assigned': timmy_assigned,
|
|
}
|
|
|
|
print(f"\n=== GITEA REPO AUDIT ===")
|
|
print(f"{'repo':<20} {'issues':>6} {'prs':>4} {'unassign':>8} {'timmy':>5}")
|
|
for repo, state in repos_state.items():
|
|
print(f"{repo:<20} {state['open_issues']:>6} {state['open_prs']:>4} {state['unassigned']:>8} {state['timmy_assigned']:>5}")
|
|
|
|
total_issues = sum(s['open_issues'] for s in repos_state.values())
|
|
total_prs = sum(s['open_prs'] for s in repos_state.values())
|
|
total_unassigned = sum(s['unassigned'] for s in repos_state.values())
|
|
total_timmy = sum(s['timmy_assigned'] for s in repos_state.values())
|
|
print(f"{'TOTAL':<20} {total_issues:>6} {total_prs:>4} {total_unassigned:>8} {total_timmy:>5}")
|
|
|
|
# Issues by assignee
|
|
by_assignee = {}
|
|
for iss in all_issues:
|
|
by_assignee.setdefault(iss['assignee'], []).append(iss)
|
|
|
|
print(f"\n=== ISSUES BY ASSIGNEE ===")
|
|
for assignee in sorted(by_assignee.keys()):
|
|
issues = by_assignee[assignee]
|
|
print(f" {assignee}: {len(issues)}")
|
|
for iss in issues[:5]:
|
|
print(f" {iss['repo']}/#{iss['num']}: {iss['title']}")
|
|
|
|
# Issues older than 30 days
|
|
old_issues = [i for i in all_issues if i['created'] < '2026-03-07']
|
|
print(f"\n=== STALE ISSUES (>30 days old): {len(old_issues)} ===")
|
|
for iss in old_issues[:10]:
|
|
print(f" {iss['repo']}/#{iss['num']} (created {iss['created']}) @{iss['assignee']}: {iss['title']}")
|
|
|
|
# ============================================================
|
|
# 2. CRON JOB AUDIT
|
|
# ============================================================
|
|
print(f"\n=== CRON JOBS ===")
|
|
import subprocess
|
|
r = subprocess.run(
|
|
['hermes', 'cron', 'list'],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
cron_output = r.stdout + r.stderr
|
|
print(cron_output[:2000])
|
|
|
|
# ============================================================
|
|
# 3. VPS HEALTH
|
|
# ============================================================
|
|
print(f"\n=== VPS HEALTH ===")
|
|
for vps_name, vps_ip in [('Hermes VPS', '143.198.27.163'), ('TestBed VPS', '67.205.155.108')]:
|
|
r = subprocess.run(
|
|
['ssh', '-o', 'ConnectTimeout=5', 'root@' + vps_ip,
|
|
'echo "uptime: $(uptime)"; echo "disk:"; df -h / | tail -1; echo "memory:"; free -h | head -2; echo "services:"; systemctl list-units --type=service --state=running --no-pager 2>/dev/null | grep -c running; echo "hermes:"; systemctl list-units --state=running --no-pager 2>/dev/null | grep -c hermes'],
|
|
capture_output=True, text=True, timeout=15
|
|
)
|
|
status = r.stdout.strip() if r.returncode == 0 else "UNREACHABLE"
|
|
print(f"\n {vps_name} ({vps_ip}):")
|
|
if status == "UNREACHABLE":
|
|
print(f" SSH FAILED - VPS may be down")
|
|
else:
|
|
for line in status.split('\n'):
|
|
print(f" {line.strip()}")
|
|
|
|
# ============================================================
|
|
# 4. LOCAL MAC HEALTH
|
|
# ============================================================
|
|
print(f"\n=== MAC HEALTH ===")
|
|
r = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
|
|
hermes_procs = [l for l in r.stdout.split('\n') if 'hermes' in l or 'evennia' in l or 'twistd' in l]
|
|
print(f" Hermes/Evennia processes: {len(hermes_procs)}")
|
|
for p in hermes_procs[:5]:
|
|
print(f" {p[:100]}...")
|
|
|
|
r = subprocess.run(['ollama', 'list'], capture_output=True, text=True, timeout=10)
|
|
print(f"\n Ollama models:")
|
|
print(r.stdout.strip()[:500])
|
|
|
|
import pathlib
|
|
worktrees = pathlib.Path(os.path.expanduser('~/worktrees')).glob('*')
|
|
wt_count = len(list(worktrees))
|
|
print(f"\n Worktrees: {wt_count}")
|
|
|
|
# ============================================================
|
|
# 5. IDENTIFIED GAPS
|
|
# ============================================================
|
|
print(f"\n{'=' * 60}")
|
|
print(f"IDENTIFIED GAPS & GAPS TO FILE")
|
|
print(f"{'=' * 60}")
|
|
|
|
# The cross-audit results will be used to file issues
|
|
gaps = []
|
|
|
|
# Always-present gaps
|
|
if total_unassigned > 0:
|
|
gaps.append(f"{total_unassigned} unassigned issues exist — need assignment or closing")
|
|
if total_timmy > 10:
|
|
gaps.append(f"Timmy has {total_timmy} assigned issues — likely overloaded")
|
|
if len(old_issues) > 0:
|
|
gaps.append(f"{len(old_issues)} issues older than 30 days — stale, needs triage")
|
|
|
|
# Known gaps from previous RCA (Tower Game)
|
|
gaps.append("Tower Game: No contextual dialogue (NPCs repeat lines)")
|
|
gaps.append("Tower Game: No meaningful conflict/trust system")
|
|
gaps.append("Tower Game: World events exist but have no gameplay impact")
|
|
gaps.append("Tower Game: Energy system doesn't constrain")
|
|
gaps.append("Tower Game: No narrative arc (tick 200 = tick 20)")
|
|
gaps.append("Tower Game: No item system")
|
|
gaps.append("Tower Game: No NPC-NPC relationships")
|
|
gaps.append("Tower Game: Chronicle is tick data, not narrative")
|
|
|
|
# System gaps (discovered during this audit)
|
|
gaps.append("No comms audit: Telegram deprecated? Nostr operational?")
|
|
gaps.append("Sonnet workforce: loop created but not tested end-to-end")
|
|
gaps.append("No cross-agent quality audit: which agents produce mergeable PRs?")
|
|
gaps.append("No burn-down velocity tracking: how many issues closed per day?")
|
|
gaps.append("No fleet cost tracking: how much does each agent cost per day?")
|
|
|
|
print(f"\nTotal gaps identified: {len(gaps)}")
|
|
for i, gap in enumerate(gaps, 1):
|
|
print(f" {i}. {gap}")
|
|
|
|
# Save for issue filing
|
|
with open(f'/tmp/cross_audit_gaps.json', 'w') as f:
|
|
json.dump(gaps, f, indent=2)
|
|
|
|
print(f"\nAudit complete. Gaps saved to /tmp/cross_audit_gaps.json")
|