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).
Scanned 9/5/2026
Install to Claude Code
npx -y skills add anajuliabit/aeon --skill atrium-watch --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Atrium Watch?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anajuliabit-atrium-watch)More formats (shields.io, HTML) on the badges page.
---
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:
```json
{
"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`:
```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 the 28-day window `ecosystem-links` uses 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
```bash
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
```bash
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`:
```markdown
# 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`:
```markdown
---
*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) run** — `state.skills` was empty before this run. One-liner per step 3.
2. **Host switch** — `ATRIUM_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`:
```markdown
## 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 `sparkleware-catalog` and `ecosystem-links` 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 the 28-day window `ecosystem-links` uses 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 a GitHub-repo-backed catalog entry (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 `sparkleware-catalog` and `ecosystem-pulse`.** The weekly catalog digests use the same baseline+composition+removal trigger pattern so the operator's mental model is consistent across them.
- **`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, `ecosystem-pulse` 11:00, `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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!