Skip to content

Instantly share code, notes, and snippets.

@intellectronica
Created May 29, 2026 07:30
Show Gist options
  • Select an option

  • Save intellectronica/a8f1978a6d8d5bd0b7d2231f5e85babb to your computer and use it in GitHub Desktop.

Select an option

Save intellectronica/a8f1978a6d8d5bd0b7d2231f5e85babb to your computer and use it in GitHub Desktop.
Hermes Agent dog skill: quiet cron-backed online change watchdogs

Dog — Online Change Watchdogs

This gist contains the Hermes Agent dog skill.

Install by copying SKILL.md into a Hermes skill directory, for example:

mkdir -p ~/.hermes/skills/productivity/dog
curl -L <raw-SKILL.md-url> -o ~/.hermes/skills/productivity/dog/SKILL.md
mkdir -p ~/.hermes/skills/productivity/dog/references
curl -L <raw-statuspage-incident-api.md-url> -o ~/.hermes/skills/productivity/dog/references/statuspage-incident-api.md

The skill creates quiet cron-backed watchdogs for changing online things: status incidents, live blogs, release pages, and similar sources.

name dog
description Use when Eleanor invokes `/dog` or asks to track an online changing thing and be kept updated: service status pages, outages, live blogs, release/status dashboards, shipment/event pages, feeds, or any periodically-refreshing page/API. Establish a quiet Hermes cron subscription that detects meaningful changes, notifies only on changes/new items, and stops/cleans up when the watched situation is over.
version 1.0.0
author Fnord
license MIT
metadata
hermes
tags related_skills
monitoring
cron
watchdog
slash-command
status
live-updates
hermes-agent
hermes-runtime-operations
snooze
tavily
agent-browser

Dog — Online Change Watchdogs

Use this skill to turn /dog ... into a short-lived, quiet subscription to an online stream of changes.

The user intent is: “watch this thing for me, tell me when something changes, and stop when it is done.”

Command shape

/dog <what to track> [optional hints]

Examples:

/dog GitHub Actions status until resolved
/dog this live blog for new updates: https://example.com/live/...
/dog https://status.openai.com/ every 1m until the API outage is resolved
/dog this package release page and tell me when 2.4.0 is available

Core behaviour

  1. Identify the target, the meaningful change signal, and the terminal condition.
  2. Prefer machine-readable endpoints over page scraping.
  3. Pick a refresh cadence. Default to 3m, but adjust when the source or situation warrants it.
  4. Create a persisted Hermes cron job that is silent when nothing worth reporting changed.
  5. Deliver updates to discord by default unless Eleanor explicitly names another destination.
  6. Stop and clean up when the terminal condition is reached.
  7. Confirm what was set up: target, signal, cadence, delivery, stopping condition, job name/id.

Triage before creating anything

Parse the request and determine:

  • Target: URL, service/status page, live blog, API endpoint, release page, social feed, etc.
  • Scope: whole page/service or a named component/incident/item?
  • Signal: status transition, latest incident update, new live-blog entry, new release/version, changed text block, changed JSON field.
  • Terminal condition: incident resolved, component operational, live blog closed/no longer updating, desired release appears, event ended, user-specified stop condition.
  • Cadence: choose from the source and urgency:
    • 1m for active outages, fast live blogs, incident bridges, or explicit urgent requests.
    • 3m default for most status pages and live streams.
    • 5m15m for slower pages, release pages, shipment tracking, or sites with rate limits.
    • Never poll aggressively if robots/rate-limit signals or source etiquette suggest otherwise.
  • Lifetime guard: set finite repeats unless the user explicitly asks for permanent monitoring. For example: repeat=240 at 3m is roughly 12 hours; repeat=1440 at 1m is one day. Choose a guard appropriate to the situation and mention it.

Ask a clarifying question only if missing information changes the monitoring implementation in a material way and cannot be inferred or looked up. Otherwise act.

Implementation strategy

Prefer script-only no_agent cron jobs

For /dog, prefer a deterministic no-agent cron job:

  • no_agent=True
  • script under ~/.hermes/scripts/dog-<slug>.py or .sh
  • state/config under ~/.hermes/state/dog-<slug>.json
  • stdout empty when unchanged
  • concise stdout when changed or done
  • non-zero exit only for real monitor breakage

This shape is important because no-agent cron jobs with empty stdout are silent. Agent-backed cron jobs tend to produce a final response every run, which is wrong for a quiet subscription.

Use an agent-backed cron only when change detection genuinely requires reasoning that cannot be encoded in a small script. If you do, make the prompt explicitly require no response unless there is a meaningful change, but treat it as a fallback, not the default.

Machine-readable sources first

Before scraping, look for:

  • Statuspage APIs:
    • /api/v2/status.json
    • /api/v2/components.json
    • /api/v2/incidents/unresolved.json
  • RSS/Atom/JSON feeds.
  • Vendor REST endpoints or release APIs.
  • Embedded JSON (application/ld+json, __NEXT_DATA__, etc.).
  • GitHub releases/tags API for GitHub release watches.

