cd /news/ai-agents/the-estian-tattler-a-gossip-rag-for-… · home › topics › ai-agents › article
[ARTICLE · art-139929] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

The Estian Tattler: a gossip rag for my RimWorld colony that has to cite the save file

A developer built The Estian Tattler, a tabloid-style news site for a RimWorld colony in which a Claude reporter writes gossip drawn from a real save file and a plain TypeScript fact-checker rejects any claim naming a colonist or quoting a number not present in the cited records. The newsroom runs on a Sanity dataset of 631 records — 354 tales, 53 letters, 149 messages and 75 overheard conversations from the Tribe of Estian across days 117 to 306 — with six editions published so far citing 136 of those records, plus a Night Desk App SDK board and a coverage dashboard. The project was vibe-coded end to end by Claude Code on Opus 5.5 in a single session, with the developer supplying only the save file and the prompts.

by read9 min views4 publishedSep 25, 2026

This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange I'll be straight about this one since the build process is part of what's judged: I didn't build it. I gave Claude Code the challenge and told it to go all out for the win. It picked the idea, wrote every line, ran the newsroom, and kept a build log as it went. What I brought was the save file, about 300 in-game days of a heavily modded colony.

The Estian Tattler is a small-town tabloid for my colony, the Tribe of Estian. A Claude reporter writes gossip from what's in the save: who married whom, who hit whom, who keeps visiting whose grave, why the berry pile keeps rotting.

The strange part is that it can't lie, or at least it can't get a lie past the desk. A RimWorld save remembers more than you'd think. Tales ("Snake Rato married Reiraborvas 'Grasshopper' Canga"), the letters the game pops up, the message log, the last few dozen conversations between pawns. That's 631 records in Sanity. Every sentence the reporter writes is one of two things:

A fact-checker, which is plain TypeScript and not a model, fails any claim that names a colonist or quotes a number its records don't contain. Failed drafts go back to the reporter, and three strikes spikes the story. Drafts that pass wait for a person at the editor's desk, who can print, send back with a note, or spike. The front page underlines every claim, and hovering one shows its receipts.

Six editions are out. Printed stories cite 136 of the 631 records, and there's a dashboard app for finding out who the paper has ignored so far.

The front page is live: https://booyaka101.github.io/estian-tattler/. Hover or tab onto any underlined sentence. "39 times" is there because the reporter cited 39 records. One of the checker's rules is that a number can be the count of records cited, and the reporter leans on that a lot:

The Studio and the dashboard app need a Sanity login, so these are screenshots. In the Studio, the Receipts view puts each sentence next to the records it cites:

The Night Desk is an App SDK app with every story on a board by stage, and a coverage meter down the side. That run is waiting at the editor's desk:

A gossip paper for one RimWorld colony, where every sentence has to prove itself.

The newsroom is a Sanity dataset built from a real save: 354 tales, 53 letters, 149 messages and 75 overheard conversations from the Tribe of Estian, 17 colonists, days 117 to 306. A Claude reporter writes the stories. A fact-checker that is plain code, not a model, reads every sentence against the records it cites. An editor approves or sends it back in Sanity Studio, and the press publishes it and rebuilds the front page.

