app
Adom Agent Watch
Public Made by Adomby adom
A background cron that pulls your unresolved Adom Wiki notifications and wakes your AI to resolve them — the last mile that turns the notifications feed into your agent doing the follow-up.
← Commit history
Publish 0.1.0
6 files changed
+290
SKILL.mdadded+42@@ -0,0 +1,42 @@+---+name: adom-agent-watch+description: A background cron that pulls your unresolved Adom Wiki notifications and wakes your AI to resolve them. Use when the user wants their agent to auto-follow-up on wiki issue updates (closes, replies) in the background, or asks to "watch my notifications", "wake my agent on updates", or "resolve wiki updates automatically".+---++# adom-agent-watch++The last mile for the notifications feed (adom/wiki #58): pull YOUR unresolved+wiki notifications and wake your agent to resolve them — verify a shipped fix,+reply to a review, resume related work — then mark them handled.++## Use it++```bash+adompkg install adom-agent-watch # PATH binary + cron (every 30 min)+adom-agent-watch status # source, wake cmd, watched pages+adom-agent-watch run # one poll cycle now+adom-agent-watch test-wake # confirm the wake command fires+```++## How it resolves updates++Each cycle it reads your unresolved notifications from the best source available+and, on anything new, wakes your agent with a digest:++- **feed** — `adom-wiki notifications list --unread --json` (the #58 inbox), else+- **interim** — polls issues you authored on the watched pages (default+ `adom/wiki`) and diffs status/reply-count vs a local snapshot.++The wake command (default `claude -p` when `claude` is on PATH; configurable in+`~/.config/adom-agent-watch/config.json`) is invoked with the digest + an+instruction to open each item, do the follow-up, and mark it handled+(`adom-wiki notifications read <id>`). It writes the digest to+`~/.local/state/adom-agent-watch/inbox.md` regardless, so nothing is lost if the+wake command isn't set.++## When you (the agent) are woken by it++You'll get a prompt listing unresolved wiki updates. For each: open it+(`adom-wiki issue view` / the page), take the follow-up (verify a shipped fix on+the live page, reply, or resume the related work), then mark it handled. If+nothing is actionable, say why. Be concise.
bin/adom-agent-watchadded+201@@ -0,0 +1,201 @@+#!/usr/bin/env python3+"""adom-agent-watch — pull YOUR unresolved Adom Wiki notifications and wake your+agent to resolve them in the background. One poll cycle; run it from cron.++Sources, best available first (degrades gracefully, so it works today):+ 1. the #58 notifications feed: adom-wiki notifications list --unread --json+ 2. interim fallback: poll issues you authored on watched pages,+ diff each poll vs a local snapshot.++On new items it writes a digest to ~/.local/state/adom-agent-watch/inbox.md and+invokes your wake command (default: `claude -p <prompt>` if `claude` is on PATH),+so your agent opens the issues, does the follow-up, and marks them handled.++Config (optional): ~/.config/adom-agent-watch/config.json+ { "wake_cmd": "claude -p", "pages": ["adom/wiki"], "user": "<handle>" }++Subcommands: run (default) | status | test-wake+"""+import os, sys, json, subprocess, pathlib++HOME = pathlib.Path.home()+STATE_DIR = HOME / ".local/state/adom-agent-watch"+STATE = STATE_DIR / "state.json"+INBOX = STATE_DIR / "inbox.md"+CONFIG = HOME / ".config/adom-agent-watch/config.json"+++def cfg():+ try:+ return json.loads(CONFIG.read_text())+ except Exception:+ return {}+++def sh(args, timeout=60):+ try:+ return subprocess.run(args, capture_output=True, text=True, timeout=timeout)+ except Exception:+ return None+++def load_state():+ try:+ return json.loads(STATE.read_text())+ except Exception:+ return {"seen": {}}+++def save_state(s):+ STATE_DIR.mkdir(parents=True, exist_ok=True)+ STATE.write_text(json.dumps(s))+++def have(cmd):+ r = sh(["bash", "-lc", f"command -v {cmd}"])+ return bool(r and r.returncode == 0 and r.stdout.strip())+++# ---- source 1: the #58 notifications feed ---------------------------------+def from_feed():+ """Returns a list of unread items, or None if the endpoint isn't available."""+ r = sh(["adom-wiki", "notifications", "list", "--unread", "--json"])+ if not r or r.returncode != 0:+ return None+ try:+ d = json.loads(r.stdout)+ except Exception:+ return None+ if d.get("status") == "unavailable": # verb wired, endpoint not deployed yet+ return None+ items = (d.get("data") or {}).get("notifications")+ if items is None:+ return None+ return [{"id": i.get("id"), "ref": i.get("ref"), "title": i.get("title"),+ "actor": i.get("actor"), "type": i.get("type")} for i in items]+++# ---- source 2: interim direct-issue poll ----------------------------------+def whoami():+ r = sh(["adom-wiki", "whoami", "--json"])+ if not r or r.returncode != 0:+ return None+ try:+ u = (json.loads(r.stdout).get("data") or {})+ return (u.get("user") or u).get("username") or u.get("name")+ except Exception:+ return None+++def from_interim(pages, me, state):+ seen = state.setdefault("seen", {})+ updates = []+ for page in pages:+ r = sh(["adom-wiki", "issue", "list", page, "--json"])+ if not r or r.returncode != 0:+ continue+ try:+ data = json.loads(r.stdout).get("data", [])+ except Exception:+ continue+ rows = data if isinstance(data, list) else data.get("discussions", [])+ for it in rows:+ if me and (it.get("author_name") or "").lower() != me.lower():+ continue+ key = f"{page}#{it.get('number')}"+ cur = {"status": it.get("status"), "replies": it.get("reply_count")}+ prev = seen.get(key)+ seen[key] = cur+ if prev is None: # first sight: record, don't alert+ continue+ if prev != cur:+ what = []+ if prev.get("status") != cur.get("status"):+ what.append(str(cur.get("status")))+ if (cur.get("replies") or 0) > (prev.get("replies") or 0):+ what.append("new reply")+ updates.append({"id": None, "ref": key, "title": it.get("title"),+ "actor": None, "type": ", ".join(what) or "updated"})+ return updates+++def digest_of(updates, mode):+ lines = []+ for u in updates:+ line = f"- [{u.get('type', 'update')}] {u.get('ref')} — {u.get('title') or ''}"+ if u.get("actor"):+ line += f" (by {u['actor']})"+ lines.append(line)+ return f"You have {len(updates)} unresolved Adom Wiki update(s) [{mode}]:\n" + "\n".join(lines)+++def wake(prompt, c):+ cmd = c.get("wake_cmd")+ if cmd is None:+ cmd = "claude -p" if have("claude") else None+ if not cmd:+ return None+ try:+ subprocess.Popen(cmd.split() + [prompt],+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,+ stdin=subprocess.DEVNULL, start_new_session=True)+ return cmd+ except Exception:+ return None+++def cmd_run():+ c = cfg()+ state = load_state()+ feed = from_feed()+ if feed is not None:+ # feed items are already the UNREAD set; the server's read-state is the dedup.+ updates, mode, feed_ids = feed, "feed", [u["id"] for u in feed if u.get("id")]+ else:+ me = c.get("user") or whoami()+ updates, mode, feed_ids = from_interim(c.get("pages") or ["adom/wiki"], me, state), "interim", []+ save_state(state)+ if not updates:+ return++ d = digest_of(updates, mode)+ STATE_DIR.mkdir(parents=True, exist_ok=True)+ INBOX.write_text(d + "\n")+ prompt = (d + "\n\nFor each: open it (`adom-wiki issue view` / the page), do the follow-up "+ "(verify a shipped fix on the live page, reply, or resume the related work), then "+ "mark it handled (`adom-wiki notifications read <id>` when available). If nothing is "+ "actionable, note why. Be concise.")+ used = wake(prompt, c)+ if used and feed_ids:+ # Woke the agent — clear these from the server inbox so they don't re-fire.+ sh(["adom-wiki", "notifications", "read"] + [str(i) for i in feed_ids])+ if used:+ print(f"woke agent on {len(updates)} update(s) via `{used}`")+ else:+ print(f"{len(updates)} update(s) written to {INBOX} "+ f"(set wake_cmd in {CONFIG} or install `claude` to auto-resolve)")+++def cmd_status():+ c = cfg()+ print(f"config: {CONFIG} {'(present)' if CONFIG.exists() else '(defaults)'}")+ print(f"wake: {c.get('wake_cmd') or ('claude -p' if have('claude') else 'NONE — set wake_cmd')}")+ print(f"source: {'feed (#58)' if from_feed() is not None else 'interim poll (feed not live yet)'}")+ print(f"pages: {c.get('pages') or ['adom/wiki']}")+ print(f"state: {STATE} {'(present)' if STATE.exists() else '(empty)'}")+ if INBOX.exists():+ print("last inbox:\n" + INBOX.read_text())+++def cmd_test_wake():+ used = wake("adom-agent-watch test wake — you can ignore this. Reply 'ok'.", cfg())+ print(f"wake via `{used}`" if used else "no wake command (set wake_cmd or install claude)")+++def main():+ cmd = sys.argv[1] if len(sys.argv) > 1 else "run"+ {"run": cmd_run, "status": cmd_status, "test-wake": cmd_test_wake}.get(cmd, cmd_run)()+++if __name__ == "__main__":+ main()
install.shadded+22@@ -0,0 +1,22 @@+#!/usr/bin/env bash+# Install adom-agent-watch: put the poller on PATH + a cron that runs it every+# 30 min. No secrets. Idempotent.+set -uo pipefail+DIR="$(cd "$(dirname "$0")" && pwd)"+mkdir -p "$HOME/.local/bin" "$HOME/.config/adom-agent-watch"+install -m0755 "$DIR/bin/adom-agent-watch" "$HOME/.local/bin/adom-agent-watch"+[ -f "$HOME/.config/adom-agent-watch/config.json" ] || cat > "$HOME/.config/adom-agent-watch/config.json" <<JSON+{+ "_comment": "wake_cmd runs your agent with the digest appended; pages is the interim-poll watchlist (used only until the #58 feed is live).",+ "wake_cmd": null,+ "pages": ["adom/wiki"]+}+JSON+BIN="$HOME/.local/bin/adom-agent-watch"+CRON="*/30 * * * * $BIN run >> /tmp/adom-agent-watch.log 2>&1"+EXISTING="$(crontab -l 2>/dev/null || true)"+FILTERED="$(echo "$EXISTING" | grep -v 'adom-agent-watch' || true)"+printf '%s\n%s\n' "$FILTERED" "$CRON" | sed '/^$/d' | crontab -+echo "adom-agent-watch installed (cron every 30 min)."+echo " wake command: $("$BIN" status 2>/dev/null | grep -m1 wake || echo 'set wake_cmd in ~/.config/adom-agent-watch/config.json')"+echo " try: adom-agent-watch status | adom-agent-watch run"
package.jsonadded+20@@ -0,0 +1,20 @@+{+ "slug": "adom-agent-watch",+ "version": "0.1.0",+ "type": "app",+ "description": "Pull your unresolved Adom Wiki notifications and wake your AI to resolve them in the background — the last mile that turns the notifications feed (#58) into your agent actually doing the follow-up.",+ "dependencies": {},+ "license": "MIT",+ "needs_sudo": false,+ "files": ["bin/**", "install.sh", "uninstall.sh", "package.json", "page.json", "README.md", "SKILL.md", "screenshots/**"],+ "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh" },+ "tags": ["notifications", "agent", "automation", "cron", "background", "feed", "ai", "wiki"],+ "discovery_triggers": ["agent watch", "notification agent", "background resolver", "wake my agent", "auto resolve issues", "wiki notifications agent", "watch my issues", "poll my notifications"],+ "discovery_pitch": "A background cron that pulls your unresolved wiki notifications and wakes your AI to resolve them — prefers the #58 feed, falls back to a direct issue poll, so it works today.",+ "sample_prompts": [+ {"label": "Set it up", "prompt": "install adom-agent-watch so my AI resolves wiki updates in the background"},+ {"label": "Check status", "prompt": "run adom-agent-watch status"},+ {"label": "One cycle now", "prompt": "run adom-agent-watch once and resolve anything new"}+ ],+ "hero": { "path": "screenshots/hero.png" }+}
screenshots/hero.pngadded⋯ 1 unchanged line ⋯
uninstall.shadded+5@@ -0,0 +1,5 @@+#!/usr/bin/env bash+set -uo pipefail+rm -f "$HOME/.local/bin/adom-agent-watch"+printf '%s\n' "$(crontab -l 2>/dev/null | grep -v 'adom-agent-watch' || true)" | sed '/^$/d' | crontab - 2>/dev/null || true+echo "adom-agent-watch removed (cron + binary). Config/state left at ~/.config & ~/.local/state."