DEV Community

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

Posted on

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

Sanity Challenge Path Two Submission

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.

What I Built

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 claim, a Portable Text annotation holding references to the exact records that prove it
  • an aside, the paper's own voice, which isn't allowed to name anyone or contain a number

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.

Demo

The front page is live: https://booyaka101.github.io/estian-tattler/. Hover or tab onto any underlined sentence.

The Estian Tattler front page, edition 6

"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 berry story with the receipts for

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 Receipts view in Sanity Studio

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:

The Night Desk, with a run at the editor's desk

Code

GitHub logo Booyaka101 / estian-tattler

A RimWorld colony tabloid where every sentence cites the save-file record that proves it. Sanity Workflows + Claude Agent SDK.

The Estian Tattler

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/ Studio: https://estian-tattler.sanity.studio/

The rule

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

  • A claim cites one or more records. Every colonist it names must appear in one of those records. Every number in it, in digits or words, must appear in a cited record's text, be a…
  • ingest/ is Python that reads the .rws save and writes NDJSON
  • studio/ is the schema, the Receipts view, a Pitch action, and the Workflows plugin
  • newsroom/ is the workflow definition, the reporter and the checker
  • nightdesk/ is the App SDK app
  • frontpage/ is a static Next.js site on GitHub Pages

My Build Process

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.

The idea

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.

Checker first

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

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.

What the checker can't see

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.

Workflows

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 Night Desk

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:

  • Pitch awaited 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.
  • Spiking a story left everyone in its leads marked "on the desk" with no Pitch button, because "on the desk" meant any unprinted story. It now means a story with a run still in flight.
  • Then pitching Marcellina again failed with "A draft of this document already exists", because the new story wanted the spiked story's id.

Other things that broke

  • @sanity/icons v5 ships one module per icon. The root import typechecked and then failed the Studio build with MISSING_EXPORT.
  • The Workflows packages needed an npm override pinning @sanity/mutate to 0.18.2 to install cleanly.
  • The Receipts view crashed the structure tool when opened from a link, because the view had no .id(). Local sanity dev wanted a login, so Claude debugged it by attaching to the deployed Studio's iframe over the Chrome DevTools protocol.
  • Three times, writing a file through a shell heredoc mangled a backslash. After the third, files got written with the editor tool.

Sanity Project Details

  • Project ID: lcvgtfvq, dataset production, public
  • Printed editions, no token needed: query
  • Studio: https://estian-tattler.sanity.studio (needs a login)
  • Schema: 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.
  • Workflows: the story-desk definition, three effects, the Studio plugin for the editor's buttons
  • App SDK: the Night Desk, deployed to the org dashboard

Agent Session

Claude Code Session claude-opus-5-5
You

Please handle everything in the best possible way so we can win this

Agent

I'm creating the Path Two Sanity project and starting an honest build log, which the post needs.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

I'm building Path Two now. I'm mapping which pawns in the save are the colony's cast so the ingest script pulls the right people.

Agent
Agent
Agent
Agent
Agent

I've mapped the save: 354 tales plus letters, and I'm writing the ingest script that turns them into Sanity records next.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

The ingest works: 556 records and 12 pawns, covering days 117 to 306. I'm fixing three things now: slow def-label lookups, the missing play-log talks, and a leftover name tag.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

I've read the Workflows docs, and the engine fits the plan: stories move through desk stages, and effects call Claude and GitHub. I'm checking the rebuilt data next, then writing the schema.

Agent
Agent

Before the schema, I'm fixing the pawn name so it comes from the latest snapshot instead of the first one seen.

Agent
Agent
Agent
Agent

Data looks right now: Grasshopper is back to Canga after the day-252 breakup, and 631 records. Writing the Sanity schema.

Agent
Agent
Agent

Now the fact-check desk. It's one pure function, shared by the Studio validation and the workflow effect.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

The 649 documents are imported into Sanity. I'm fixing two type errors in the Studio annotation components.

Agent
Agent
Agent
Agent

Studio typechecks. Next is the story-desk workflow definition, and I'm checking the engine's field types first.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Writing the Tattler's story-desk workflow now, checking the engine's field-op shapes first.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

The workflow is deployed as story-desk v1. Now I'm creating a project token for the newsroom runner and building the effect handlers.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Writing the newsroom's effect handlers and the local desk runner now.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

The effect handlers are written. Next is the runner that drains the workflow queue, then a live test on a real story.

Agent
Agent
Agent
Agent