Front page: [https://booyaka101.github.io/estian-tattler/](https://booyaka101.github.io/estian-tattler/)
Studio: [https://estian-tattler.sanity.studio/](https://estian-tattler.sanity.studio/)

Each sentence in a story is a claim or an aside, stored as Portable Text annotations.

ingest/ is Python that reads the .rws save and writes NDJSONstudio/ is the schema, the Receipts view, a Pitch action, and the Workflows pluginnewsroom/ is the workflow definition, the reporter and the checkernightdesk/ is the App SDK appfrontpage/ is a static Next.js site on GitHub Pages Claude Code on Opus 5.5, in one long session. My prompts were basically "As long as we go full out for the win" and "Please handle everything in the best possible way so we can win this." The prompts that actually shaped the paper are the ones Claude wrote for its own reporter, so those are below. Its full log is BUILDLOG.md. This is the short version.

Its first idea overlapped with a project I'd already shut down, so it dropped it. It knew I play heavily modded RimWorld, and it noticed that a save file is basically a colony's gossip with timestamps. A tabloid is the obvious format for gossip. What makes a tabloid worth building is making it unable to lie.

It wrote the checker before the reporter, with tests, so the reporter would get written against a rule and not the other way round. The first real story found a real bug. Snake and Grasshopper share the surname Rato, so "Snake Rato" counted as naming Grasshopper too, and a correct claim failed. Names now map to every pawn who answers to them.

The reporter is a Claude Agent SDK session with no built-in tools. It gets three MCP tools over the dataset, search_records, who_is, and check_draft, which runs the same checker the workflow does. Its system prompt, as it stands now:

You are a reporter for the Estian Tattler, the gossip paper of a RimWorld colony called the Tribe of Estian. Everything you know comes from the colony's records: tales, letters, messages and overheard conversations pulled from the save file. The fact-checker is a program, not a person, and it checks every sentence: Each sentence is either a claim or an aside. A claim cites the ids of the records that prove it. Only say what those records say. Every colonist a claim names must appear in one of its cited records. Every number in a claim, in digits or words, must appear in a cited record's text, be a cited record's colony day, or be the count of records cited. An aside is the paper's own voice: a quip, a question, a raised eyebrow. Asides may not name anyone and may not contain numbers. The headline and dek follow the claim rules, checked against every record the body cites. Write like a small-town tabloid that loves these people: sharp, warm, a little nosy. Dates are colony days ('on Day 142'). Short paragraphs, 150 to 350 words in all. Don't invent motives, feelings or events. If the records don't say why something happened, wonder about it in an aside. Only use he or she for someone who_is gives a gender for. Otherwise use their name. Use check_draft before you file, and fix everything it reports.

The pronoun line wasn't there at the start. It came out of the last course correction below.

No draft has ever failed the checker, because the reporter runs check_draft itself and fixes things before filing. The interesting failures all got past it, and they came from reading drafts against their records at the editor's desk.

edition story at the editor's desk
1 Snake and Grasshopper printed first time
2 The berry pile sent back for claims about absence
3 Who hits whom printed first time
4 The restaurant printed first time
5 Flubber Flubber's grave pitched from the Night Desk, sent back for comparisons
Marcellina Triarius spiked
6 Marcellina Triarius, again re-pitched after a prompt fix, printed

Absence. The first berry draft said "the only food drama on file is Grasshopper's binge" and "the records show nobody remarking on it". Every record it cited backed the words around those phrases, and both phrases were unprovable. It went back with the note "You can prove what a record says, not that nothing else exists." The second draft turned both into questions.

Comparison. Flubber Flubber is a dead colonist with ten records, all of them grave visits. The draft said one visitor came "more than anyone else on record" while citing only some of the visits. It got sent back from the Night Desk with a note naming the three sentences, and the rewrite cited all ten visits, so a reader can now count it.

Reading a record backwards. The first Marcellina draft passed with 7 claims and every one checked out. Claude spiked it anyway. It was two days of small talk. It ended with "the last word so far went to Aquila Summanus" on a record where Marcellina is the one talking. And it called Marcellina "she". Visitors come through the ingest with no gender, so that was a guess from the name. That part was fixable upstream: who_is now says "gender not on file" instead of leaving a blank, and the prompt got the pronoun rule. The re-pitch uses the name all the way through, got the last word right, found an actual angle (insulted twice, chatting with both insulters within hours) and printed as edition 6.

story-desk has six stages: reporting, fact-check, editor, printing, printed, spiked. The loop lives in the transitions. A failed fact-check goes back to reporting while drafts < 3, and to spiked after that. Send back resets the count and hands the reporter the editor's note as a field. The three effects (draft the story, fact-check it, print it) run in a small desk-runner.ts that claims pending effects with a 20-minute lease, because a reporter session takes a minute or two. Printing numbers the edition, publishes the draft and fires a repository_dispatch that rebuilds the front page.

The Studio's Workflows tool shows one run at a time, and the question a paper actually cares about is who it hasn't written about yet. So Claude built an App SDK app on top of the same workflow: useWorkflowInstances for the board, useWorkflowSession for the open run, useQuery for the story and coverage counts, and useCreateDocument plus startInstance for Pitch. The editor's buttons come from the session's evaluation, not from the app, so Send back stays disabled until you write its note, because the action declares that param. Runs driven from the app show up in the instance history with an sdk / browser execution context.

What broke there:

startInstance before opening the run, and nothing seemed to happen. The run was on the board within seconds, but the promise settled much later. Now it mints the instance id up front and opens the panel straight away.@sanity/icons v5 ships one module per icon. The root import typechecked and then failed the Studio build with MISSING_EXPORT.@sanity/mutate to 0.18.2 to install cleanly..id(). Local sanity dev wanted a login, so Claude debugged it by attaching to the deployed Studio's iframe over the Chrome DevTools protocol.lcvgtfvq, dataset production, public colony, pawn, record and story. A story's body is Portable Text with two annotations, claim (at least one record reference) and aside. That's the whole trick. A claim isn't free text next to a list of sources, it's the exact span of words tied to the exact records, which is what lets plain code check it and lets the front page show receipts per sentence.story-desk definition, three effects, the Studio plugin for the editor's buttons

── more in #ai-agents 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-estian-tattler-a…] indexed:0 read:9min 2026-09-25 · —