Files
blackroad/fleet/alice/opt-blackroad/bin/br-github
Alexa Amundson 78fbe80f2a Initial monorepo — everything BlackRoad in one place
bin/       230 CLI tools (ask-*, br-*, agent-*, roadid, carpool)
scripts/   99 automation scripts
fleet/     Node configs and deployment
workers/   Cloudflare Worker sources (roadpay, road-search, squad webhooks)
roadc/     RoadC programming language
roadnet/   Mesh network (5 APs, WireGuard)
operator/  Memory system scripts
config/    System configs
dotfiles/  Shell configs
docs/      Documentation

BlackRoad OS — Pave Tomorrow.

RoadChain-SHA2048: d1a24f55318d338b
RoadChain-Identity: alexa@sovereign
RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
2026-03-14 17:08:41 -05:00

462 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# BlackRoad OS — Fleet GitHub Agent
# Nodes that can triage, comment, review, fix, and ship on GitHub.
# "We don't just watch the repo — we contribute."
# ============================================================================
set -euo pipefail
NODE="${BLACKROAD_NODE_NAME:-$(hostname)}"
LOG="$HOME/.blackroad-autonomy/github.log"
REPO_DIR="$HOME/blackroad-sync/BlackRoad-Operating-System"
ORG="blackboxprogramming"
MAIN_REPO="BlackRoad-Operating-System"
mkdir -p "$(dirname "$LOG")"
SSH_OPTS="-o ConnectTimeout=3 -o BatchMode=yes -o StrictHostKeyChecking=no -o LogLevel=ERROR"
declare -A SIBLINGS=(
[alice]="pi@192.168.4.49"
[cecilia]="blackroad@192.168.4.96"
[octavia]="pi@192.168.4.101"
[aria]="blackroad@192.168.4.98"
[lucidia]="octavia@192.168.4.38"
)
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$NODE] $*" >> "$LOG"; }
# ─── RATE LIMIT CHECK ────────────────────────────────────────────────
check_rate() {
local remaining
remaining=$(gh api rate_limit --jq '.resources.core.remaining' 2>/dev/null || echo 0)
if [ "$remaining" -lt 50 ]; then
log "RATELIMIT only $remaining REST calls left — pausing GitHub work"
return 1
fi
return 0
}
# ═══════════════════════════════════════════════════════════════════════
# TRIAGE — Read and label new issues
# ═══════════════════════════════════════════════════════════════════════
cmd_triage() {
log "TRIAGE scanning for unlabeled issues"
check_rate || return
# Get issues without priority labels
local issues
issues=$(gh api "repos/${ORG}/${MAIN_REPO}/issues?state=open&per_page=30" \
--jq '.[] | select(.pull_request == null) | select((.labels | map(.name) | any(test("urgent|high|medium|low"))) | not) | "\(.number)|\(.title)"' 2>/dev/null)
if [ -z "$issues" ]; then
log "TRIAGE all issues are labeled"
return
fi
echo "$issues" | while IFS='|' read -r num title; do
[ -z "$num" ] && continue
log "TRIAGE #$num needs labels: $title"
# Auto-classify based on keywords
local labels=""
local title_lower=$(echo "$title" | tr '[:upper:]' '[:lower:]')
# Priority detection
if echo "$title_lower" | grep -qE "crash|down|broken|urgent|critical|security"; then
labels="urgent"
elif echo "$title_lower" | grep -qE "bug|fix|error|fail|wrong"; then
labels="high,bug"
elif echo "$title_lower" | grep -qE "feature|add|new|implement|create"; then
labels="medium,feature"
elif echo "$title_lower" | grep -qE "docs|readme|comment|typo"; then
labels="low,documentation"
elif echo "$title_lower" | grep -qE "refactor|clean|improve|optimize"; then
labels="medium,enhancement"
elif echo "$title_lower" | grep -qE "fleet|node|pi|raspberry|ssh"; then
labels="medium,fleet"
elif echo "$title_lower" | grep -qE "cloudflare|worker|pages|kv|d1|r2"; then
labels="medium,cloudflare"
else
labels="medium"
fi
# Apply labels
for label in $(echo "$labels" | tr ',' ' '); do
gh api --method POST "repos/${ORG}/${MAIN_REPO}/issues/${num}/labels" \
-f "labels[]=$label" 2>/dev/null && \
log "TRIAGE labeled #$num with $label" || true
done
done
}
# ═══════════════════════════════════════════════════════════════════════
# RESPOND — Comment on issues that need attention
# ═══════════════════════════════════════════════════════════════════════
cmd_respond() {
log "RESPOND checking for issues I can help with"
check_rate || return
# Get open issues with fleet/node labels that have no comments from agents
local issues
issues=$(gh api "repos/${ORG}/${MAIN_REPO}/issues?state=open&labels=fleet&per_page=10" \
--jq '.[] | select(.pull_request == null) | "\(.number)|\(.title)|\(.comments)"' 2>/dev/null)
if [ -z "$issues" ]; then
# Also check unlabeled issues
issues=$(gh api "repos/${ORG}/${MAIN_REPO}/issues?state=open&per_page=10" \
--jq '.[] | select(.pull_request == null) | select(.comments == 0) | "\(.number)|\(.title)|\(.comments)"' 2>/dev/null)
fi
[ -z "$issues" ] && { log "RESPOND no issues need my attention"; return; }
echo "$issues" | head -5 | while IFS='|' read -r num title comments; do
[ -z "$num" ] && continue
[ "$comments" -gt 2 ] && continue # Already has discussion
# Check if I already commented
local my_comments
my_comments=$(gh api "repos/${ORG}/${MAIN_REPO}/issues/${num}/comments" \
--jq "[.[] | select(.body | test(\"\\\\[$NODE\\\\]\"))] | length" 2>/dev/null || echo "0")
[ "$my_comments" -gt 0 ] && continue
# Gather relevant fleet data for the response
local fleet_context=""
local title_lower=$(echo "$title" | tr '[:upper:]' '[:lower:]')
if echo "$title_lower" | grep -qiE "fleet|node|pi|ssh|offline|down|reboot"; then
# This is a fleet issue — gather live status
fleet_context="**Fleet Status (live from $NODE):**\n"
for sname in alice cecilia octavia aria lucidia; do
local target_user target_ip
target_user=$(echo "${SIBLINGS[$sname]}" | cut -d@ -f1)
target_ip=$(echo "${SIBLINGS[$sname]}" | cut -d@ -f2)
if ssh $SSH_OPTS "${target_user}@${target_ip}" 'true' 2>/dev/null; then
local temp load mem
temp=$(ssh $SSH_OPTS "${target_user}@${target_ip}" 'echo $(($(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0) / 1000))' 2>/dev/null || echo "?")
load=$(ssh $SSH_OPTS "${target_user}@${target_ip}" 'cat /proc/loadavg | cut -d" " -f1' 2>/dev/null || echo "?")
mem=$(ssh $SSH_OPTS "${target_user}@${target_ip}" 'free | awk "/Mem:/{printf \"%.0f\", \$3*100/\$2}"' 2>/dev/null || echo "?")
fleet_context+="- **$sname**: online, ${temp}°C, load ${load}, mem ${mem}%\n"
else
fleet_context+="- **$sname**: unreachable\n"
fi
done
fi
if echo "$title_lower" | grep -qiE "disk|storage|space|full"; then
fleet_context+="**Disk Status:**\n"
for sname in alice cecilia octavia aria lucidia; do
local target_user target_ip
target_user=$(echo "${SIBLINGS[$sname]}" | cut -d@ -f1)
target_ip=$(echo "${SIBLINGS[$sname]}" | cut -d@ -f2)
local disk
disk=$(ssh $SSH_OPTS "${target_user}@${target_ip}" 'df -h / | awk "NR==2{printf \"%s used of %s (%s)\", \$3, \$2, \$5}"' 2>/dev/null || echo "unreachable")
fleet_context+="- **$sname**: $disk\n"
done
fi
# Build comment
local comment="**[$NODE] Fleet Agent Report**\n\n"
comment+="I noticed this issue and checked the fleet.\n\n"
if [ -n "$fleet_context" ]; then
comment+="$fleet_context\n"
fi
# Add suggestions based on issue type
if echo "$title_lower" | grep -qiE "offline|down|unreachable"; then
comment+="**Suggested actions:**\n"
comment+="1. Check physical power connections\n"
comment+="2. Try SSH from another node: \`ssh $SSH_OPTS <user>@<ip>\`\n"
comment+="3. Check WireGuard: \`sudo wg show\`\n"
comment+="4. If DHCP changed IP, scan: \`arp -a | grep 'raspberry\\|88:a2:9e'\`\n"
elif echo "$title_lower" | grep -qiE "disk|full|storage"; then
comment+="**Suggested actions:**\n"
comment+="1. Clean journal: \`sudo journalctl --vacuum-size=50M\`\n"
comment+="2. Clean apt: \`sudo apt-get clean\`\n"
comment+="3. Find large files: \`du -h --max-depth=2 / 2>/dev/null | sort -rh | head -20\`\n"
comment+="4. Clean old logs: \`find /var/log -name '*.gz' -delete\`\n"
elif echo "$title_lower" | grep -qiE "undervoltage|power|throttl"; then
comment+="**Suggested actions:**\n"
comment+="1. Use 5V/5A USB-C PSU (Pi 5 with Hailo-8 needs this)\n"
comment+="2. Check: \`vcgencmd get_throttled\`\n"
comment+="3. Reduce CPU: \`echo conservative | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor\`\n"
fi
comment+="\n---\n*Automated by $NODE via br-github — BlackRoad Fleet Agent*"
# Post comment
gh api --method POST "repos/${ORG}/${MAIN_REPO}/issues/${num}/comments" \
-f body="$(echo -e "$comment")" 2>/dev/null && \
log "RESPOND commented on #$num" || \
log "RESPOND failed to comment on #$num"
done
}
# ═══════════════════════════════════════════════════════════════════════
# WATCH — Monitor repo activity and react
# ═══════════════════════════════════════════════════════════════════════
cmd_watch() {
log "WATCH checking repo activity"
check_rate || return
# Check for new PRs
local prs
prs=$(gh api "repos/${ORG}/${MAIN_REPO}/pulls?state=open&per_page=5" \
--jq '.[] | "\(.number)|\(.title)|\(.user.login)|\(.changed_files)"' 2>/dev/null)
if [ -n "$prs" ]; then
echo "$prs" | while IFS='|' read -r num title author files; do
[ -z "$num" ] && continue
log "WATCH PR #$num by $author: $title ($files files)"
# Check if already reviewed by an agent
local reviewed
reviewed=$(gh api "repos/${ORG}/${MAIN_REPO}/pulls/${num}/comments" \
--jq "[.[] | select(.body | test(\"Fleet Agent\"))] | length" 2>/dev/null || echo "0")
[ "$reviewed" -gt 0 ] && continue
# Quick review: check if PR touches fleet config
local pr_files
pr_files=$(gh api "repos/${ORG}/${MAIN_REPO}/pulls/${num}/files" \
--jq '.[].filename' 2>/dev/null)
local review_comment=""
if echo "$pr_files" | grep -qiE "fleet|node|config|ssh"; then
review_comment="**[$NODE] Fleet Agent Review**\n\nThis PR touches fleet-related files. I've noted the changes:\n\n"
echo "$pr_files" | while read -r fname; do
review_comment+="- \`$fname\`\n"
done
review_comment+="\nPlease ensure fleet config changes are tested on at least one node before merging."
fi
if [ -n "$review_comment" ]; then
gh api --method POST "repos/${ORG}/${MAIN_REPO}/issues/${num}/comments" \
-f body="$(echo -e "$review_comment")" 2>/dev/null && \
log "WATCH reviewed PR #$num" || true
fi
done
fi
# Check for stale issues (no activity in 30 days)
local stale
stale=$(gh api "repos/${ORG}/${MAIN_REPO}/issues?state=open&per_page=50&sort=updated&direction=asc" \
--jq '.[] | select(.pull_request == null) | select(.updated_at < (now - 2592000 | todate)) | "\(.number)|\(.title)"' 2>/dev/null)
if [ -n "$stale" ]; then
echo "$stale" | head -5 | while IFS='|' read -r num title; do
[ -z "$num" ] && continue
log "WATCH stale issue #$num: $title"
# Add stale label
gh api --method POST "repos/${ORG}/${MAIN_REPO}/issues/${num}/labels" \
-f "labels[]=stale" 2>/dev/null || true
done
fi
}
# ═══════════════════════════════════════════════════════════════════════
# REPORT — Create fleet status issue
# ═══════════════════════════════════════════════════════════════════════
cmd_report() {
log "REPORT creating fleet status report"
check_rate || return
local body="## Fleet Status Report\n"
body+="**Generated by:** $NODE\n"
body+="**Timestamp:** $(date -u '+%Y-%m-%d %H:%M UTC')\n\n"
body+="### Node Status\n\n"
body+="| Node | Status | Temp | Load | Mem | Disk | Uptime |\n"
body+="|------|--------|------|------|-----|------|--------|\n"
local online=0 total=0 issues_found=""
for sname in alice cecilia octavia aria lucidia; do
total=$((total + 1))
local target_user target_ip
target_user=$(echo "${SIBLINGS[$sname]}" | cut -d@ -f1)
target_ip=$(echo "${SIBLINGS[$sname]}" | cut -d@ -f2)
if ssh $SSH_OPTS "${target_user}@${target_ip}" 'true' 2>/dev/null; then
online=$((online + 1))
local temp load mem disk uptime_str throttle
temp=$(ssh $SSH_OPTS "${target_user}@${target_ip}" 'echo $(($(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0) / 1000))' 2>/dev/null)
load=$(ssh $SSH_OPTS "${target_user}@${target_ip}" 'cat /proc/loadavg | cut -d" " -f1' 2>/dev/null)
mem=$(ssh $SSH_OPTS "${target_user}@${target_ip}" 'free | awk "/Mem:/{printf \"%.0f\", \$3*100/\$2}"' 2>/dev/null)
disk=$(ssh $SSH_OPTS "${target_user}@${target_ip}" "df / | awk 'NR==2{print \$5}'" 2>/dev/null)
uptime_str=$(ssh $SSH_OPTS "${target_user}@${target_ip}" 'uptime -p 2>/dev/null | sed "s/up //"' 2>/dev/null)
throttle=$(ssh $SSH_OPTS "${target_user}@${target_ip}" 'vcgencmd get_throttled 2>/dev/null' 2>/dev/null || echo "")
body+="| $sname | ✅ online | ${temp}°C | ${load} | ${mem}% | ${disk} | ${uptime_str} |\n"
# Flag issues
[ "${temp:-0}" -gt 65 ] && issues_found+="- ⚠️ **$sname** running hot: ${temp}°C\n"
[ "${disk:-0%}" = "$(echo "$disk" | grep -oP '\d+' | head -1)" ] && {
local dpct=$(echo "$disk" | grep -oP '\d+' | head -1)
[ "${dpct:-0}" -gt 85 ] && issues_found+="- ⚠️ **$sname** disk ${disk} full\n"
}
echo "$throttle" | grep -qv "0x0" 2>/dev/null && [ -n "$throttle" ] && echo "$throttle" | grep -qv "Can't open" && \
issues_found+="- ⚠️ **$sname** undervoltage: $throttle\n"
else
body+="| $sname | ❌ offline | — | — | — | — | — |\n"
issues_found+="- 🔴 **$sname** is offline/unreachable\n"
fi
done
body+="\n### Summary\n\n"
body+="- **$online/$total** nodes online\n"
if [ -n "$issues_found" ]; then
body+="\n### Issues Detected\n\n$issues_found"
fi
body+="\n---\n*Automated fleet report by $NODE — BlackRoad Fleet Agent*\n"
# Create the issue
gh api --method POST "repos/${ORG}/${MAIN_REPO}/issues" \
-f title="Fleet Status Report — $(date '+%Y-%m-%d') [$NODE]" \
-f body="$(echo -e "$body")" \
-f "labels[]=fleet" \
-f "labels[]=report" 2>/dev/null && \
log "REPORT fleet status issue created" || \
log "REPORT failed to create issue"
}
# ═══════════════════════════════════════════════════════════════════════
# CLOSE — Auto-close resolved issues
# ═══════════════════════════════════════════════════════════════════════
cmd_close_resolved() {
log "CLOSE checking for resolved issues"
check_rate || return
# Get open fleet issues
local issues
issues=$(gh api "repos/${ORG}/${MAIN_REPO}/issues?state=open&labels=fleet&per_page=20" \
--jq '.[] | select(.pull_request == null) | "\(.number)|\(.title)"' 2>/dev/null)
[ -z "$issues" ] && return
echo "$issues" | while IFS='|' read -r num title; do
[ -z "$num" ] && continue
local title_lower=$(echo "$title" | tr '[:upper:]' '[:lower:]')
local resolved=false
local resolution=""
# Check if "offline" issues are resolved
if echo "$title_lower" | grep -qiE "offline|down|unreachable"; then
# Extract node name from title
for sname in alice cecilia octavia aria lucidia; do
if echo "$title_lower" | grep -qi "$sname"; then
local target_user target_ip
target_user=$(echo "${SIBLINGS[$sname]}" | cut -d@ -f1)
target_ip=$(echo "${SIBLINGS[$sname]}" | cut -d@ -f2)
if ssh $SSH_OPTS "${target_user}@${target_ip}" 'true' 2>/dev/null; then
resolved=true
resolution="$sname is back online and responding to SSH."
fi
fi
done
fi
if [ "$resolved" = true ]; then
# Comment and close
gh api --method POST "repos/${ORG}/${MAIN_REPO}/issues/${num}/comments" \
-f body="**[$NODE] Auto-Resolved**
$resolution
Verified by fleet agent at $(date -u '+%Y-%m-%d %H:%M UTC').
---
*Automated by $NODE — BlackRoad Fleet Agent*" 2>/dev/null
gh api --method PATCH "repos/${ORG}/${MAIN_REPO}/issues/${num}" \
-f state="closed" \
-f state_reason="completed" 2>/dev/null && \
log "CLOSE resolved #$num: $resolution" || true
fi
done
}
# ═══════════════════════════════════════════════════════════════════════
# SCAN-REPOS — Check health across all org repos
# ═══════════════════════════════════════════════════════════════════════
cmd_scan_repos() {
log "SCAN checking health of all repos"
check_rate || return
local repos
repos=$(gh api "users/${ORG}/repos?per_page=100&sort=updated" \
--jq '.[] | select(.archived == false) | "\(.name)|\(.default_branch)|\(.open_issues_count)|\(.pushed_at)"' 2>/dev/null)
local stale_repos="" issues_repos=""
echo "$repos" | while IFS='|' read -r name branch issues pushed; do
[ -z "$name" ] && continue
# Check for repos not pushed in 90 days
local pushed_epoch now_epoch
pushed_epoch=$(date -d "$pushed" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$pushed" +%s 2>/dev/null || echo 0)
now_epoch=$(date +%s)
local days_since=$(( (now_epoch - pushed_epoch) / 86400 ))
if [ "$days_since" -gt 90 ]; then
log "SCAN stale repo: $name (${days_since}d since last push)"
fi
if [ "$issues" -gt 5 ]; then
log "SCAN repo with many issues: $name ($issues open)"
fi
done
}
# ═══════════════════════════════════════════════════════════════════════
# FULL CYCLE
# ═══════════════════════════════════════════════════════════════════════
cmd_cycle() {
log "=== GitHub agent cycle started ==="
cmd_triage
cmd_respond
cmd_watch
cmd_close_resolved
log "=== GitHub agent cycle complete ==="
}
# ═══════════════════════════════════════════════════════════════════════
# HELP
# ═══════════════════════════════════════════════════════════════════════
cmd_help() {
echo ""
echo " br-github — $NODE's GitHub Agent"
echo " ─────────────────────────────────"
echo ""
echo " cycle Full GitHub cycle (triage + respond + watch + close)"
echo " triage Auto-label unlabeled issues"
echo " respond Comment on issues with fleet data"
echo " watch Monitor PRs and stale issues"
echo " report Create fleet status report issue"
echo " close Auto-close resolved fleet issues"
echo " scan-repos Health check across all org repos"
echo " help This help"
echo ""
}
# ═══════════════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════════════
ACTION="${1:-help}"
case "$ACTION" in
cycle) cmd_cycle ;;
triage) cmd_triage ;;
respond) cmd_respond ;;
watch) cmd_watch ;;
report) cmd_report ;;
close) cmd_close_resolved ;;
scan-repos) cmd_scan_repos ;;
help|--help) cmd_help ;;
*)
echo "Unknown: $ACTION"
cmd_help
exit 1
;;
esac