Switching to bundler resolution, which suits tsx.

Agent
Agent
Agent

Typecheck is clean. Next I'll pitch a real story and run it end to end through the live workflow.

Agent
Agent
Agent
Agent

The heredoc swallowed the backslashes again. Fixing those two lines with Edit.

Agent
Agent

Now a small test suite for the fact-checker. It's the core of the idea, so it should be pinned down.

Agent
Agent
Agent

All 8 pass. Now the live end-to-end run: I'll pitch the Snake and Grasshopper divorce story and start a workflow instance on it.

Agent
Agent
Agent

Instance started at reporting. Now running the newsroom once, in the foreground. The reporter is Claude working through the records.

Agent
Agent

It ran first time: drafted in 74s, fact-check passed. Let me read what the reporter actually wrote and confirm the instance is sitting at the editor's desk.

Agent
Agent

The reporter's output is strong and every claim carries receipts. Two things the mechanical checker can't verify are pronouns and "a few hours later", so I'll spot-check those myself.

Agent
Agent

Everything checks out against the records: she/he, hour 1 binge, hour 23 fight, 9h then 13h chats. Now testing the editor's approve → print path.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

The Studio is getting live fact-check validation, so a failing story can't be published. Fixing a heredoc newline slip first.

Agent
Agent
Agent

Now the Receipts view: every sentence of the story next to the records it stands on, plus the live verdict.

Agent
Agent
Agent
Agent
Agent
Agent

Typecheck is clean. Building the Studio, then deploying the schema and the Studio.

Agent
Agent
Agent
Agent
Agent
Agent

Icons v5 ships one module per icon. Switching to the per-file import.

Agent
Agent
Agent

Build passes. Deploying the Studio and the schema.

Agent
Agent
Agent

Studio and schema are deployed at estian-tattler.sanity.studio. Now I'll check it visually in the automation browser.

Agent
Agent
Agent
Agent
Agent
Agent

The structure tool crashed. Running the Studio locally to get the actual error.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

The Studio's desk structure renders. The crash is only in the document pane, and I'm tracking it down now.

Agent
Agent
Agent
Agent

The editor opens fine, so the crash must be in the Receipts view. Clicking into it to capture the error.

Agent
Agent

The view works; only the deep-link URL crashed it, because the view had no id. Giving it one and redeploying.

Agent
Agent

Now I'll take proper screenshots. I'm hiding the cookie banner in my own tab only, not dismissing it on the user's account.

Agent
Agent
Agent

The Receipts view looks right: every sentence sits next to its records, with a live verdict at the top. Committing this checkpoint locally.

Agent
Agent
Agent

Committed. Next up is the public front page: a Next.js static export to GitHub Pages, with the printed stories and a hover receipt on every claim.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

The front page's data layer and pages are in place, so next up is the stylesheet with the hover receipts. After that I'll print the first story so there's something to build against.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

The whole pipeline ran end to end and the first story is printed. Building the front page against it now.

Agent
Agent
Agent
Agent
Agent
Agent

The front page renders the way I wanted: a tabloid nameplate, and each red-underlined claim opens its save-file receipt on hover or focus. Next I'm setting up the GitHub Pages deploy.

Agent
Agent
Agent
Agent
Agent
Agent

While that runs I'll queue more stories. First I'm checking the shape of the existing pitch so the new ones match it.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

I'm picking leads for three more stories out of the save records: the berry rot, the brawls, and the restaurant and visitor scores.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

The rewrite dropped both unprovable lines and still passed the fact-check. Now I'll try approving through the Studio's Workflows panel rather than the CLI, so the demo shows the editor side for real.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

The action dialog offers "Send to press", "Send back…" and "Spike it". I'll approve this story from the Studio, then fix the banner by hiding every match with !important.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Committed. Before the repo goes public I'm scanning every tracked file for anything that looks like a secret.

Agent
Agent
Agent
Agent

The old receipts screenshot still has the cookie banner in it, so I'm retaking it with the fixed script.

Agent
Agent
Agent

The banner's gone and the Receipts view comes out clean. Next I'm swapping it in, creating the public repo, and enabling Pages.

Agent
Agent
Agent
Agent

The press fired a rebuild for each story, and the newer runs cancelled the older ones as designed. I'm waiting on the last run now.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Now I'm updating memory with the Path Two state so it survives the next compaction.

Agent
Agent
Agent

