feed-watcher
Polls a configurable set of sources from source-registry and returns the items that are new since this skill's last run. Every Category 2 scheduled agent (weekly-intelligence-digest, voices-watcher, monthly-copilot-changelog, etc.) calls this at the top of its run to know what changed.
When to use
- A scheduled agent's first action of the run — fetch new items across its source list.
- A user wants a one-shot "what's new in the last N days from sources tagged X" lookup.
When NOT to use
- For arbitrary URLs not in
source-registry— usesource-fetcherdirectly. - For sources without published feeds AND without a stable HTML page — feed-watcher will fail; consider a custom skill.
How it works
- Get the source list — call
source-registrywith the caller's filter (topic_tags, credibility tier, or an explicit source-id list). Default if nothing specified: every source whereverified: true. - Per source, poll:
type: rss | atom | json-feed: fetch the feed URL viasource-fetcher(which runs the result throughprompt-injection-guard). Parse items.type: github-releases: fetchhttps://api.github.com/repos/{owner}/{repo}/releasesviasource-fetcher. Parse JSON.type: html: fetch the index page viasource-fetcher. Heuristically extract item titles + links from the page (look for<article>,<li class*="post">, dated headings, etc.). Mark items with a warning that they came from HTML scrape, not a feed.type: api: skill-specific; not implemented in v1 — return a warning.
- Compute new items — delegate to
seen-tracker: callseen-tracker.bulk_filter(<agent_name>, items), where<agent_name>is the calling scheduled agent (e.g.weekly-intelligence-digest). State lives at<repo>/.state/<agent_name>/seen.jsonl; feed-watcher performs no state file I/O of its own. Items in thenewandupdatedbuckets are "new." - Marking surfaced is the caller's job — the scheduled agent calls
seen-tracker.mark_surfacedfor every item it actually publishes (runner lifecycle step 10), so an item that fetched but never made a digest is retried next run. - Return the new items, grouped by source.
Output shape
{
"run_at": "2026-06-20T15:00:00Z",
"sources_polled": 6,
"sources_failed": 1,
"new_items": [
{
"source_id": "github-changelog",
"source_name": "GitHub Changelog",
"source_tier": 1,
"items": [
{
"title": "Copilot can now ...",
"url": "https://github.blog/changelog/2026-06-19-...",
"published_at": "2026-06-19T14:00:00Z",
"summary": "<first 300 chars of item body, post injection-guard>",
"warnings": []
}
]
}
],
"failures": [
{
"source_id": "anthropic-news",
"reason": "html scrape returned 0 items — selector heuristics did not match",
"url": "https://www.anthropic.com/news"
}
]
}
State management
All dedup state is owned by seen-tracker at <repo>/.state/<agent_name>/seen.jsonl — one file per calling agent, so each scheduled agent's "what have I already surfaced?" is isolated from every other agent's. Record schema, pruning caps, append-only semantics, and concurrency posture are defined in seen-tracker's SKILL.md; feed-watcher never touches state files directly.
Failure handling (stop and report)
Per the "stop and report" guardrail in _meta/conventions.md: every failed source goes into the failures array in the output. Never silently skip a feed that didn't poll. Downstream scheduled agents must surface failures in their digest's Sources section (e.g., could not poll: anthropic-news — html scrape failed).
Common failures:
404or5xxfromsource-fetcher— log and continue with next source.- Empty HTML scrape (0 items extracted) — likely selector heuristics need tuning; flag.
- Malformed RSS/Atom — try lenient parse; if still fails, log and continue.
prompt-injection-guardreturnssuspiciouson a feed body — drop those items from the new-items list, log to failures.
Composes with
source-registry— which sources to poll.source-fetcher— the actual web fetch.prompt-injection-guard— applied via source-fetcher; suspicious items dropped.seen-tracker— cross-run dedup state, keyed by the calling agent.
Acceptance test (for step 3 done-criteria)
Poll the three minimum sources from source-registry:
github-changelog— RSS feed, should return parseable items.anthropic-news— HTML scrape; if 0 items, the failure must appear in thefailuresarray (not silently dropped).occ-news-releases(or another regulator) — similar HTML handling.
On the first run, all items are "new." On a second back-to-back run, all items should be filtered out (state populated). Confirm:
- Output has
sources_polled≥ 3. - State files exist at
<repo>/.state/feed-watcher/github-changelog.jsonletc. - A second run returns zero new items for github-changelog (the only verified-true source).
- Failures from
anthropic-newsandocc-news-releases(currentlyverified: false) appear infailures, not silently dropped.