From 2bf79c2286958cb3bf96ba4b2b5fb818d3371bed Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sat, 4 Apr 2026 22:43:48 +0000 Subject: [PATCH] Refresh ops tooling around current agent lanes (#142) Co-authored-by: Codex Agent Co-committed-by: Codex Agent --- bin/ops-gitea.sh | 199 ++++++++++++++------ bin/ops-helpers.sh | 451 +++++++++++++++++++++++++-------------------- bin/ops-panel.sh | 450 +++++++++++++++++++------------------------- 3 files changed, 584 insertions(+), 516 deletions(-) diff --git a/bin/ops-gitea.sh b/bin/ops-gitea.sh index 5a31d43a..187095e5 100755 --- a/bin/ops-gitea.sh +++ b/bin/ops-gitea.sh @@ -1,70 +1,155 @@ #!/usr/bin/env bash -# ── Gitea Feed Panel ─────────────────────────────────────────────────── -# Shows open PRs, recent merges, and issue queue. Called by watch. +# ── Gitea Workflow Feed ──────────────────────────────────────────────── +# Shows open PRs, review pressure, and issue queues across core repos. # ─────────────────────────────────────────────────────────────────────── -B='\033[1m' ; D='\033[2m' ; R='\033[0m' -G='\033[32m' ; Y='\033[33m' ; RD='\033[31m' ; C='\033[36m' ; M='\033[35m' +set -euo pipefail -TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null) -API="http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard" +B='\033[1m' +D='\033[2m' +R='\033[0m' +C='\033[36m' +G='\033[32m' +Y='\033[33m' -echo -e "${B}${C} ◈ GITEA${R} ${D}$(date '+%H:%M:%S')${R}" +resolve_gitea_url() { + if [ -n "${GITEA_URL:-}" ]; then + printf '%s\n' "${GITEA_URL%/}" + return 0 + fi + if [ -f "$HOME/.hermes/gitea_api" ]; then + python3 - "$HOME/.hermes/gitea_api" <<'PY' +from pathlib import Path +import sys + +raw = Path(sys.argv[1]).read_text().strip().rstrip("/") +print(raw[:-7] if raw.endswith("/api/v1") else raw) +PY + return 0 + fi + if [ -f "$HOME/.config/gitea/base-url" ]; then + tr -d '[:space:]' < "$HOME/.config/gitea/base-url" + return 0 + fi + echo "ERROR: set GITEA_URL or create ~/.hermes/gitea_api" >&2 + return 1 +} + +resolve_ops_token() { + local token_file + for token_file in \ + "$HOME/.config/gitea/timmy-token" \ + "$HOME/.hermes/gitea_token_vps" \ + "$HOME/.hermes/gitea_token_timmy"; do + if [ -f "$token_file" ]; then + tr -d '[:space:]' < "$token_file" + return 0 + fi + done + return 1 +} + +GITEA_URL="$(resolve_gitea_url)" +CORE_REPOS="${CORE_REPOS:-Timmy_Foundation/the-nexus Timmy_Foundation/timmy-home Timmy_Foundation/timmy-config Timmy_Foundation/hermes-agent}" +TOKEN="$(resolve_ops_token || true)" +[ -z "$TOKEN" ] && echo "WARN: no approved Timmy Gitea token found; feed will use unauthenticated API calls" >&2 + +echo -e "${B}${C} ◈ GITEA WORKFLOW${R} ${D}$(date '+%H:%M:%S')${R}" echo -e "${D}────────────────────────────────────────${R}" -# Open PRs -echo -e " ${B}Open PRs${R}" -curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=open&limit=10" 2>/dev/null | python3 -c " -import json,sys -try: - prs = json.loads(sys.stdin.read()) - if not prs: print(' (none)') - for p in prs: - age_h = '' - print(f' #{p[\"number\"]:3d} {p[\"user\"][\"login\"]:8s} {p[\"title\"][:45]}') -except: print(' (error)') -" 2>/dev/null +python3 - "$GITEA_URL" "$TOKEN" "$CORE_REPOS" <<'PY' +import json +import sys +import urllib.error +import urllib.request -echo -e "${D}────────────────────────────────────────${R}" +base = sys.argv[1].rstrip("/") +token = sys.argv[2] +repos = sys.argv[3].split() +headers = {"Authorization": f"token {token}"} if token else {} -# Recent merged (last 5) -echo -e " ${B}Recently Merged${R}" -curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=closed&sort=updated&limit=5" 2>/dev/null | python3 -c " -import json,sys -try: - prs = json.loads(sys.stdin.read()) - merged = [p for p in prs if p.get('merged')] - if not merged: print(' (none)') - for p in merged[:5]: - t = p['merged_at'][:16].replace('T',' ') - print(f' ${G}✓${R} #{p[\"number\"]:3d} {p[\"title\"][:35]} ${D}{t}${R}') -except: print(' (error)') -" 2>/dev/null -echo -e "${D}────────────────────────────────────────${R}" +def fetch(path): + req = urllib.request.Request(f"{base}{path}", headers=headers) + with urllib.request.urlopen(req, timeout=5) as resp: + return json.loads(resp.read().decode()) -# Issue queue (assigned to kimi) -echo -e " ${B}Kimi Queue${R}" -curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c " -import json,sys -try: - all_issues = json.loads(sys.stdin.read()) - issues = [i for i in all_issues if 'kimi' in [a.get('login','') for a in (i.get('assignees') or [])]] - if not issues: print(' (empty — assign more!)') - for i in issues[:8]: - print(f' #{i[\"number\"]:3d} {i[\"title\"][:50]}') - if len(issues) > 8: print(f' ... +{len(issues)-8} more') -except: print(' (error)') -" 2>/dev/null -echo -e "${D}────────────────────────────────────────${R}" +def short_repo(repo): + return repo.split("/", 1)[1] -# Unassigned issues -UNASSIGNED=$(curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c " -import json,sys -try: - issues = json.loads(sys.stdin.read()) - print(len([i for i in issues if not i.get('assignees')])) -except: print('?') -" 2>/dev/null) -echo -e " Unassigned issues: ${Y}$UNASSIGNED${R}" + +issues = [] +pulls = [] +errors = [] + +for repo in repos: + try: + repo_pulls = fetch(f"/api/v1/repos/{repo}/pulls?state=open&limit=20") + for pr in repo_pulls: + pr["_repo"] = repo + pulls.append(pr) + repo_issues = fetch(f"/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues") + for issue in repo_issues: + issue["_repo"] = repo + issues.append(issue) + except urllib.error.URLError as exc: + errors.append(f"{repo}: {exc.reason}") + except Exception as exc: # pragma: no cover - defensive panel path + errors.append(f"{repo}: {exc}") + +print(" \033[1mOpen PRs\033[0m") +if not pulls: + print(" (none)") +else: + for pr in pulls[:8]: + print( + f" #{pr['number']:3d} {short_repo(pr['_repo']):12s} " + f"{pr['user']['login'][:12]:12s} {pr['title'][:40]}" + ) + +print("\033[2m────────────────────────────────────────\033[0m") +print(" \033[1mNeeds Timmy / Allegro Review\033[0m") +reviewers = [] +for repo in repos: + try: + repo_items = fetch(f"/api/v1/repos/{repo}/issues?state=open&limit=50&type=pulls") + for item in repo_items: + assignees = [a.get("login", "") for a in (item.get("assignees") or [])] + if any(name in assignees for name in ("Timmy", "allegro")): + item["_repo"] = repo + reviewers.append(item) + except Exception: + continue + +if not reviewers: + print(" (clear)") +else: + for item in reviewers[:8]: + names = ",".join(a.get("login", "") for a in (item.get("assignees") or [])) + print( + f" #{item['number']:3d} {short_repo(item['_repo']):12s} " + f"{names[:18]:18s} {item['title'][:34]}" + ) + +print("\033[2m────────────────────────────────────────\033[0m") +print(" \033[1mIssue Queues\033[0m") +queue_agents = ["allegro", "codex-agent", "groq", "claude", "ezra", "perplexity", "KimiClaw"] +for agent in queue_agents: + assigned = [ + issue + for issue in issues + if agent in [a.get("login", "") for a in (issue.get("assignees") or [])] + ] + print(f" {agent:12s} {len(assigned):2d}") + +unassigned = [issue for issue in issues if not issue.get("assignees")] +print("\033[2m────────────────────────────────────────\033[0m") +print(f" Unassigned issues: \033[33m{len(unassigned)}\033[0m") + +if errors: + print("\033[2m────────────────────────────────────────\033[0m") + print(" \033[1mErrors\033[0m") + for err in errors[:4]: + print(f" {err}") +PY diff --git a/bin/ops-helpers.sh b/bin/ops-helpers.sh index 02df3f4d..5817b368 100755 --- a/bin/ops-helpers.sh +++ b/bin/ops-helpers.sh @@ -1,235 +1,294 @@ #!/usr/bin/env bash -# ── Dashboard Control Helpers ────────────────────────────────────────── +# ── Workflow Control Helpers ─────────────────────────────────────────── # Source this in the controls pane: source ~/.hermes/bin/ops-helpers.sh +# These helpers intentionally target the current Hermes + Gitea workflow +# and do not revive deprecated bash worker loops. # ─────────────────────────────────────────────────────────────────────── -export TOKEN=*** ~/.hermes/gitea_token_vps 2>/dev/null) -export GITEA="http://143.198.27.163:3000" -export REPO_API="$GITEA/api/v1/repos/rockachopa/Timmy-time-dashboard" +resolve_gitea_url() { + if [ -n "${GITEA:-}" ]; then + printf '%s\n' "${GITEA%/}" + return 0 + fi + if [ -f "$HOME/.hermes/gitea_api" ]; then + python3 - "$HOME/.hermes/gitea_api" <<'PY' +from pathlib import Path +import sys + +raw = Path(sys.argv[1]).read_text().strip().rstrip("/") +print(raw[:-7] if raw.endswith("/api/v1") else raw) +PY + return 0 + fi + if [ -f "$HOME/.config/gitea/base-url" ]; then + tr -d '[:space:]' < "$HOME/.config/gitea/base-url" + return 0 + fi + echo "ERROR: set GITEA or create ~/.hermes/gitea_api" >&2 + return 1 +} + +export GITEA="$(resolve_gitea_url)" +export OPS_DEFAULT_REPO="${OPS_DEFAULT_REPO:-Timmy_Foundation/timmy-home}" +export OPS_CORE_REPOS="${OPS_CORE_REPOS:-Timmy_Foundation/the-nexus Timmy_Foundation/timmy-home Timmy_Foundation/timmy-config Timmy_Foundation/hermes-agent}" + +ops-token() { + local token_file + for token_file in \ + "$HOME/.config/gitea/timmy-token" \ + "$HOME/.hermes/gitea_token_vps" \ + "$HOME/.hermes/gitea_token_timmy"; do + if [ -f "$token_file" ]; then + tr -d '[:space:]' < "$token_file" + return 0 + fi + done + return 1 +} ops-help() { echo "" - echo -e "\033[1m\033[35m ◈ CONTROLS\033[0m" + echo -e "\033[1m\033[35m ◈ WORKFLOW CONTROLS\033[0m" echo -e "\033[2m ──────────────────────────────────────\033[0m" echo "" - echo -e " \033[1mWake Up\033[0m" - echo " ops-wake-kimi Restart Kimi loop" - echo " ops-wake-claude Restart Claude loop" - echo " ops-wake-gemini Restart Gemini loop" - echo " ops-wake-gateway Restart gateway" - echo " ops-wake-all Restart everything" + echo -e " \033[1mReview\033[0m" + echo " ops-prs [repo] List open PRs across the core repos or one repo" + echo " ops-review-queue Show PRs waiting on Timmy or Allegro" + echo " ops-merge PR REPO Squash-merge a reviewed PR" echo "" - echo -e " \033[1mManage\033[0m" - echo " ops-merge PR_NUM Squash-merge a PR" - echo " ops-assign ISSUE Assign issue to Kimi" - echo " ops-assign-claude ISSUE [REPO] Assign to Claude" - echo " ops-audit Run efficiency audit now" - echo " ops-prs List open PRs" - echo " ops-queue Show Kimi's queue" - echo " ops-claude-queue Show Claude's queue" - echo " ops-gemini-queue Show Gemini's queue" + echo -e " \033[1mDispatch\033[0m" + echo " ops-assign ISSUE AGENT [repo] Assign an issue to an agent" + echo " ops-unassign ISSUE [repo] Remove all assignees from an issue" + echo " ops-queue AGENT [repo|all] Show an agent's queue" + echo " ops-unassigned [repo|all] Show unassigned issues" echo "" - echo -e " \033[1mEmergency\033[0m" - echo " ops-kill-kimi Stop Kimi loop" - echo " ops-kill-claude Stop Claude loop" - echo " ops-kill-gemini Stop Gemini loop" - echo " ops-kill-zombies Kill stuck git/pytest" + echo -e " \033[1mWorkflow Health\033[0m" + echo " ops-gitea-feed Render the Gitea workflow feed" + echo " ops-freshness Check Hermes session/export freshness" echo "" - echo -e " \033[1mOrchestrator\033[0m" - echo " ops-wake-timmy Start Timmy (Ollama)" - echo " ops-kill-timmy Stop Timmy" - echo "" - echo -e " \033[1mWatchdog\033[0m" - echo " ops-wake-watchdog Start loop watchdog" - echo " ops-kill-watchdog Stop loop watchdog" - echo "" - echo -e " \033[2m Type ops-help to see this again\033[0m" + echo -e " \033[1mShortcuts\033[0m" + echo " ops-assign-allegro ISSUE [repo]" + echo " ops-assign-codex ISSUE [repo]" + echo " ops-assign-groq ISSUE [repo]" + echo " ops-assign-claude ISSUE [repo]" + echo " ops-assign-ezra ISSUE [repo]" echo "" } -ops-wake-kimi() { - pkill -f "kimi-loop.sh" 2>/dev/null - sleep 1 - nohup bash ~/.hermes/bin/kimi-loop.sh >> ~/.hermes/logs/kimi-loop.log 2>&1 & - echo " Kimi loop started (PID $!)" -} - -ops-wake-gateway() { - hermes gateway start 2>&1 -} - -ops-wake-claude() { - local workers="${1:-3}" - pkill -f "claude-loop.sh" 2>/dev/null - sleep 1 - nohup bash ~/.hermes/bin/claude-loop.sh "$workers" >> ~/.hermes/logs/claude-loop.log 2>&1 & - echo " Claude loop started — $workers workers (PID $!)" -} - -ops-wake-gemini() { - pkill -f "gemini-loop.sh" 2>/dev/null - sleep 1 - nohup bash ~/.hermes/bin/gemini-loop.sh >> ~/.hermes/logs/gemini-loop.log 2>&1 & - echo " Gemini loop started (PID $!)" -} - -ops-wake-all() { - ops-wake-gateway - sleep 1 - ops-wake-kimi - sleep 1 - ops-wake-claude - sleep 1 - ops-wake-gemini - echo " All services started" -} - -ops-merge() { - local pr=$1 - [ -z "$pr" ] && { echo "Usage: ops-merge PR_NUMBER"; return 1; } - curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ - "$REPO_API/pulls/$pr/merge" -d '{"Do":"squash"}' | python3 -c " -import json,sys -d=json.loads(sys.stdin.read()) -if 'sha' in d: print(f' ✓ PR #{$pr} merged ({d[\"sha\"][:8]})') -else: print(f' ✗ {d.get(\"message\",\"unknown error\")}') -" 2>/dev/null -} - -ops-assign() { - local issue=$1 - [ -z "$issue" ] && { echo "Usage: ops-assign ISSUE_NUMBER"; return 1; } - curl -s -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ - "$REPO_API/issues/$issue" -d '{"assignees":["kimi"]}' | python3 -c " -import json,sys; d=json.loads(sys.stdin.read()); print(f' ✓ #{$issue} assigned to kimi') -" 2>/dev/null -} - -ops-audit() { - bash ~/.hermes/bin/efficiency-audit.sh +ops-python() { + local token + token=$(ops-token) || { echo "No Gitea token found"; return 1; } + OPS_TOKEN="$token" python3 - "$@" } ops-prs() { - curl -s -H "Authorization: token $TOKEN" "$REPO_API/pulls?state=open&limit=20" | python3 -c " + local target="${1:-all}" + ops-python "$GITEA" "$OPS_CORE_REPOS" "$target" <<'PY' +import json +import os +import sys +import urllib.request + +base = sys.argv[1].rstrip("/") +repos = sys.argv[2].split() +target = sys.argv[3] +token = os.environ["OPS_TOKEN"] +headers = {"Authorization": f"token {token}"} + +if target != "all": + repos = [target] + +pulls = [] +for repo in repos: + req = urllib.request.Request( + f"{base}/api/v1/repos/{repo}/pulls?state=open&limit=20", + headers=headers, + ) + with urllib.request.urlopen(req, timeout=5) as resp: + for pr in json.loads(resp.read().decode()): + pr["_repo"] = repo + pulls.append(pr) + +if not pulls: + print(" (none)") +else: + for pr in pulls: + print(f" #{pr['number']:4d} {pr['_repo'].split('/', 1)[1]:12s} {pr['user']['login'][:12]:12s} {pr['title'][:60]}") +PY +} + +ops-review-queue() { + ops-python "$GITEA" "$OPS_CORE_REPOS" <<'PY' +import json +import os +import sys +import urllib.request + +base = sys.argv[1].rstrip("/") +repos = sys.argv[2].split() +token = os.environ["OPS_TOKEN"] +headers = {"Authorization": f"token {token}"} + +items = [] +for repo in repos: + 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: + for item in json.loads(resp.read().decode()): + assignees = [a.get("login", "") for a in (item.get("assignees") or [])] + if any(name in assignees for name in ("Timmy", "allegro")): + item["_repo"] = repo + items.append(item) + +if not items: + print(" (clear)") +else: + for item in items: + names = ",".join(a.get("login", "") for a in (item.get("assignees") or [])) + print(f" #{item['number']:4d} {item['_repo'].split('/', 1)[1]:12s} {names[:20]:20s} {item['title'][:56]}") +PY +} + +ops-assign() { + local issue="$1" + local agent="$2" + local repo="${3:-$OPS_DEFAULT_REPO}" + local token + [ -z "$issue" ] && { echo "Usage: ops-assign ISSUE_NUMBER AGENT [owner/repo]"; return 1; } + [ -z "$agent" ] && { echo "Usage: ops-assign ISSUE_NUMBER AGENT [owner/repo]"; return 1; } + token=$(ops-token) || { echo "No Gitea token found"; return 1; } + curl -s -X PATCH -H "Authorization: token $token" -H "Content-Type: application/json" \ + "$GITEA/api/v1/repos/$repo/issues/$issue" -d "{\"assignees\":[\"$agent\"]}" | python3 -c " import json,sys -prs=json.loads(sys.stdin.read()) -for p in prs: print(f' #{p[\"number\"]:4d} {p[\"user\"][\"login\"]:8s} {p[\"title\"][:60]}') -if not prs: print(' (none)') +d=json.loads(sys.stdin.read()) +names=','.join(a.get('login','') for a in (d.get('assignees') or [])) +print(f' ✓ #{d.get(\"number\", \"?\")} assigned to {names or \"(none)\"}') +" 2>/dev/null +} + +ops-unassign() { + local issue="$1" + local repo="${2:-$OPS_DEFAULT_REPO}" + local token + [ -z "$issue" ] && { echo "Usage: ops-unassign ISSUE_NUMBER [owner/repo]"; return 1; } + token=$(ops-token) || { echo "No Gitea token found"; return 1; } + curl -s -X PATCH -H "Authorization: token $token" -H "Content-Type: application/json" \ + "$GITEA/api/v1/repos/$repo/issues/$issue" -d '{"assignees":[]}' | python3 -c " +import json,sys +d=json.loads(sys.stdin.read()) +print(f' ✓ #{d.get(\"number\", \"?\")} unassigned') " 2>/dev/null } ops-queue() { - curl -s -H "Authorization: token $TOKEN" "$REPO_API/issues?state=open&limit=50&type=issues" | python3 -c " -import json,sys -all_issues=json.loads(sys.stdin.read()) -issues=[i for i in all_issues if 'kimi' in [a.get('login','') for a in (i.get('assignees') or [])]] -for i in issues: print(f' #{i[\"number\"]:4d} {i[\"title\"][:60]}') -if not issues: print(' (empty)') -" 2>/dev/null -} + local agent="$1" + local target="${2:-all}" + [ -z "$agent" ] && { echo "Usage: ops-queue AGENT [repo|all]"; return 1; } + ops-python "$GITEA" "$OPS_CORE_REPOS" "$agent" "$target" <<'PY' +import json +import os +import sys +import urllib.request -ops-kill-kimi() { - pkill -f "kimi-loop.sh" 2>/dev/null - pkill -f "kimi.*--print" 2>/dev/null - echo " Kimi stopped" -} +base = sys.argv[1].rstrip("/") +repos = sys.argv[2].split() +agent = sys.argv[3] +target = sys.argv[4] +token = os.environ["OPS_TOKEN"] +headers = {"Authorization": f"token {token}"} -ops-kill-claude() { - pkill -f "claude-loop.sh" 2>/dev/null - pkill -f "claude.*--print.*--dangerously" 2>/dev/null - rm -rf ~/.hermes/logs/claude-locks/*.lock 2>/dev/null - echo '{}' > ~/.hermes/logs/claude-active.json 2>/dev/null - echo " Claude stopped (all workers)" -} +if target != "all": + repos = [target] -ops-kill-gemini() { - pkill -f "gemini-loop.sh" 2>/dev/null - pkill -f "gemini.*--print" 2>/dev/null - echo " Gemini stopped" -} - -ops-assign-claude() { - local issue=$1 - local repo="${2:-rockachopa/Timmy-time-dashboard}" - [ -z "$issue" ] && { echo "Usage: ops-assign-claude ISSUE_NUMBER [owner/repo]"; return 1; } - curl -s -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ - "$GITEA/api/v1/repos/$repo/issues/$issue" -d '{"assignees":["claude"]}' | python3 -c " -import json,sys; d=json.loads(sys.stdin.read()); print(f' ✓ #{$issue} assigned to claude') -" 2>/dev/null -} - -ops-claude-queue() { - python3 -c " -import json, urllib.request -token=*** ~/.hermes/claude_token 2>/dev/null)' -base = 'http://143.198.27.163:3000' -repos = ['rockachopa/Timmy-time-dashboard','rockachopa/alexanderwhitestone.com','replit/timmy-tower','replit/token-gated-economy','rockachopa/hermes-agent'] +rows = [] for repo in repos: - url = f'{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues' - try: - req = urllib.request.Request(url, headers={'Authorization': f'token {token}'}) - resp = urllib.request.urlopen(req, timeout=5) - raw = json.loads(resp.read()) - issues = [i for i in raw if 'claude' in [a.get('login','') for a in (i.get('assignees') or [])]] - for i in issues: - print(f' #{i[\"number\"]:4d} {repo.split(\"/\")[1]:20s} {i[\"title\"][:50]}') - except: continue -" 2>/dev/null || echo " (error)" + 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: + for issue in json.loads(resp.read().decode()): + assignees = [a.get("login", "") for a in (issue.get("assignees") or [])] + if agent in assignees: + rows.append((repo, issue["number"], issue["title"])) + +if not rows: + print(" (empty)") +else: + for repo, number, title in rows: + print(f" #{number:4d} {repo.split('/', 1)[1]:12s} {title[:60]}") +PY } -ops-assign-gemini() { - local issue=$1 - local repo="${2:-rockachopa/Timmy-time-dashboard}" - [ -z "$issue" ] && { echo "Usage: ops-assign-gemini ISSUE_NUMBER [owner/repo]"; return 1; } - curl -s -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ - "$GITEA/api/v1/repos/$repo/issues/$issue" -d '{"assignees":["gemini"]}' | python3 -c " -import json,sys; d=json.loads(sys.stdin.read()); print(f' ✓ #{$issue} assigned to gemini') -" 2>/dev/null +ops-unassigned() { + local target="${1:-all}" + ops-python "$GITEA" "$OPS_CORE_REPOS" "$target" <<'PY' +import json +import os +import sys +import urllib.request + +base = sys.argv[1].rstrip("/") +repos = sys.argv[2].split() +target = sys.argv[3] +token = os.environ["OPS_TOKEN"] +headers = {"Authorization": f"token {token}"} + +if target != "all": + repos = [target] + +rows = [] +for repo in repos: + 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: + for issue in json.loads(resp.read().decode()): + if not issue.get("assignees"): + rows.append((repo, issue["number"], issue["title"])) + +if not rows: + print(" (none)") +else: + for repo, number, title in rows[:20]: + print(f" #{number:4d} {repo.split('/', 1)[1]:12s} {title[:60]}") + if len(rows) > 20: + print(f" ... +{len(rows) - 20} more") +PY } -ops-gemini-queue() { - curl -s -H "Authorization: token $TOKEN" "$REPO_API/issues?state=open&limit=50&type=issues" | python3 -c " +ops-merge() { + local pr="$1" + local repo="${2:-$OPS_DEFAULT_REPO}" + local token + [ -z "$pr" ] && { echo "Usage: ops-merge PR_NUMBER [owner/repo]"; return 1; } + token=$(ops-token) || { echo "No Gitea token found"; return 1; } + curl -s -X POST -H "Authorization: token $token" -H "Content-Type: application/json" \ + "$GITEA/api/v1/repos/$repo/pulls/$pr/merge" -d '{"Do":"squash"}' | python3 -c " import json,sys -all_issues=json.loads(sys.stdin.read()) -issues=[i for i in all_issues if 'gemini' in [a.get('login','') for a in (i.get('assignees') or [])]] -for i in issues: print(f' #{i[\"number\"]:4d} {i[\"title\"][:60]}') -if not issues: print(' (empty)') +d=json.loads(sys.stdin.read()) +if 'sha' in d: + print(f' ✓ PR merged ({d[\"sha\"][:8]})') +else: + print(f' ✗ {d.get(\"message\", \"unknown error\")}') " 2>/dev/null } -ops-kill-zombies() { - local killed=0 - for pid in $(ps aux | grep "pytest tests/" | grep -v grep | awk '{print $2}'); do - kill "$pid" 2>/dev/null && killed=$((killed+1)) - done - for pid in $(ps aux | grep "git.*push\|git-remote-http" | grep -v grep | awk '{print $2}'); do - kill "$pid" 2>/dev/null && killed=$((killed+1)) - done - echo " Killed $killed zombie processes" +ops-gitea-feed() { + bash "$HOME/.hermes/bin/ops-gitea.sh" } -ops-wake-timmy() { - pkill -f "timmy-orchestrator.sh" 2>/dev/null - rm -f ~/.hermes/logs/timmy-orchestrator.pid - sleep 1 - nohup bash ~/.hermes/bin/timmy-orchestrator.sh >> ~/.hermes/logs/timmy-orchestrator.log 2>&1 & - echo " Timmy orchestrator started (PID $!)" +ops-freshness() { + bash "$HOME/.hermes/bin/pipeline-freshness.sh" } -ops-kill-timmy() { - pkill -f "timmy-orchestrator.sh" 2>/dev/null - rm -f ~/.hermes/logs/timmy-orchestrator.pid - echo " Timmy stopped" -} - -ops-wake-watchdog() { - pkill -f "loop-watchdog.sh" 2>/dev/null - sleep 1 - nohup bash ~/.hermes/bin/loop-watchdog.sh >> ~/.hermes/logs/watchdog.log 2>&1 & - echo " Watchdog started (PID $!)" -} - -ops-kill-watchdog() { - pkill -f "loop-watchdog.sh" 2>/dev/null - echo " Watchdog stopped" -} +ops-assign-allegro() { ops-assign "$1" "allegro" "${2:-$OPS_DEFAULT_REPO}"; } +ops-assign-codex() { ops-assign "$1" "codex-agent" "${2:-$OPS_DEFAULT_REPO}"; } +ops-assign-groq() { ops-assign "$1" "groq" "${2:-$OPS_DEFAULT_REPO}"; } +ops-assign-claude() { ops-assign "$1" "claude" "${2:-$OPS_DEFAULT_REPO}"; } +ops-assign-ezra() { ops-assign "$1" "ezra" "${2:-$OPS_DEFAULT_REPO}"; } +ops-assign-perplexity() { ops-assign "$1" "perplexity" "${2:-$OPS_DEFAULT_REPO}"; } +ops-assign-kimiclaw() { ops-assign "$1" "KimiClaw" "${2:-$OPS_DEFAULT_REPO}"; } diff --git a/bin/ops-panel.sh b/bin/ops-panel.sh index 33c0333e..3741a79b 100755 --- a/bin/ops-panel.sh +++ b/bin/ops-panel.sh @@ -1,300 +1,224 @@ #!/usr/bin/env bash -# ── Consolidated Ops Panel ───────────────────────────────────────────── -# Everything in one view. Designed for a half-screen pane (~100x45). +# ── Workflow Ops Panel ───────────────────────────────────────────────── +# Current-state dashboard for review, dispatch, and freshness. +# This intentionally reflects the post-loop, Hermes-sidecar workflow. # ─────────────────────────────────────────────────────────────────────── -B='\033[1m' ; D='\033[2m' ; R='\033[0m' ; U='\033[4m' -G='\033[32m' ; Y='\033[33m' ; RD='\033[31m' ; C='\033[36m' ; M='\033[35m' ; W='\033[37m' -OK="${G}●${R}" ; WARN="${Y}●${R}" ; FAIL="${RD}●${R}" ; OFF="${D}○${R}" +set -euo pipefail -TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null) -API="http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard" +B='\033[1m' +D='\033[2m' +R='\033[0m' +U='\033[4m' +G='\033[32m' +Y='\033[33m' +RD='\033[31m' +M='\033[35m' +OK="${G}●${R}" +WARN="${Y}●${R}" +FAIL="${RD}●${R}" + +resolve_gitea_url() { + if [ -n "${GITEA_URL:-}" ]; then + printf '%s\n' "${GITEA_URL%/}" + return 0 + fi + if [ -f "$HOME/.hermes/gitea_api" ]; then + python3 - "$HOME/.hermes/gitea_api" <<'PY' +from pathlib import Path +import sys + +raw = Path(sys.argv[1]).read_text().strip().rstrip("/") +print(raw[:-7] if raw.endswith("/api/v1") else raw) +PY + return 0 + fi + if [ -f "$HOME/.config/gitea/base-url" ]; then + tr -d '[:space:]' < "$HOME/.config/gitea/base-url" + return 0 + fi + echo "ERROR: set GITEA_URL or create ~/.hermes/gitea_api" >&2 + return 1 +} + +resolve_ops_token() { + local token_file + for token_file in \ + "$HOME/.config/gitea/timmy-token" \ + "$HOME/.hermes/gitea_token_vps" \ + "$HOME/.hermes/gitea_token_timmy"; do + if [ -f "$token_file" ]; then + tr -d '[:space:]' < "$token_file" + return 0 + fi + done + return 1 +} + +GITEA_URL="$(resolve_gitea_url)" +CORE_REPOS="${CORE_REPOS:-Timmy_Foundation/the-nexus Timmy_Foundation/timmy-home Timmy_Foundation/timmy-config Timmy_Foundation/hermes-agent}" +TOKEN="$(resolve_ops_token || true)" +[ -z "$TOKEN" ] && echo "WARN: no approved Timmy Gitea token found; panel will use unauthenticated API calls" >&2 -# ── HEADER ───────────────────────────────────────────────────────────── echo "" -echo -e " ${B}${M}◈ HERMES OPERATIONS${R} ${D}$(date '+%a %b %d %H:%M:%S')${R}" +echo -e " ${B}${M}◈ WORKFLOW OPERATIONS${R} ${D}$(date '+%a %b %d %H:%M:%S')${R}" echo -e " ${D}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${R}" echo "" -# ── SERVICES ─────────────────────────────────────────────────────────── echo -e " ${B}${U}SERVICES${R}" echo "" -# Gateway -GW_PID=$(pgrep -f "hermes.*gateway.*run" 2>/dev/null | head -1) -[ -n "$GW_PID" ] && echo -e " ${OK} Gateway ${D}pid $GW_PID${R}" \ - || echo -e " ${FAIL} Gateway ${RD}DOWN — run: hermes gateway start${R}" - -# Kimi Code loop -KIMI_PID=$(pgrep -f "kimi-loop.sh" 2>/dev/null | head -1) -[ -n "$KIMI_PID" ] && echo -e " ${OK} Kimi Loop ${D}pid $KIMI_PID${R}" \ - || echo -e " ${FAIL} Kimi Loop ${RD}DOWN — run: ops-wake-kimi${R}" - -# Active Kimi Code worker -KIMI_WORK=$(pgrep -f "kimi.*--print" 2>/dev/null | head -1) -if [ -n "$KIMI_WORK" ]; then - echo -e " ${OK} Kimi Code ${D}pid $KIMI_WORK ${G}working${R}" -elif [ -n "$KIMI_PID" ]; then - echo -e " ${WARN} Kimi Code ${Y}between issues${R}" +GW_PID=$(pgrep -f "hermes.*gateway.*run" 2>/dev/null | head -1 || true) +if [ -n "${GW_PID:-}" ]; then + echo -e " ${OK} Hermes Gateway ${D}pid $GW_PID${R}" else - echo -e " ${OFF} Kimi Code ${D}not running${R}" + echo -e " ${FAIL} Hermes Gateway ${RD}down${R}" fi -# Claude Code loop (parallel workers) -CLAUDE_PID=$(pgrep -f "claude-loop.sh" 2>/dev/null | head -1) -CLAUDE_WORKERS=$(pgrep -f "claude.*--print.*--dangerously" 2>/dev/null | wc -l | tr -d ' ') -if [ -n "$CLAUDE_PID" ]; then - echo -e " ${OK} Claude Loop ${D}pid $CLAUDE_PID ${G}${CLAUDE_WORKERS} workers active${R}" +if curl -s --max-time 3 "$GITEA_URL/api/v1/version" >/dev/null 2>&1; then + echo -e " ${OK} Gitea ${D}${GITEA_URL}${R}" else - echo -e " ${FAIL} Claude Loop ${RD}DOWN — run: ops-wake-claude${R}" + echo -e " ${FAIL} Gitea ${RD}unreachable${R}" fi -# Gemini Code loop -GEMINI_PID=$(pgrep -f "gemini-loop.sh" 2>/dev/null | head -1) -GEMINI_WORK=$(pgrep -f "gemini.*--print" 2>/dev/null | head -1) -if [ -n "$GEMINI_PID" ]; then - if [ -n "$GEMINI_WORK" ]; then - echo -e " ${OK} Gemini Loop ${D}pid $GEMINI_PID ${G}working${R}" - else - echo -e " ${WARN} Gemini Loop ${D}pid $GEMINI_PID ${Y}between issues${R}" - fi +if hermes cron list >/dev/null 2>&1; then + echo -e " ${OK} Hermes Cron ${D}reachable${R}" else - echo -e " ${FAIL} Gemini Loop ${RD}DOWN — run: ops-wake-gemini${R}" + echo -e " ${WARN} Hermes Cron ${Y}not responding${R}" fi -# Timmy Orchestrator -TIMMY_PID=$(pgrep -f "timmy-orchestrator.sh" 2>/dev/null | head -1) -if [ -n "$TIMMY_PID" ]; then - TIMMY_LAST=$(tail -1 "$HOME/.hermes/logs/timmy-orchestrator.log" 2>/dev/null | sed 's/.*TIMMY: //') - echo -e " ${OK} Timmy (Ollama) ${D}pid $TIMMY_PID ${G}${TIMMY_LAST:0:30}${R}" +FRESHNESS_OUTPUT=$("$HOME/.hermes/bin/pipeline-freshness.sh" 2>/dev/null || true) +FRESHNESS_STATUS=$(printf '%s\n' "$FRESHNESS_OUTPUT" | awk -F= '/^status=/{print $2}') +FRESHNESS_REASON=$(printf '%s\n' "$FRESHNESS_OUTPUT" | awk -F= '/^reason=/{print $2}') +if [ "$FRESHNESS_STATUS" = "ok" ]; then + echo -e " ${OK} Export Freshness ${D}${FRESHNESS_REASON:-within freshness window}${R}" +elif [ -n "$FRESHNESS_STATUS" ]; then + echo -e " ${WARN} Export Freshness ${Y}${FRESHNESS_REASON:-lagging}${R}" else - echo -e " ${FAIL} Timmy ${RD}DOWN — run: ops-wake-timmy${R}" -fi - -# Gitea VPS -if curl -s --max-time 3 "http://143.198.27.163:3000/api/v1/version" >/dev/null 2>&1; then - echo -e " ${OK} Gitea VPS ${D}143.198.27.163:3000${R}" -else - echo -e " ${FAIL} Gitea VPS ${RD}unreachable${R}" -fi - -# Matrix staging -HTTP=$(curl -s --max-time 3 -o /dev/null -w "%{http_code}" "http://143.198.27.163/") -[ "$HTTP" = "200" ] && echo -e " ${OK} Matrix Staging ${D}143.198.27.163${R}" \ - || echo -e " ${FAIL} Matrix Staging ${RD}HTTP $HTTP${R}" - -# Dev cycle cron -CRON_LINE=$(hermes cron list 2>&1 | grep -B1 "consolidated-dev-cycle" | head -1 2>/dev/null) -if echo "$CRON_LINE" | grep -q "active"; then - NEXT=$(hermes cron list 2>&1 | grep -A4 "consolidated-dev-cycle" | grep "Next" | awk '{print $NF}' | cut -dT -f2 | cut -d. -f1) - echo -e " ${OK} Dev Cycle ${D}every 30m, next ${NEXT:-?}${R}" -else - echo -e " ${FAIL} Dev Cycle Cron ${RD}MISSING${R}" + echo -e " ${WARN} Export Freshness ${Y}unknown${R}" fi echo "" -# ── KIMI STATS ───────────────────────────────────────────────────────── -echo -e " ${B}${U}KIMI${R}" -echo "" -KIMI_LOG="$HOME/.hermes/logs/kimi-loop.log" -if [ -f "$KIMI_LOG" ]; then - COMPLETED=$(grep -c "SUCCESS:" "$KIMI_LOG" 2>/dev/null | tail -1 || echo 0) - FAILED=$(grep -c "FAILED:" "$KIMI_LOG" 2>/dev/null | tail -1 || echo 0) - LAST_ISSUE=$(grep "=== ISSUE" "$KIMI_LOG" | tail -1 | sed 's/.*=== //' | sed 's/ ===//') - LAST_TIME=$(grep "=== ISSUE\|SUCCESS\|FAILED" "$KIMI_LOG" | tail -1 | cut -d']' -f1 | tr -d '[') - RATE="" - if [ "$COMPLETED" -gt 0 ] && [ "$FAILED" -gt 0 ]; then - TOTAL=$((COMPLETED + FAILED)) - PCT=$((COMPLETED * 100 / TOTAL)) - RATE=" (${PCT}% success)" - fi - echo -e " Completed ${G}${B}$COMPLETED${R} Failed ${RD}$FAILED${R}${D}$RATE${R}" - echo -e " Current ${C}$LAST_ISSUE${R}" - echo -e " Last seen ${D}$LAST_TIME${R}" -fi -echo "" - -# ── CLAUDE STATS ────────────────────────────────────────────────── -echo -e " ${B}${U}CLAUDE${R}" -echo "" -CLAUDE_LOG="$HOME/.hermes/logs/claude-loop.log" -if [ -f "$CLAUDE_LOG" ]; then - CL_COMPLETED=$(grep -c "SUCCESS" "$CLAUDE_LOG" 2>/dev/null | tail -1 || echo 0) - CL_FAILED=$(grep -c "FAILED" "$CLAUDE_LOG" 2>/dev/null | tail -1 || echo 0) - CL_RATE_LIM=$(grep -c "RATE LIMITED" "$CLAUDE_LOG" 2>/dev/null | tail -1 || echo 0) - CL_RATE="" - if [ "$CL_COMPLETED" -gt 0 ] || [ "$CL_FAILED" -gt 0 ]; then - CL_TOTAL=$((CL_COMPLETED + CL_FAILED)) - [ "$CL_TOTAL" -gt 0 ] && CL_PCT=$((CL_COMPLETED * 100 / CL_TOTAL)) && CL_RATE=" (${CL_PCT}%)" - fi - echo -e " ${G}${B}$CL_COMPLETED${R} done ${RD}$CL_FAILED${R} fail ${Y}$CL_RATE_LIM${R} rate-limited${D}$CL_RATE${R}" - - # Show active workers - ACTIVE="$HOME/.hermes/logs/claude-active.json" - if [ -f "$ACTIVE" ]; then - python3 -c " +python3 - "$GITEA_URL" "$TOKEN" "$CORE_REPOS" <<'PY' import json -try: - with open('$ACTIVE') as f: active = json.load(f) - for wid, info in sorted(active.items()): - iss = info.get('issue','') - repo = info.get('repo','').split('/')[-1] if info.get('repo') else '' - st = info.get('status','') - if st == 'working': - print(f' \033[36mW{wid}\033[0m \033[33m#{iss}\033[0m \033[2m{repo}\033[0m') - elif st == 'idle': - print(f' \033[2mW{wid} idle\033[0m') -except: pass -" 2>/dev/null - fi -else - echo -e " ${D}(no log yet — start with ops-wake-claude)${R}" -fi -echo "" +import sys +import urllib.error +import urllib.request +from datetime import datetime, timedelta, timezone -# ── GEMINI STATS ───────────────────────────────────────────────────── -echo -e " ${B}${U}GEMINI${R}" -echo "" -GEMINI_LOG="$HOME/.hermes/logs/gemini-loop.log" -if [ -f "$GEMINI_LOG" ]; then - GM_COMPLETED=$(grep -c "SUCCESS:" "$GEMINI_LOG" 2>/dev/null | tail -1 || echo 0) - GM_FAILED=$(grep -c "FAILED:" "$GEMINI_LOG" 2>/dev/null | tail -1 || echo 0) - GM_RATE="" - if [ "$GM_COMPLETED" -gt 0 ] || [ "$GM_FAILED" -gt 0 ]; then - GM_TOTAL=$((GM_COMPLETED + GM_FAILED)) - [ "$GM_TOTAL" -gt 0 ] && GM_PCT=$((GM_COMPLETED * 100 / GM_TOTAL)) && GM_RATE=" (${GM_PCT}%)" - fi - GM_LAST=$(grep "=== ISSUE" "$GEMINI_LOG" | tail -1 | sed 's/.*=== //' | sed 's/ ===//') - echo -e " ${G}${B}$GM_COMPLETED${R} done ${RD}$GM_FAILED${R} fail${D}$GM_RATE${R}" - [ -n "$GM_LAST" ] && echo -e " Current ${C}$GM_LAST${R}" -else - echo -e " ${D}(no log yet — start with ops-wake-gemini)${R}" -fi -echo "" +base = sys.argv[1].rstrip("/") +token = sys.argv[2] +repos = sys.argv[3].split() +headers = {"Authorization": f"token {token}"} if token else {} -# ── OPEN PRS ─────────────────────────────────────────────────────────── -echo -e " ${B}${U}PULL REQUESTS${R}" -echo "" -curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=open&limit=8" 2>/dev/null | python3 -c " -import json,sys -try: - prs = json.loads(sys.stdin.read()) - if not prs: print(' \033[2m(none open)\033[0m') - for p in prs[:6]: - n = p['number'] - t = p['title'][:55] - u = p['user']['login'] - print(f' \033[33m#{n:<4d}\033[0m \033[2m{u:8s}\033[0m {t}') - if len(prs) > 6: print(f' \033[2m... +{len(prs)-6} more\033[0m') -except: print(' \033[31m(error fetching)\033[0m') -" 2>/dev/null -echo "" -# ── RECENTLY MERGED ──────────────────────────────────────────────────── -echo -e " ${B}${U}RECENTLY MERGED${R}" -echo "" -curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=closed&sort=updated&limit=5" 2>/dev/null | python3 -c " -import json,sys -try: - prs = json.loads(sys.stdin.read()) - merged = [p for p in prs if p.get('merged')][:5] - if not merged: print(' \033[2m(none recent)\033[0m') - for p in merged: - n = p['number'] - t = p['title'][:50] - when = p['merged_at'][11:16] - print(f' \033[32m✓ #{n:<4d}\033[0m {t} \033[2m{when}\033[0m') -except: print(' \033[31m(error)\033[0m') -" 2>/dev/null -echo "" +def fetch(path): + req = urllib.request.Request(f"{base}{path}", headers=headers) + with urllib.request.urlopen(req, timeout=5) as resp: + return json.loads(resp.read().decode()) -# ── KIMI QUEUE ───────────────────────────────────────────────────────── -echo -e " ${B}${U}KIMI QUEUE${R}" -echo "" -curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c " -import json,sys -try: - all_issues = json.loads(sys.stdin.read()) - issues = [i for i in all_issues if 'kimi' in [a.get('login','') for a in (i.get('assignees') or [])]] - if not issues: print(' \033[33m⚠ Queue empty — assign more issues to kimi\033[0m') - for i in issues[:6]: - n = i['number'] - t = i['title'][:55] - print(f' #{n:<4d} {t}') - if len(issues) > 6: print(f' \033[2m... +{len(issues)-6} more\033[0m') -except: print(' \033[31m(error)\033[0m') -" 2>/dev/null -echo "" -# ── CLAUDE QUEUE ────────────────────────────────────────────────── -echo -e " ${B}${U}CLAUDE QUEUE${R}" -echo "" -# Claude works across multiple repos -python3 -c " -import json, sys, urllib.request -token = '$(cat ~/.hermes/claude_token 2>/dev/null)' -base = 'http://143.198.27.163:3000' -repos = ['rockachopa/Timmy-time-dashboard','rockachopa/alexanderwhitestone.com','replit/timmy-tower','replit/token-gated-economy','rockachopa/hermes-agent'] -all_issues = [] +def short(repo): + return repo.split("/", 1)[1] + + +issues = [] +pulls = [] +review_queue = [] +errors = [] + for repo in repos: - url = f'{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues' try: - req = urllib.request.Request(url, headers={'Authorization': f'token {token}'}) - resp = urllib.request.urlopen(req, timeout=5) - raw = json.loads(resp.read()) - issues = [i for i in raw if 'claude' in [a.get('login','') for a in (i.get('assignees') or [])]] - for i in issues: - i['_repo'] = repo.split('/')[1] - all_issues.extend(issues) - except: continue -if not all_issues: - print(' \033[33m\u26a0 Queue empty \u2014 assign issues to claude\033[0m') + repo_pulls = fetch(f"/api/v1/repos/{repo}/pulls?state=open&limit=20") + for pr in repo_pulls: + pr["_repo"] = repo + pulls.append(pr) + repo_issues = fetch(f"/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues") + for issue in repo_issues: + issue["_repo"] = repo + issues.append(issue) + repo_pull_issues = fetch(f"/api/v1/repos/{repo}/issues?state=open&limit=50&type=pulls") + for item in repo_pull_issues: + assignees = [a.get("login", "") for a in (item.get("assignees") or [])] + if any(name in assignees for name in ("Timmy", "allegro")): + item["_repo"] = repo + review_queue.append(item) + except urllib.error.URLError as exc: + errors.append(f"{repo}: {exc.reason}") + except Exception as exc: # pragma: no cover - defensive panel path + errors.append(f"{repo}: {exc}") + +print(" \033[1m\033[4mREVIEW QUEUE\033[0m\n") +if not review_queue: + print(" \033[2m(clear)\033[0m\n") else: - for i in all_issues[:6]: - n = i['number'] - t = i['title'][:45] - r = i['_repo'][:12] - print(f' #{n:<4d} \033[2m{r:12s}\033[0m {t}') - if len(all_issues) > 6: - print(f' \033[2m... +{len(all_issues)-6} more\033[0m') -" 2>/dev/null + for item in review_queue[:8]: + names = ",".join(a.get("login", "") for a in (item.get("assignees") or [])) + print(f" #{item['number']:<4d} {short(item['_repo']):12s} {names[:20]:20s} {item['title'][:44]}") + print() + +print(" \033[1m\033[4mOPEN PRS\033[0m\n") +if not pulls: + print(" \033[2m(none open)\033[0m\n") +else: + for pr in pulls[:8]: + print(f" #{pr['number']:<4d} {short(pr['_repo']):12s} {pr['user']['login'][:12]:12s} {pr['title'][:48]}") + print() + +print(" \033[1m\033[4mDISPATCH QUEUES\033[0m\n") +queue_agents = [ + ("allegro", "dispatch"), + ("codex-agent", "cleanup"), + ("groq", "fast ship"), + ("claude", "refactor"), + ("ezra", "archive"), + ("perplexity", "research"), + ("KimiClaw", "digest"), +] +for agent, label in queue_agents: + assigned = [ + issue + for issue in issues + if agent in [a.get("login", "") for a in (issue.get("assignees") or [])] + ] + print(f" {agent:12s} {len(assigned):2d} \033[2m{label}\033[0m") +print() + +unassigned = [issue for issue in issues if not issue.get("assignees")] +stale_cutoff = (datetime.now(timezone.utc) - timedelta(days=2)).strftime("%Y-%m-%d") +stale_prs = [pr for pr in pulls if pr.get("updated_at", "")[:10] < stale_cutoff] +overloaded = [] +for agent in ("allegro", "codex-agent", "groq", "claude", "ezra", "perplexity", "KimiClaw"): + count = sum( + 1 + for issue in issues + if agent in [a.get("login", "") for a in (issue.get("assignees") or [])] + ) + if count > 3: + overloaded.append((agent, count)) + +print(" \033[1m\033[4mWARNINGS\033[0m\n") +warns = [] +if len(unassigned) > 10: + warns.append(f"{len(unassigned)} unassigned issues across core repos") +if stale_prs: + warns.append(f"{len(stale_prs)} open PRs look stale and may need a review nudge") +for agent, count in overloaded: + warns.append(f"{agent} has {count} assigned issues; rebalance dispatch") + +if warns: + for warn in warns: + print(f" \033[33m⚠ {warn}\033[0m") +else: + print(" \033[2m(no major workflow warnings)\033[0m") + +if errors: + print("\n \033[1m\033[4mFETCH ERRORS\033[0m\n") + for err in errors[:4]: + print(f" \033[31m{err}\033[0m") +PY + echo "" - -# ── GEMINI QUEUE ───────────────────────────────────────────────────── -echo -e " ${B}${U}GEMINI QUEUE${R}" -echo "" -curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c " -import json,sys -try: - all_issues = json.loads(sys.stdin.read()) - issues = [i for i in all_issues if 'gemini' in [a.get('login','') for a in (i.get('assignees') or [])]] - if not issues: print(' \033[33m⚠ Queue empty — assign issues to gemini\033[0m') - for i in issues[:6]: - n = i['number'] - t = i['title'][:55] - print(f' #{n:<4d} {t}') - if len(issues) > 6: print(f' \033[2m... +{len(issues)-6} more\033[0m') -except: print(' \033[31m(error)\033[0m') -" 2>/dev/null -echo "" - -# ── WARNINGS ─────────────────────────────────────────────────────────── -HERMES_PROCS=$(ps aux | grep -E "hermes.*python" | grep -v grep | wc -l | tr -d ' ') -STUCK_GIT=$(ps aux | grep "git.*push\|git-remote-http" | grep -v grep | wc -l | tr -d ' ') -ORPHAN_PY=$(ps aux | grep "pytest tests/" | grep -v grep | wc -l | tr -d ' ') -UNASSIGNED=$(curl -s --max-time 3 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c "import json,sys; issues=json.loads(sys.stdin.read()); print(len([i for i in issues if not i.get('assignees')]))" 2>/dev/null) - -WARNS="" -[ "$STUCK_GIT" -gt 0 ] && WARNS+=" ${RD}⚠ $STUCK_GIT stuck git processes${R}\n" -[ "$ORPHAN_PY" -gt 0 ] && WARNS+=" ${Y}⚠ $ORPHAN_PY orphaned pytest runs${R}\n" -[ "${UNASSIGNED:-0}" -gt 10 ] && WARNS+=" ${Y}⚠ $UNASSIGNED unassigned issues — feed the queue${R}\n" - -if [ -n "$WARNS" ]; then - echo -e " ${B}${U}WARNINGS${R}" - echo "" - echo -e "$WARNS" -fi - echo -e " ${D}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${R}" -echo -e " ${D}hermes sessions: $HERMES_PROCS unassigned: ${UNASSIGNED:-?} ↻ 20s${R}" +echo -e " ${D}repos: $(printf '%s' "$CORE_REPOS" | wc -w | tr -d ' ') refresh via watch or rerun script${R}"