For web pages with dynamic rendering, use browser tools or agent-browser to inspect the page once, then make the cron script fetch the stable underlying endpoint if possible.

State and signatures

Store compact state in JSON. Compare signatures, not raw pages.

Good signature fields:

  • Status pages: component status, overall status, unresolved incident IDs, incident status/impact/updated_at, latest incident update ID/body/status/time.
  • Live blogs: newest item IDs/timestamps/headlines; keep a bounded set of seen IDs.
  • Releases: latest version/tag, release timestamp, URL.
  • Generic pages: extracted watched text block hash plus a short rendered/extracted summary.

Avoid signatures from unstable fields such as tracking parameters, ad markup, random IDs, page build hashes, or relative “3 minutes ago” strings.

Notifications

Print one compact message when something changes. Include:

  • What changed.
  • Current status/new item(s).
  • Source URL.
  • Timestamp in Europe/Zurich as YYYY-MM-DD HH:mm when available or when reporting the check time.
  • Whether monitoring continues or has stopped.

Do not print routine “checked; unchanged” messages.

Cleanup when done

A /dog job should clean up after itself when the terminal condition is reached.

Recommended pattern:

  1. Create a config/state JSON file that includes job_id after the cron job is created.
  2. The monitor script reads that file.
  3. When the terminal condition is reached, the script prints the final update, then disables/removes its own cron job if job_id is present.
  4. Keep the final state file for audit/debugging; do not delete logs/state unless Eleanor asks. “Clean up” means stop the recurring cron job and avoid future runs.

If self-removal fails, the script should still print the final update plus a concise cleanup warning. Then you should fix/remove the cron job manually if you are present in the initial setup flow.

Self-removal may be done with the cron tool during setup if the job is already known to be done, or from the script with the Hermes CLI, for example:

