Compare commits

...

2 Commits

Author SHA1 Message Date
Alexander Whitestone
50d96b23bf Refresh branch tip for mergeability recalculation 2026-04-04 17:51:19 -04:00
Alexander Whitestone
ccfab7189a Cut over status surfaces to live workflow state 2026-04-04 17:51:19 -04:00
2 changed files with 323 additions and 406 deletions

346
bin/timmy-dashboard Executable file → Normal file
View File

@@ -1,20 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Timmy Model Dashboard — where are my models, what are they doing. """Timmy workflow dashboard.
Usage: Shows current workflow state from the active local surfaces instead of the
timmy-dashboard # one-shot archived dashboard/loop era, while preserving useful local/session metrics.
timmy-dashboard --watch # live refresh every 30s
timmy-dashboard --hours=48 # look back 48h
""" """
from __future__ import annotations
import json import json
import os import os
import sqlite3 import sqlite3
import subprocess
import sys import sys
import time import time
import urllib.request import urllib.request
from datetime import datetime, timezone, timedelta from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent REPO_ROOT = Path(__file__).resolve().parent.parent
@@ -26,37 +25,83 @@ from metrics_helpers import summarize_local_metrics, summarize_session_rows
HERMES_HOME = Path.home() / ".hermes" HERMES_HOME = Path.home() / ".hermes"
TIMMY_HOME = Path.home() / ".timmy" TIMMY_HOME = Path.home() / ".timmy"
METRICS_DIR = TIMMY_HOME / "metrics" METRICS_DIR = TIMMY_HOME / "metrics"
CORE_REPOS = [
"Timmy_Foundation/the-nexus",
"Timmy_Foundation/timmy-home",
"Timmy_Foundation/timmy-config",
"Timmy_Foundation/hermes-agent",
]
GITEA_URL = os.environ.get("GITEA_URL", "http://143.198.27.163:3000").rstrip("/")
# ── Data Sources ──────────────────────────────────────────────────────
def get_ollama_models(): def read_token() -> str | None:
for path in [
Path.home() / ".hermes" / "gitea_token_vps",
Path.home() / ".config" / "gitea" / "token",
Path.home() / ".config" / "gitea" / "codex-token",
]:
if path.exists():
return path.read_text().strip()
return None
def gitea_get(path: str, token: str | None) -> list | dict:
headers = {"Authorization": f"token {token}"} if token else {}
req = urllib.request.Request(f"{GITEA_URL}/api/v1{path}", headers=headers)
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read().decode())
def get_model_health() -> dict:
path = HERMES_HOME / "model_health.json"
if not path.exists():
return {}
try: try:
req = urllib.request.Request("http://localhost:11434/api/tags") return json.loads(path.read_text())
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read()).get("models", [])
except Exception: except Exception:
return [] return {}
def get_loaded_models(): def get_last_tick() -> dict:
path = TIMMY_HOME / "heartbeat" / "last_tick.json"
if not path.exists():
return {}
try: try:
req = urllib.request.Request("http://localhost:11434/api/ps") return json.loads(path.read_text())
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read()).get("models", [])
except Exception: except Exception:
return [] return {}
def get_huey_pid(): def get_archive_checkpoint() -> dict:
path = TIMMY_HOME / "twitter-archive" / "checkpoint.json"
if not path.exists():
return {}
try: try:
r = subprocess.run(["pgrep", "-f", "huey_consumer"], return json.loads(path.read_text())
capture_output=True, text=True, timeout=5)
return r.stdout.strip().split("\n")[0] if r.returncode == 0 else None
except Exception: except Exception:
return None return {}
def get_hermes_sessions(): def get_local_metrics(hours: int = 24) -> list[dict]:
records = []
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
if not METRICS_DIR.exists():
return records
for path in sorted(METRICS_DIR.glob("local_*.jsonl")):
for line in path.read_text().splitlines():
if not line.strip():
continue
try:
record = json.loads(line)
ts = datetime.fromisoformat(record["timestamp"])
if ts >= cutoff:
records.append(record)
except Exception:
continue
return records
def get_hermes_sessions() -> list[dict]:
sessions_file = HERMES_HOME / "sessions" / "sessions.json" sessions_file = HERMES_HOME / "sessions" / "sessions.json"
if not sessions_file.exists(): if not sessions_file.exists():
return [] return []
@@ -67,7 +112,7 @@ def get_hermes_sessions():
return [] return []
def get_session_rows(hours=24): def get_session_rows(hours: int = 24):
state_db = HERMES_HOME / "state.db" state_db = HERMES_HOME / "state.db"
if not state_db.exists(): if not state_db.exists():
return [] return []
@@ -91,14 +136,14 @@ def get_session_rows(hours=24):
return [] return []
def get_heartbeat_ticks(date_str=None): def get_heartbeat_ticks(date_str: str | None = None) -> list[dict]:
if not date_str: if not date_str:
date_str = datetime.now().strftime("%Y%m%d") date_str = datetime.now().strftime("%Y%m%d")
tick_file = TIMMY_HOME / "heartbeat" / f"ticks_{date_str}.jsonl" tick_file = TIMMY_HOME / "heartbeat" / f"ticks_{date_str}.jsonl"
if not tick_file.exists(): if not tick_file.exists():
return [] return []
ticks = [] ticks = []
for line in tick_file.read_text().strip().split("\n"): for line in tick_file.read_text().splitlines():
if not line.strip(): if not line.strip():
continue continue
try: try:
@@ -108,42 +153,33 @@ def get_heartbeat_ticks(date_str=None):
return ticks return ticks
def get_local_metrics(hours=24): def get_review_and_issue_state(token: str | None) -> dict:
"""Read local inference metrics from jsonl files.""" state = {"prs": [], "review_queue": [], "unassigned": 0}
records = [] for repo in CORE_REPOS:
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours) try:
if not METRICS_DIR.exists(): prs = gitea_get(f"/repos/{repo}/pulls?state=open&limit=20", token)
return records for pr in prs:
for f in sorted(METRICS_DIR.glob("local_*.jsonl")): pr["_repo"] = repo
for line in f.read_text().strip().split("\n"): state["prs"].append(pr)
if not line.strip(): except Exception:
continue continue
try: try:
r = json.loads(line) issue_prs = gitea_get(f"/repos/{repo}/issues?state=open&limit=50&type=pulls", token)
ts = datetime.fromisoformat(r["timestamp"]) for item in issue_prs:
if ts >= cutoff: assignees = [a.get("login", "") for a in (item.get("assignees") or [])]
records.append(r) if any(name in assignees for name in ("Timmy", "allegro")):
except Exception: item["_repo"] = repo
continue state["review_queue"].append(item)
return records except Exception:
continue
try:
issues = gitea_get(f"/repos/{repo}/issues?state=open&limit=50&type=issues", token)
state["unassigned"] += sum(1 for issue in issues if not issue.get("assignees"))
except Exception:
continue
return state
def get_cron_jobs():
"""Get Hermes cron job status."""
try:
r = subprocess.run(
["hermes", "cron", "list", "--json"],
capture_output=True, text=True, timeout=10
)
if r.returncode == 0:
return json.loads(r.stdout).get("jobs", [])
except Exception:
pass
return []
# ── Rendering ─────────────────────────────────────────────────────────
DIM = "\033[2m" DIM = "\033[2m"
BOLD = "\033[1m" BOLD = "\033[1m"
GREEN = "\033[32m" GREEN = "\033[32m"
@@ -154,119 +190,133 @@ RST = "\033[0m"
CLR = "\033[2J\033[H" CLR = "\033[2J\033[H"
def render(hours=24): def render(hours: int = 24) -> None:
models = get_ollama_models() token = read_token()
loaded = get_loaded_models()
huey_pid = get_huey_pid()
ticks = get_heartbeat_ticks()
metrics = get_local_metrics(hours) metrics = get_local_metrics(hours)
local_summary = summarize_local_metrics(metrics)
ticks = get_heartbeat_ticks()
health = get_model_health()
last_tick = get_last_tick()
checkpoint = get_archive_checkpoint()
sessions = get_hermes_sessions() sessions = get_hermes_sessions()
session_rows = get_session_rows(hours) session_rows = get_session_rows(hours)
local_summary = summarize_local_metrics(metrics)
session_summary = summarize_session_rows(session_rows) session_summary = summarize_session_rows(session_rows)
gitea = get_review_and_issue_state(token)
loaded_names = {m.get("name", "") for m in loaded}
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(CLR, end="") print(CLR, end="")
print(f"{BOLD}{'=' * 70}") print(f"{BOLD}{'=' * 72}")
print(f" TIMMY MODEL DASHBOARD") print(" TIMMY WORKFLOW DASHBOARD")
print(f" {now} | Huey: {GREEN}PID {huey_pid}{RST if huey_pid else f'{RED}DOWN{RST}'}") print(f" {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"{'=' * 70}{RST}") print(f"{'=' * 72}{RST}")
# ── LOCAL MODELS ── print(f"\n {BOLD}HEARTBEAT{RST}")
print(f"\n {BOLD}LOCAL MODELS (Ollama){RST}") print(f" {DIM}{'-' * 58}{RST}")
print(f" {DIM}{'-' * 55}{RST}") if last_tick:
if models: sev = last_tick.get("decision", {}).get("severity", "?")
for m in models: tick_id = last_tick.get("tick_id", "?")
name = m.get("name", "?") model_decisions = sum(
size_gb = m.get("size", 0) / 1e9 1
if name in loaded_names: for tick in ticks
status = f"{GREEN}IN VRAM{RST}" if isinstance(tick.get("decision"), dict)
else: and tick["decision"].get("severity") != "fallback"
status = f"{DIM}on disk{RST}" )
print(f" {name:35s} {size_gb:5.1f}GB {status}") print(f" last tick: {tick_id}")
print(f" severity: {sev}")
print(f" ticks today: {len(ticks)} | model decisions: {model_decisions}")
else: else:
print(f" {RED}(Ollama not responding){RST}") print(f" {DIM}(no heartbeat data){RST}")
# ── LOCAL INFERENCE ACTIVITY ── print(f"\n {BOLD}MODEL HEALTH{RST}")
print(f"\n {BOLD}LOCAL INFERENCE ({len(metrics)} calls, last {hours}h){RST}") print(f" {DIM}{'-' * 58}{RST}")
print(f" {DIM}{'-' * 55}{RST}") if health:
provider = GREEN if health.get("api_responding") else RED
inference = GREEN if health.get("inference_ok") else YELLOW
print(f" provider: {provider}{health.get('api_responding')}{RST}")
print(f" inference: {inference}{health.get('inference_ok')}{RST}")
print(f" models: {', '.join(health.get('models_loaded', [])[:4]) or '(none reported)'}")
else:
print(f" {DIM}(no model_health.json){RST}")
print(f"\n {BOLD}ARCHIVE PIPELINE{RST}")
print(f" {DIM}{'-' * 58}{RST}")
if checkpoint:
print(f" batches completed: {checkpoint.get('batches_completed', '?')}")
print(f" next offset: {checkpoint.get('next_offset', '?')}")
print(f" phase: {checkpoint.get('phase', '?')}")
else:
print(f" {DIM}(no archive checkpoint yet){RST}")
print(f"\n {BOLD}LOCAL METRICS ({len(metrics)} calls, last {hours}h){RST}")
print(f" {DIM}{'-' * 58}{RST}")
if metrics: if metrics:
print(f" Tokens: {local_summary['input_tokens']} in | {local_summary['output_tokens']} out | {local_summary['total_tokens']} total") print(
if local_summary.get('avg_latency_s') is not None: f" Tokens: {local_summary['input_tokens']} in | "
f"{local_summary['output_tokens']} out | "
f"{local_summary['total_tokens']} total"
)
if local_summary.get("avg_latency_s") is not None:
print(f" Avg latency: {local_summary['avg_latency_s']:.2f}s") print(f" Avg latency: {local_summary['avg_latency_s']:.2f}s")
if local_summary.get('avg_tokens_per_second') is not None: if local_summary.get("avg_tokens_per_second") is not None:
print(f" Avg throughput: {GREEN}{local_summary['avg_tokens_per_second']:.2f} tok/s{RST}") print(f" Avg throughput: {GREEN}{local_summary['avg_tokens_per_second']:.2f} tok/s{RST}")
for caller, stats in sorted(local_summary['by_caller'].items()): for caller, stats in sorted(local_summary["by_caller"].items()):
err = f" {RED}err:{stats['failed_calls']}{RST}" if stats['failed_calls'] else "" err = f" {RED}err:{stats['failed_calls']}{RST}" if stats["failed_calls"] else ""
print(f" {caller:25s} calls:{stats['calls']:4d} tokens:{stats['total_tokens']:5d} {GREEN}ok:{stats['successful_calls']}{RST}{err}") print(
f" {caller:24s} calls={stats['calls']:3d} "
print(f"\n {DIM}Models used:{RST}") f"tok={stats['total_tokens']:5d} {GREEN}ok:{stats['successful_calls']}{RST}{err}"
for model, stats in sorted(local_summary['by_model'].items(), key=lambda x: -x[1]['calls']): )
print(f" {model:30s} {stats['calls']} calls {stats['total_tokens']} tok")
else: else:
print(f" {DIM}(no local calls recorded yet){RST}") print(f" {DIM}(no local metrics yet){RST}")
# ── HEARTBEAT STATUS ── print(f"\n {BOLD}SESSION LOAD{RST}")
print(f"\n {BOLD}HEARTBEAT ({len(ticks)} ticks today){RST}") print(f" {DIM}{'-' * 58}{RST}")
print(f" {DIM}{'-' * 55}{RST}") local_sessions = [s for s in sessions if "localhost" in str(s.get("base_url", ""))]
if ticks:
last = ticks[-1]
decision = last.get("decision", last.get("actions", {}))
if isinstance(decision, dict):
severity = decision.get("severity", "unknown")
reasoning = decision.get("reasoning", "")
sev_color = GREEN if severity == "ok" else YELLOW if severity == "warning" else RED
print(f" Last tick: {last.get('tick_id', '?')}")
print(f" Severity: {sev_color}{severity}{RST}")
if reasoning:
print(f" Reasoning: {reasoning[:65]}")
else:
print(f" Last tick: {last.get('tick_id', '?')}")
actions = last.get("actions", [])
print(f" Actions: {actions if actions else 'none'}")
model_decisions = sum(1 for t in ticks
if isinstance(t.get("decision"), dict)
and t["decision"].get("severity") != "fallback")
fallback = len(ticks) - model_decisions
print(f" {CYAN}Model: {model_decisions}{RST} | {DIM}Fallback: {fallback}{RST}")
else:
print(f" {DIM}(no ticks today){RST}")
# ── HERMES SESSIONS / SOVEREIGNTY LOAD ──
local_sessions = [s for s in sessions if "localhost:11434" in str(s.get("base_url", ""))]
cloud_sessions = [s for s in sessions if s not in local_sessions] cloud_sessions = [s for s in sessions if s not in local_sessions]
print(f"\n {BOLD}HERMES SESSIONS / SOVEREIGNTY LOAD{RST}") print(
print(f" {DIM}{'-' * 55}{RST}") f" Session cache: {len(sessions)} total | "
print(f" Session cache: {len(sessions)} total | {GREEN}{len(local_sessions)} local{RST} | {YELLOW}{len(cloud_sessions)} cloud{RST}") f"{GREEN}{len(local_sessions)} local{RST} | "
f"{YELLOW}{len(cloud_sessions)} remote{RST}"
)
if session_rows: if session_rows:
print(f" Session DB: {session_summary['total_sessions']} total | {GREEN}{session_summary['local_sessions']} local{RST} | {YELLOW}{session_summary['cloud_sessions']} cloud{RST}") print(
print(f" Token est: {GREEN}{session_summary['local_est_tokens']} local{RST} | {YELLOW}{session_summary['cloud_est_tokens']} cloud{RST}") f" Session DB: {session_summary['total_sessions']} total | "
print(f" Est cloud cost: ${session_summary['cloud_est_cost_usd']:.4f}") f"{GREEN}{session_summary['local_sessions']} local{RST} | "
f"{YELLOW}{session_summary['cloud_sessions']} remote{RST}"
)
print(
f" Token est: {GREEN}{session_summary['local_est_tokens']} local{RST} | "
f"{YELLOW}{session_summary['cloud_est_tokens']} remote{RST}"
)
print(f" Est remote cost: ${session_summary['cloud_est_cost_usd']:.4f}")
else: else:
print(f" {DIM}(no session-db stats available){RST}") print(f" {DIM}(no session-db stats available){RST}")
# ── ACTIVE LOOPS ── print(f"\n {BOLD}REVIEW QUEUE{RST}")
print(f"\n {BOLD}ACTIVE LOOPS{RST}") print(f" {DIM}{'-' * 58}{RST}")
print(f" {DIM}{'-' * 55}{RST}") if gitea["review_queue"]:
print(f" {CYAN}heartbeat_tick{RST} 10m hermes4:14b DECIDE phase") for item in gitea["review_queue"][:8]:
print(f" {DIM}model_health{RST} 5m (local check) Ollama ping") repo = item["_repo"].split("/", 1)[1]
print(f" {DIM}gemini_worker{RST} 20m gemini-2.5-pro aider") print(f" {repo:12s} #{item['number']:<4d} {item['title'][:42]}")
print(f" {DIM}grok_worker{RST} 20m grok-3-fast opencode") else:
print(f" {DIM}cross_review{RST} 30m gemini+grok PR review") print(f" {DIM}(clear){RST}")
print(f"\n{BOLD}{'=' * 70}{RST}") print(f"\n {BOLD}OPEN PRS / UNASSIGNED{RST}")
print(f" {DIM}{'-' * 58}{RST}")
print(f" open PRs: {len(gitea['prs'])}")
print(f" unassigned issues: {gitea['unassigned']}")
for pr in gitea["prs"][:6]:
repo = pr["_repo"].split("/", 1)[1]
print(f" PR {repo:10s} #{pr['number']:<4d} {pr['title'][:40]}")
print(f"\n{BOLD}{'=' * 72}{RST}")
print(f" {DIM}Refresh: timmy-dashboard --watch | History: --hours=N{RST}") print(f" {DIM}Refresh: timmy-dashboard --watch | History: --hours=N{RST}")
if __name__ == "__main__": if __name__ == "__main__":
watch = "--watch" in sys.argv watch = "--watch" in sys.argv
hours = 24 hours = 24
for a in sys.argv[1:]: for arg in sys.argv[1:]:
if a.startswith("--hours="): if arg.startswith("--hours="):
hours = int(a.split("=")[1]) hours = int(arg.split("=", 1)[1])
if watch: if watch:
try: try:

View File

@@ -1,284 +1,151 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ── Timmy Loop Status Panel ──────────────────────────────────────────── # ── Timmy Status Sidebar ───────────────────────────────────────────────
# Compact, info-dense sidebar for the tmux development loop. # Compact current-state view for the local Hermes + Timmy workflow.
# Refreshes every 10s. Designed for ~40-col wide pane.
# ─────────────────────────────────────────────────────────────────────── # ───────────────────────────────────────────────────────────────────────
STATE="$HOME/Timmy-Time-dashboard/.loop/state.json" set -euo pipefail
REPO="$HOME/Timmy-Time-dashboard"
TOKEN=$(cat ~/.hermes/gitea_token 2>/dev/null)
API="http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard"
# ── Colors ── GITEA_URL="${GITEA_URL:-http://143.198.27.163:3000}"
B='\033[1m' # bold CORE_REPOS="${CORE_REPOS:-Timmy_Foundation/the-nexus Timmy_Foundation/timmy-home Timmy_Foundation/timmy-config Timmy_Foundation/hermes-agent}"
D='\033[2m' # dim
R='\033[0m' # reset
G='\033[32m' # green
Y='\033[33m' # yellow
RD='\033[31m' # red
C='\033[36m' # cyan
M='\033[35m' # magenta
W='\033[37m' # white
BG='\033[42;30m' # green bg
BY='\033[43;30m' # yellow bg
BR='\033[41;37m' # red bg
# How wide is our pane? for token_file in "$HOME/.hermes/gitea_token_vps" "$HOME/.config/gitea/token" "$HOME/.config/gitea/codex-token"; do
COLS=$(tput cols 2>/dev/null || echo 40) if [ -f "$token_file" ]; then
TOKEN=$(cat "$token_file")
break
fi
done
TOKEN="${TOKEN:-}"
B='\033[1m'
D='\033[2m'
R='\033[0m'
G='\033[32m'
Y='\033[33m'
RD='\033[31m'
C='\033[36m'
COLS=$(tput cols 2>/dev/null || echo 48)
hr() { printf "${D}"; printf '─%.0s' $(seq 1 "$COLS"); printf "${R}\n"; } hr() { printf "${D}"; printf '─%.0s' $(seq 1 "$COLS"); printf "${R}\n"; }
while true; do while true; do
clear clear
echo -e "${B}${C} TIMMY STATUS${R} ${D}$(date '+%H:%M:%S')${R}"
# ── Header ──
echo -e "${B}${C} ⚙ TIMMY DEV LOOP${R} ${D}$(date '+%H:%M:%S')${R}"
hr hr
# ── Loop State ── python3 - "$HOME/.timmy" "$HOME/.hermes" <<'PY'
if [ -f "$STATE" ]; then
eval "$(python3 -c "
import json, sys
with open('$STATE') as f: s = json.load(f)
print(f'CYCLE={s.get(\"cycle\",\"?\")}')" 2>/dev/null)"
STATUS=$(python3 -c "import json; print(json.load(open('$STATE'))['status'])" 2>/dev/null || echo "?")
LAST_OK=$(python3 -c "
import json import json
from datetime import datetime, timezone import sys
s = json.load(open('$STATE')) from pathlib import Path
t = s.get('last_completed','')
if t: timmy = Path(sys.argv[1])
dt = datetime.fromisoformat(t.replace('Z','+00:00')) hermes = Path(sys.argv[2])
delta = datetime.now(timezone.utc) - dt
mins = int(delta.total_seconds() / 60) last_tick = timmy / "heartbeat" / "last_tick.json"
if mins < 60: print(f'{mins}m ago') model_health = hermes / "model_health.json"
else: print(f'{mins//60}h {mins%60}m ago') checkpoint = timmy / "twitter-archive" / "checkpoint.json"
else: print('never')
" 2>/dev/null || echo "?") if last_tick.exists():
CLOSED=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('issues_closed',[])))" 2>/dev/null || echo 0) try:
CREATED=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('issues_created',[])))" 2>/dev/null || echo 0) tick = json.loads(last_tick.read_text())
ERRS=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('errors',[])))" 2>/dev/null || echo 0) sev = tick.get("decision", {}).get("severity", "?")
LAST_ISSUE=$(python3 -c "import json; print(json.load(open('$STATE')).get('last_issue','—'))" 2>/dev/null || echo "—") tick_id = tick.get("tick_id", "?")
LAST_PR=$(python3 -c "import json; print(json.load(open('$STATE')).get('last_pr','—'))" 2>/dev/null || echo "—") print(f" heartbeat {tick_id} severity={sev}")
TESTS=$(python3 -c " except Exception:
import json print(" heartbeat unreadable")
s = json.load(open('$STATE'))
t = s.get('test_results',{})
if t:
print(f\"{t.get('passed',0)} pass, {t.get('failed',0)} fail, {t.get('coverage','?')} cov\")
else: else:
print('no data') print(" heartbeat missing")
" 2>/dev/null || echo "no data")
# Status badge if model_health.exists():
case "$STATUS" in try:
working) BADGE="${BY} WORKING ${R}" ;; health = json.loads(model_health.read_text())
idle) BADGE="${BG} IDLE ${R}" ;; provider_ok = health.get("api_responding")
error) BADGE="${BR} ERROR ${R}" ;; inference_ok = health.get("inference_ok")
*) BADGE="${D} $STATUS ${R}" ;; models = len(health.get("models_loaded", []) or [])
esac print(f" model api={provider_ok} inference={inference_ok} models={models}")
except Exception:
echo -e " ${B}Status${R} $BADGE ${D}cycle${R} ${B}$CYCLE${R}" print(" model unreadable")
echo -e " ${B}Last OK${R} ${G}$LAST_OK${R} ${D}issue${R} #$LAST_ISSUE ${D}PR${R} #$LAST_PR"
echo -e " ${G}${R} $CLOSED closed ${C}+${R} $CREATED created ${RD}${R} $ERRS errs"
echo -e " ${D}Tests:${R} $TESTS"
else
echo -e " ${RD}No state file${R}"
fi
hr
# ── Ollama Status ──
echo -e " ${B}${M}◆ OLLAMA${R}"
OLLAMA_PS=$(curl -s http://localhost:11434/api/ps 2>/dev/null)
if [ -n "$OLLAMA_PS" ] && echo "$OLLAMA_PS" | python3 -c "import sys,json; json.load(sys.stdin)" &>/dev/null; then
python3 -c "
import json, sys
data = json.loads('''$OLLAMA_PS''')
models = data.get('models', [])
if not models:
print(' \033[2m(no models loaded)\033[0m')
for m in models:
name = m.get('name','?')
vram = m.get('size_vram', 0) / 1e9
exp = m.get('expires_at','')
print(f' \033[32m●\033[0m {name} \033[2m{vram:.1f}GB VRAM\033[0m')
" 2>/dev/null
else
echo -e " ${RD}● offline${R}"
fi
# ── Timmy Health ──
TIMMY_HEALTH=$(curl -s --max-time 2 http://localhost:8000/health 2>/dev/null)
if [ -n "$TIMMY_HEALTH" ]; then
python3 -c "
import json
h = json.loads('''$TIMMY_HEALTH''')
status = h.get('status','?')
ollama = h.get('services',{}).get('ollama','?')
model = h.get('llm_model','?')
agent_st = list(h.get('agents',{}).values())[0].get('status','?') if h.get('agents') else '?'
up = int(h.get('uptime_seconds',0))
hrs, rem = divmod(up, 3600)
mins = rem // 60
print(f' \033[1m\033[35m◆ TIMMY DASHBOARD\033[0m')
print(f' \033[32m●\033[0m {status} model={model}')
print(f' \033[2magent={agent_st} ollama={ollama} up={hrs}h{mins}m\033[0m')
" 2>/dev/null
else
echo -e " ${B}${M}◆ TIMMY DASHBOARD${R}"
echo -e " ${RD}● unreachable${R}"
fi
hr
# ── Open Issues ──
echo -e " ${B}${Y}▶ OPEN ISSUES${R}"
if [ -n "$TOKEN" ]; then
curl -s "${API}/issues?state=open&limit=10&sort=created&direction=desc" \
-H "Authorization: token $TOKEN" 2>/dev/null | \
python3 -c "
import json, sys
try:
issues = json.load(sys.stdin)
if not issues:
print(' \033[2m(none)\033[0m')
for i in issues[:10]:
num = i['number']
title = i['title'][:36]
labels = ','.join(l['name'][:8] for l in i.get('labels',[]))
lbl = f' \033[2m[{labels}]\033[0m' if labels else ''
print(f' \033[33m#{num:<4d}\033[0m {title}{lbl}')
if len(issues) > 10:
print(f' \033[2m... +{len(issues)-10} more\033[0m')
except: print(' \033[2m(fetch failed)\033[0m')
" 2>/dev/null
else
echo -e " ${RD}(no token)${R}"
fi
# ── Open PRs ──
echo -e " ${B}${G}▶ OPEN PRs${R}"
if [ -n "$TOKEN" ]; then
curl -s "${API}/pulls?state=open&limit=5" \
-H "Authorization: token $TOKEN" 2>/dev/null | \
python3 -c "
import json, sys
try:
prs = json.load(sys.stdin)
if not prs:
print(' \033[2m(none)\033[0m')
for p in prs[:5]:
num = p['number']
title = p['title'][:36]
print(f' \033[32mPR #{num:<4d}\033[0m {title}')
except: print(' \033[2m(fetch failed)\033[0m')
" 2>/dev/null
else
echo -e " ${RD}(no token)${R}"
fi
hr
# ── Git Log ──
echo -e " ${B}${D}▶ RECENT COMMITS${R}"
cd "$REPO" 2>/dev/null && git log --oneline --no-decorate -6 2>/dev/null | while read line; do
HASH=$(echo "$line" | cut -c1-7)
MSG=$(echo "$line" | cut -c9- | cut -c1-32)
echo -e " ${C}${HASH}${R} ${D}${MSG}${R}"
done
hr
# ── Claims ──
CLAIMS_FILE="$REPO/.loop/claims.json"
if [ -f "$CLAIMS_FILE" ]; then
CLAIMS=$(python3 -c "
import json
with open('$CLAIMS_FILE') as f: c = json.load(f)
active = [(k,v) for k,v in c.items() if v.get('status') == 'active']
if active:
for k,v in active:
print(f' \033[33m⚡\033[0m #{k} claimed by {v.get(\"agent\",\"?\")[:12]}')
else: else:
print(' \033[2m(none active)\033[0m') print(" model missing")
" 2>/dev/null)
if [ -n "$CLAIMS" ]; then
echo -e " ${B}${Y}▶ CLAIMED${R}"
echo "$CLAIMS"
fi
fi
# ── System ── if checkpoint.exists():
echo -e " ${B}${D}▶ SYSTEM${R}" try:
# Disk cp = json.loads(checkpoint.read_text())
DISK=$(df -h / 2>/dev/null | tail -1 | awk '{print $4 " free / " $2}') print(f" archive batches={cp.get('batches_completed', '?')} next={cp.get('next_offset', '?')} phase={cp.get('phase', '?')}")
echo -e " ${D}Disk:${R} $DISK" except Exception:
# Memory (macOS) print(" archive unreadable")
if command -v memory_pressure &>/dev/null; then else:
MEM_PRESS=$(memory_pressure 2>/dev/null | grep "System-wide" | head -1 | sed 's/.*: //') print(" archive missing")
echo -e " ${D}Mem:${R} $MEM_PRESS" PY
elif [ -f /proc/meminfo ]; then
MEM=$(awk '/MemAvailable/{printf "%.1fGB free", $2/1048576}' /proc/meminfo 2>/dev/null)
echo -e " ${D}Mem:${R} $MEM"
fi
# CPU load
LOAD=$(uptime | sed 's/.*averages: //' | cut -d',' -f1 | xargs)
echo -e " ${D}Load:${R} $LOAD"
hr hr
echo -e " ${B}freshness${R}"
~/.hermes/bin/pipeline-freshness.sh 2>/dev/null | sed 's/^/ /' || echo -e " ${Y}unknown${R}"
# ── Notes from last cycle ── hr
if [ -f "$STATE" ]; then echo -e " ${B}review queue${R}"
NOTES=$(python3 -c " python3 - "$GITEA_URL" "$TOKEN" "$CORE_REPOS" <<'PY'
import json import json
s = json.load(open('$STATE')) import sys
n = s.get('notes','') import urllib.request
if n:
lines = n[:150]
if len(n) > 150: lines += '...'
print(lines)
" 2>/dev/null)
if [ -n "$NOTES" ]; then
echo -e " ${B}${D}▶ LAST CYCLE NOTE${R}"
echo -e " ${D}${NOTES}${R}"
hr
fi
# Timmy observations base = sys.argv[1].rstrip("/")
TIMMY_OBS=$(python3 -c " token = sys.argv[2]
repos = sys.argv[3].split()
headers = {"Authorization": f"token {token}"} if token else {}
count = 0
for repo in repos:
try:
req = urllib.request.Request(f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=pulls", headers=headers)
with urllib.request.urlopen(req, timeout=5) as resp:
items = json.loads(resp.read().decode())
for item in items:
assignees = [a.get("login", "") for a in (item.get("assignees") or [])]
if any(name in assignees for name in ("Timmy", "allegro")):
print(f" {repo.split('/',1)[1]:12s} #{item['number']:<4d} {item['title'][:28]}")
count += 1
if count >= 6:
raise SystemExit
except SystemExit:
break
except Exception:
continue
if count == 0:
print(" (clear)")
PY
hr
echo -e " ${B}unassigned${R}"
python3 - "$GITEA_URL" "$TOKEN" "$CORE_REPOS" <<'PY'
import json import json
s = json.load(open('$STATE')) import sys
obs = s.get('timmy_observations','') import urllib.request
if obs:
lines = obs[:120]
if len(obs) > 120: lines += '...'
print(lines)
" 2>/dev/null)
if [ -n "$TIMMY_OBS" ]; then
echo -e " ${B}${M}▶ TIMMY SAYS${R}"
echo -e " ${D}${TIMMY_OBS}${R}"
hr
fi
fi
# ── Watchdog: restart loop if it died ────────────────────────────── base = sys.argv[1].rstrip("/")
LOOP_LOCK="/tmp/timmy-loop.lock" token = sys.argv[2]
if [ -f "$LOOP_LOCK" ]; then repos = sys.argv[3].split()
LOOP_PID=$(cat "$LOOP_LOCK" 2>/dev/null) headers = {"Authorization": f"token {token}"} if token else {}
if ! kill -0 "$LOOP_PID" 2>/dev/null; then
echo -e " ${BR} ⚠ LOOP DIED — RESTARTING ${R}"
rm -f "$LOOP_LOCK"
tmux send-keys -t "dev:2.1" "bash ~/.hermes/bin/timmy-loop.sh" Enter 2>/dev/null
fi
else
# No lock file at all — loop never started or was killed
if ! pgrep -f "timmy-loop.sh" >/dev/null 2>&1; then
echo -e " ${BR} ⚠ LOOP NOT RUNNING — STARTING ${R}"
tmux send-keys -t "dev:2.1" "bash ~/.hermes/bin/timmy-loop.sh" Enter 2>/dev/null
fi
fi
echo -e " ${D}↻ 8s${R}" count = 0
sleep 8 for repo in repos:
try:
req = urllib.request.Request(f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues", headers=headers)
with urllib.request.urlopen(req, timeout=5) as resp:
items = json.loads(resp.read().decode())
for item in items:
if not item.get("assignees"):
print(f" {repo.split('/',1)[1]:12s} #{item['number']:<4d} {item['title'][:28]}")
count += 1
if count >= 6:
raise SystemExit
except SystemExit:
break
except Exception:
continue
if count == 0:
print(" (none)")
PY
hr
sleep 10
done done