atrium-watch

star 517

Diff of the Atrium marketplace catalog at https://atriumhermes.tech/.well-known/skills/index.json against the prior snapshot — surfaces newly-published skills, removed skills, and updated descriptions. Supply-side complement to sparkleware-catalog (curated skill-packs.json registry) and skill-update (version drift of installed skills).

aaronjmars By aaronjmars schedule Updated 6/15/2026

name: Atrium Watch category: meta description: Diff of the Atrium marketplace catalog at https://atriumhermes.tech/.well-known/skills/index.json against the prior snapshot — surfaces newly-published skills, removed skills, and updated descriptions. Supply-side complement to sparkleware-catalog (curated skill-packs.json registry) and skill-update (version drift of installed skills). var: "" tags: [dev, community]

${var} — Optional. dry-run skips notify (state still updates and article still writes). Empty = normal run.

Today is ${today}. The install-from-atrium script (merged in PR #335, the third skill install path after add-skill and install-skill-pack) fetches skills from the Atrium onchain marketplace at https://atriumhermes.tech/.well-known/skills/index.json. The endpoint is the live source of truth for everything publishable through ./install-from-atrium <name>, but the operator has no signal when new skills appear in it. A skill that ships on Atrium is discoverable today only by running ./install-from-atrium --list by hand. By the time install-from-atrium is used reactively (after someone mentions a skill), the skill may already have been live for days.

This skill closes that gap. It is a weekly Friday watcher of the Atrium catalog — what was added, what was removed, what was renamed or had its description changed, all reported as a structured digest. Pairs with sparkleware-catalog (Tuesday 09:00 — curated skill-packs.json registry) as the supply-side equivalent for the discovered Atrium marketplace, and with skill-update (Sunday 19:00 — version drift on already-installed skills) as the upstream-arrivals signal that precedes any install decision.

Read memory/MEMORY.md for context. Read the last 8 days of memory/logs/ for prior-run context. Read soul/SOUL.md + soul/STYLE.md if populated to match voice in the notification and article.

Why a separate skill from sparkleware-catalog and skill-update

Skill Source Question answered Cadence
sparkleware-catalog skill-packs.json (curated registry, Aeon repo) "What's in the curated pack registry and is it healthy?" Tuesday 09:00 UTC
skill-update skills.lock × upstream SHAs "Did any of my already-installed skills change upstream?" Sunday 19:00 UTC
atrium-watch atriumhermes.tech/.well-known/skills/index.json (Atrium marketplace) "What new skills are publishable on Atrium this week?" Friday 12:00 UTC

The three signals compose without overlap. sparkleware-catalog watches the curated registry that ships in the repo; skill-update watches installed skills against their upstream sources; atrium-watch watches the public marketplace for new arrivals before any install decision. A skill that lands in the Atrium catalog and is also added to skill-packs.json will appear in both Tuesday and Friday digests — that's expected and not duplication (the two skills answer different questions: "curated and trusted?" vs. "publishable on the chain?").

Building this into install-from-atrium itself would have entangled the install path with surveillance: today the script is a thin fetcher (curl → scan → copy), and adding a state file + diff logic to a single-use install command would conflate one-shot install with weekly polling. Keeping them separate keeps each surface structurally simple.

Inputs

Source Purpose Auth
https://atriumhermes.tech/.well-known/skills/index.json Live Atrium marketplace catalog — array of {name, description, files, skill_id} entries None (public)
memory/topics/atrium-catalog-state.json Prior-run snapshot of the catalog, keyed by skill_id Local file

No new secrets. The Atrium endpoint is public — no auth header, no env-var expansion. ATRIUM_HOST (default https://atriumhermes.tech) is honored as an override so a self-hosted Atrium can be pointed at without editing the skill, mirroring the existing install-from-atrium convention.

Writes:

  • memory/topics/atrium-catalog-state.json — current parsed catalog (keyed by skill_id) + last_run timestamp + last_status
  • articles/atrium-watch-${today}.md — digest on every non-error run (including QUIET; the article is the durable record even when the notification is suppressed)
  • memory/logs/${today}.md — one log block per run
  • Notification via ./notify — only when ≥1 added or removed entry since the last run (see step 6)

Catalog schema

The Atrium endpoint returns a JSON object with one top-level key skills, an array of skill objects:

{
  "skills": [
    {
      "name": "string",
      "description": "string",
      "files": ["SKILL.md"],
      "skill_id": "0x<64-hex>"
    }
  ]
}

skill_id is the canonical key for an entry — it is the onchain identifier and is collision-free by design (it is the same id install-from-atrium 0x... accepts). A skill whose name is renamed but keeps the same skill_id is reported as updated, not as an add+remove pair. A skill that publishes a new version under a new skill_id is treated as a fresh entrant — that matches the operator's mental model (a new id is a new installable artefact).

The endpoint may evolve to include a cid or version field per skill (the install-from-atrium script already reads a cid from the per-skill SKILL.md frontmatter for skills.lock provenance). The parser tolerates extra fields and ignores anything not in the four documented keys above — additions don't break the diff.

State schema

memory/topics/atrium-catalog-state.json:

{
  "last_run": "2026-06-05",
  "last_status": "ATRIUM_CATALOG_WATCHER_OK",
  "atrium_host": "https://atriumhermes.tech",
  "skills": {
    "0xabc...": {
      "name": "example-skill",
      "description": "string",
      "files": ["SKILL.md"],
      "first_seen": "2026-04-20",
      "last_seen": "2026-06-05"
    }
  }
}

first_seen is the date this skill_id first appeared in any run — never overwritten. last_seen is the most recent run where the id was present — overwritten every run that sees it. An entry whose last_seen is more than 56 days old is pruned from the state file (longer than ecosystem-entrants' 28-day window because the Atrium marketplace is smaller and a re-publish of the same skill_id after two months is still naturally a "this is back" signal worth a recovered mention; pruning at 56d keeps the state file from growing unbounded if the catalog churns). Pruning is silent (no notify).

atrium_host is recorded so that a switch to ATRIUM_HOST=... (e.g. to a self-hosted endpoint) re-baselines automatically — switching hosts is a deliberate operator action and the diff against the prior host's snapshot would be misleading.

Steps

0. Bootstrap

mkdir -p memory/topics articles
[ -f memory/topics/atrium-catalog-state.json ] || cat > memory/topics/atrium-catalog-state.json <<'EOF'
{"last_run":null,"last_status":null,"atrium_host":null,"skills":{}}
EOF

If jq empty fails on the state file (corrupt JSON from an aborted write), back it up to .bak, reset to the empty template, and tag the run STATE_CORRUPT. Continue — a fresh state file means re-notifying every currently-listed skill as a baseline on this one run, which is the safer post-corruption outcome than silently swallowing a real new arrival.

1. Parse var

  • Lowercase, trim. If the resulting string equals dry-run, set MODE=dry-run. Empty → MODE=execute.
  • Any other non-empty value → log ATRIUM_CATALOG_WATCHER_BAD_VAR: ${var} and exit (no writes, no notify).

2. Fetch the Atrium catalog

ATRIUM_HOST="${ATRIUM_HOST:-https://atriumhermes.tech}"
ENDPOINT="${ATRIUM_HOST}/.well-known/skills/index.json"
curl -fsS --max-time 30 "${ENDPOINT}" > /tmp/atrium-catalog.json

If curl fails for any reason (network, sandbox block, 5xx, non-2xx) → use WebFetch for the same URL as a fallback, asking for the raw JSON verbatim. The Atrium endpoint is public and the sandbox blocks outbound HTTPS intermittently — WebFetch bypasses the sandbox (CLAUDE.md sandbox pattern 1).

If WebFetch also fails to return parseable JSON → log ATRIUM_CATALOG_WATCHER_FETCH_FAIL, write a one-line failure notification (atrium-watch: could not reach ${ENDPOINT}), exit. The endpoint is the floor — if it's unreachable, the skill has no signal to compute on. State is not advanced on fetch failure (the next run still has the previous snapshot to diff against).

Validate the response shape: jq '.skills | type == "array"' must be true. If the top-level skills key is missing or not an array → log ATRIUM_CATALOG_WATCHER_BAD_SHAPE, write a one-line failure notification, exit. State not advanced. The endpoint shape changed in a way the skill doesn't understand — fail loudly rather than guess.

3. Host-switch detection

If state.atrium_host is non-null and differs from the resolved ATRIUM_HOST, treat the run as a baseline (state.skills will be re-seeded from the new endpoint, the diff is against the empty set, no flood of "added" notifications). Log ATRIUM_CATALOG_WATCHER_HOST_SWITCH: ${old_host} -> ${new_host} for the operator's audit trail. The one-liner baseline notification (step 7) explicitly names the host switch so the operator knows why the diff went quiet.

4. Parse and diff against prior state

Parse /tmp/atrium-catalog.json:

  • Build current = {skill_id: {name, description, files}} map from .skills[].

  • Reject any entry whose skill_id is missing, empty, or doesn't match ^0x[0-9a-fA-F]{64}$ — those are catalog rows the skill can't key on (the same regex install-from-atrium enforces). Log ATRIUM_CATALOG_WATCHER_INVALID_ID_SKIPPED: ${entry_name} for each. Continue with the rest.

  • Let previous = the keys of state.skills (or empty on baseline / host-switch).

  • added = current - previous (skill_ids present this run, absent last run)

  • removed = previous - current (skill_ids present last run, absent this run)

  • updated = skill_ids in both sets but where name, description, or files differ from the stored snapshot

If state.last_run is null (first run) → added is the full current set; do not report this as N entrants in the notification body — instead notify a single one-liner "baseline run: indexed N skills in the Atrium catalog, will diff from next Friday onward." The full list goes to the article. Reporting a flood of "new!" entries on the first run would be misleading; the entries already existed, this skill just hadn't been measuring them yet. Identical behavior for host-switch runs.

5. Write the article

Overwrite articles/atrium-watch-${today}.md:

# Atrium Catalog Watcher — ${today}

*Atrium marketplace ({ATRIUM_HOST}): N total skills. Added since last run: A. Removed: R. Updated: U.*

## Added ({A})

| Skill | skill_id | Description | Install command |
|-------|----------|-------------|-----------------|
| {name} | `0xabc…` (full id in the row) | {description, truncated to 200 chars} | `./install-from-atrium {name}` |

## Removed ({R})

| Skill | skill_id | Last seen | First seen |
|-------|----------|-----------|------------|
| {name} | `0xabc…` | {last_seen date} | {first_seen date} |

## Updated ({U})

| Skill | skill_id | Field changed | Before | After |
|-------|----------|---------------|--------|-------|

## Full catalog ({N})

*Snapshot of the current Atrium endpoint — for one-click install, copy any of the commands below.*

| Skill | skill_id | Description |
|-------|----------|-------------|

Always write the article on a non-error run, even when added/removed/updated are all zero — the snapshot section is the durable record. On a baseline / host-switch run, omit the Added / Removed / Updated tables entirely (they would echo the full catalog under "Added") and lead with a Baseline established for {ATRIUM_HOST} note.

The article footer cites the source endpoint and the resolved ATRIUM_HOST:

---
*Source: `${ATRIUM_HOST}/.well-known/skills/index.json`. Each skill is installable via `./install-from-atrium <name>` or `./install-from-atrium <skill_id>` (canonical, collision-free). Generated by `atrium-watch`.*

6. Decide whether to notify (gated)

Skip notify entirely on BAD_VAR, BAD_SHAPE, FETCH_FAIL, DRY_RUN, STATE_CORRUPT.

Otherwise notify only if any of:

  1. First (baseline) runstate.skills was empty before this run. One-liner per step 3.
  2. Host switchATRIUM_HOST changed since the last run. One-liner per step 3, naming both hosts.
  3. ≥1 added entry since the last run.
  4. ≥1 removed entry since the last run.

updated entries are reported in the article only — not the notification. A description tweak is editorial polish, and surfacing it as a notification would re-create the dependabot-noise pattern other skills work to suppress. A name rename also lands in updated only — the skill_id is the canonical id, and a name swap on a stable id is not a fresh-arrival signal.

7. Notification format

Baseline (first) run:

*Atrium Catalog Watcher — baseline — ${today}*

atrium-watch is now tracking N skills publishable at ${ATRIUM_HOST}.
Next Friday will report the diff. Full snapshot in
articles/atrium-watch-${today}.md.

Host-switch run:

*Atrium Catalog Watcher — host switch — ${today}*

ATRIUM_HOST changed: {old_host} → {new_host}.
Re-baselined to N skills at the new host. Full snapshot in
articles/atrium-watch-${today}.md.

Normal run with added/removed entries:

*Atrium Catalog Watcher — ${today}*

Atrium marketplace: N skills · {A} added · {R} removed since last Friday

Added:
- {name} — `./install-from-atrium {name}`
- ...

{If R > 0:}
Removed:
- {name} (was first seen YYYY-MM-DD)

Full digest: articles/atrium-watch-${today}.md

Keep under 900 chars. If added has more than 8 entries, list the first 8 and append "+M more (see article)" — preserves the dashboard render and the article carries the full list. Each added line includes the operator's install-from-atrium <name> install command so the notification is one-click actionable (no need to copy a skill_id or click through to the article for the common case).

Send via ./notify "$MSG" (single positional argument).

8. Persist state

Atomically overwrite memory/topics/atrium-catalog-state.json with the post-run snapshot:

  • For every entry in current: set last_seen=${today}; preserve first_seen if it exists, otherwise set it to ${today}; update name/description/files fields to the latest parsed values.
  • Drop entries whose last_seen is older than 56 days from ${today} (silent pruning per the state schema rule above).
  • Set last_run=${today}, last_status to the exit-taxonomy code from below, and atrium_host to the resolved ATRIUM_HOST.

Write to memory/topics/atrium-catalog-state.json.tmp first, then mv over the live path so a mid-write crash never leaves a half-formed JSON.

9. Log

Append to memory/logs/${today}.md:

## atrium-watch
- **Atrium host**: ${ATRIUM_HOST}
- **Catalog skills**: N
- **Added**: A · **Removed**: R · **Updated**: U
- **Baseline run**: yes/no
- **Host switch**: yes (from {old}) / no
- **Invalid IDs skipped**: K (catalog rows with malformed skill_id)
- **Article**: articles/atrium-watch-${today}.md
- **Notification**: sent / skipped (gated)
- **Status**: ATRIUM_CATALOG_WATCHER_OK

Exit taxonomy

Status Meaning Notify?
ATRIUM_CATALOG_WATCHER_OK Diff written; at least one added or removed entry, or a baseline / host-switch run Yes
ATRIUM_CATALOG_WATCHER_QUIET Diff written; no added/removed entries since last run No (article + state still write)
ATRIUM_CATALOG_WATCHER_FETCH_FAIL Both curl and WebFetch failed to reach the endpoint Yes (one-line failure notify)
ATRIUM_CATALOG_WATCHER_BAD_SHAPE Endpoint reachable but response shape unrecognized Yes (one-line failure notify)
ATRIUM_CATALOG_WATCHER_DRY_RUN MODE=dry-run; article + state wrote, notify skipped No
ATRIUM_CATALOG_WATCHER_STATE_CORRUPT State JSON unreadable, recreated; silent recovery this run No
ATRIUM_CATALOG_WATCHER_BAD_VAR ${var} parse failed No

OK and QUIET are the two success states. The split lets the dashboard show "ran clean, nothing changed" without overloading the OK row — the same pattern ecosystem-entrants, competitor-radar, and sparkleware-catalog use.

Design notes (do not edit without reading)

  • Updates are article-only, never notified. A description tweak or a name rename on a stable skill_id is cosmetic; surfacing it as a Friday notification would dilute the "new arrival" signal. The article carries the full update log for archaeology.
  • Baseline and host-switch runs do not fire N notifications. On the first run (or after ATRIUM_HOST changes), every currently-listed skill is technically "new to the skill", but reporting a flood would be misleading — the entries already existed. A single one-liner notification establishes the watermark; the next week's run reports the actual diff.
  • State entries prune after 56 days of absence. A skill removed from the Atrium catalog is reported as removed the week of removal, then forgotten 56 days later. A re-publish of the same skill_id after the prune window is treated as a fresh entrant — that's the operator's actual question on re-publish ("what is this skill?") rather than a stale "it returned" footnote. The 56-day window is twice ecosystem-entrants' window because the Atrium catalog is smaller (currently ~18 entries) and a longer memory keeps recovered semantics meaningful at that scale.
  • skill_id is the canonical key. A name rename or description change does NOT trigger an add+remove pair — those would noisily fire two notifications for one editorial change. The diff is entirely keyed on the onchain skill_id, which is the same id install-from-atrium 0x... accepts and the same id Atrium guarantees collision-free.
  • Read-only against the Atrium endpoint. This skill never publishes, modifies, or removes catalog entries — Atrium publishing is an onchain action the operator takes deliberately (or another agent does on their behalf). The skill only observes the catalog and reports diffs.
  • No multi-host comparison. A switch of ATRIUM_HOST re-baselines from scratch. Comparing snapshots across hosts would be meaningless because the underlying registries are independent.
  • PR / commit enrichment deliberately not added. Unlike ecosystem-entrants (which can map a row to a merged PR on aaronjmars/aeon), Atrium catalog entries are not tied to a single GitHub repo or PR — they are onchain artefacts. Surfacing a "publisher" attribution would require resolving each skill_id to its publishing wallet via Atrium SDK + chain calls, which is out of scope for a read-only weekly watcher. The article does, however, surface the install command (./install-from-atrium <name>) on every added row, which is the operator's actual next-step.
  • Notify gating mirrors ecosystem-entrants and sparkleware-catalog. The three Monday/Tuesday/Friday weekly digests use the same baseline+composition+removal trigger pattern so the operator's mental model is consistent across all three.
  • install-from-atrium --list is the manual sibling. Anyone can run ./install-from-atrium --list on demand for an ad-hoc browse. This skill exists because nobody runs --list weekly by hand; the cron makes the discovery automatic.

Sandbox Note

The Atrium endpoint is public — no auth header, no env-var-in-headers, so curl works directly when the sandbox allows outbound HTTPS. When curl is blocked (intermittent GitHub Actions sandbox behaviour per CLAUDE.md pattern 1), the skill falls back to WebFetch for the exact same URL. WebFetch bypasses the sandbox and returns the JSON as text; the skill re-parses it with jq. If both curl and WebFetch fail, the run exits FETCH_FAIL with a one-line failure notification (so a long unreachable run isn't silent), and state is not advanced — the next run still has the prior snapshot to diff against.

No pre-fetch / post-process script needed. ./notify is the only other outbound call and is already sandbox-safe.

Required Env Vars

  • None required. ATRIUM_HOST (default https://atriumhermes.tech) is honored as an optional override, mirroring the existing install-from-atrium convention. No API keys, no on-chain reads, no GitHub auth.

Why Friday 12:00 UTC

The Monday intelligence stack is already busy (fleet-state 08:00, competitor-radar 10:00, ecosystem-pulse 11:00, ecosystem-entrants 11:45, wallet-risk 11:15, capabilities-map 11:30). Tuesday 09:00 holds sparkleware-catalog (the curated-registry equivalent). Friday afternoon is the first quiet weekly slot, mid-day enough that a fresh-arrivals digest makes it into the operator's late-week skim rather than landing in the weekend lull. Weekly, not daily: the Atrium catalog grows on a publish-event cadence (days to weeks), and a daily crawl would surface nothing the weekly run misses while adding ~7× the noise floor.

Install via CLI
npx skills add https://github.com/aaronjmars/aeon --skill atrium-watch
Repository Details
star Stars 517
call_split Forks 176
navigation Branch main
article Path SKILL.md
More from Creator