#!/usr/bin/env bash # ── 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. # ─────────────────────────────────────────────────────────────────────── 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 ◈ WORKFLOW CONTROLS\033[0m" echo -e "\033[2m ──────────────────────────────────────\033[0m" echo "" 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[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[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[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-python() { local token token=$(ops-token) || { echo "No Gitea token found"; return 1; } OPS_TOKEN="$token" python3 - "$@" } ops-prs() { 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 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() { 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 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}"} 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()): 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-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-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 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-gitea-feed() { bash "$HOME/.hermes/bin/ops-gitea.sh" } ops-freshness() { bash "$HOME/.hermes/bin/pipeline-freshness.sh" } 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}"; }