"""Wait until something in a Harbor project needs the coordinator, then print what and exit. py -3.12 -B tools/watch.py [--project open-seas] [--as Claude] [--minutes 40] [--every 60] It exits (status 0) as soon as any of these appears after it started: a review item submitted or resubmitted, an item approved (ready to land), a new message to the coordinator that is not an automatic notice, an agent resting under autopilot, or a stop changing. It also exits after --minutes with a short status line, so a caller that re-runs it gets a regular check. Read-only: it never writes to Harbor. """ import argparse import json import sqlite3 import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from harbor import projects # noqa: E402 def snapshot(conn, me): q = lambda sql, *a: conn.execute(sql, a).fetchall() return { "events": q("SELECT id, item_id, to_state, agent FROM item_events ORDER BY id DESC LIMIT 1"), "messages": q("SELECT m.id FROM messages m JOIN deliveries d ON d.message_id=m.id WHERE d.agent=? AND " "m.origin != 'system' ORDER BY m.id DESC LIMIT 1", me), "flags": q("SELECT key, value, set_at FROM flags ORDER BY key"), "resting": q("SELECT key FROM meta WHERE key LIKE 'autopilot:%' AND value LIKE '%\"resting\": true%'"), } def stalled(conn, project, me, minutes): """Running AI agents (not stopped, not me) whose last tool call is older than minutes: their session ended and nothing restarted it (an app whose stop hook does not fire, or a turn that ended on its own).""" if minutes <= 0: return [] import datetime cutoff = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=minutes)).strftime( "%Y-%m-%dT%H:%M:%SZ") stopped = {k for (k,) in conn.execute("SELECT key FROM flags WHERE value='on'")} out = [] for spec in project.agents: name = spec["name"] if spec.get("kind") == "human" or name == me or "stop:all" in stopped or f"stop:{name}" in stopped: continue last = conn.execute("SELECT MAX(at) FROM activity_log WHERE agent=?", (name,)).fetchone()[0] if last and last < cutoff: out.append((name, last)) return out def main(): ap = argparse.ArgumentParser() ap.add_argument("--project") ap.add_argument("--as", dest="me", default="Claude") ap.add_argument("--minutes", type=float, default=40) ap.add_argument("--every", type=float, default=60) ap.add_argument("--stall", type=float, default=15, help="minutes without a tool call before a running agent " "counts as stalled (0: off)") args = ap.parse_args() project = projects.resolve(args.project) conn = sqlite3.connect(f"file:{project.db}?mode=ro", uri=True, timeout=30) start = snapshot(conn, args.me) last_event = start["events"][0][0] if start["events"] else 0 last_msg = start["messages"][0][0] if start["messages"] else 0 deadline = time.time() + args.minutes * 60 while time.time() < deadline: time.sleep(args.every) now = snapshot(conn, args.me) found = [] for eid, item, state, agent in conn.execute( "SELECT id, item_id, to_state, agent FROM item_events WHERE id > ? ORDER BY id", (last_event,)): if state in ("submitted", "approved", "changes_requested") and agent != args.me: # not my own moves title = conn.execute("SELECT title FROM items WHERE id=?", (item,)).fetchone()[0] found.append(f"item #{item} {state} by {agent}: {title}") for mid, sender, body in conn.execute( "SELECT m.id, m.sender, m.body FROM messages m JOIN deliveries d ON d.message_id=m.id WHERE d.agent=? " "AND m.id > ? AND m.origin != 'system' ORDER BY m.id", (args.me, last_msg)): found.append(f"message #{mid} from {sender}: {body.splitlines()[0][:140]}") if now["flags"] != start["flags"]: found.append("stop flags changed") if now["resting"] != start["resting"] and now["resting"]: found.append("resting under autopilot: " + ", ".join(k.split(":", 1)[1] for (k,) in now["resting"])) for agent, last in stalled(conn, project, args.me, args.stall): found.append(f"stalled: {agent} has made no tool call since {last[11:16]}Z (not stopped) - wake it") if found: print("\n".join(found)) return 0 open_items = conn.execute("SELECT COUNT(*) FROM items WHERE state IN ('submitted','in_review','approved')").fetchone()[0] tasks = conn.execute("SELECT state, COUNT(*) FROM tasks GROUP BY state").fetchall() last = conn.execute("SELECT agent, MAX(at) FROM activity_log GROUP BY agent").fetchall() print(f"quiet for {args.minutes:g} min; open review items {open_items}; tasks {dict(tasks)}; last tool call " + ", ".join(f"{a} {t[11:16]}Z" for a, t in last)) return 0 if __name__ == "__main__": sys.exit(main())