subprocess.run(["hermes", "cron", "remove", job_id], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

Prefer remove for completed /dog subscriptions. Use pause only if retaining the job definition is useful.

Creating the cron job

Use cronjob(action='create') with:

  • name: dog-<short-slug> using lowercase hyphens.
  • schedule: chosen cadence, normally 3m. For recurring short intervals, say every 1m / every 3m; bare 1m can create a one-shot “once in 1m” job.
  • repeat: finite guard unless permanent monitoring was requested.
  • deliver: discord unless Eleanor explicitly requests another target.
  • script: relative script name under ~/.hermes/scripts/, e.g. dog-github-actions-status.py.
  • no_agent: True.
  • prompt: ignored for no-agent jobs, but still put a short self-contained description for human audit.

After cronjob(action='create') returns the job id, update the script config/state with that job_id so the script can remove its own job when complete.

Script skeleton

Use Python for most monitors; it is portable enough and easy to JSON-parse.

#!/usr/bin/env python3
import datetime as dt
import json
import os
import subprocess
import sys
import tempfile
import urllib.request
from pathlib import Path
from zoneinfo import ZoneInfo

SLUG = "dog-example"
STATE_PATH = Path.home() / ".hermes" / "state" / f"{SLUG}.json"
USER_AGENT = "Hermes-Dog/1.0 (+https://hermes-agent.nousresearch.com)"
TZ = ZoneInfo("Europe/Zurich")


def now_local():
    return dt.datetime.now(TZ).strftime("%Y-%m-%d %H:%M")


def load_state():
    try:
        return json.loads(STATE_PATH.read_text())
    except FileNotFoundError:
        return {}


def save_state(state):
    STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp = tempfile.mkstemp(prefix=STATE_PATH.name, dir=str(STATE_PATH.parent))
    with os.fdopen(fd, "w") as f:
        json.dump(state, f, indent=2, sort_keys=True)
        f.write("\n")
    os.replace(tmp, STATE_PATH)


def fetch_json(url):
    req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
    with urllib.request.urlopen(req, timeout=20) as r:
        return json.loads(r.read().decode("utf-8"))


def maybe_remove_self(state):
    job_id = state.get("job_id")
    if job_id:
        subprocess.run(["hermes", "cron", "remove", job_id], check=False,
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def main():
    state = load_state()

    # TODO: fetch source, extract meaningful data, compute signature and done flag.
    signature = "..."
    done = False
    message = None

    if state.get("signature") != signature:
        message = f"Dog update ({now_local()}): ...\nSource: ..."

    state["signature"] = signature
    state["last_checked_at"] = now_local()
    save_state(state)

    if message:
        if done:
            print(message + "\nMonitoring complete; stopping this dog job.")
            maybe_remove_self(state)
        else:
            print(message)


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(f"Dog monitor failed: {e}", file=sys.stderr)
        raise

Adapt the skeleton instead of overfitting it into a generic framework.

Verification during setup

Before reporting success:

  1. Syntax-check the script:
    • Python: python3 -m py_compile ~/.hermes/scripts/dog-<slug>.py
    • Shell: bash -n ~/.hermes/scripts/dog-<slug>.sh
  2. Run the script once manually if safe. It may emit an initial baseline notification; that is acceptable if useful. If baseline spam is undesirable, seed state before creating the job.
  3. Create the cron job.
  4. Update state/config with the returned job_id.
  5. Call cronjob(action='list') and verify name, schedule, delivery, no_agent, script, enabled status, and next run.
  6. If practical, trigger one scheduled run or wait past a scheduler tick and verify last_status=ok. Do not wait excessively for slow/lower-priority monitors.
  7. Tell Eleanor exactly what is now being watched and when it will stop.

Bare /dog or management requests

If Eleanor sends bare /dog with no target, list active dog jobs:

  1. Call cronjob(action='list').
  2. Filter names beginning with dog- or prompts containing /dog/Dog monitor.
  3. Show active job name/id, target summary if visible, cadence, next run, and last status.
  4. If none exist, say there are no active dog subscriptions.

If Eleanor asks to stop a dog subscription:

  1. List jobs first; never guess job ids.
  2. Remove the matching job with cronjob(action='remove').
  3. Leave the state file unless she asks to delete it.
  4. Confirm the removed job.

Common patterns

Statuspage component/incident

Use the Statuspage API endpoints when present. Watch both component status and unresolved incidents affecting or mentioning the component. Stop when the component is operational and no relevant unresolved incident remains.

For a specific Statuspage incident URL (/incidents/<id>), prefer /api/v2/incidents/<id>.json, seed the current incident signature before scheduling, and compare status, impact, resolved_at, updated_at, and the latest incident_updates[0] fields. See references/statuspage-incident-api.md for the reusable pattern and schedule pitfall.

Live blog

Extract stable item identifiers. Prefer embedded JSON or RSS. If scraping HTML, use IDs, canonical timestamps, or headline+timestamp hashes. Notify with only new items since the last run, newest first or chronological depending on readability. Stop when the page marks the live blog as closed/ended, or after a conservative inactivity guard if the event is over.

Release/version watch

Use GitHub/GitLab/package-registry APIs where possible. Stop when the requested version/tag appears. For “latest release changed” watches, continue until the finite repeat guard expires unless the user names a stop condition.

Generic page block

Extract a stable content block and compare its normalised text hash. Notify with a short excerpt and URL. Avoid reporting cosmetic changes.

Common pitfalls

  1. Creating an agent cron that speaks every run. Use no-agent with empty stdout for unchanged state.
  2. Polling rendered HTML when an API exists. Find the API first.
  3. Forgetting delivery. Set deliver='discord' by default for Eleanor’s proactive updates.
  4. No terminal condition. If none is obvious, set a finite repeat guard and say so.
  5. No job_id in state. Without it, self-cleanup cannot work reliably.
  6. Raw page hashes. They are noisy; extract meaningful stable fields.
  7. Using rm for cleanup. Do not use rm; leave state files or use trash only if Eleanor explicitly asks to delete artifacts.

Confirmation format

After setup, respond compactly:

Dog is watching: <target>
Signal: <what counts as an update>
Cadence: <schedule>; guard: <repeat/lifetime>
Delivery: <destination>
Stops when: <terminal condition>
Job: <name> (<id>)
Script/state: <paths>

Mention any assumptions, especially if the stop condition is inferred.

Statuspage incident API monitors

Reusable pattern for /dog monitors on Statuspage-hosted incident URLs such as https://www.githubstatus.com/incidents/<id>.

Endpoint discovery

For a specific incident, prefer the incident JSON endpoint over scraping HTML or RSS:

https://<status-host>/api/v2/incidents/<incident-id>.json

Useful fallbacks:

https://<status-host>/api/v2/incidents/unresolved.json
https://<status-host>/history.rss

Stable signature fields

Compare a compact signature built from:

  • incident.id
  • incident.status
  • incident.impact
  • incident.updated_at
  • incident.resolved_at
  • latest incident.incident_updates[0].id
  • latest update status, body, and display_at

This catches meaningful changes without hashing volatile page markup.

Terminal condition

Treat the monitor as complete when either:

  • incident.resolved_at is non-null, or
  • incident.status is one of resolved, completed, or postmortem.

When done, print the final update and remove the cron job if job_id is present in state.

Seeding

Before scheduling, fetch the incident once and seed the current signature into state. Then a manual verification run should be silent when unchanged, avoiding an immediate baseline notification.

Cron schedule pitfall

For recurring short-interval jobs, create/update with schedule="every 1m". Passing schedule="1m" can be interpreted as a one-shot “once in 1m”; if that happens, immediately cronjob(action="update", schedule="every 1m") and verify with cronjob(action="list").

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment