DEV Community

lxivlog64
lxivlog64

Posted on Edited on

Resurrecting RSS for Kiroween: Haunted Feeds, a spec-driven feed reader

This post was written as part of the Kiroween 2026 hackathon.

RSS never died. Podcasts still run on it, and most blogs still publish a feed. What went away was the habit of reading feeds. For Kiroween's Resurrection category I built Haunted Feeds, a small self-hosted reader that turns the sites you follow into one daily digest with short summaries, in a Halloween costume.

What it does

  • Follows RSS 2.0, RSS 1.0 and Atom feeds, imports and exports OPML
  • Removes tracking parameters from links and de-duplicates items across polls
  • Builds a digest for the last 24 hours, 3 days or a week, grouped by site
  • Runs with no API key: offline demo feeds and an extractive summarizer. Set OPENAI_API_KEY to get LLM summaries, with automatic fallback if the API fails

How the project is set up for Kiro

Everything Kiro reads lives in the .kiro/ folder of the repo, in the formats described in the Kiro docs. If you open the repo in Kiro, this is what you get:

Steering (docs). .kiro/steering/ has the three foundation files Kiro defines: product.md (goal, zero-key requirement, tone), tech.md (Node 20, no build step, parsers must never throw, no secrets in code) and structure.md (module map). product.md and tech.md use inclusion: always; structure.md uses inclusion: fileMatch so it only loads when you work in src/ or test/.

Specs (docs). Each feature has the standard trio in .kiro/specs/<feature>/: requirements.md with user stories and EARS acceptance criteria, design.md, and a tasks.md checklist. There are three: feed-ingestion, digest-summaries and opml-import. A typical criterion:

WHEN a feed is re-polled, THE SYSTEM SHALL NOT store an item whose id already exists.

Writing the EARS lines first forced edge cases into the open: what counts as the same item, what happens with malformed XML (record an error, never throw), what to do with non-http URLs (HTTP 400). In Kiro, the tasks list in tasks.md is what its task execution UI tracks.

Correctness properties. The feed-ingestion design lists properties that the code must satisfy, and test/parser.test.js checks them with fast-check: de-duplication is idempotent and order-preserving, URL normalization is idempotent and ignores utm_* and fragments, and the parser never throws on generated feeds.

Hooks (docs). Two v1 hook files in .kiro/hooks/ use the PostFileSave trigger. One runs npm test when a file under src/ is saved; the other runs node --check on the source modules when any .js file is saved.

MCP (docs). .kiro/settings/mcp.json is a workspace MCP config with the fetch server (uvx mcp-server-fetch), so the agent can pull a real feed when you ask it to look at one.

A real bug: escaped HTML vs. entity limits

After deploying, the Rust blog's Atom feed (https://blog.rust-lang.org/feed.xml) showed up as invalid-xml, even though it is perfectly valid. The cause: fast-xml-parser counts every standard entity (&lt;, &amp;…) toward a default limit of 1,000 expansions per document, a guard against "billion laughs" attacks. Blogs that put escaped HTML in <content type="html"> easily go past that; this one had about 1,400. Standard entities can't recurse, so the fix raises only the total-expansion cap and keeps the DOCTYPE entity size and depth limits strict. A fixture with the same shape is now a regression test, which failed before the fix and passes after it. The steering rule "parsers must never throw" is why this surfaced as an error on the feed rather than a crash.

Try it

git clone https://github.com/lxivlog64/rss-resurrected
cd rss-resurrected && npm install && npm start   # http://localhost:3000
npm test                                          # 10 tests incl. 3 property tests
Enter fullscreen mode Exit fullscreen mode

Or open https://rss-resurrected.onrender.com and click 👻 Load demo. Happy Kiroween 🎃

Update: using Kiro on the project for real

After the first version, I opened the repo in Kiro IDE 1.4.86 (Free plan) and used it on a new feature. Here is what happened, no polish.

Spec mode. I asked for a Markdown export of the digest. Kiro first added requirement 7 to .kiro/specs/digest-summaries/requirements.md, in EARS form: WHEN the user requests GET /api/digest.md, THE SYSTEM SHALL return a Markdown document with content-type text/markdown…. Then it added task 6 to tasks.md, wrote digestToMarkdown in src/poller.js, added the route in src/server.js, and added a test. It asked before running npm test, which passed 12/12. One of its edits to server.js broke the file, and it caught and fixed that itself. Total: about 4.4 credits.

Hooks. Both hook files showed up in the Agent Hooks panel, and I enabled them there. When I saved a source file by hand, I didn't see the run-tests hook fire. I'm still looking into that.

MCP. The fetch server connected with one tool, and Kiro did call @fetch/fetch on a feed URL. The call was refused because DNS in my sandbox resolved the host to 198.18.0.1, a reserved address. So it's wired up, but it didn't fetch anything in my setup.

The biggest change for me: I now write the requirement before the code, because that's what Kiro works from. Screenshots are in docs/KIRO_USAGE.md.

Top comments (0)