#!/bin/bash # ============================================================================ # Fleet Dispatch — Shell wrapper for Emacs Control Plane operations # ============================================================================ # # Usage: # scripts/fleet_dispatch.sh append "Message text" # scripts/fleet_dispatch.sh poll [agent_name] # scripts/fleet_dispatch.sh claim TASK_ID agent_name # scripts/fleet_dispatch.sh complete TASK_ID "Result text" # scripts/fleet_dispatch.sh status # # Environment: # FLEET_DAEMON_HOST — Bezalel host (default: 159.203.146.185) # FLEET_DAEMON_USER — SSH user (default: root) # FLEET_DAEMON_SOCKET — Emacs socket path (default: /root/.emacs.d/server/bezalel) # FLEET_DISPATCH_FILE — Path to dispatch.org on remote (default: /srv/fleet/workspace/dispatch.org) # ============================================================================ set -euo pipefail # ── Configuration ────────────────────────────────────────────────────────── FLEET_DAEMON_HOST="${FLEET_DAEMON_HOST:-159.203.146.185}" FLEET_DAEMON_USER="${FLEET_DAEMON_USER:-root}" FLEET_DAEMON_SOCKET="${FLEET_DAEMON_SOCKET:-/root/.emacs.d/server/bezalel}" FLEET_DISPATCH_FILE="${FLEET_DISPATCH_FILE:-/srv/fleet/workspace/dispatch.org}" # Colors GREEN='\033[0;32m' CYAN='\033[0;36m' YELLOW='\033[0;33m' RED='\033[0;31m' NC='\033[0m' # ── Helper: Run emacsclient command on Bezalel ───────────────────────────── run_emacs() { local elisp="$1" ssh "${FLEET_DAEMON_USER}@${FLEET_DAEMON_HOST}" \ "emacsclient -s ${FLEET_DAEMON_SOCKET} -e '${elisp}'" 2>/dev/null } # ── Helper: Read dispatch.org via SSH ────────────────────────────────────── read_dispatch() { ssh "${FLEET_DAEMON_USER}@${FLEET_DAEMON_HOST}" \ "cat ${FLEET_DISPATCH_FILE}" 2>/dev/null } # ── Helper: Write dispatch.org via SSH ───────────────────────────────────── write_dispatch() { ssh "${FLEET_DAEMON_USER}@${FLEET_DAEMON_HOST}" \ "cat > ${FLEET_DISPATCH_FILE}" 2>/dev/null } # ── Commands ─────────────────────────────────────────────────────────────── cmd_append() { local message="${1:?Usage: fleet_dispatch.sh append \"message\"}" local timestamp timestamp=$(date -u +"%Y-%m-%d %H:%M:%S UTC") echo -e "${CYAN}Appending to fleet log...${NC}" # Use the fleet-append wrapper on Bezalel if available, otherwise emacsclient if ssh "${FLEET_DAEMON_USER}@${FLEET_DAEMON_HOST}" "which fleet-append" &>/dev/null; then ssh "${FLEET_DAEMON_USER}@${FLEET_DAEMON_HOST}" \ "fleet-append '${timestamp} — ${message}'" else run_emacs "(with-current-buffer (find-file-noselect \"${FLEET_DISPATCH_FILE}\") (goto-char (point-max)) (insert \"\\n- ${timestamp} — ${message}\") (save-buffer))" fi echo -e "${GREEN}✓ Appended: ${message}${NC}" } cmd_poll() { local agent="${1:-}" echo -e "${CYAN}Polling dispatch.org for tasks...${NC}" local content content=$(read_dispatch) if [ -z "$content" ]; then echo -e "${RED}Could not read dispatch.org${NC}" return 1 fi # Filter TODO items, optionally by agent echo -e "${YELLOW}=== Pending Tasks ===${NC}" if [ -n "$agent" ]; then echo "$content" | grep -E "^\*\* TODO \[${agent}\]" || echo " No tasks for ${agent}" else echo "$content" | grep -E "^\*\* TODO " || echo " No pending tasks" fi } cmd_claim() { local task_id="${1:?Usage: fleet_dispatch.sh claim TASK_ID agent}" local agent="${2:?Usage: fleet_dispatch.sh claim TASK_ID agent}" local timestamp timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo -e "${CYAN}Claiming task #${task_id} for ${agent}...${NC}" # Use emacsclient to update the Org heading run_emacs "(progn (with-current-buffer (find-file-noselect \"${FLEET_DISPATCH_FILE}\") (org-mode) (goto-char (point-min)) (if (re-search-forward (format \"^\\\\*\\\\* TODO.*#%s\" \"${task_id}\") nil t) (progn (org-todo \"IN_PROGRESS\") (org-set-property \"STARTED\" \"${timestamp}\") (save-buffer) (message \"Task %s claimed\" \"${task_id}\")) (message \"Task %s not found\" \"${task_id}\"))))" echo -e "${GREEN}✓ Task #${task_id} claimed by ${agent}${NC}" } cmd_complete() { local task_id="${1:?Usage: fleet_dispatch.sh complete TASK_ID \"result\"}" local result="${2:-Completed}" local timestamp timestamp=$(date -u +"%Y-%m-%d %H:%M") echo -e "${CYAN}Completing task #${task_id}...${NC}" run_emacs "(progn (with-current-buffer (find-file-noselect \"${FLEET_DISPATCH_FILE}\") (org-mode) (goto-char (point-min)) (if (re-search-forward (format \"^\\\\*\\\\* IN_PROGRESS.*#%s\" \"${task_id}\") nil t) (progn (org-todo \"DONE\") (org-set-property \"RESULT\" \"${result}\") (org-add-planning-info 'closed (org-current-effective-time)) (save-buffer) (message \"Task %s completed\" \"${task_id}\")) (message \"Task %s not found\" \"${task_id}\"))))" echo -e "${GREEN}✓ Task #${task_id} completed: ${result}${NC}" } cmd_status() { echo -e "${CYAN}Fleet Control Plane Status${NC}" echo -e " Host: ${FLEET_DAEMON_HOST}" echo -e " Socket: ${FLEET_DAEMON_SOCKET}" echo -e " Dispatch: ${FLEET_DISPATCH_FILE}" echo "" # Test connectivity if ssh -o ConnectTimeout=5 "${FLEET_DAEMON_USER}@${FLEET_DAEMON_HOST}" "echo ok" &>/dev/null; then echo -e " SSH: ${GREEN}✓ reachable${NC}" else echo -e " SSH: ${RED}✗ unreachable${NC}" return 1 fi # Test emacs daemon local daemon_status daemon_status=$(run_emacs "(if (server-running-p) \"running\" \"stopped\")" 2>/dev/null || echo "error") if [ "$daemon_status" = "\"running\"" ]; then echo -e " Daemon: ${GREEN}✓ running${NC}" else echo -e " Daemon: ${RED}✗ ${daemon_status}${NC}" fi # Count tasks local content content=$(read_dispatch 2>/dev/null || echo "") if [ -n "$content" ]; then local todo_count in_progress_count done_count todo_count=$(echo "$content" | grep -c "^\*\* TODO " || echo 0) in_progress_count=$(echo "$content" | grep -c "^\*\* IN_PROGRESS " || echo 0) done_count=$(echo "$content" | grep -c "^\*\* DONE " || echo 0) echo -e " Tasks: ${YELLOW}${todo_count} TODO${NC}, ${CYAN}${in_progress_count} IN_PROGRESS${NC}, ${GREEN}${done_count} DONE${NC}" fi } # ── Main ─────────────────────────────────────────────────────────────────── case "${1:-help}" in append|log) shift cmd_append "$@" ;; poll|check) shift cmd_poll "$@" ;; claim) shift cmd_claim "$@" ;; complete|done) shift cmd_complete "$@" ;; status) cmd_status ;; help|--help|-h) echo "Fleet Dispatch — Emacs Control Plane wrapper" echo "" echo "Usage:" echo " $0 append \"message\" Append to fleet log" echo " $0 poll [agent] Check for pending tasks" echo " $0 claim TASK_ID agent Claim a task" echo " $0 complete TASK_ID \"result\" Mark task complete" echo " $0 status Show control plane status" echo "" echo "Environment:" echo " FLEET_DAEMON_HOST Bezalel host (default: 159.203.146.185)" echo " FLEET_DAEMON_USER SSH user (default: root)" echo " FLEET_DAEMON_SOCKET Emacs socket (default: /root/.emacs.d/server/bezalel)" ;; *) echo -e "${RED}Unknown command: $1${NC}" echo "Run '$0 help' for usage." exit 1 ;; esac