There are @⁠sanity/workflow-react, workflow-sdk and workflow-components packages as well, which look built for App SDK apps. Checking those.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Building the Night Desk App SDK app now: reading the Pitch action and the workflow-react types to wire the board and actions.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Setting up the Night Desk app's dependencies (SDK 3.x plus the workflow packages).

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Writing the Night Desk components now (board, run panel with editor actions, coverage meter with pitching).

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Typechecking the Night Desk app and fixing the few type errors.

Agent
Agent
Agent

Now the typed create in Coverage.

Agent
Agent
Agent
Agent

Typecheck is clean. Building and deploying the app to the Sanity dashboard.

Agent
Agent
Agent
Agent
Agent
Agent

Deployed. Now screenshotting it from my own tab to see if it actually renders.

Agent
Agent
Agent
Agent

Flubber Flubber is a dead colonist whose grave got 10 visits, 5 of them from Rocaniraalbo. That's a real Graves story. Pitching it from the app.

Agent
Agent

The pitch landed: a new run is in Reporting. The run panel didn't open on its own, though. Checking the documents first, then that.

Agent
Agent
Agent

The panel opens fine from a card click, so the pitch path is losing its state. Watching the DOM right after a pitch click to see what resets it.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Aborted the test run. Now fixing the story id slug, hiding aborted runs from the board, and then re-pitching while watching the panel state.

Agent
Agent
Agent
Agent
Agent
Agent

Minting the instance id up front so the panel can open straight away, and not blocking the UI on the start promise. Meanwhile I'm running the desk runner so the reporter drafts the Flubber story.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Build is fresh. Checking on the reporter.

Agent
Agent
Agent

While the reporter works, I'll write up the Night Desk in the README and BUILDLOG.

Agent
Agent
Agent
Agent
Agent
Agent

The reporter filed "Flubber Flubber's Grave Draws a Faithful Few" and it passed fact-check (8 claims). Before the editor decides in the app, I'm checking it by hand against the records.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

My CDP helper timed out at its own limit before the poll finished, so the timing log is lost. Checking server state and the screenshot.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

The panel now opens on the pitch. Running the reporter.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

That's the README shot. Now I'm spiking it from the app, which tests the one editor path we haven't used yet.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

The fix type-checks. The engine's default filter is in-flight runs only, which is exactly right. Deploying the app now and checking the coverage list.

Agent
Agent
Agent

Fixed: the spiked story's people can be pitched again, and the board reads Printed 5, Spiked 1. Next I'm checking that edition 5 is live on the Pages site, then writing up the BUILDLOG.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Writing the BUILDLOG section on the Night Desk now. First I'm pulling one detail from the earlier session so I don't misstate it.

Agent
Agent
Agent

The earlier Rocaniraalbo detail isn't recoverable, so I'll leave it out rather than guess. I'm checking the printed Flubber story's pronouns directly instead.

Agent
Agent

The printed edition 5 calls Flubber "he" and "the man". I'm checking whether anything on file actually supports that, since the pawn documents carry no gender.

Agent
Agent
Agent
Agent

Pushed as 3d798ec. Updating the project memory, then moving on to the Path Two post draft.

Agent

The build is committed. One gap is still worth closing before the post: the reporter guessed a pronoun. I'm checking what who_is gives it.

Agent
Agent
Agent
Agent
Agent

Confirmed: "A draft of this document already exists." A new pitch after a spike should be a new story, not a reuse of the spiked one, so the id gets a suffix when the slug is taken.

Agent
Agent
Agent

The re-pitch works: new run 5c212766f2fd, no error. Running the reporter with the new pronoun rule.

Agent
Agent
Agent
Agent

Edition 6 is printed. Adding the re-pitch bug and this result to the BUILDLOG, then committing.

Agent
Agent

I just did the same thing the reporter did: "her name" and "called her" assume a gender that isn't on file. Fixing both.

Agent
Agent
Agent
Agent
Agent

Edition 6 is live on the front page. Next I'm refreshing the front-page screenshot so it shows all six editions, then writing the post draft.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Both screenshots look right: the front page shows edition 6, and the berry story has its 39 receipts open. Now I'm checking that the public query URL for the post works without a token.

Agent
Agent

The public query works with no token, and the drafts don't leak. Writing the post draft now.

Agent
Agent

A few lines in the draft say more than I actually know. Tightening them.

Agent
Agent

The draft is in place. Next is the Agent Session. I'm opening DEV's upload page in my own tab to see what format it takes. This is read-only for now.

Top comments (0)