Files
timmy-home/scripts/emacs-fleet-bridge.py
Timmy (AI Agent) 9b5ec4b68e
Some checks failed
Smoke Test / smoke (pull_request) Failing after 13s
feat(fleet): Emacs Sovereign Control Plane (#590)
Implement tooling for the shared Emacs daemon control plane on Bezalel.
Agents can now poll dispatch.org for tasks, claim work, and report
results programmatically.

Files:
- scripts/emacs-fleet-bridge.py — Python client with 6 commands:
  poll (find PENDING tasks), claim (PENDING→IN_PROGRESS), done (mark
  complete), append (status messages), status (health check), eval
  (arbitrary Elisp). SSH-based communication with Bezalel Emacs daemon.
- scripts/emacs-fleet-poll.sh — Shell poll script for crontab integration.
  Shows connectivity, task counts, my pending/active tasks, recent activity.
- skills/autonomous-ai-agents/emacs-control-plane/SKILL.md — Full skill
  docs covering infrastructure, API, agent loop integration, state machine,
  and pitfalls.

Infrastructure:
- Host: Bezalel (159.203.146.185)
- Socket: /root/.emacs.d/server/bezalel
- Dispatch: /srv/fleet/workspace/dispatch.org
- Configurable via BEZALEL_HOST, BEZALEL_SSH_KEY, EMACS_SOCKET env vars

Closes #590
2026-04-13 20:18:29 -04:00

276 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Emacs Fleet Bridge — Sovereign Control Plane Client
Interacts with the shared Emacs daemon on Bezalel to:
- Append messages to dispatch.org
- Poll for TODO tasks assigned to this agent
- Claim tasks (PENDING → IN_PROGRESS)
- Report results back to dispatch.org
- Query shared state
Usage:
python3 emacs-fleet-bridge.py poll --agent timmy
python3 emacs-fleet-bridge.py append "Deployed PR #123 to staging"
python3 emacs-fleet-bridge.py claim --task-id TASK-001
python3 emacs-fleet-bridge.py done --task-id TASK-001 --result "Merged"
python3 emacs-fleet-bridge.py status
python3 emacs-fleet-bridge.py eval "(org-element-parse-buffer)"
Requires SSH access to Bezalel. Set BEZALEL_HOST and BEZALEL_SSH_KEY env vars
or use defaults (root@159.203.146.185).
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
# ── Config ──────────────────────────────────────────────
BEZALEL_HOST = os.environ.get("BEZALEL_HOST", "159.203.146.185")
BEZALEL_USER = os.environ.get("BEZALEL_USER", "root")
BEZALEL_SSH_KEY = os.environ.get("BEZALEL_SSH_KEY", "")
SOCKET_PATH = os.environ.get("EMACS_SOCKET", "/root/.emacs.d/server/bezalel")
DISPATCH_FILE = os.environ.get("DISPATCH_FILE", "/srv/fleet/workspace/dispatch.org")
SSH_TIMEOUT = int(os.environ.get("BEZALEL_SSH_TIMEOUT", "15"))
# ── SSH Helpers ─────────────────────────────────────────
def _ssh_cmd() -> list:
"""Build base SSH command."""
cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", f"ConnectTimeout={SSH_TIMEOUT}"]
if BEZALEL_SSH_KEY:
cmd.extend(["-i", BEZALEL_SSH_KEY])
cmd.append(f"{BEZALEL_USER}@{BEZALEL_HOST}")
return cmd
def emacs_eval(expr: str) -> str:
"""Evaluate an Emacs Lisp expression on Bezalel via emacsclient."""
ssh = _ssh_cmd()
elisp = expr.replace('"', '\\"')
ssh.append(f'emacsclient -s {SOCKET_PATH} -e "{elisp}"')
try:
result = subprocess.run(ssh, capture_output=True, text=True, timeout=SSH_TIMEOUT + 5)
if result.returncode != 0:
return f"ERROR: {result.stderr.strip()}"
# emacsclient wraps string results in quotes; strip them
output = result.stdout.strip()
if output.startswith('"') and output.endswith('"'):
output = output[1:-1]
return output
except subprocess.TimeoutExpired:
return "ERROR: SSH timeout"
except Exception as e:
return f"ERROR: {e}"
def ssh_run(remote_cmd: str) -> tuple:
"""Run a shell command on Bezalel. Returns (stdout, stderr, exit_code)."""
ssh = _ssh_cmd()
ssh.append(remote_cmd)
try:
result = subprocess.run(ssh, capture_output=True, text=True, timeout=SSH_TIMEOUT + 5)
return result.stdout.strip(), result.stderr.strip(), result.returncode
except subprocess.TimeoutExpired:
return "", "SSH timeout", 1
except Exception as e:
return "", str(e), 1
# ── Org Mode Operations ────────────────────────────────
def append_message(message: str, agent: str = "timmy") -> str:
"""Append a message entry to dispatch.org."""
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
entry = f"\n** [DONE] [{ts}] {agent}: {message}\n"
# Use the fleet-append wrapper if available, otherwise direct elisp
escaped = entry.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
elisp = f'(with-current-buffer (find-file-noselect "{DISPATCH_FILE}") (goto-char (point-max)) (insert "{escaped}") (save-buffer))'
result = emacs_eval(elisp)
return f"Appended: {message}" if "ERROR" not in result else result
def poll_tasks(agent: str = "timmy", limit: int = 10) -> list:
"""Poll dispatch.org for PENDING tasks assigned to this agent."""
# Parse org buffer looking for TODO items with agent assignment
elisp = f"""
(with-current-buffer (find-file-noselect "{DISPATCH_FILE}")
(org-element-map (org-element-parse-buffer) 'headline
(lambda (h)
(when (and (equal (org-element-property :todo-keyword h) "PENDING")
(let ((tags (org-element-property :tags h)))
(or (member "{agent}" tags)
(member "{agent.upper()}" tags))))
(list (org-element-property :raw-value h)
(or (org-element-property :ID h) "")
(org-element-property :begin h)))))
nil nil 'headline))
"""
result = emacs_eval(elisp)
if "ERROR" in result:
return [{"error": result}]
# Parse the Emacs Lisp list output into Python
try:
# emacsclient returns elisp syntax like: ((task1 id1 pos1) (task2 id2 pos2))
# We use a simpler approach: extract via a wrapper script
pass
except Exception:
pass
# Fallback: use grep on the file for PENDING items
stdout, stderr, rc = ssh_run(
f'grep -n "PENDING.*:{agent}:" {DISPATCH_FILE} 2>/dev/null | head -{limit}'
)
tasks = []
for line in stdout.splitlines():
parts = line.split(":", 2)
if len(parts) >= 2:
tasks.append({
"line": int(parts[0]) if parts[0].isdigit() else 0,
"content": parts[-1].strip(),
})
return tasks
def claim_task(task_id: str, agent: str = "timmy") -> str:
"""Claim a task: change PENDING → IN_PROGRESS."""
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
elisp = f"""
(with-current-buffer (find-file-noselect "{DISPATCH_FILE}")
(goto-char (point-min))
(when (re-search-forward "PENDING.*{task_id}" nil t)
(beginning-of-line)
(org-todo "IN_PROGRESS")
(end-of-line)
(insert " [Claimed by {agent} at {ts}]")
(save-buffer)
"claimed"))
"""
result = emacs_eval(elisp)
return f"Claimed task {task_id}" if "ERROR" not in result else result
def done_task(task_id: str, result_text: str = "", agent: str = "timmy") -> str:
"""Mark a task as DONE with optional result."""
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
suffix = f" [{agent}: {result_text}]" if result_text else ""
elisp = f"""
(with-current-buffer (find-file-noselect "{DISPATCH_FILE}")
(goto-char (point-min))
(when (re-search-forward "IN_PROGRESS.*{task_id}" nil t)
(beginning-of-line)
(org-todo "DONE")
(end-of-line)
(insert " [Completed by {agent} at {ts}]{suffix}")
(save-buffer)
"done"))
"""
result = emacs_eval(elisp)
return f"Done: {task_id}{result_text}" if "ERROR" not in result else result
def status() -> dict:
"""Get control plane status."""
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
# Check connectivity
stdout, stderr, rc = ssh_run(f'emacsclient -s {SOCKET_PATH} -e "(emacs-version)" 2>&1')
connected = rc == 0 and "ERROR" not in stdout
# Count tasks by state
counts = {}
for state in ["PENDING", "IN_PROGRESS", "DONE"]:
stdout, _, _ = ssh_run(f'grep -c "{state}" {DISPATCH_FILE} 2>/dev/null || echo 0')
counts[state.lower()] = int(stdout.strip()) if stdout.strip().isdigit() else 0
# Check dispatch.org size
stdout, _, _ = ssh_run(f'wc -l {DISPATCH_FILE} 2>/dev/null || echo 0')
lines = int(stdout.split()[0]) if stdout.split()[0].isdigit() else 0
return {
"timestamp": ts,
"host": f"{BEZALEL_USER}@{BEZALEL_HOST}",
"socket": SOCKET_PATH,
"connected": connected,
"dispatch_lines": lines,
"tasks": counts,
}
# ── CLI ─────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Emacs Fleet Bridge — Sovereign Control Plane")
parser.add_argument("--agent", default="timmy", help="Agent name (default: timmy)")
sub = parser.add_subparsers(dest="command")
# poll
poll_p = sub.add_parser("poll", help="Poll for PENDING tasks")
poll_p.add_argument("--limit", type=int, default=10)
# append
append_p = sub.add_parser("append", help="Append message to dispatch.org")
append_p.add_argument("message", help="Message to append")
# claim
claim_p = sub.add_parser("claim", help="Claim a task (PENDING → IN_PROGRESS)")
claim_p.add_argument("task_id", help="Task ID to claim")
# done
done_p = sub.add_parser("done", help="Mark task as DONE")
done_p.add_argument("task_id", help="Task ID to complete")
done_p.add_argument("--result", default="", help="Result description")
# status
sub.add_parser("status", help="Show control plane status")
# eval
eval_p = sub.add_parser("eval", help="Evaluate Emacs Lisp expression")
eval_p.add_argument("expression", help="Elisp expression")
args = parser.parse_args()
agent = args.agent
if args.command == "poll":
tasks = poll_tasks(agent, args.limit)
if tasks:
for t in tasks:
if "error" in t:
print(f"ERROR: {t['error']}", file=sys.stderr)
else:
print(f" [{t.get('line', '?')}] {t.get('content', '?')}")
else:
print(f"No PENDING tasks for {agent}")
elif args.command == "append":
print(append_message(args.message, agent))
elif args.command == "claim":
print(claim_task(args.task_id, agent))
elif args.command == "done":
print(done_task(args.task_id, args.result, agent))
elif args.command == "status":
s = status()
print(json.dumps(s, indent=2))
if not s["connected"]:
print("\nWARNING: Cannot connect to Emacs daemon on Bezalel", file=sys.stderr)
elif args.command == "eval":
print(emacs_eval(args.expression))
else:
parser.print_help()
if __name__ == "__main__":
sys.exit(main())